1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

fix: memory lane assets in ascending order (#8309)

* fix: memory lane asset order

* chore: deprecate title

* chore: open-api

* chore: rename years => yearsAgo
This commit is contained in:
Jason Rasmussen 2024-03-27 16:14:29 -04:00 committed by GitHub
parent 13b11a39a9
commit 9fe80c25eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 48 additions and 26 deletions

Binary file not shown.

View file

@ -8427,12 +8427,17 @@
"type": "array" "type": "array"
}, },
"title": { "title": {
"deprecated": true,
"type": "string" "type": "string"
},
"yearsAgo": {
"type": "number"
} }
}, },
"required": [ "required": [
"assets", "assets",
"title" "title",
"yearsAgo"
], ],
"type": "object" "type": "object"
}, },

View file

@ -273,6 +273,7 @@ export type MapMarkerResponseDto = {
export type MemoryLaneResponseDto = { export type MemoryLaneResponseDto = {
assets: AssetResponseDto[]; assets: AssetResponseDto[];
title: string; title: string;
yearsAgo: number;
}; };
export type UpdateStackParentDto = { export type UpdateStackParentDto = {
newParentId: string; newParentId: string;

View file

@ -131,7 +131,9 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
} }
export class MemoryLaneResponseDto { export class MemoryLaneResponseDto {
@ApiProperty({ deprecated: true })
title!: string; title!: string;
yearsAgo!: number;
assets!: AssetResponseDto[]; assets!: AssetResponseDto[];
} }

View file

@ -93,7 +93,7 @@ export class AssetRepository implements IAssetRepository {
}, },
) )
.leftJoinAndSelect('entity.exifInfo', 'exifInfo') .leftJoinAndSelect('entity.exifInfo', 'exifInfo')
.orderBy('entity.localDateTime', 'DESC') .orderBy('entity.localDateTime', 'ASC')
.getMany(); .getMany();
} }

View file

@ -307,13 +307,17 @@ describe(AssetService.name, () => {
jest.useRealTimers(); 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([]); 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([ await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([
{ title: '1 year since...', assets: [mapAsset(assetStub.image)] }, { yearsAgo: 1, title: '1 year since...', assets: [mapAsset(image1), mapAsset(image2)] },
{ title: '9 years since...', assets: [mapAsset(assetStub.imageFrom2015)] }, { yearsAgo: 9, title: '9 years since...', assets: [mapAsset(image3)] },
]); ]);
expect(assetMock.getByDayOfYear.mock.calls).toEqual([[[authStub.admin.user.id], { day: 15, month: 1 }]]); 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 () => { it('should get memories with partners with inTimeline enabled', async () => {
partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]); partnerMock.getAll.mockResolvedValue([partnerStub.user1ToAdmin1]);
assetMock.getByDayOfYear.mockResolvedValue([]);
await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 }); await sut.getMemoryLane(authStub.admin, { day: 15, month: 1 });

View file

@ -174,20 +174,25 @@ export class AssetService {
userIds.push(...partnersIds); userIds.push(...partnersIds);
const assets = await this.assetRepository.getByDayOfYear(userIds, dto); const assets = await this.assetRepository.getByDayOfYear(userIds, dto);
const groups: Record<number, AssetEntity[]> = {};
for (const asset of assets) {
const yearsAgo = currentYear - asset.localDateTime.getFullYear();
if (!groups[yearsAgo]) {
groups[yearsAgo] = [];
}
groups[yearsAgo].push(asset);
}
return _.chain(assets) return Object.keys(groups)
.filter((asset) => asset.localDateTime.getFullYear() < currentYear) .map(Number)
.map((asset) => { .sort()
const years = currentYear - asset.localDateTime.getFullYear(); .filter((yearsAgo) => yearsAgo > 0)
.map((yearsAgo) => ({
return { yearsAgo,
title: `${years} year${years > 1 ? 's' : ''} since...`, // TODO move this to clients
asset: mapAsset(asset, { auth }), title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`,
}; assets: groups[yearsAgo].map((asset) => mapAsset(asset, { auth })),
}) }));
.groupBy((asset) => asset.title)
.map((items, title) => ({ title, assets: items.map(({ asset }) => asset) }))
.value();
} }
private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) {

View file

@ -8,7 +8,7 @@
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import type { Viewport } from '$lib/stores/assets.store'; import type { Viewport } from '$lib/stores/assets.store';
import { memoryStore } from '$lib/stores/memory.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 { shortcuts } from '$lib/utils/shortcut';
import { fromLocalDateTime } from '$lib/utils/timeline-util'; import { fromLocalDateTime } from '$lib/utils/timeline-util';
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk'; import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
@ -102,7 +102,7 @@
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark> <ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
<p class="text-lg"> <p class="text-lg">
{currentMemory.title} {memoryLaneTitle(currentMemory.yearsAgo)}
</p> </p>
</svelte:fragment> </svelte:fragment>
@ -181,7 +181,7 @@
{#if previousMemory} {#if previousMemory}
<div class="absolute bottom-4 right-4 text-left text-white"> <div class="absolute bottom-4 right-4 text-left text-white">
<p class="text-xs font-semibold text-gray-200">PREVIOUS</p> <p class="text-xs font-semibold text-gray-200">PREVIOUS</p>
<p class="text-xl">{previousMemory.title}</p> <p class="text-xl">{memoryLaneTitle(previousMemory.yearsAgo)}</p>
</div> </div>
{/if} {/if}
</button> </button>
@ -254,7 +254,7 @@
{#if nextMemory} {#if nextMemory}
<div class="absolute bottom-4 left-4 text-left text-white"> <div class="absolute bottom-4 left-4 text-left text-white">
<p class="text-xs font-semibold text-gray-200">UP NEXT</p> <p class="text-xs font-semibold text-gray-200">UP NEXT</p>
<p class="text-xl">{nextMemory.title}</p> <p class="text-xl">{memoryLaneTitle(nextMemory.yearsAgo)}</p>
</div> </div>
{/if} {/if}
</button> </button>

View file

@ -3,7 +3,7 @@
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { memoryStore } from '$lib/stores/memory.store'; 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 { getAltText } from '$lib/utils/thumbnail-util';
import { ThumbnailFormat, getMemoryLane } from '@immich/sdk'; import { ThumbnailFormat, getMemoryLane } from '@immich/sdk';
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js'; import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
@ -66,7 +66,7 @@
</div> </div>
{/if} {/if}
<div class="inline-block" bind:offsetWidth={innerWidth}> <div class="inline-block" bind:offsetWidth={innerWidth}>
{#each $memoryStore as memory, index (memory.title)} {#each $memoryStore as memory, index (memory.yearsAgo)}
<button <button
class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl" class="memory-card relative mr-8 inline-block aspect-video h-[215px] rounded-xl"
on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)} on:click={() => goto(`${AppRoute.MEMORY}?${QueryParameter.MEMORY_INDEX}=${index}`)}
@ -77,7 +77,9 @@
alt={`Memory Lane ${getAltText(memory.assets[0])}`} alt={`Memory Lane ${getAltText(memory.assets[0])}`}
draggable="false" draggable="false"
/> />
<p class="absolute bottom-2 left-4 z-10 text-lg text-white">{memory.title}</p> <p class="absolute bottom-2 left-4 z-10 text-lg text-white">
{memoryLaneTitle(memory.yearsAgo)}
</p>
<div <div
class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20" class="absolute left-0 top-0 z-0 h-full w-full rounded-xl bg-gradient-to-t from-black/40 via-transparent to-transparent transition-all hover:bg-black/20"
/> />

View file

@ -277,3 +277,5 @@ export const asyncTimeout = (ms: number) => {
export const handlePromiseError = <T>(promise: Promise<T>): void => { export const handlePromiseError = <T>(promise: Promise<T>): void => {
promise.catch((error) => console.error(`[utils.ts]:handlePromiseError ${error}`, error)); promise.catch((error) => console.error(`[utils.ts]:handlePromiseError ${error}`, error));
}; };
export const memoryLaneTitle = (yearsAgo: number) => `${yearsAgo} ${yearsAgo ? 'years' : 'year'} since...`;