diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index e1f6b44e5b..50ee28f0af 100644 Binary files a/mobile/openapi/lib/model/person_response_dto.dart and b/mobile/openapi/lib/model/person_response_dto.dart differ diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index b15e620250..af2e7101c3 100644 Binary files a/mobile/openapi/lib/model/person_with_faces_response_dto.dart and b/mobile/openapi/lib/model/person_with_faces_response_dto.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index d403cf7530..0ac2cd53c9 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9345,6 +9345,11 @@ }, "thumbnailPath": { "type": "string" + }, + "updatedAt": { + "description": "This property was added in v1.107.0", + "format": "date-time", + "type": "string" } }, "required": [ @@ -9414,6 +9419,11 @@ }, "thumbnailPath": { "type": "string" + }, + "updatedAt": { + "description": "This property was added in v1.107.0", + "format": "date-time", + "type": "string" } }, "required": [ diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7a1da9d13d..ddf6c958b8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -158,6 +158,8 @@ export type PersonWithFacesResponseDto = { isHidden: boolean; name: string; thumbnailPath: string; + /** This property was added in v1.107.0 */ + updatedAt?: string; }; export type SmartInfoResponseDto = { objects?: string[] | null; @@ -432,6 +434,8 @@ export type PersonResponseDto = { isHidden: boolean; name: string; thumbnailPath: string; + /** This property was added in v1.107.0 */ + updatedAt?: string; }; export type AssetFaceResponseDto = { boundingBoxX1: number; diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index b28f18603a..3ad41ecff2 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator'; +import { PropertyLifecycle } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { PersonEntity } from 'src/entities/person.entity'; @@ -71,6 +72,8 @@ export class PersonResponseDto { birthDate!: Date | null; thumbnailPath!: string; isHidden!: boolean; + @PropertyLifecycle({ addedAt: 'v1.107.0' }) + updatedAt?: Date; } export class PersonWithFacesResponseDto extends PersonResponseDto { @@ -138,6 +141,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto { birthDate: person.birthDate, thumbnailPath: person.thumbnailPath, isHidden: person.isHidden, + updatedAt: person.updatedAt, }; } diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index eb0e3ad1e9..2aea5c2798 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -42,6 +42,7 @@ const responseDto: PersonResponseDto = { birthDate: null, thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, + updatedAt: expect.any(Date), }; const statistics = { assets: 3 }; @@ -126,6 +127,7 @@ describe(PersonService.name, () => { birthDate: null, thumbnailPath: '/path/to/thumbnail.jpg', isHidden: true, + updatedAt: expect.any(Date), }, ], }); @@ -255,6 +257,7 @@ describe(PersonService.name, () => { birthDate: new Date('1976-06-30'), thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, + updatedAt: expect.any(Date), }); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); expect(jobMock.queue).not.toHaveBeenCalled(); @@ -407,6 +410,7 @@ describe(PersonService.name, () => { id: personStub.noName.id, name: personStub.noName.name, thumbnailPath: personStub.noName.thumbnailPath, + updatedAt: expect.any(Date), }); expect(jobMock.queue).not.toHaveBeenCalledWith(); diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index ac3a4b4c9e..6af265e3da 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -213,7 +213,7 @@ - +
@@ -65,7 +65,7 @@ border={potentialMergePeople.length > 0} circle shadow - url={getPeopleThumbnailUrl(personMerge2.id)} + url={getPeopleThumbnailUrl(personMerge2)} altText={personMerge2.name} widthStyle="100%" /> @@ -84,7 +84,7 @@ border={true} circle shadow - url={getPeopleThumbnailUrl(person.id)} + url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" on:click={() => changePersonToMerge(person)} diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index b532899353..b5a00cf23d 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -50,7 +50,7 @@ togglePersonSelection(person.id)} > - +

{person.name}

{/each} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index c8e9ac49b5..9e6eb74894 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -621,6 +621,7 @@ "unable_to_save_settings": "Unable to save settings", "unable_to_scan_libraries": "Unable to scan libraries", "unable_to_scan_library": "Unable to scan library", + "unable_to_set_feature_photo": "Unable to set feature photo", "unable_to_set_profile_picture": "Unable to set profile picture", "unable_to_submit_job": "Unable to submit job", "unable_to_trash_asset": "Unable to trash asset", diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 7743d9b592..58bf49c43b 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -17,6 +17,7 @@ import { startOAuth, unlinkOAuthAccount, type AssetResponseDto, + type PersonResponseDto, type SharedLinkResponseDto, } from '@immich/sdk'; import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js'; @@ -205,7 +206,8 @@ export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: s export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileImagePath(userId)); -export const getPeopleThumbnailUrl = (personId: string) => createUrl(getPeopleThumbnailPath(personId)); +export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) => + createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt }); export const getAssetJobName = derived(t, ($t) => { return (job: AssetJobName) => { diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte index 3b25cdc0c1..d0d7a9936f 100644 --- a/web/src/routes/(user)/explore/+page.svelte +++ b/web/src/routes/(user)/explore/+page.svelte @@ -61,7 +61,7 @@ diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index 432410798f..41db2c05b6 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -508,7 +508,7 @@ preload={searchName !== '' || index < 20} bind:hidden={person.isHidden} shadow - url={getPeopleThumbnailUrl(person.id)} + url={getPeopleThumbnailUrl(person)} altText={person.name} widthStyle="100%" bind:eyeColor={eyeColorMap[person.id]} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index eecfbf29b2..f1fbe716a6 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -91,7 +91,7 @@ let refreshAssetGrid = false; let personName = ''; - $: thumbnailData = getPeopleThumbnailUrl(data.person.id); + $: thumbnailData = getPeopleThumbnailUrl(data.person); let name: string = data.person.name; let suggestedPeople: PersonResponseDto[] = []; @@ -121,7 +121,7 @@ return websocketEvents.on('on_person_thumbnail', (personId: string) => { if (data.person.id === personId) { - thumbnailData = getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`; + thumbnailData = getPeopleThumbnailUrl(data.person, Date.now().toString()); } }); }); @@ -206,10 +206,13 @@ if (viewMode !== ViewMode.SELECT_PERSON) { return; } + try { + await updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } }); + notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info }); + } catch (error) { + handleError(error, $t('errors.unable_to_set_feature_photo')); + } - await updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } }); - - notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info }); assetInteractionStore.clearMultiselect(); viewMode = ViewMode.VIEW_ASSETS; @@ -525,7 +528,7 @@