diff --git a/mobile/openapi/doc/MemoryLaneResponseDto.md b/mobile/openapi/doc/MemoryLaneResponseDto.md index 4712744466..102c45f161 100644 --- a/mobile/openapi/doc/MemoryLaneResponseDto.md +++ b/mobile/openapi/doc/MemoryLaneResponseDto.md @@ -10,6 +10,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **assets** | [**List**](AssetResponseDto.md) | | [default to const []] **title** | **String** | | +**yearsAgo** | **num** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) 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 --- a/mobile/openapi/lib/model/memory_lane_response_dto.dart +++ b/mobile/openapi/lib/model/memory_lane_response_dto.dart @@ -15,30 +15,36 @@ class MemoryLaneResponseDto { MemoryLaneResponseDto({ this.assets = const [], required this.title, + required this.yearsAgo, }); List assets; String title; + num yearsAgo; + @override bool operator ==(Object other) => identical(this, other) || other is MemoryLaneResponseDto && _deepEquality.equals(other.assets, assets) && - other.title == title; + other.title == title && + other.yearsAgo == yearsAgo; @override int get hashCode => // ignore: unnecessary_parenthesis (assets.hashCode) + - (title.hashCode); + (title.hashCode) + + (yearsAgo.hashCode); @override - String toString() => 'MemoryLaneResponseDto[assets=$assets, title=$title]'; + String toString() => 'MemoryLaneResponseDto[assets=$assets, title=$title, yearsAgo=$yearsAgo]'; Map toJson() { final json = {}; json[r'assets'] = this.assets; json[r'title'] = this.title; + json[r'yearsAgo'] = this.yearsAgo; return json; } @@ -52,6 +58,7 @@ class MemoryLaneResponseDto { return MemoryLaneResponseDto( assets: AssetResponseDto.listFromJson(json[r'assets']), title: mapValueOfType(json, r'title')!, + yearsAgo: num.parse('${json[r'yearsAgo']}'), ); } return null; @@ -101,6 +108,7 @@ class MemoryLaneResponseDto { static const requiredKeys = { 'assets', 'title', + 'yearsAgo', }; } 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 --- a/mobile/openapi/test/memory_lane_response_dto_test.dart +++ b/mobile/openapi/test/memory_lane_response_dto_test.dart @@ -26,6 +26,11 @@ void main() { // TODO }); + // num yearsAgo + test('to test the property `yearsAgo`', () async { + // TODO + }); + }); 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)}