From 445f9174eaea85c205db5d13e81e43db2879c60a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 2 Jul 2025 14:18:37 -0500 Subject: [PATCH] feat: memories sync (#19644) * feat: memories sync * Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update mobile/lib/infrastructure/repositories/sync_stream.repository.dart Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * show sync information * tests and pr feedback * pr feedback --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../drift_schemas/main/drift_schema_v1.json | Bin 21916 -> 42185 bytes mobile/lib/domain/models/memory.model.dart | 202 ++++++++++++++++++ .../domain/services/sync_stream.service.dart | 8 + .../entities/memory.entity.dart | 36 ++++ .../entities/memory.entity.drift.dart | Bin 0 -> 38238 bytes .../entities/memory_asset.entity.dart | 17 ++ .../entities/memory_asset.entity.drift.dart | Bin 0 -> 21195 bytes .../repositories/db.repository.dart | 4 + .../repositories/db.repository.drift.dart | Bin 9078 -> 10615 bytes .../repositories/memory.repository.dart | 37 ++++ .../repositories/sync_api.repository.dart | 6 + .../repositories/sync_stream.repository.dart | 164 +++++++++++--- .../pages/dev/media_stat.page.dart | 8 + mobile/lib/providers/memory.provider.dart | 6 + .../services/sync_stream_service_test.dart | 97 +++++++++ mobile/test/fixtures/sync_stream.stub.dart | 43 ++++ 16 files changed, 601 insertions(+), 27 deletions(-) create mode 100644 mobile/lib/domain/models/memory.model.dart create mode 100644 mobile/lib/infrastructure/entities/memory.entity.dart create mode 100644 mobile/lib/infrastructure/entities/memory.entity.drift.dart create mode 100644 mobile/lib/infrastructure/entities/memory_asset.entity.dart create mode 100644 mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart create mode 100644 mobile/lib/infrastructure/repositories/memory.repository.dart diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 30f92cf8db425e6403411e7317694208c74c58bd..32ba52e366d2ec98891c4f36c09579684794ffaf 100644 GIT binary patch literal 42185 zcmeGlYj51R@q2!Spf7QNAc)gzdo6+hjm-gk9$JT`V z%o>M5;_@K2Tq|Y~cYXc`^Q=1-#QL!UlorKF{kpndvwbo#q>^u?BY_+ndU=ho5lr`GY4Cv+tq2q-pA zPVRvp-(;k!KToG6V6FI@&lADn4KtOfGZ$ZyKH({ZEr8#FkE~O{Q=^ADP&kUqqsPx5 zSC^b~)Y|z4q%^O*Izj&{kj|T8=NJLdf zMBDxe=yZyEae4Co z;}$vkTN);=6N5#DrpNX71QaQu8bYOF#03mE3tpSRHH3o|Kp$7a^+AwmhQTyOChvm6 zD;%*r4vCq#gdtW-uWQpdN*^kt8uTD2={p`Jsp}hz;Cr4-!!&WC@LpKyIt67rP_}1b ztD}+zi*_tsfP%tp_AvMPm?zHs7Uuhm#{buRT|dPCb30OM{meT!mQNtw!E?0}Tbk0X z-GhkDLU3H%IA+O)+ys+xOW556qOo{Sw7Gbl@~!7?1VNPvL_^hLX}CI0Ua3n(_Kcf! zz%~Vs9Q;XO8Oa#Zlm!Pk0{R7u6@JWR$|y)@XMx8az>>}ezC1xPzl4XqI#Pc=%lvg} z-=cAh&lFP&dV~k7K^O`s{$)IyeQx6cjw5g%+ZY%*;9y{s(A@^Xg9W$d7zQZ9M$f_s z(?K-pY8JW6-alVr%J|K+VvZ6SZU*Wbs=PFEF^-D{ZOlO0dre+}9B~$0nBn_|!XW~i z#o485*3ASJfrZHb_%=>w)Jr66SqG+3RZlEYaF8bMT^M1^kr-zLYdrp~22jJ>Hsi5! zCmRXLnkOq6H2WEq4etZU>@*4-0d`hN3~8jW0Tu!m0A`W-A^X$lw-H3cPDdA3DL-Z% zaqo%s;oN#NdN;ZpS*Ir#rzdYll-`$#)pwh?3z4kS{j$^GHh?#$f)XfC+t}THIClLa z6-qHjB!z%}QxhH4D5OOQ^&qUpWSY*dov9l)MePOPTLYYiwnoM}2&8$;XRW;H9B_GW z`XJDD$76oYA&FS73qVGvS^IiGjo5}b6NxkZTOl})HWgoVjyV-W7et;zNLnivNrkU`#Hb;c0aUPnb( zZ6b1)66Zr)dx&fAMiHYqWKCqa8K{F4KL|rxMyZFm_D`40Hint102-AwI>I}e23Hn+ zJF}`TU`^Pv4y+r>4uUs_ur?IM>%mbyX96Cixzcza!`h0SlwY|o4)R}W9OM^jC(1+S zSn{GGuZEK$;-rl7xB&yQx`lfWPT6fX$O$9{sIbv}M+g`5Zz)7ro7M%PdCyPb%#ci&wAlY&<>VwD zveNzI>(&s^ee0`w-+!yw?=qE*BC&&q#UOd$Zm6_T%vrTXi2g;WAtfe{uN-(v49?J~ zlYElO^MD*7iv^I~EHlEDINL5%PLbVD4hVU3)>~B-3V6VztoVzt(8*s^8oCgjep-``77gLrT2S0jdIMeAkm`t?7^(aS2Qy zeMHre_gr({^V%KXrgJ!Ufn6xDmyVP}nOj9m!ZPrSI%thVhq^H3tiF6L(5&$G>WF;N zr7nyNUEtN0-=!YiFG9bu!^7Cg6_$(2qJac>d=|zRLcyJwz%?4!o+VY?*9I)Arb&0( zvT8vtGdH@0bGT(lunCFs%77dS)rBzh&^pSZj)5#awN*6b$=0@hQZUWaj z!p$D1X+OUrePpKwzjl@U_tU3pggR~xc?qyRAd!+Mx)l;>G5>Cnh+2&jZ`+sEh1jxe zMV~DK9LIGnE)A*e*0MX#VzUQ6*-(h1*uHu>LOJi7)YkI<+Ys9w7NB8zALKSPcxpOZ zWdhL*jN#4ka*sCUR5WwfG=7wA^RA0ez*8+yqj?Tt6h6 z`3M9NZDt@1TZ^Np2SH-Seo+KdMwq@K3fkO-F?1gnQjH28f-nhdYCx;utsm?-aiA&S zbz|sqa&Cmu5axwxxdnf&OAbx$i)PZ;sked75DxqEQZNl=JrH6O(E-J0!A)a-JOTR7 z2&rMMAB5&Z@3$oNDg;F2Sshp(F%||ltx#46*F&7~IHVGa5SRf%!&47r*v!3cVnY*% zhNga)6SEL{3N;Xv5vGT@0`D6FjH1J51I89W_7HDa)9U;YHWsxa+%N9J-(y^-RYP^g- zp0QaNVQ2}eC(CDo*5XJts^2Pr#z>7d0m~c4yKtIq$bqdnMKk2U){NlcO2rr&zJHd{ zvWl@&V}A~*!+U7o`zC7*sfQ?ZjMX_OLYpqmS?-+$YP!gdSfC~lO>qwKe4wVRQTWUE z!s5aD!&gVe-(76aV$=e|v+P6a#H*U7;l%=_xCcrJ>T8-nv^U!mlL!)bv3u7d_jzH0 z(lEBojHlRxo^dSG3B3Phf~co&N2kAAM_;fHmtm$ZlCL(mxwd}1WjxgIOYW<GzD%vj(w;tZjd$Jo+!CcEH%#nbS<1;PMoKp0-yWKX%rnh}OjeL$EJvpbXq zY8havy*3Im_rIv^bT9aqg1b~BRubWB2{jdoFZDi5_LTrTtCUn}2EztSEZx_!4e6E= z>NXqHmkGNT3(zpV4>|jsE~`;T;$LNOyG$neP8PeqWfN;jQ?o>&t)|{B(om~WG7ZoH zM)Mtyk`($oRWBmMD;UBwaiS19$iVNqp_5t5kqd5i7WXMIZKL!UC)FX)SjFeizRj86 b!jGTrLSR{TL6CEiwI_ literal 21916 zcmeGkYj4{&@UIB^(iAX)ID2)$P{4Iu)L5Eyv9ked0ztOun2R2+NafKW|9wY_k|;{H zC0UZ4!azSXcNBR%9`8**>xP4RS79vw0FDUk0{X`$nG0|q&M%vVKsfk^pnHoVp zHf{VJS=v4F35?XdnMS5_F6F`gRnIT z^?YPlCPfCt4l?ddJ3z!Ri9yjL)laA&AowDhT0i;vtYb?*IqK*>njs&#Qv?%zz3J%m zX@OuBYEEnf>%yFTSFY(WZ~&wme3~afKEMJNn%bt1ECaiS<9WV*GBa(0I{MVJ1IJ~v zKI`IZShAzvB1&Nr8N|Lb#KMI7*u8~uZeZJ-h@wEY-(d5H;p2;F+(N|{BX}NotQvQluDui?hKI_4%Tw8o8=|x3Ajw_#PjSb>`XCQ z?|&Nf|I`ky^=S2LT^pWj2S**P`}Qr&Z-GRkDeXOgFr0=5l9~a8^qY+-6QVV9fuN>O z6hsNmTMY8nKTIMaNSCko0$l_oxB1Ve`I2Qfh2D(JA(E?Ya6koJHu zky=gs6&q8G`bg~;ld7d>RQ^tb(E_D(k9``Lc5HqgFr9k=HGJ=ZSvtX6*?2H($L{u2 z7)Y>>j|&@5F*O!LSO;M$lE#+?xKLBYZp0n$#G) z6GU2pZ?YhWSr~%g4q90Q8I2jbqj@Za{P zrk$iwGP26?r%8Y!vp6?yY0VQlUc6R@cHz-`*1hCfeO@@N|3t>?U(b%yPjjavF!mGWH z#gatps}j$BEVevpy^qBplvky>HOmkOU1_TjgI8IE)`*PatbHujMAb+Nxza))A!ISw zrsuT}#p)>}wd3aeq_{c9iJya6#*&3tqCm^4&EOS@)FZYY4cX<9BoWGRQIRxo@wWgf za*<~mS8Vloe3AhFS)rx7nij~H3tF!JGE%AVFm{bsfFyjDBB2>`2!gtI6#FK${Zf@S z#-|ijI6EY11{*kM*L74T_tasmv#PYJAT1jtPZK(RUQk5D7OG;WQC zk3^?fd8}+YL)^vXr4A+oU*f&Ql@6{zlw!tJYA9(kri&ytxyiqkzB~&k#;{~q<*pPm z=lv^_r@VYxi2U%hzdzOPk6LL{2+ouc^?_i9Nmp78JNN#ImhemU*}2g%Zs6R$LE z&7_?CtLT6Dfm1LOb9xsnpn}V~`&dt_>IH=ZMiO3@9W^i0u;5hg`1ruL8Smo#fdW{S z1XqTpdO$#@V2>E$)|>3IW;lq+&?6)6h4jP#nT2&L>nugJDD8FAHmSUm5}srFchKmg z3TX&70KSs5#S(JqnrPh38m>_XFY{)~u&;E8&{ApUcg2n!cixpUWgFIL!q0Qe8b`F@ zWy*P%Fes_JKwf->jin|xDCTM`&zzzTKp7;Z{4UZ`{};?qlf&#w9q zL`v>0@jgY?3_apmb?;N6DxEoR?Xsqlf6d(15=+!6N{LN`j^=FJf&j|qFvrg?W)RcQ zYbrg*AQakaGG;r1?h2JtFL0@!!^{}IE0nT!r0^{mwB6le7dqorpD5~Bx=04d)JJIe zj=37)Q>@nHL4HM-AzXT4mv-p3DZz2YH?7HHu*_REDr$|bQ5$ob#t6C-Btmb#N8oK9 zm)?xfMp*GgNTzt3hd(pm5y2!U@R*%PaG6xg3&Q7>Tew;LIT?Gml;JJIu-8g}kcr)z zu+Dfprj6X&oQ8akJd1ELz1w824K439u(G_B5IaMw=iV+MR>JHGwU~Grfem3Yg5BKg-W>Z*@q$mxGOY*PX)08f8LVeXBxLF6tfzU@!iWQ z5;4rbA!1jEWe+3i0Z-dwR`=_|_1;`EU zYdWere`6u;^iy>!N3@Y=<_${Cqy-C1-Ap==KL~T-*H6Sx6XdAOcu0>ugfGJAVi}Bd zV7gd+MqZBl ztHCNsbyP5OcAB0x!{DX5zq^6j_k1fSClf(swyJJqIc-?F7F)Sbp~nhR{8nDDy}@I> zG+Fk~^rr*6VhRS diff --git a/mobile/lib/domain/models/memory.model.dart b/mobile/lib/domain/models/memory.model.dart new file mode 100644 index 000000000..d24e1ae1c --- /dev/null +++ b/mobile/lib/domain/models/memory.model.dart @@ -0,0 +1,202 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +enum MemoryTypeEnum { + // do not change this order! + onThisDay, +} + +class MemoryData { + final int year; + + const MemoryData({ + required this.year, + }); + + MemoryData copyWith({ + int? year, + }) { + return MemoryData( + year: year ?? this.year, + ); + } + + Map toMap() { + return { + 'year': year, + }; + } + + factory MemoryData.fromMap(Map map) { + return MemoryData( + year: map['year'] as int, + ); + } + + String toJson() => json.encode(toMap()); + + factory MemoryData.fromJson(String source) => + MemoryData.fromMap(json.decode(source) as Map); + + @override + String toString() => 'MemoryData(year: $year)'; + + @override + bool operator ==(covariant MemoryData other) { + if (identical(this, other)) return true; + + return other.year == year; + } + + @override + int get hashCode => year.hashCode; +} + +// Model for a memory stored in the server +class Memory { + final String id; + final DateTime createdAt; + final DateTime updatedAt; + final DateTime? deletedAt; + final String ownerId; + + // enum + final MemoryTypeEnum type; + final MemoryData data; + final bool isSaved; + final DateTime memoryAt; + final DateTime? seenAt; + final DateTime? showAt; + final DateTime? hideAt; + + const Memory({ + required this.id, + required this.createdAt, + required this.updatedAt, + this.deletedAt, + required this.ownerId, + required this.type, + required this.data, + required this.isSaved, + required this.memoryAt, + this.seenAt, + this.showAt, + this.hideAt, + }); + + Memory copyWith({ + String? id, + DateTime? createdAt, + DateTime? updatedAt, + DateTime? deletedAt, + String? ownerId, + MemoryTypeEnum? type, + MemoryData? data, + bool? isSaved, + DateTime? memoryAt, + DateTime? seenAt, + DateTime? showAt, + DateTime? hideAt, + }) { + return Memory( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + deletedAt: deletedAt ?? this.deletedAt, + ownerId: ownerId ?? this.ownerId, + type: type ?? this.type, + data: data ?? this.data, + isSaved: isSaved ?? this.isSaved, + memoryAt: memoryAt ?? this.memoryAt, + seenAt: seenAt ?? this.seenAt, + showAt: showAt ?? this.showAt, + hideAt: hideAt ?? this.hideAt, + ); + } + + Map toMap() { + return { + 'id': id, + 'createdAt': createdAt.millisecondsSinceEpoch, + 'updatedAt': updatedAt.millisecondsSinceEpoch, + 'deletedAt': deletedAt?.millisecondsSinceEpoch, + 'ownerId': ownerId, + 'type': type.index, + 'data': data.toMap(), + 'isSaved': isSaved, + 'memoryAt': memoryAt.millisecondsSinceEpoch, + 'seenAt': seenAt?.millisecondsSinceEpoch, + 'showAt': showAt?.millisecondsSinceEpoch, + 'hideAt': hideAt?.millisecondsSinceEpoch, + }; + } + + factory Memory.fromMap(Map map) { + return Memory( + id: map['id'] as String, + createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int), + updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int), + deletedAt: map['deletedAt'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['deletedAt'] as int) + : null, + ownerId: map['ownerId'] as String, + type: MemoryTypeEnum.values[map['type'] as int], + data: MemoryData.fromMap(map['data'] as Map), + isSaved: map['isSaved'] as bool, + memoryAt: DateTime.fromMillisecondsSinceEpoch(map['memoryAt'] as int), + seenAt: map['seenAt'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['seenAt'] as int) + : null, + showAt: map['showAt'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['showAt'] as int) + : null, + hideAt: map['hideAt'] != null + ? DateTime.fromMillisecondsSinceEpoch(map['hideAt'] as int) + : null, + ); + } + + String toJson() => json.encode(toMap()); + + factory Memory.fromJson(String source) => + Memory.fromMap(json.decode(source) as Map); + + @override + String toString() { + return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt)'; + } + + @override + bool operator ==(covariant Memory other) { + if (identical(this, other)) return true; + + return other.id == id && + other.createdAt == createdAt && + other.updatedAt == updatedAt && + other.deletedAt == deletedAt && + other.ownerId == ownerId && + other.type == type && + other.data == data && + other.isSaved == isSaved && + other.memoryAt == memoryAt && + other.seenAt == seenAt && + other.showAt == showAt && + other.hideAt == hideAt; + } + + @override + int get hashCode { + return id.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + deletedAt.hashCode ^ + ownerId.hashCode ^ + type.hashCode ^ + data.hashCode ^ + isSaved.hashCode ^ + memoryAt.hashCode ^ + seenAt.hashCode ^ + showAt.hashCode ^ + hideAt.hashCode; + } +} diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 2160018df..c4e40726b 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -146,6 +146,14 @@ class SyncStreamService { // to acknowledge that the client has processed all the backfill events case SyncEntityType.syncAckV1: return; + case SyncEntityType.memoryV1: + return _syncStreamRepository.updateMemoriesV1(data.cast()); + case SyncEntityType.memoryDeleteV1: + return _syncStreamRepository.deleteMemoriesV1(data.cast()); + case SyncEntityType.memoryToAssetV1: + return _syncStreamRepository.updateMemoryAssetsV1(data.cast()); + case SyncEntityType.memoryToAssetDeleteV1: + return _syncStreamRepository.deleteMemoryAssetsV1(data.cast()); default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/infrastructure/entities/memory.entity.dart b/mobile/lib/infrastructure/entities/memory.entity.dart new file mode 100644 index 000000000..0e1980210 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory.entity.dart @@ -0,0 +1,36 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class MemoryEntity extends Table with DriftDefaultsMixin { + const MemoryEntity(); + + TextColumn get id => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get deletedAt => dateTime().nullable()(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get type => intEnum()(); + + TextColumn get data => text()(); + + BoolColumn get isSaved => boolean().withDefault(const Constant(false))(); + + DateTimeColumn get memoryAt => dateTime()(); + + DateTimeColumn get seenAt => dateTime().nullable()(); + + DateTimeColumn get showAt => dateTime().nullable()(); + + DateTimeColumn get hideAt => dateTime().nullable()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/memory.entity.drift.dart b/mobile/lib/infrastructure/entities/memory.entity.drift.dart new file mode 100644 index 0000000000000000000000000000000000000000..cb88651ba411fe8e85481617810d9f434e885d3a GIT binary patch literal 38238 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 literal 0 HcmV?d00001 diff --git a/mobile/lib/infrastructure/entities/memory_asset.entity.dart b/mobile/lib/infrastructure/entities/memory_asset.entity.dart new file mode 100644 index 000000000..c304b0372 --- /dev/null +++ b/mobile/lib/infrastructure/entities/memory_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class MemoryAssetEntity extends Table with DriftDefaultsMixin { + const MemoryAssetEntity(); + + TextColumn get assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get memoryId => + text().references(MemoryEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, memoryId}; +} diff --git a/mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart new file mode 100644 index 0000000000000000000000000000000000000000..9253e8bc05a75b29e5ded9a83cdce2c066f0f9b0 GIT binary patch literal 21195 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 BqPqY9 literal 0 HcmV?d00001 diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index dbe491b03..e71598a99 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -7,6 +7,8 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; @@ -46,6 +48,8 @@ class IsarDatabaseRepository implements IDatabaseRepository { RemoteAlbumEntity, RemoteAlbumAssetEntity, RemoteAlbumUserEntity, + MemoryEntity, + MemoryAssetEntity, ], include: { 'package:immich_mobile/infrastructure/entities/merged_asset.drift', diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index 69fd84b79a50377eb64bfacce0587fc8ff74895d..925591def814795c41e6e9f64f5444141f69aa41 100644 GIT binary patch delta 483 zcmez7_C09BJ{FeT{G!Uq|Cwbc-(wM;n6Ji`n+g{B$*jz1JTcZ{@*Wl?Mw7|+SxgvB zH;b@7Vwud($*XUqr{W7%=bBfNSyCC2n3R*M0MQ8%QLt6WG=!^~{E$-=q))*Zrq8js zIJE>*^JGOfQ7%Kcs>uu3q?j`eO(y^3)L}H8tj47;ZldR#T9lre0ye;xx z7z&l4%Xx2Zkom&KrvULTG*l+fmlL17Uy)S_*%=D9c2MVmLk6K#Q5IR5rZrGiCMXPG eD){AOK!(8_@m^Mp8&z6O3}OIMpaM;)> getAll(String userId) { + final query = _db.memoryEntity.select() + ..where((e) => e.ownerId.equals(userId)); + + return query.map((memory) { + return memory.toDto(); + }).get(); + } +} + +extension on MemoryEntityData { + Memory toDto() { + return Memory( + id: id, + createdAt: createdAt, + updatedAt: updatedAt, + deletedAt: deletedAt, + ownerId: ownerId, + type: type, + data: MemoryData.fromJson(data), + isSaved: isSaved, + memoryAt: memoryAt, + seenAt: seenAt, + showAt: showAt, + hideAt: hideAt, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index ccc79fa81..99199a7fc 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -52,6 +52,8 @@ class SyncApiRepository { SyncRequestType.albumAssetsV1, SyncRequestType.albumAssetExifsV1, SyncRequestType.albumToAssetsV1, + SyncRequestType.memoriesV1, + SyncRequestType.memoryToAssetsV1, ], ).toJson(), ); @@ -157,6 +159,10 @@ const _kResponseMap = { SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson, SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson, SyncEntityType.syncAckV1: _SyncAckV1.fromJson, + SyncEntityType.memoryV1: SyncMemoryV1.fromJson, + SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson, + SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson, + SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson, }; class _SyncAckV1 { diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 015e09d66..b88083aa0 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,12 +1,17 @@ +import 'dart:convert'; + import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/memory.model.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; @@ -64,8 +69,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: SyncPartnerDeleteV1', e, s); + } catch (error, stackTrace) { + _logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace); rethrow; } } @@ -87,8 +92,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: SyncPartnerV1', e, s); + } catch (error, stackTrace) { + _logger.severe('Error: SyncPartnerV1', error, stackTrace); rethrow; } } @@ -98,10 +103,11 @@ class SyncStreamRepository extends DriftDatabaseRepository { String debugLabel = 'user', }) async { try { - await _db.remoteAssetEntity - .deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId))); - } catch (e, s) { - _logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s); + await _db.remoteAssetEntity.deleteWhere( + (row) => row.id.isIn(data.map((error) => error.assetId)), + ); + } catch (error, stackTrace) { + _logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace); rethrow; } } @@ -136,8 +142,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAssetsV1 - $debugLabel', e, s); + } catch (error, stackTrace) { + _logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace); rethrow; } } @@ -180,18 +186,23 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s); + } catch (error, stackTrace) { + _logger.severe( + 'Error: updateAssetsExifV1 - $debugLabel', + error, + stackTrace, + ); rethrow; } } Future deleteAlbumsV1(Iterable data) async { try { - await _db.remoteAlbumEntity - .deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId))); - } catch (e, s) { - _logger.severe('Error: deleteAlbumsV1', e, s); + await _db.remoteAlbumEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.albumId)), + ); + } catch (error, stackTrace) { + _logger.severe('Error: deleteAlbumsV1', error, stackTrace); rethrow; } } @@ -218,8 +229,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAlbumsV1', e, s); + } catch (error, stackTrace) { + _logger.severe('Error: updateAlbumsV1', error, stackTrace); rethrow; } } @@ -237,8 +248,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: deleteAlbumUsersV1', e, s); + } catch (error, stackTrace) { + _logger.severe('Error: deleteAlbumUsersV1', error, stackTrace); rethrow; } } @@ -264,8 +275,12 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s); + } catch (error, stackTrace) { + _logger.severe( + 'Error: updateAlbumUsersV1 - $debugLabel', + error, + stackTrace, + ); rethrow; } } @@ -285,8 +300,8 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: deleteAlbumToAssetsV1', e, s); + } catch (error, stackTrace) { + _logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace); rethrow; } } @@ -310,8 +325,96 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); - } catch (e, s) { - _logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s); + } catch (error, stackTrace) { + _logger.severe( + 'Error: updateAlbumToAssetsV1 - $debugLabel', + error, + stackTrace, + ); + rethrow; + } + } + + Future updateMemoriesV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final memory in data) { + final companion = MemoryEntityCompanion( + createdAt: Value(memory.createdAt), + deletedAt: Value(memory.deletedAt), + ownerId: Value(memory.ownerId), + type: Value(memory.type.toMemoryType()), + data: Value(jsonEncode(memory.data)), + isSaved: Value(memory.isSaved), + memoryAt: Value(memory.memoryAt), + seenAt: Value.absentIfNull(memory.seenAt), + showAt: Value.absentIfNull(memory.showAt), + hideAt: Value.absentIfNull(memory.hideAt), + ); + + batch.insert( + _db.memoryEntity, + companion.copyWith(id: Value(memory.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (error, stackTrace) { + _logger.severe('Error: updateMemoriesV1', error, stackTrace); + rethrow; + } + } + + Future deleteMemoriesV1(Iterable data) async { + try { + await _db.memoryEntity.deleteWhere( + (row) => row.id.isIn(data.map((e) => e.memoryId)), + ); + } catch (error, stackTrace) { + _logger.severe('Error: deleteMemoriesV1', error, stackTrace); + rethrow; + } + } + + Future updateMemoryAssetsV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final asset in data) { + final companion = MemoryAssetEntityCompanion( + memoryId: Value(asset.memoryId), + assetId: Value(asset.assetId), + ); + + batch.insert( + _db.memoryAssetEntity, + companion, + onConflict: DoNothing(), + ); + } + }); + } catch (error, stackTrace) { + _logger.severe('Error: updateMemoryAssetsV1', error, stackTrace); + rethrow; + } + } + + Future deleteMemoryAssetsV1( + Iterable data, + ) async { + try { + await _db.batch((batch) { + for (final asset in data) { + batch.delete( + _db.memoryAssetEntity, + MemoryAssetEntityCompanion( + memoryId: Value(asset.memoryId), + assetId: Value(asset.assetId), + ), + ); + } + }); + } catch (error, stackTrace) { + _logger.severe('Error: deleteMemoryAssetsV1', error, stackTrace); rethrow; } } @@ -335,6 +438,13 @@ extension on AssetOrder { }; } +extension on MemoryType { + MemoryTypeEnum toMemoryType() => switch (this) { + MemoryType.onThisDay => MemoryTypeEnum.onThisDay, + _ => throw Exception('Unknown MemoryType value: $this'), + }; +} + extension on api.AlbumUserRole { AlbumUserRole toAlbumUserRole() => switch (this) { api.AlbumUserRole.editor => AlbumUserRole.editor, @@ -357,7 +467,7 @@ extension on String { Duration? toDuration() { try { final parts = split(':') - .map((e) => double.parse(e).toInt()) + .map((error) => double.parse(error).toInt()) .toList(growable: false); return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]); diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index 10d09f8de..f0a648fd5 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -154,6 +154,14 @@ final _remoteStats = [ name: 'Remote Albums', load: (db) => db.managers.remoteAlbumEntity.count(), ), + _Stat( + name: 'Memories', + load: (db) => db.managers.memoryEntity.count(), + ), + _Stat( + name: 'Memories Assets', + load: (db) => db.managers.memoryAssetEntity.count(), + ), ]; @RoutePage() diff --git a/mobile/lib/providers/memory.provider.dart b/mobile/lib/providers/memory.provider.dart index aed546002..37f84dca9 100644 --- a/mobile/lib/providers/memory.provider.dart +++ b/mobile/lib/providers/memory.provider.dart @@ -1,5 +1,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/services/memory.service.dart'; final memoryFutureProvider = @@ -8,3 +10,7 @@ final memoryFutureProvider = return await service.getMemoryLane(); }); + +final driftMemoryProvider = Provider( + (ref) => DriftMemoryRepository(ref.watch(driftProvider)), +); diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index a0b61bcaf..28288422f 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -81,6 +81,14 @@ void main() { debugLabel: any(named: 'debugLabel'), ), ).thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateMemoriesV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteMemoriesV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())) + .thenAnswer(successHandler); + when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any())) + .thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, @@ -227,5 +235,94 @@ void main() { verify(() => mockSyncApiRepo.ack(["2"])).called(1); }, ); + + test("processes memory sync events successfully", () async { + final events = [ + SyncStreamStub.memoryV1, + SyncStreamStub.memoryDeleteV1, + SyncStreamStub.memoryToAssetV1, + SyncStreamStub.memoryToAssetDeleteV1, + ]; + + await simulateEvents(events); + + verifyInOrder([ + () => mockSyncStreamRepo.updateMemoriesV1(any()), + () => mockSyncApiRepo.ack(["5"]), + () => mockSyncStreamRepo.deleteMemoriesV1(any()), + () => mockSyncApiRepo.ack(["6"]), + () => mockSyncStreamRepo.updateMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["7"]), + () => mockSyncStreamRepo.deleteMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["8"]), + ]); + verifyNever(() => mockAbortCallbackWrapper()); + }); + + test("processes mixed memory and user events in correct order", () async { + final events = [ + SyncStreamStub.memoryDeleteV1, + SyncStreamStub.userV1Admin, + SyncStreamStub.memoryToAssetV1, + SyncStreamStub.memoryV1, + ]; + + await simulateEvents(events); + + verifyInOrder([ + () => mockSyncStreamRepo.deleteMemoriesV1(any()), + () => mockSyncApiRepo.ack(["6"]), + () => mockSyncStreamRepo.updateUsersV1(any()), + () => mockSyncApiRepo.ack(["1"]), + () => mockSyncStreamRepo.updateMemoryAssetsV1(any()), + () => mockSyncApiRepo.ack(["7"]), + () => mockSyncStreamRepo.updateMemoriesV1(any()), + () => mockSyncApiRepo.ack(["5"]), + ]); + verifyNever(() => mockAbortCallbackWrapper()); + }); + + test("handles memory sync failure gracefully", () async { + when(() => mockSyncStreamRepo.updateMemoriesV1(any())) + .thenThrow(Exception("Memory sync failed")); + + final events = [ + SyncStreamStub.memoryV1, + SyncStreamStub.userV1Admin, + ]; + + expect( + () async => await simulateEvents(events), + throwsA(isA()), + ); + }); + + test("processes memory asset events with correct data types", () async { + final events = [SyncStreamStub.memoryToAssetV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["7"])).called(1); + }); + + test("processes memory delete events with correct data types", () async { + final events = [SyncStreamStub.memoryDeleteV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.deleteMemoriesV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["6"])).called(1); + }); + + test("processes memory create/update events with correct data types", + () async { + final events = [SyncStreamStub.memoryV1]; + + await simulateEvents(events); + + verify(() => mockSyncStreamRepo.updateMemoriesV1(any())).called(1); + verify(() => mockSyncApiRepo.ack(["5"])).called(1); + }); }); } diff --git a/mobile/test/fixtures/sync_stream.stub.dart b/mobile/test/fixtures/sync_stream.stub.dart index ba97f1434..de2d58bc9 100644 --- a/mobile/test/fixtures/sync_stream.stub.dart +++ b/mobile/test/fixtures/sync_stream.stub.dart @@ -42,4 +42,47 @@ abstract final class SyncStreamStub { data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"), ack: "4", ); + + static final memoryV1 = SyncEvent( + type: SyncEntityType.memoryV1, + data: SyncMemoryV1( + createdAt: DateTime(2023, 1, 1), + data: {"year": 2023, "title": "Test Memory"}, + deletedAt: null, + hideAt: null, + id: "memory-1", + isSaved: false, + memoryAt: DateTime(2023, 1, 1), + ownerId: "user-1", + seenAt: null, + showAt: DateTime(2023, 1, 1), + type: MemoryType.onThisDay, + updatedAt: DateTime(2023, 1, 1), + ), + ack: "5", + ); + + static final memoryDeleteV1 = SyncEvent( + type: SyncEntityType.memoryDeleteV1, + data: SyncMemoryDeleteV1(memoryId: "memory-2"), + ack: "6", + ); + + static final memoryToAssetV1 = SyncEvent( + type: SyncEntityType.memoryToAssetV1, + data: SyncMemoryAssetV1( + assetId: "asset-1", + memoryId: "memory-1", + ), + ack: "7", + ); + + static final memoryToAssetDeleteV1 = SyncEvent( + type: SyncEntityType.memoryToAssetDeleteV1, + data: SyncMemoryAssetDeleteV1( + assetId: "asset-2", + memoryId: "memory-1", + ), + ack: "8", + ); }