1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

feat(web): show original uploader in shared album photo details (#3977)

* feat(web): show original uploader in shared album photo details

* feat: send owner in asset by id response

* chore: open api

* fix: linting

* fix: change to Shared By

* openapi

* openapi

* api

* styling

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Maarten Rijke 2023-09-06 05:14:44 +02:00 committed by GitHub
parent b1467bd1da
commit b4fa60d4fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 87 additions and 43 deletions

View file

@ -645,6 +645,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto * @memberof AssetResponseDto
*/ */
'originalPath': string; 'originalPath': string;
/**
*
* @type {UserResponseDto}
* @memberof AssetResponseDto
*/
'owner'?: UserResponseDto;
/** /**
* *
* @type {string} * @type {string}

View file

@ -21,6 +21,7 @@ Name | Type | Description | Notes
**livePhotoVideoId** | **String** | | [optional] **livePhotoVideoId** | **String** | | [optional]
**originalFileName** | **String** | | **originalFileName** | **String** | |
**originalPath** | **String** | | **originalPath** | **String** | |
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]
**ownerId** | **String** | | **ownerId** | **String** | |
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []] **people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
**resized** | **bool** | | **resized** | **bool** | |

View file

@ -26,6 +26,7 @@ class AssetResponseDto {
this.livePhotoVideoId, this.livePhotoVideoId,
required this.originalFileName, required this.originalFileName,
required this.originalPath, required this.originalPath,
this.owner,
required this.ownerId, required this.ownerId,
this.people = const [], this.people = const [],
required this.resized, required this.resized,
@ -69,6 +70,14 @@ class AssetResponseDto {
String originalPath; String originalPath;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
UserResponseDto? owner;
String ownerId; String ownerId;
List<PersonResponseDto> people; List<PersonResponseDto> people;
@ -107,6 +116,7 @@ class AssetResponseDto {
other.livePhotoVideoId == livePhotoVideoId && other.livePhotoVideoId == livePhotoVideoId &&
other.originalFileName == originalFileName && other.originalFileName == originalFileName &&
other.originalPath == originalPath && other.originalPath == originalPath &&
other.owner == owner &&
other.ownerId == ownerId && other.ownerId == ownerId &&
other.people == people && other.people == people &&
other.resized == resized && other.resized == resized &&
@ -132,6 +142,7 @@ class AssetResponseDto {
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(originalFileName.hashCode) + (originalFileName.hashCode) +
(originalPath.hashCode) + (originalPath.hashCode) +
(owner == null ? 0 : owner!.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(people.hashCode) + (people.hashCode) +
(resized.hashCode) + (resized.hashCode) +
@ -142,7 +153,7 @@ class AssetResponseDto {
(updatedAt.hashCode); (updatedAt.hashCode);
@override @override
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, livePhotoVideoId=$livePhotoVideoId, originalFileName=$originalFileName, originalPath=$originalPath, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]'; String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, livePhotoVideoId=$livePhotoVideoId, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -167,6 +178,11 @@ class AssetResponseDto {
} }
json[r'originalFileName'] = this.originalFileName; json[r'originalFileName'] = this.originalFileName;
json[r'originalPath'] = this.originalPath; json[r'originalPath'] = this.originalPath;
if (this.owner != null) {
json[r'owner'] = this.owner;
} else {
// json[r'owner'] = null;
}
json[r'ownerId'] = this.ownerId; json[r'ownerId'] = this.ownerId;
json[r'people'] = this.people; json[r'people'] = this.people;
json[r'resized'] = this.resized; json[r'resized'] = this.resized;
@ -207,6 +223,7 @@ class AssetResponseDto {
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'), livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
originalFileName: mapValueOfType<String>(json, r'originalFileName')!, originalFileName: mapValueOfType<String>(json, r'originalFileName')!,
originalPath: mapValueOfType<String>(json, r'originalPath')!, originalPath: mapValueOfType<String>(json, r'originalPath')!,
owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
people: PersonResponseDto.listFromJson(json[r'people']), people: PersonResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized')!, resized: mapValueOfType<bool>(json, r'resized')!,

View file

@ -82,6 +82,11 @@ void main() {
// TODO // TODO
}); });
// UserResponseDto owner
test('to test the property `owner`', () async {
// TODO
});
// String ownerId // String ownerId
test('to test the property `ownerId`', () async { test('to test the property `ownerId`', () async {
// TODO // TODO

View file

@ -5208,6 +5208,9 @@
"originalPath": { "originalPath": {
"type": "string" "type": "string"
}, },
"owner": {
"$ref": "#/components/schemas/UserResponseDto"
},
"ownerId": { "ownerId": {
"type": "string" "type": "string"
}, },
@ -5246,8 +5249,8 @@
"type", "type",
"id", "id",
"deviceAssetId", "deviceAssetId",
"ownerId",
"deviceId", "deviceId",
"ownerId",
"originalPath", "originalPath",
"originalFileName", "originalFileName",
"resized", "resized",

View file

@ -2,14 +2,16 @@ import { AssetEntity, AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { PersonResponseDto, mapFace } from '../../person/person.dto'; import { PersonResponseDto, mapFace } from '../../person/person.dto';
import { TagResponseDto, mapTag } from '../../tag'; import { TagResponseDto, mapTag } from '../../tag';
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
import { ExifResponseDto, mapExif } from './exif-response.dto'; import { ExifResponseDto, mapExif } from './exif-response.dto';
import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto'; import { SmartInfoResponseDto, mapSmartInfo } from './smart-info-response.dto';
export class AssetResponseDto { export class AssetResponseDto {
id!: string; id!: string;
deviceAssetId!: string; deviceAssetId!: string;
ownerId!: string;
deviceId!: string; deviceId!: string;
ownerId!: string;
owner?: UserResponseDto;
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type!: AssetType; type!: AssetType;
@ -33,11 +35,12 @@ export class AssetResponseDto {
checksum!: string; checksum!: string;
} }
export function mapAsset(entity: AssetEntity): AssetResponseDto { function _map(entity: AssetEntity, withExif: boolean): AssetResponseDto {
return { return {
id: entity.id, id: entity.id,
deviceAssetId: entity.deviceAssetId, deviceAssetId: entity.deviceAssetId,
ownerId: entity.ownerId, ownerId: entity.ownerId,
owner: entity.owner ? mapUser(entity.owner) : undefined,
deviceId: entity.deviceId, deviceId: entity.deviceId,
type: entity.type, type: entity.type,
originalPath: entity.originalPath, originalPath: entity.originalPath,
@ -50,7 +53,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
isFavorite: entity.isFavorite, isFavorite: entity.isFavorite,
isArchived: entity.isArchived, isArchived: entity.isArchived,
duration: entity.duration ?? '0:00:00.00000', duration: entity.duration ?? '0:00:00.00000',
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, exifInfo: withExif ? (entity.exifInfo ? mapExif(entity.exifInfo) : undefined) : undefined,
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId, livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map(mapTag), tags: entity.tags?.map(mapTag),
@ -59,30 +62,12 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
}; };
} }
export function mapAsset(entity: AssetEntity): AssetResponseDto {
return _map(entity, true);
}
export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto { export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
return { return _map(entity, false);
id: entity.id,
deviceAssetId: entity.deviceAssetId,
ownerId: entity.ownerId,
deviceId: entity.deviceId,
type: entity.type,
originalPath: entity.originalPath,
originalFileName: entity.originalFileName,
resized: !!entity.resizePath,
thumbhash: entity.thumbhash?.toString('base64') || null,
fileCreatedAt: entity.fileCreatedAt,
fileModifiedAt: entity.fileModifiedAt,
updatedAt: entity.updatedAt,
isFavorite: entity.isFavorite,
isArchived: entity.isArchived,
duration: entity.duration ?? '0:00:00.00000',
exifInfo: undefined,
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map(mapTag),
people: entity.faces?.map(mapFace),
checksum: entity.checksum.toString('base64'),
};
} }
export class MemoryLaneResponseDto { export class MemoryLaneResponseDto {

View file

@ -107,6 +107,7 @@ export class AssetRepository implements IAssetRepository {
tags: true, tags: true,
sharedLinks: true, sharedLinks: true,
smartInfo: true, smartInfo: true,
owner: true,
faces: { faces: {
person: true, person: true,
}, },

View file

@ -199,6 +199,10 @@ export class AssetService {
data.people = []; data.people = [];
} }
if (authUser.isPublicUser) {
delete data.owner;
}
return data; return data;
} }

View file

@ -1,5 +1,5 @@
import { AlbumResponseDto, AssetResponseDto, ExifResponseDto, mapUser, SharedLinkResponseDto } from '@app/domain'; import { AlbumResponseDto, AssetResponseDto, ExifResponseDto, mapUser, SharedLinkResponseDto } from '@app/domain';
import { AssetType, SharedLinkEntity, SharedLinkType } from '@app/infra/entities'; import { AssetType, SharedLinkEntity, SharedLinkType, UserEntity } from '@app/infra/entities';
import { assetStub } from './asset.stub'; import { assetStub } from './asset.stub';
import { authStub } from './auth.stub'; import { authStub } from './auth.stub';
import { userStub } from './user.stub'; import { userStub } from './user.stub';
@ -158,7 +158,7 @@ export const sharedLinkStub = {
assets: [ assets: [
{ {
id: 'id_1', id: 'id_1',
owner: userStub.user1, owner: undefined as unknown as UserEntity,
ownerId: 'user_id_1', ownerId: 'user_id_1',
deviceAssetId: 'device_asset_id_1', deviceAssetId: 'device_asset_id_1',
deviceId: 'device_id_1', deviceId: 'device_id_1',

View file

@ -645,6 +645,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto * @memberof AssetResponseDto
*/ */
'originalPath': string; 'originalPath': string;
/**
*
* @type {UserResponseDto}
* @memberof AssetResponseDto
*/
'owner'?: UserResponseDto;
/** /**
* *
* @type {string} * @type {string}

View file

@ -13,6 +13,7 @@
import { asByteUnitString } from '../../utils/byte-units'; import { asByteUnitString } from '../../utils/byte-units';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import { getAssetFilename } from '$lib/utils/asset-utils'; import { getAssetFilename } from '$lib/utils/asset-utils';
import UserAvatar from '../shared-components/user-avatar.svelte';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = []; export let albums: AlbumResponseDto[] = [];
@ -20,6 +21,8 @@
let textarea: HTMLTextAreaElement; let textarea: HTMLTextAreaElement;
let description: string; let description: string;
$: isOwner = $page?.data?.user?.id === asset.ownerId;
$: { $: {
// Get latest description from server // Get latest description from server
if (asset.id && !api.isSharedLink) { if (asset.id && !api.isSharedLink) {
@ -93,20 +96,17 @@
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p> <p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p>
</div> </div>
<section <section class="mx-4 mt-10" style:display={!isOwner && textarea?.value == '' ? 'none' : 'block'}>
class="mx-4 mt-10"
style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == '' ? 'none' : 'block'}
>
<textarea <textarea
bind:this={textarea} bind:this={textarea}
class="max-h-[500px] class="max-h-[500px]
w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary" w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary"
placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'} placeholder={!isOwner ? '' : 'Add a description'}
on:focusin={handleFocusIn} on:focusin={handleFocusIn}
on:focusout={handleFocusOut} on:focusout={handleFocusOut}
on:input={autoGrowHeight} on:input={autoGrowHeight}
bind:value={description} bind:value={description}
disabled={$page?.data?.user?.id !== asset.ownerId} disabled={!isOwner}
/> />
</section> </section>
@ -291,11 +291,27 @@
</div> </div>
{/if} {/if}
<section class="p-2 dark:text-immich-dark-fg"> {#if asset.owner && !isOwner}
<div class="px-4 py-4"> <section class="px-6 pt-6 dark:text-immich-dark-fg">
{#if albums.length > 0} <p class="text-sm">SHARED BY</p>
<p class="pb-4 text-sm">APPEARS IN</p> <div class="flex gap-4 pt-4">
{/if} <div>
<UserAvatar user={asset.owner} size="md" autoColor />
</div>
<div class="mb-auto mt-auto">
<p>
{asset.owner.firstName}
{asset.owner.lastName}
</p>
</div>
</div>
</section>
{/if}
{#if albums.length > 0}
<section class="p-6 dark:text-immich-dark-fg">
<p class="pb-4 text-sm">APPEARS IN</p>
{#each albums as album} {#each albums as album}
<a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}> <a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}>
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
@ -326,5 +342,5 @@
</div> </div>
</a> </a>
{/each} {/each}
</div> </section>
</section> {/if}