Merge f17d8c4836 into 5ade152bc5
commit
3fa65000b2
|
|
@ -962,6 +962,7 @@
|
|||
"cannot_navigate_previous_asset": "Cannot navigate to previous asset",
|
||||
"cant_apply_changes": "Can't apply changes",
|
||||
"cant_change_activity": "Can't {enabled, select, true {disable} other {enable}} activity",
|
||||
"cant_change_timeline_visibility": "Unable to change timeline visibility",
|
||||
"cant_change_asset_favorite": "Can't change favorite for asset",
|
||||
"cant_change_metadata_assets_count": "Can't change metadata of {count, plural, one {# asset} other {# assets}}",
|
||||
"cant_get_faces": "Can't get faces",
|
||||
|
|
@ -1177,6 +1178,8 @@
|
|||
"hide_person": "Hide person",
|
||||
"hide_text_recognition": "Hide text recognition",
|
||||
"hide_unnamed_people": "Hide unnamed people",
|
||||
"hide_from_timeline": "Hide from timeline",
|
||||
"hide_from_timeline_description": "Photos in this album will not appear in your main timeline",
|
||||
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
|
||||
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
|
||||
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
|
||||
|
|
@ -2081,6 +2084,7 @@
|
|||
"time_based_memories": "Time-based memories",
|
||||
"time_based_memories_duration": "Number of seconds to display each image.",
|
||||
"timeline": "Timeline",
|
||||
"timeline_visibility_changed": "Album is now {hidden, select, true {hidden from} other {visible in}} timeline",
|
||||
"timezone": "Timezone",
|
||||
"to_archive": "Archive",
|
||||
"to_change_password": "Change password",
|
||||
|
|
|
|||
|
|
@ -14671,6 +14671,9 @@
|
|||
"hasSharedLink": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideFromTimeline": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
|
|
@ -14715,6 +14718,7 @@
|
|||
"createdAt",
|
||||
"description",
|
||||
"hasSharedLink",
|
||||
"hideFromTimeline",
|
||||
"id",
|
||||
"isActivityEnabled",
|
||||
"owner",
|
||||
|
|
@ -16143,6 +16147,9 @@
|
|||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideFromTimeline": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
|
@ -22528,6 +22535,9 @@
|
|||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideFromTimeline": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isActivityEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -53,6 +53,9 @@ export class CreateAlbumDto {
|
|||
|
||||
@ValidateUUID({ optional: true, each: true })
|
||||
assetIds?: string[];
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
hideFromTimeline?: boolean;
|
||||
}
|
||||
|
||||
export class AlbumsAddAssetsDto {
|
||||
|
|
@ -86,6 +89,9 @@ export class UpdateAlbumDto {
|
|||
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
|
||||
order?: AssetOrder;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
hideFromTimeline?: boolean;
|
||||
}
|
||||
|
||||
export class GetAlbumsDto {
|
||||
|
|
@ -157,6 +163,7 @@ export class AlbumResponseDto {
|
|||
isActivityEnabled!: boolean;
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
|
||||
order?: AssetOrder;
|
||||
hideFromTimeline!: boolean;
|
||||
|
||||
// Optional per-user contribution counts for shared albums
|
||||
@Type(() => ContributorCountResponseDto)
|
||||
|
|
@ -178,6 +185,7 @@ export type MapAlbumDto = {
|
|||
owner: User;
|
||||
isActivityEnabled: boolean;
|
||||
order: AssetOrder;
|
||||
hideFromTimeline: boolean;
|
||||
};
|
||||
|
||||
export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDto): AlbumResponseDto => {
|
||||
|
|
@ -225,6 +233,7 @@ export const mapAlbum = (entity: MapAlbumDto, withAssets: boolean, auth?: AuthDt
|
|||
assetCount: entity.assets?.length || 0,
|
||||
isActivityEnabled: entity.isActivityEnabled,
|
||||
order: entity.order,
|
||||
hideFromTimeline: entity.hideFromTimeline,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { ExpressionBuilder, Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { isUndefined, omitBy } from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { columns, Exif } from 'src/database';
|
||||
import { Chunked, ChunkedArray, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
|
||||
|
|
@ -294,9 +295,10 @@ export class AlbumRepository {
|
|||
}
|
||||
|
||||
update(id: string, album: Updateable<AlbumTable>) {
|
||||
const value = omitBy(album, isUndefined);
|
||||
return this.db
|
||||
.updateTable('album')
|
||||
.set(album)
|
||||
.set(value)
|
||||
.where('id', '=', id)
|
||||
.returningAll('album')
|
||||
.returning(withOwner)
|
||||
|
|
|
|||
|
|
@ -328,6 +328,18 @@ export class AssetRepository {
|
|||
.where('asset_file.type', '=', AssetFileType.Preview),
|
||||
),
|
||||
)
|
||||
.where((eb) =>
|
||||
eb.not(
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_asset')
|
||||
.innerJoin('album', 'album.id', 'album_asset.albumId')
|
||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
||||
.where('album.hideFromTimeline', '=', true)
|
||||
.where('album.deletedAt', 'is', null),
|
||||
),
|
||||
),
|
||||
)
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.orderBy(sql`(asset."localDateTime" at time zone 'UTC')::date`, 'desc')
|
||||
.limit(20)
|
||||
|
|
@ -624,7 +636,21 @@ export class AssetRepository {
|
|||
.$if(options.isDuplicate !== undefined, (qb) =>
|
||||
qb.where('asset.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
|
||||
)
|
||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)),
|
||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
||||
.$if(!options.albumId, (qb) =>
|
||||
qb.where((eb) =>
|
||||
eb.not(
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_asset')
|
||||
.innerJoin('album', 'album.id', 'album_asset.albumId')
|
||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
||||
.where('album.hideFromTimeline', '=', true)
|
||||
.where('album.deletedAt', 'is', null),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.selectFrom('asset')
|
||||
.select(sql<string>`("timeBucket" AT TIME ZONE 'UTC')::date::text`.as('timeBucket'))
|
||||
|
|
@ -725,6 +751,20 @@ export class AssetRepository {
|
|||
)
|
||||
.$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
|
||||
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
|
||||
.$if(!options.albumId, (qb) =>
|
||||
qb.where((eb) =>
|
||||
eb.not(
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('album_asset')
|
||||
.innerJoin('album', 'album.id', 'album_asset.albumId')
|
||||
.whereRef('album_asset.assetId', '=', 'asset.id')
|
||||
.where('album.hideFromTimeline', '=', true)
|
||||
.where('album.deletedAt', 'is', null),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.orderBy('asset.fileCreatedAt', options.order ?? 'desc'),
|
||||
)
|
||||
.with('agg', (qb) =>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "album" ADD "hideFromTimeline" boolean NOT NULL DEFAULT false;`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`ALTER TABLE "album" DROP COLUMN "hideFromTimeline";`.execute(db);
|
||||
}
|
||||
|
|
@ -60,6 +60,9 @@ export class AlbumTable {
|
|||
@Column({ default: AssetOrder.Desc })
|
||||
order!: Generated<AssetOrder>;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
hideFromTimeline!: Generated<boolean>;
|
||||
|
||||
@UpdateIdColumn({ index: true })
|
||||
updateId!: Generated<string>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ export class AlbumService extends BaseService {
|
|||
description: dto.description,
|
||||
albumThumbnailAssetId: assetIds[0] || null,
|
||||
order: getPreferences(userMetadata).albums.defaultAssetOrder,
|
||||
hideFromTimeline: dto.hideFromTimeline,
|
||||
},
|
||||
assetIds,
|
||||
albumUsers,
|
||||
|
|
@ -153,6 +154,7 @@ export class AlbumService extends BaseService {
|
|||
albumThumbnailAssetId: dto.albumThumbnailAssetId,
|
||||
isActivityEnabled: dto.isActivityEnabled,
|
||||
order: dto.order,
|
||||
hideFromTimeline: dto.hideFromTimeline,
|
||||
});
|
||||
|
||||
return mapAlbumWithoutAssets({ ...updatedAlbum, assets: album.assets });
|
||||
|
|
|
|||
|
|
@ -44,6 +44,34 @@ describe(TimelineService.name, () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should exclude assets from albums with hideFromTimeline=true', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
// Create two assets
|
||||
const { asset: asset1 } = await ctx.newAsset({
|
||||
ownerId: user.id,
|
||||
localDateTime: new Date('1970-01-01'),
|
||||
});
|
||||
const { asset: asset2 } = await ctx.newAsset({
|
||||
ownerId: user.id,
|
||||
localDateTime: new Date('1970-01-01'),
|
||||
});
|
||||
|
||||
// Create an album with hideFromTimeline=true and add asset1
|
||||
const { album } = await ctx.newAlbum({
|
||||
ownerId: user.id,
|
||||
hideFromTimeline: true,
|
||||
});
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset1.id });
|
||||
|
||||
// Get time buckets - should only include asset2, not asset1
|
||||
const response = await sut.getTimeBuckets(auth, {});
|
||||
|
||||
expect(response).toEqual([{ count: 1, timeBucket: '1970-01-01' }]);
|
||||
});
|
||||
|
||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
||||
const { sut } = setup();
|
||||
const auth = factory.auth();
|
||||
|
|
|
|||
|
|
@ -73,6 +73,21 @@
|
|||
}
|
||||
};
|
||||
|
||||
const handleToggleHideFromTimeline = async () => {
|
||||
try {
|
||||
album = await updateAlbumInfo({
|
||||
id: album.id,
|
||||
updateAlbumDto: {
|
||||
hideFromTimeline: !album.hideFromTimeline,
|
||||
},
|
||||
});
|
||||
|
||||
toastManager.success($t('timeline_visibility_changed', { values: { hidden: album.hideFromTimeline } }));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.cant_change_timeline_visibility'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveUser = async (user: UserResponseDto): Promise<void> => {
|
||||
const confirmed = await modalManager.showDialog({
|
||||
title: $t('album_remove_user'),
|
||||
|
|
@ -127,6 +142,12 @@
|
|||
checked={album.isActivityEnabled}
|
||||
onToggle={handleToggleActivity}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('hide_from_timeline')}
|
||||
subtitle={$t('hide_from_timeline_description')}
|
||||
checked={album.hideFromTimeline}
|
||||
onToggle={handleToggleHideFromTimeline}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
|
|
|
|||
Loading…
Reference in New Issue