diff --git a/mobile/openapi/doc/MemoryLaneResponseDto.md b/mobile/openapi/doc/MemoryLaneResponseDto.md index 4712744466..102c45f161 100644 Binary files a/mobile/openapi/doc/MemoryLaneResponseDto.md and b/mobile/openapi/doc/MemoryLaneResponseDto.md differ diff --git a/mobile/openapi/lib/model/memory_lane_response_dto.dart b/mobile/openapi/lib/model/memory_lane_response_dto.dart index 7d761131d7..4d6f86fb47 100644 Binary files a/mobile/openapi/lib/model/memory_lane_response_dto.dart and b/mobile/openapi/lib/model/memory_lane_response_dto.dart differ diff --git a/mobile/openapi/test/memory_lane_response_dto_test.dart b/mobile/openapi/test/memory_lane_response_dto_test.dart index 2dad2c356b..1106ee7d95 100644 Binary files a/mobile/openapi/test/memory_lane_response_dto_test.dart and b/mobile/openapi/test/memory_lane_response_dto_test.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index d04b91aa89..129b001a8d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8427,12 +8427,17 @@ "type": "array" }, "title": { + "deprecated": true, "type": "string" + }, + "yearsAgo": { + "type": "number" } }, "required": [ "assets", - "title" + "title", + "yearsAgo" ], "type": "object" }, diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 8834564031..35b7167d4a 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -273,6 +273,7 @@ export type MapMarkerResponseDto = { export type MemoryLaneResponseDto = { assets: AssetResponseDto[]; title: string; + yearsAgo: number; }; export type UpdateStackParentDto = { newParentId: string; diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 04e36645e1..59c9b90707 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -131,7 +131,9 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As } export class MemoryLaneResponseDto { + @ApiProperty({ deprecated: true }) title!: string; + yearsAgo!: number; assets!: AssetResponseDto[]; } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index dc8d21f005..735cfa325d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -93,7 +93,7 @@ export class AssetRepository implements IAssetRepository { }, ) .leftJoinAndSelect('entity.exifInfo', 'exifInfo') - .orderBy('entity.localDateTime', 'DESC') + .orderBy('entity.localDateTime', 'ASC') .getMany(); } diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index df1f819b47..d5bce3d1ec 100644 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -307,13 +307,17 @@ describe(AssetService.name, () => { jest.useRealTimers(); }); - it('should set the title correctly', async () => { + it('should group the assets correctly', async () => { + const image1 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 0, 0, 0) }; + const image2 = { ...assetStub.image, localDateTime: new Date(2023, 1, 15, 1, 0, 0) }; + const image3 = { ...assetStub.image, localDateTime: new Date(2015, 1, 15) }; + partnerMock.getAll.mockResolvedValue([]); - assetMock.getByDayOfYear.mockResolvedValue([assetStub.image, assetStub.imageFrom2015]); + assetMock.getByDayOfYear.mockResolvedValue([image1, image2, image3]); await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([ - { title: '1 year since...', assets: [mapAsset(assetStub.image)] }, - { title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] }, + { yearsAgo: 1, title: '1 year since...', assets: [mapAsset(image1), mapAsset(image2)] }, + { yearsAgo: 9, title: '9 years since...', assets: [mapAsset(image3)] }, ]); expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]); @@ -321,6 +325,7 @@ describe(AssetService.name, () => { it('should get memories with partners with inTimeline enabled', async () => { partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]); + assetMock.getByDayOfYear.mockResolvedValue([]); await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 230415e80e..17fe147c01 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -174,20 +174,25 @@ export class AssetService { userIds.push(...partnersIds); const assets = await this.assetRepository.getByDayOfYear(userIds, dto); + const groups: Record = {}; + for (const asset of assets) { + const yearsAgo = currentYear - asset.localDateTime.getFullYear(); + if (!groups[yearsAgo]) { + groups[yearsAgo] = []; + } + groups[yearsAgo].push(asset); + } - return _.chain(assets) - .filter((asset) => asset.localDateTime.getFullYear() < currentYear) - .map((asset) => { - const years = currentYear - asset.localDateTime.getFullYear(); - - return { - title: `${years} year${years > 1 ? 's' : ''} since...`, - asset: mapAsset(asset, { auth }), - }; - }) - .groupBy((asset) => asset.title) - .map((items, title) => ({ title, assets: items.map(({ asset }) => asset) })) - .value(); + return Object.keys(groups) + .map(Number) + .sort() + .filter((yearsAgo) => yearsAgo > 0) + .map((yearsAgo) => ({ + yearsAgo, + // TODO move this to clients + title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`, + assets: groups[yearsAgo].map((asset) => mapAsset(asset, { auth })), + })); } private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index e4d21f3fc8..cc002897a2 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -8,7 +8,7 @@ import { AppRoute, QueryParameter } from '$lib/constants'; import type { Viewport } from '$lib/stores/assets.store'; import { memoryStore } from '$lib/stores/memory.store'; - import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; + import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils'; import { shortcuts } from '$lib/utils/shortcut'; import { fromLocalDateTime } from '$lib/utils/timeline-util'; import { ThumbnailFormat, getMemoryLane } from '@immich/sdk'; @@ -102,7 +102,7 @@ goto(AppRoute.PHOTOS)} forceDark>

- {currentMemory.title} + {memoryLaneTitle(currentMemory.yearsAgo)}

@@ -181,7 +181,7 @@ {#if previousMemory}

PREVIOUS

-

{previousMemory.title}

+

{memoryLaneTitle(previousMemory.yearsAgo)}

{/if} @@ -254,7 +254,7 @@ {#if nextMemory}

UP NEXT

-

{nextMemory.title}

+

{memoryLaneTitle(nextMemory.yearsAgo)}

{/if} diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 6faa41362f..e481d8fd3e 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -3,7 +3,7 @@ import Icon from '$lib/components/elements/icon.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; import { memoryStore } from '$lib/stores/memory.store'; - import { getAssetThumbnailUrl } from '$lib/utils'; + import { getAssetThumbnailUrl, memoryLaneTitle } from '$lib/utils'; import { getAltText } from '$lib/utils/thumbnail-util'; import { ThumbnailFormat, getMemoryLane } from '@immich/sdk'; import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'; @@ -66,7 +66,7 @@ {/if}
- {#each $memoryStore as memory, index (memory.title)} + {#each $memoryStore as memory, index (memory.yearsAgo)}