From b3ee394fdc05de15285996e14c9c595e5673ee14 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 3 Jun 2024 16:00:20 -0500 Subject: [PATCH] feat(web): email notification preference settings (#9934) * feat(web): email notification preference settings * Update * remove failed api generation file * fix handle album invite return value * Update web/src/lib/components/user-settings-page/notifications-settings.svelte Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> * wording * test --------- Co-authored-by: Daniel Dietzler Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- e2e/src/api/specs/user-admin.e2e-spec.ts | 12 ++- mobile/openapi/README.md | Bin 28679 -> 28811 bytes mobile/openapi/lib/api.dart | Bin 10078 -> 10172 bytes mobile/openapi/lib/api_client.dart | Bin 26798 -> 27002 bytes .../model/email_notifications_response.dart | Bin 0 -> 3535 bytes .../lib/model/email_notifications_update.dart | Bin 0 -> 4811 bytes .../model/user_preferences_response_dto.dart | Bin 3234 -> 3634 bytes .../model/user_preferences_update_dto.dart | Bin 4054 -> 4904 bytes open-api/immich-openapi-specs.json | 40 ++++++++++ open-api/typescript-sdk/src/fetch-client.ts | 12 +++ server/src/dtos/user-preferences.dto.ts | 23 ++++++ server/src/entities/user-metadata.entity.ts | 10 +++ server/src/services/notification.service.ts | 13 +++ .../notifications-settings.svelte | 75 ++++++++++++++++++ .../user-settings-list.svelte | 5 ++ 15 files changed, 188 insertions(+), 2 deletions(-) create mode 100644 mobile/openapi/lib/model/email_notifications_response.dart create mode 100644 mobile/openapi/lib/model/email_notifications_update.dart create mode 100644 web/src/lib/components/user-settings-page/notifications-settings.svelte diff --git a/e2e/src/api/specs/user-admin.e2e-spec.ts b/e2e/src/api/specs/user-admin.e2e-spec.ts index a041d9841..4acc0664f 100644 --- a/e2e/src/api/specs/user-admin.e2e-spec.ts +++ b/e2e/src/api/specs/user-admin.e2e-spec.ts @@ -250,10 +250,18 @@ describe('/admin/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); - expect(body).toEqual({ avatar: { color: 'orange' }, memories: { enabled: false } }); + expect(body).toEqual({ + avatar: { color: 'orange' }, + memories: { enabled: false }, + emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true }, + }); const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); - expect(after).toEqual({ avatar: { color: 'orange' }, memories: { enabled: false } }); + expect(after).toEqual({ + avatar: { color: 'orange' }, + memories: { enabled: false }, + emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true }, + }); }); }); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 5e543c026bf8de95e05963a68402d56a3e3066cd..93ea6a551c1a553eb8dfc8890ec8b35681c11811 100644 GIT binary patch delta 142 zcmZpFz}Wqeal`fC`rO3K9KZaM%(Tqp#FEVXyyBqL;sPL*8mp0#pRBL%ic^7JZi*(C bg04a|rjpQtl*AI;CP1XY`Zr$+HkAPY){Zva delta 14 VcmeBv$k_gXal`fC&CwwSG5|Gg23`OF diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 4c4c75f1876d999f3cf58d1d5635725b5565b2ff..17602fafafc7b0403281cf226cf2de6f607ff7fc 100644 GIT binary patch delta 59 ycmccTx5s~jy?{_|VrEW!UVcetT4r)$NoIat@#G7_vcjoY#B+G1H`@s)hywu6_!g@G delta 12 Tcmdnvf6s4&y};(V0`lSjCABM%j28eBvcgMT?zB}Gv&>IZk!)Yz$Z98@L>wU7UYA zLNStjUod6T-;>|o_Gl^YDyeujlPa5ulAl0RHJTS0FZqI3CT`baQA=fZYOr$2RywO2 zlPUghBNVDvY=NIOQ}}JT(r8@i-Rillv|*LWM1dEInc&K*>z$XCqT)I$Wp#&QX3nbY z?XOurXG-^ac%1<`16lKm)gru1^=Ui*1R>WK+3&2u z-TnQ%ngq_IuYyJFQt3SLCvFk9iAi61f2Muw$0e)Zxocx6R#>(oAEGB8wXDz&N8p*L zSP9>83sQI@{jff@SpX|BQT@Q7{uDJH(|(t@^YW3n+k7PM-QS3Ih`U;&8n&L0-kK`p zCux~7=gXg?#kweEK>myYsy4LOkY+-6e zlALMt1l`DWnIO0Oc^RS0?k}Lg2`pIsl~OmJ-K=W7Z8R}pa$s#gm3jV~>yg zC31<+nHAlfxBPJ2Ne?hEp>nVhVlX~+l)eq~j#8Vx&^Wz`6k5WH!tpcEbct^P*n+`p z8A$;~ZTe22{bjA#n)eum-R(dQiK07^YMpkeLaDF;9x&MU#1Itp&ywqW~uN1MD z`HAfWy5TCr`3A>08rR%owS_P4s#S|{LMy>_8y6wvV*5WxTa$^^2ylA_aZKLCDgMJ( z^ek9?$6a#c0PLvR6(1%5m(*%)zNDK{mcq<&izQq z%ivukwjCgA-kTv^k+EsLi)UfioVweo#a`jVT9Wh*pE{}PuX3jgFbdxz!QsbP$kOU1<=#~RM(i*R&!eQ|7^Z1HJ pecx5=%iWFjf0J%E7Y!e5Vz!1L_o}%q|4{j@_Vuyz{Amwc*MDLIjKcr` literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/email_notifications_update.dart b/mobile/openapi/lib/model/email_notifications_update.dart new file mode 100644 index 0000000000000000000000000000000000000000..dcd1ec432206dc90cc7b42bfd759e7f71142dd49 GIT binary patch literal 4811 zcmeHLQEwAD5Pr|Em?A_|-I8+mgbE2DC?_~*PEbxdMOEbNI*nno&e`5ZQR#obZ)|Vk zwAr?I4?OYEH0zxi&&>DDc$~w-&fy`w`f@sa{QL3A@uyd($8YHEyAQ`1y&2NU@RU9d z-@JSK_a4Mp@?{~6>;KmK^=XG6)p@0j98a|wPgN<8sIF=&i?J-_Tvje?p4EJzjoYaq z%B5J@@uGHPBmb$DfqEt8_*@8s|4teUE_+aHlj^cnA~#dEC(21Ze^*^7C$np-!G&ZMeosnS z@XFNzeD|(}Px`Zz!b+;N14^f`ObdfZja!kO>AEavCN3mRq^uxRJ+A7qWNUl$Syj1& zGm`8==Iaa)10IbRT3R-Z=<>2IB+^jGsi;dw7ow~sb!)4tb8qB9d~@_*?ZH7tGrg1- z(qvxbTvpN`yG1l_b#356u3>pzTSrq-mNXIhH*%W&X4}YobeY_0;q%MDyaraFh9OZD zL2QQu@Vg~IzX9~|8Y$!KF>o-9&xzLM3rYa`nQ~tXKPKgY{XbLwV^SVli%<7UjY>5r zidX~OF;TACP`}?DPQJ<~aZZ&71Gv!#6@%we>-I^axu}5KKEE zHcIPmhg@vWja+=(vKK>QTW!&)%F(%WXfby7n!8Vb;2m0@e0<s2@hG5t}5+c}23sp&7gY=xy4Z z?5MHBl?ddmt^}EDu7t&VxRPkMy3%l?&5ihe<3e1xefw1F`qswJL_TM1 zQ56(MNK4AuFjQEY8a;<$KITV$&cJvzg}^?~VIwf>EDJT~yU|kFczDOL^#}uqoMI64 zGo-0=v@kp7<~GJi=~O<(^f}W77jsx>?Nl$kFP^lK#KFggcg)Q&38CG=v1XkxMJ3$! zBE4K#cZ22KFwk*3jCpOV&aS5*3nnB#qPbZ7#k1IOdW!MrIS(&`F4t5>+vRW&Cd##e zK(6~3h%`(LM0(tz+>mIkh*)|1EB_h8ZP?{$6yf-wkg2vj45-BGx8TSYE+OPgj-Sqr zUhR}=dq)aqJ%A3@00T8vE z=J2{r>Wg2ep1plQ85f5vioD~4feczg4i8cBXOgk_)V_(r1ScL zv+TqzfI~Iu+YA)tqNB}zHr0proDblh5=?- zd)WxPL<~|gRZ9VB7cQOp|ZG+#2P>&&8r!pa0wl42Y zp=WFM1@dfl32d2=<`6$M$TdHj-**U9v>3Sr;OFc7MDj^&u`QR2p!c7LCw$G0UEUwh WXjp_>9d%tXgo9Fx3xKkz3RrY}V%(vD zDXd^?i%szyrfzJuiZYj}VVYs9f+;q+lX8Xh s;nB$#StYSJaPtS&`E0satkvOC00Q;g)ZF}{%+z9a5Wm)%tCouk07>G9Pyhe` delta 57 zcmV-90LK5a9HJSp$^nz`0WFhH0<)7i1AUX(17wqB1d+2d1>ym-bO(_J6K!Q}Z*pm6 Pb0;hcARv7^3VjL+hKdvK diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 887293931c282acb441fe7979fba57b6b0f4b731..21da7c7bac640719d56794857bc2866dc639b542 100644 GIT binary patch delta 393 zcmca6zd~)pCPtyu+{DZrzx4-GBqOsJi`?XIY;xFwNs8T<35#2-6kIWaZn7<>I5s1qIBVDd0NMYjCe|OYvH`Qt0p620tkAOqzL1)E($LKv$+g!1~zIAwg3PC diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 5ffd86a07..eeef262ea 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8152,6 +8152,39 @@ ], "type": "object" }, + "EmailNotificationsResponse": { + "properties": { + "albumInvite": { + "type": "boolean" + }, + "albumUpdate": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + } + }, + "required": [ + "albumInvite", + "albumUpdate", + "enabled" + ], + "type": "object" + }, + "EmailNotificationsUpdate": { + "properties": { + "albumInvite": { + "type": "boolean" + }, + "albumUpdate": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, "EntityType": { "enum": [ "ASSET", @@ -11205,12 +11238,16 @@ "avatar": { "$ref": "#/components/schemas/AvatarResponse" }, + "emailNotifications": { + "$ref": "#/components/schemas/EmailNotificationsResponse" + }, "memories": { "$ref": "#/components/schemas/MemoryResponse" } }, "required": [ "avatar", + "emailNotifications", "memories" ], "type": "object" @@ -11220,6 +11257,9 @@ "avatar": { "$ref": "#/components/schemas/AvatarUpdate" }, + "emailNotifications": { + "$ref": "#/components/schemas/EmailNotificationsUpdate" + }, "memories": { "$ref": "#/components/schemas/MemoryUpdate" } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index b533a8f3a..c835ff190 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -78,21 +78,33 @@ export type UserAdminUpdateDto = { export type AvatarResponse = { color: UserAvatarColor; }; +export type EmailNotificationsResponse = { + albumInvite: boolean; + albumUpdate: boolean; + enabled: boolean; +}; export type MemoryResponse = { enabled: boolean; }; export type UserPreferencesResponseDto = { avatar: AvatarResponse; + emailNotifications: EmailNotificationsResponse; memories: MemoryResponse; }; export type AvatarUpdate = { color?: UserAvatarColor; }; +export type EmailNotificationsUpdate = { + albumInvite?: boolean; + albumUpdate?: boolean; + enabled?: boolean; +}; export type MemoryUpdate = { enabled?: boolean; }; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; + emailNotifications?: EmailNotificationsUpdate; memories?: MemoryUpdate; }; export type AlbumUserResponseDto = { diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 2dd9492d0..64120be22 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -16,6 +16,17 @@ class MemoryUpdate { enabled?: boolean; } +class EmailNotificationsUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; + + @ValidateBoolean({ optional: true }) + albumInvite?: boolean; + + @ValidateBoolean({ optional: true }) + albumUpdate?: boolean; +} + export class UserPreferencesUpdateDto { @Optional() @ValidateNested() @@ -26,6 +37,11 @@ export class UserPreferencesUpdateDto { @ValidateNested() @Type(() => MemoryUpdate) memories?: MemoryUpdate; + + @Optional() + @ValidateNested() + @Type(() => EmailNotificationsUpdate) + emailNotifications?: EmailNotificationsUpdate; } class AvatarResponse { @@ -37,9 +53,16 @@ class MemoryResponse { enabled!: boolean; } +class EmailNotificationsResponse { + enabled!: boolean; + albumInvite!: boolean; + albumUpdate!: boolean; +} + export class UserPreferencesResponseDto implements UserPreferences { memories!: MemoryResponse; avatar!: AvatarResponse; + emailNotifications!: EmailNotificationsResponse; } export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 26715e05e..b10945531 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -36,6 +36,11 @@ export interface UserPreferences { avatar: { color: UserAvatarColor; }; + emailNotifications: { + enabled: boolean; + albumInvite: boolean; + albumUpdate: boolean; + }; } export const getDefaultPreferences = (user: { email: string }): UserPreferences => { @@ -51,6 +56,11 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences avatar: { color: values[randomIndex], }, + emailNotifications: { + enabled: true, + albumInvite: true, + albumUpdate: true, + }, }; }; diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index ddd8d61e3..8efc6a6c3 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -19,6 +19,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { getPreferences } from 'src/utils/preferences'; @Injectable() export class NotificationService { @@ -95,6 +96,12 @@ export class NotificationService { return JobStatus.SKIPPED; } + const { emailNotifications } = getPreferences(recipient); + + if (!emailNotifications.enabled || !emailNotifications.albumInvite) { + return JobStatus.SKIPPED; + } + const attachment = await this.getAlbumThumbnailAttachment(album); const { server } = await this.configCore.getConfig(); @@ -142,6 +149,12 @@ export class NotificationService { const { server } = await this.configCore.getConfig(); for (const recipient of recipients) { + const { emailNotifications } = getPreferences(recipient); + + if (!emailNotifications.enabled || !emailNotifications.albumUpdate) { + continue; + } + const { html, text } = this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { diff --git a/web/src/lib/components/user-settings-page/notifications-settings.svelte b/web/src/lib/components/user-settings-page/notifications-settings.svelte new file mode 100644 index 000000000..3005a08a8 --- /dev/null +++ b/web/src/lib/components/user-settings-page/notifications-settings.svelte @@ -0,0 +1,75 @@ + + +
+
+
+
+
+ +
+
+ +
+
+ +
+ +
+ +
+
+
+
+
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index f88ee5887..95a792eb6 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -15,6 +15,7 @@ import PartnerSettings from './partner-settings.svelte'; import UserAPIKeyList from './user-api-key-list.svelte'; import UserProfileSettings from './user-profile-settings.svelte'; + import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte'; export let keys: ApiKeyResponseDto[] = []; export let sessions: SessionResponseDto[] = []; @@ -45,6 +46,10 @@ + + + + {#if $featureFlags.loaded && $featureFlags.oauth}