start with cloud backup
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run

This commit is contained in:
otsmr 2026-07-19 20:24:14 +02:00
parent 3c26bf55a8
commit 96f8bc39d0
44 changed files with 16898 additions and 351 deletions

@ -1 +1 @@
Subproject commit d99dba6a5e0eb13d7dcb38a842e64c23a1779f1c Subproject commit 32882a31904a0c1ffd38974bf5158ceee0f3ecb7

View file

@ -9,6 +9,24 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
class RustKeyManager { class RustKeyManager {
const RustKeyManager(); const RustKeyManager();
static Future<Uint8List> decryptCloudMediaKey({
required List<int> encryptedMediaKey,
required String addition,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKey(
encryptedMediaKey: encryptedMediaKey,
addition: addition,
);
static Future<Uint8List> encryptCloudMediaKey({
required List<int> mediaKey,
required String addition,
}) => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKey(
mediaKey: mediaKey,
addition: addition,
);
static Future<Uint8List> getLoginToken() => RustLib.instance.api static Future<Uint8List> getLoginToken() => RustLib.instance.api
.crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken(); .crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken();

View file

@ -76,7 +76,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.12.0'; String get codegenVersion => '2.12.0';
@override @override
int get rustContentHash => 340781866; int get rustContentHash => 1788847092;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@ -205,6 +205,18 @@ abstract class RustLibApi extends BaseApi {
required String password, required String password,
}); });
Future<Uint8List>
crateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKey({
required List<int> encryptedMediaKey,
required String addition,
});
Future<Uint8List>
crateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKey({
required List<int> mediaKey,
required String addition,
});
Future<Uint8List> crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken(); Future<Uint8List> crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken();
Future<(Uint8List, PlatformInt64)> Future<(Uint8List, PlatformInt64)>
@ -1141,6 +1153,82 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
argNames: ["userId", "password"], argNames: ["userId", "password"],
); );
@override
Future<Uint8List>
crateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKey({
required List<int> encryptedMediaKey,
required String addition,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_list_prim_u_8_loose(encryptedMediaKey, serializer);
sse_encode_String(addition, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 18,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta:
kCrateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKeyConstMeta,
argValues: [encryptedMediaKey, addition],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateBridgeWrapperKeyManagerRustKeyManagerDecryptCloudMediaKeyConstMeta =>
const TaskConstMeta(
debugName: "rust_key_manager_decrypt_cloud_media_key",
argNames: ["encryptedMediaKey", "addition"],
);
@override
Future<Uint8List>
crateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKey({
required List<int> mediaKey,
required String addition,
}) {
return handler.executeNormal(
NormalTask(
callFfi: (port_) {
final serializer = SseSerializer(generalizedFrbRustBinding);
sse_encode_list_prim_u_8_loose(mediaKey, serializer);
sse_encode_String(addition, serializer);
pdeCallFfi(
generalizedFrbRustBinding,
serializer,
funcId: 19,
port: port_,
);
},
codec: SseCodec(
decodeSuccessData: sse_decode_list_prim_u_8_strict,
decodeErrorData: sse_decode_AnyhowException,
),
constMeta:
kCrateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKeyConstMeta,
argValues: [mediaKey, addition],
apiImpl: this,
),
);
}
TaskConstMeta
get kCrateBridgeWrapperKeyManagerRustKeyManagerEncryptCloudMediaKeyConstMeta =>
const TaskConstMeta(
debugName: "rust_key_manager_encrypt_cloud_media_key",
argNames: ["mediaKey", "addition"],
);
@override @override
Future<Uint8List> crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken() { Future<Uint8List> crateBridgeWrapperKeyManagerRustKeyManagerGetLoginToken() {
return handler.executeNormal( return handler.executeNormal(
@ -1150,7 +1238,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 18, funcId: 20,
port: port_, port: port_,
); );
}, },
@ -1183,7 +1271,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 19, funcId: 21,
port: port_, port: port_,
); );
}, },
@ -1215,7 +1303,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 20, funcId: 22,
port: port_, port: port_,
); );
}, },
@ -1250,7 +1338,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 21, funcId: 23,
port: port_, port: port_,
); );
}, },
@ -1292,7 +1380,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 22, funcId: 24,
port: port_, port: port_,
); );
}, },
@ -1336,7 +1424,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 23, funcId: 25,
port: port_, port: port_,
); );
}, },
@ -1369,7 +1457,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 24, funcId: 26,
port: port_, port: port_,
); );
}, },
@ -1401,7 +1489,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 25, funcId: 27,
port: port_, port: port_,
); );
}, },
@ -1436,7 +1524,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 26, funcId: 28,
port: port_, port: port_,
); );
}, },
@ -1468,7 +1556,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 27, funcId: 29,
port: port_, port: port_,
); );
}, },
@ -1503,7 +1591,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 28, funcId: 30,
port: port_, port: port_,
); );
}, },
@ -1540,7 +1628,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 29, funcId: 31,
port: port_, port: port_,
); );
}, },
@ -1579,7 +1667,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 30, funcId: 32,
port: port_, port: port_,
); );
}, },
@ -1614,7 +1702,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 31, funcId: 33,
port: port_, port: port_,
); );
}, },
@ -1650,7 +1738,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 32, funcId: 34,
port: port_, port: port_,
); );
}, },
@ -1687,7 +1775,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 33, funcId: 35,
port: port_, port: port_,
); );
}, },
@ -1725,7 +1813,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 34, funcId: 36,
port: port_, port: port_,
); );
}, },
@ -1763,7 +1851,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 35, funcId: 37,
port: port_, port: port_,
); );
}, },
@ -1801,7 +1889,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 36, funcId: 38,
port: port_, port: port_,
); );
}, },
@ -1840,7 +1928,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 37, funcId: 39,
port: port_, port: port_,
); );
}, },
@ -1879,7 +1967,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 38, funcId: 40,
port: port_, port: port_,
); );
}, },
@ -1924,7 +2012,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 39, funcId: 41,
port: port_, port: port_,
); );
}, },
@ -1976,7 +2064,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 40, funcId: 42,
port: port_, port: port_,
); );
}, },
@ -2017,7 +2105,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 41, funcId: 43,
port: port_, port: port_,
); );
}, },
@ -2055,7 +2143,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 42, funcId: 44,
port: port_, port: port_,
); );
}, },
@ -2093,7 +2181,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 43, funcId: 45,
port: port_, port: port_,
); );
}, },
@ -2131,7 +2219,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 44, funcId: 46,
port: port_, port: port_,
); );
}, },
@ -2169,7 +2257,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 45, funcId: 47,
port: port_, port: port_,
); );
}, },
@ -2211,7 +2299,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 46, funcId: 48,
port: port_, port: port_,
); );
}, },
@ -2251,7 +2339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
pdeCallFfi( pdeCallFfi(
generalizedFrbRustBinding, generalizedFrbRustBinding,
serializer, serializer,
funcId: 47, funcId: 49,
port: port_, port: port_,
); );
}, },

View file

@ -120,6 +120,7 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
(t) => (t) =>
t.stored.equals(true) & t.stored.equals(true) &
(t.storedFileHash.isNull() | (t.storedFileHash.isNull() |
t.blurhash.isNull() |
t.hasCropAnalyzed.equals(false) | t.hasCropAnalyzed.equals(false) |
(t.hasThumbnail.equals(false) & (t.hasThumbnail.equals(false) &
t.type.equals(MediaType.audio.name).not()) | t.type.equals(MediaType.audio.name).not()) |
@ -141,7 +142,12 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
Stream<List<MediaFile>> watchAllStoredMediaFiles() { Stream<List<MediaFile>> watchAllStoredMediaFiles() {
final query = final query =
(select(mediaFiles)..where((t) => t.stored.equals(true))).join([]) (select(mediaFiles)..where(
(t) =>
t.stored.equals(true) |
t.cloudState.equals(CloudState.uploaded.name),
))
.join([])
..groupBy([ ..groupBy([
const CustomExpression<Object>( const CustomExpression<Object>(
'COALESCE(stored_file_hash, media_id)', 'COALESCE(stored_file_hash, media_id)',
@ -219,4 +225,12 @@ class MediaFilesDao extends DatabaseAccessor<TwonlyDB>
return stats; return stats;
} }
Future<List<MediaFile>> getMemoriesToBackup() async {
return (select(mediaFiles)..where(
(t) =>
t.stored.equals(true) & t.cloudState.equals(CloudState.none.name),
))
.get();
}
} }

File diff suppressed because it is too large Load diff

View file

@ -8,6 +8,12 @@ enum MediaType {
audio, audio,
} }
enum CloudState {
none,
pending,
uploaded,
}
enum UploadState { enum UploadState {
// Image/Video was taken. A database entry was created to track it... // Image/Video was taken. A database entry was created to track it...
initialized, initialized,
@ -43,6 +49,9 @@ class MediaFiles extends Table {
TextColumn get type => textEnum<MediaType>()(); TextColumn get type => textEnum<MediaType>()();
TextColumn get uploadState => textEnum<UploadState>().nullable()(); TextColumn get uploadState => textEnum<UploadState>().nullable()();
TextColumn get cloudState =>
textEnum<CloudState>().withDefault(const Constant('none'))();
TextColumn get blurhash => text().nullable()();
TextColumn get downloadState => textEnum<DownloadState>().nullable()(); TextColumn get downloadState => textEnum<DownloadState>().nullable()();
BoolColumn get requiresAuthentication => BoolColumn get requiresAuthentication =>
@ -69,15 +78,13 @@ class MediaFiles extends Table {
BlobColumn get storedFileHash => blob().nullable()(); BlobColumn get storedFileHash => blob().nullable()();
BoolColumn get hasThumbnail => BoolColumn get hasThumbnail => boolean().withDefault(const Constant(false))();
boolean().withDefault(const Constant(false))();
IntColumn get sizeInBytes => integer().nullable()(); IntColumn get sizeInBytes => integer().nullable()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get createdAtMonth => text().nullable()(); TextColumn get createdAtMonth => text().nullable()();
@override @override
Set<Column> get primaryKey => {mediaId}; Set<Column> get primaryKey => {mediaId};
} }

View file

@ -82,7 +82,7 @@ class TwonlyDB extends _$TwonlyDB {
TwonlyDB.forTesting(DatabaseConnection super.connection); TwonlyDB.forTesting(DatabaseConnection super.connection);
@override @override
int get schemaVersion => 22; int get schemaVersion => 23;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
final connection = driftDatabase( final connection = driftDatabase(
@ -275,6 +275,16 @@ class TwonlyDB extends _$TwonlyDB {
schema.contacts.recoveryContactsThreshold, schema.contacts.recoveryContactsThreshold,
); );
}, },
from22To23: (m, schema) async {
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.cloudState,
);
await m.addColumn(
schema.mediaFiles,
schema.mediaFiles.blurhash,
);
},
)(m, from, to); )(m, from, to);
}, },
); );

View file

@ -3094,6 +3094,27 @@ class $MediaFilesTable extends MediaFiles
requiredDuringInsert: false, requiredDuringInsert: false,
).withConverter<UploadState?>($MediaFilesTable.$converteruploadStaten); ).withConverter<UploadState?>($MediaFilesTable.$converteruploadStaten);
@override @override
late final GeneratedColumnWithTypeConverter<CloudState, String> cloudState =
GeneratedColumn<String>(
'cloud_state',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant('none'),
).withConverter<CloudState>($MediaFilesTable.$convertercloudState);
static const VerificationMeta _blurhashMeta = const VerificationMeta(
'blurhash',
);
@override
late final GeneratedColumn<String> blurhash = GeneratedColumn<String>(
'blurhash',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
@override
late final GeneratedColumnWithTypeConverter<DownloadState?, String> late final GeneratedColumnWithTypeConverter<DownloadState?, String>
downloadState = GeneratedColumn<String>( downloadState = GeneratedColumn<String>(
'download_state', 'download_state',
@ -3333,6 +3354,8 @@ class $MediaFilesTable extends MediaFiles
mediaId, mediaId,
type, type,
uploadState, uploadState,
cloudState,
blurhash,
downloadState, downloadState,
requiresAuthentication, requiresAuthentication,
stored, stored,
@ -3373,6 +3396,12 @@ class $MediaFilesTable extends MediaFiles
} else if (isInserting) { } else if (isInserting) {
context.missing(_mediaIdMeta); context.missing(_mediaIdMeta);
} }
if (data.containsKey('blurhash')) {
context.handle(
_blurhashMeta,
blurhash.isAcceptableOrUnknown(data['blurhash']!, _blurhashMeta),
);
}
if (data.containsKey('requires_authentication')) { if (data.containsKey('requires_authentication')) {
context.handle( context.handle(
_requiresAuthenticationMeta, _requiresAuthenticationMeta,
@ -3542,6 +3571,16 @@ class $MediaFilesTable extends MediaFiles
data['${effectivePrefix}upload_state'], data['${effectivePrefix}upload_state'],
), ),
), ),
cloudState: $MediaFilesTable.$convertercloudState.fromSql(
attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}cloud_state'],
)!,
),
blurhash: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}blurhash'],
),
downloadState: $MediaFilesTable.$converterdownloadStaten.fromSql( downloadState: $MediaFilesTable.$converterdownloadStaten.fromSql(
attachedDatabase.typeMapping.read( attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
@ -3637,6 +3676,8 @@ class $MediaFilesTable extends MediaFiles
const EnumNameConverter<UploadState>(UploadState.values); const EnumNameConverter<UploadState>(UploadState.values);
static JsonTypeConverter2<UploadState?, String?, String?> static JsonTypeConverter2<UploadState?, String?, String?>
$converteruploadStaten = JsonTypeConverter2.asNullable($converteruploadState); $converteruploadStaten = JsonTypeConverter2.asNullable($converteruploadState);
static JsonTypeConverter2<CloudState, String, String> $convertercloudState =
const EnumNameConverter<CloudState>(CloudState.values);
static JsonTypeConverter2<DownloadState, String, String> static JsonTypeConverter2<DownloadState, String, String>
$converterdownloadState = const EnumNameConverter<DownloadState>( $converterdownloadState = const EnumNameConverter<DownloadState>(
DownloadState.values, DownloadState.values,
@ -3655,6 +3696,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
final String mediaId; final String mediaId;
final MediaType type; final MediaType type;
final UploadState? uploadState; final UploadState? uploadState;
final CloudState cloudState;
final String? blurhash;
final DownloadState? downloadState; final DownloadState? downloadState;
final bool requiresAuthentication; final bool requiresAuthentication;
final bool stored; final bool stored;
@ -3678,6 +3721,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
required this.mediaId, required this.mediaId,
required this.type, required this.type,
this.uploadState, this.uploadState,
required this.cloudState,
this.blurhash,
this.downloadState, this.downloadState,
required this.requiresAuthentication, required this.requiresAuthentication,
required this.stored, required this.stored,
@ -3712,6 +3757,14 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
$MediaFilesTable.$converteruploadStaten.toSql(uploadState), $MediaFilesTable.$converteruploadStaten.toSql(uploadState),
); );
} }
{
map['cloud_state'] = Variable<String>(
$MediaFilesTable.$convertercloudState.toSql(cloudState),
);
}
if (!nullToAbsent || blurhash != null) {
map['blurhash'] = Variable<String>(blurhash);
}
if (!nullToAbsent || downloadState != null) { if (!nullToAbsent || downloadState != null) {
map['download_state'] = Variable<String>( map['download_state'] = Variable<String>(
$MediaFilesTable.$converterdownloadStaten.toSql(downloadState), $MediaFilesTable.$converterdownloadStaten.toSql(downloadState),
@ -3773,6 +3826,10 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
uploadState: uploadState == null && nullToAbsent uploadState: uploadState == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(uploadState), : Value(uploadState),
cloudState: Value(cloudState),
blurhash: blurhash == null && nullToAbsent
? const Value.absent()
: Value(blurhash),
downloadState: downloadState == null && nullToAbsent downloadState: downloadState == null && nullToAbsent
? const Value.absent() ? const Value.absent()
: Value(downloadState), : Value(downloadState),
@ -3833,6 +3890,10 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
uploadState: $MediaFilesTable.$converteruploadStaten.fromJson( uploadState: $MediaFilesTable.$converteruploadStaten.fromJson(
serializer.fromJson<String?>(json['uploadState']), serializer.fromJson<String?>(json['uploadState']),
), ),
cloudState: $MediaFilesTable.$convertercloudState.fromJson(
serializer.fromJson<String>(json['cloudState']),
),
blurhash: serializer.fromJson<String?>(json['blurhash']),
downloadState: $MediaFilesTable.$converterdownloadStaten.fromJson( downloadState: $MediaFilesTable.$converterdownloadStaten.fromJson(
serializer.fromJson<String?>(json['downloadState']), serializer.fromJson<String?>(json['downloadState']),
), ),
@ -3875,6 +3936,10 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
'uploadState': serializer.toJson<String?>( 'uploadState': serializer.toJson<String?>(
$MediaFilesTable.$converteruploadStaten.toJson(uploadState), $MediaFilesTable.$converteruploadStaten.toJson(uploadState),
), ),
'cloudState': serializer.toJson<String>(
$MediaFilesTable.$convertercloudState.toJson(cloudState),
),
'blurhash': serializer.toJson<String?>(blurhash),
'downloadState': serializer.toJson<String?>( 'downloadState': serializer.toJson<String?>(
$MediaFilesTable.$converterdownloadStaten.toJson(downloadState), $MediaFilesTable.$converterdownloadStaten.toJson(downloadState),
), ),
@ -3905,6 +3970,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
String? mediaId, String? mediaId,
MediaType? type, MediaType? type,
Value<UploadState?> uploadState = const Value.absent(), Value<UploadState?> uploadState = const Value.absent(),
CloudState? cloudState,
Value<String?> blurhash = const Value.absent(),
Value<DownloadState?> downloadState = const Value.absent(), Value<DownloadState?> downloadState = const Value.absent(),
bool? requiresAuthentication, bool? requiresAuthentication,
bool? stored, bool? stored,
@ -3928,6 +3995,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
mediaId: mediaId ?? this.mediaId, mediaId: mediaId ?? this.mediaId,
type: type ?? this.type, type: type ?? this.type,
uploadState: uploadState.present ? uploadState.value : this.uploadState, uploadState: uploadState.present ? uploadState.value : this.uploadState,
cloudState: cloudState ?? this.cloudState,
blurhash: blurhash.present ? blurhash.value : this.blurhash,
downloadState: downloadState.present downloadState: downloadState.present
? downloadState.value ? downloadState.value
: this.downloadState, : this.downloadState,
@ -3976,6 +4045,10 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
uploadState: data.uploadState.present uploadState: data.uploadState.present
? data.uploadState.value ? data.uploadState.value
: this.uploadState, : this.uploadState,
cloudState: data.cloudState.present
? data.cloudState.value
: this.cloudState,
blurhash: data.blurhash.present ? data.blurhash.value : this.blurhash,
downloadState: data.downloadState.present downloadState: data.downloadState.present
? data.downloadState.value ? data.downloadState.value
: this.downloadState, : this.downloadState,
@ -4038,6 +4111,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
..write('mediaId: $mediaId, ') ..write('mediaId: $mediaId, ')
..write('type: $type, ') ..write('type: $type, ')
..write('uploadState: $uploadState, ') ..write('uploadState: $uploadState, ')
..write('cloudState: $cloudState, ')
..write('blurhash: $blurhash, ')
..write('downloadState: $downloadState, ') ..write('downloadState: $downloadState, ')
..write('requiresAuthentication: $requiresAuthentication, ') ..write('requiresAuthentication: $requiresAuthentication, ')
..write('stored: $stored, ') ..write('stored: $stored, ')
@ -4066,6 +4141,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
mediaId, mediaId,
type, type,
uploadState, uploadState,
cloudState,
blurhash,
downloadState, downloadState,
requiresAuthentication, requiresAuthentication,
stored, stored,
@ -4093,6 +4170,8 @@ class MediaFile extends DataClass implements Insertable<MediaFile> {
other.mediaId == this.mediaId && other.mediaId == this.mediaId &&
other.type == this.type && other.type == this.type &&
other.uploadState == this.uploadState && other.uploadState == this.uploadState &&
other.cloudState == this.cloudState &&
other.blurhash == this.blurhash &&
other.downloadState == this.downloadState && other.downloadState == this.downloadState &&
other.requiresAuthentication == this.requiresAuthentication && other.requiresAuthentication == this.requiresAuthentication &&
other.stored == this.stored && other.stored == this.stored &&
@ -4124,6 +4203,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
final Value<String> mediaId; final Value<String> mediaId;
final Value<MediaType> type; final Value<MediaType> type;
final Value<UploadState?> uploadState; final Value<UploadState?> uploadState;
final Value<CloudState> cloudState;
final Value<String?> blurhash;
final Value<DownloadState?> downloadState; final Value<DownloadState?> downloadState;
final Value<bool> requiresAuthentication; final Value<bool> requiresAuthentication;
final Value<bool> stored; final Value<bool> stored;
@ -4148,6 +4229,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
this.mediaId = const Value.absent(), this.mediaId = const Value.absent(),
this.type = const Value.absent(), this.type = const Value.absent(),
this.uploadState = const Value.absent(), this.uploadState = const Value.absent(),
this.cloudState = const Value.absent(),
this.blurhash = const Value.absent(),
this.downloadState = const Value.absent(), this.downloadState = const Value.absent(),
this.requiresAuthentication = const Value.absent(), this.requiresAuthentication = const Value.absent(),
this.stored = const Value.absent(), this.stored = const Value.absent(),
@ -4173,6 +4256,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
required String mediaId, required String mediaId,
required MediaType type, required MediaType type,
this.uploadState = const Value.absent(), this.uploadState = const Value.absent(),
this.cloudState = const Value.absent(),
this.blurhash = const Value.absent(),
this.downloadState = const Value.absent(), this.downloadState = const Value.absent(),
this.requiresAuthentication = const Value.absent(), this.requiresAuthentication = const Value.absent(),
this.stored = const Value.absent(), this.stored = const Value.absent(),
@ -4199,6 +4284,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Expression<String>? mediaId, Expression<String>? mediaId,
Expression<String>? type, Expression<String>? type,
Expression<String>? uploadState, Expression<String>? uploadState,
Expression<String>? cloudState,
Expression<String>? blurhash,
Expression<String>? downloadState, Expression<String>? downloadState,
Expression<bool>? requiresAuthentication, Expression<bool>? requiresAuthentication,
Expression<bool>? stored, Expression<bool>? stored,
@ -4224,6 +4311,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
if (mediaId != null) 'media_id': mediaId, if (mediaId != null) 'media_id': mediaId,
if (type != null) 'type': type, if (type != null) 'type': type,
if (uploadState != null) 'upload_state': uploadState, if (uploadState != null) 'upload_state': uploadState,
if (cloudState != null) 'cloud_state': cloudState,
if (blurhash != null) 'blurhash': blurhash,
if (downloadState != null) 'download_state': downloadState, if (downloadState != null) 'download_state': downloadState,
if (requiresAuthentication != null) if (requiresAuthentication != null)
'requires_authentication': requiresAuthentication, 'requires_authentication': requiresAuthentication,
@ -4255,6 +4344,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
Value<String>? mediaId, Value<String>? mediaId,
Value<MediaType>? type, Value<MediaType>? type,
Value<UploadState?>? uploadState, Value<UploadState?>? uploadState,
Value<CloudState>? cloudState,
Value<String?>? blurhash,
Value<DownloadState?>? downloadState, Value<DownloadState?>? downloadState,
Value<bool>? requiresAuthentication, Value<bool>? requiresAuthentication,
Value<bool>? stored, Value<bool>? stored,
@ -4280,6 +4371,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
mediaId: mediaId ?? this.mediaId, mediaId: mediaId ?? this.mediaId,
type: type ?? this.type, type: type ?? this.type,
uploadState: uploadState ?? this.uploadState, uploadState: uploadState ?? this.uploadState,
cloudState: cloudState ?? this.cloudState,
blurhash: blurhash ?? this.blurhash,
downloadState: downloadState ?? this.downloadState, downloadState: downloadState ?? this.downloadState,
requiresAuthentication: requiresAuthentication:
requiresAuthentication ?? this.requiresAuthentication, requiresAuthentication ?? this.requiresAuthentication,
@ -4322,6 +4415,14 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
$MediaFilesTable.$converteruploadStaten.toSql(uploadState.value), $MediaFilesTable.$converteruploadStaten.toSql(uploadState.value),
); );
} }
if (cloudState.present) {
map['cloud_state'] = Variable<String>(
$MediaFilesTable.$convertercloudState.toSql(cloudState.value),
);
}
if (blurhash.present) {
map['blurhash'] = Variable<String>(blurhash.value);
}
if (downloadState.present) { if (downloadState.present) {
map['download_state'] = Variable<String>( map['download_state'] = Variable<String>(
$MediaFilesTable.$converterdownloadStaten.toSql(downloadState.value), $MediaFilesTable.$converterdownloadStaten.toSql(downloadState.value),
@ -4403,6 +4504,8 @@ class MediaFilesCompanion extends UpdateCompanion<MediaFile> {
..write('mediaId: $mediaId, ') ..write('mediaId: $mediaId, ')
..write('type: $type, ') ..write('type: $type, ')
..write('uploadState: $uploadState, ') ..write('uploadState: $uploadState, ')
..write('cloudState: $cloudState, ')
..write('blurhash: $blurhash, ')
..write('downloadState: $downloadState, ') ..write('downloadState: $downloadState, ')
..write('requiresAuthentication: $requiresAuthentication, ') ..write('requiresAuthentication: $requiresAuthentication, ')
..write('stored: $stored, ') ..write('stored: $stored, ')
@ -16080,6 +16183,8 @@ typedef $$MediaFilesTableCreateCompanionBuilder =
required String mediaId, required String mediaId,
required MediaType type, required MediaType type,
Value<UploadState?> uploadState, Value<UploadState?> uploadState,
Value<CloudState> cloudState,
Value<String?> blurhash,
Value<DownloadState?> downloadState, Value<DownloadState?> downloadState,
Value<bool> requiresAuthentication, Value<bool> requiresAuthentication,
Value<bool> stored, Value<bool> stored,
@ -16106,6 +16211,8 @@ typedef $$MediaFilesTableUpdateCompanionBuilder =
Value<String> mediaId, Value<String> mediaId,
Value<MediaType> type, Value<MediaType> type,
Value<UploadState?> uploadState, Value<UploadState?> uploadState,
Value<CloudState> cloudState,
Value<String?> blurhash,
Value<DownloadState?> downloadState, Value<DownloadState?> downloadState,
Value<bool> requiresAuthentication, Value<bool> requiresAuthentication,
Value<bool> stored, Value<bool> stored,
@ -16177,6 +16284,17 @@ class $$MediaFilesTableFilterComposer
builder: (column) => ColumnWithTypeConverterFilters(column), builder: (column) => ColumnWithTypeConverterFilters(column),
); );
ColumnWithTypeConverterFilters<CloudState, CloudState, String>
get cloudState => $composableBuilder(
column: $table.cloudState,
builder: (column) => ColumnWithTypeConverterFilters(column),
);
ColumnFilters<String> get blurhash => $composableBuilder(
column: $table.blurhash,
builder: (column) => ColumnFilters(column),
);
ColumnWithTypeConverterFilters<DownloadState?, DownloadState, String> ColumnWithTypeConverterFilters<DownloadState?, DownloadState, String>
get downloadState => $composableBuilder( get downloadState => $composableBuilder(
column: $table.downloadState, column: $table.downloadState,
@ -16324,6 +16442,16 @@ class $$MediaFilesTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get cloudState => $composableBuilder(
column: $table.cloudState,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get blurhash => $composableBuilder(
column: $table.blurhash,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get downloadState => $composableBuilder( ColumnOrderings<String> get downloadState => $composableBuilder(
column: $table.downloadState, column: $table.downloadState,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@ -16441,6 +16569,15 @@ class $$MediaFilesTableAnnotationComposer
builder: (column) => column, builder: (column) => column,
); );
GeneratedColumnWithTypeConverter<CloudState, String> get cloudState =>
$composableBuilder(
column: $table.cloudState,
builder: (column) => column,
);
GeneratedColumn<String> get blurhash =>
$composableBuilder(column: $table.blurhash, builder: (column) => column);
GeneratedColumnWithTypeConverter<DownloadState?, String> get downloadState => GeneratedColumnWithTypeConverter<DownloadState?, String> get downloadState =>
$composableBuilder( $composableBuilder(
column: $table.downloadState, column: $table.downloadState,
@ -16591,6 +16728,8 @@ class $$MediaFilesTableTableManager
Value<String> mediaId = const Value.absent(), Value<String> mediaId = const Value.absent(),
Value<MediaType> type = const Value.absent(), Value<MediaType> type = const Value.absent(),
Value<UploadState?> uploadState = const Value.absent(), Value<UploadState?> uploadState = const Value.absent(),
Value<CloudState> cloudState = const Value.absent(),
Value<String?> blurhash = const Value.absent(),
Value<DownloadState?> downloadState = const Value.absent(), Value<DownloadState?> downloadState = const Value.absent(),
Value<bool> requiresAuthentication = const Value.absent(), Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> stored = const Value.absent(), Value<bool> stored = const Value.absent(),
@ -16615,6 +16754,8 @@ class $$MediaFilesTableTableManager
mediaId: mediaId, mediaId: mediaId,
type: type, type: type,
uploadState: uploadState, uploadState: uploadState,
cloudState: cloudState,
blurhash: blurhash,
downloadState: downloadState, downloadState: downloadState,
requiresAuthentication: requiresAuthentication, requiresAuthentication: requiresAuthentication,
stored: stored, stored: stored,
@ -16641,6 +16782,8 @@ class $$MediaFilesTableTableManager
required String mediaId, required String mediaId,
required MediaType type, required MediaType type,
Value<UploadState?> uploadState = const Value.absent(), Value<UploadState?> uploadState = const Value.absent(),
Value<CloudState> cloudState = const Value.absent(),
Value<String?> blurhash = const Value.absent(),
Value<DownloadState?> downloadState = const Value.absent(), Value<DownloadState?> downloadState = const Value.absent(),
Value<bool> requiresAuthentication = const Value.absent(), Value<bool> requiresAuthentication = const Value.absent(),
Value<bool> stored = const Value.absent(), Value<bool> stored = const Value.absent(),
@ -16665,6 +16808,8 @@ class $$MediaFilesTableTableManager
mediaId: mediaId, mediaId: mediaId,
type: type, type: type,
uploadState: uploadState, uploadState: uploadState,
cloudState: cloudState,
blurhash: blurhash,
downloadState: downloadState, downloadState: downloadState,
requiresAuthentication: requiresAuthentication, requiresAuthentication: requiresAuthentication,
stored: stored, stored: stored,

View file

@ -11600,6 +11600,539 @@ i1.GeneratedColumn<int> _column_254(String aliasedName) =>
type: i1.DriftSqlType.int, type: i1.DriftSqlType.int,
$customConstraints: 'NULL', $customConstraints: 'NULL',
); );
final class Schema23 extends i0.VersionedSchema {
Schema23({required super.database}) : super(version: 23);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
contacts,
groups,
mediaFiles,
messages,
messageHistories,
reactions,
groupMembers,
receipts,
receivedReceipts,
signalIdentityKeyStores,
signalPreKeyStores,
signalSenderKeyStores,
signalSessionStores,
signalSignedPreKeyStores,
messageActions,
groupHistories,
keyVerifications,
verificationTokens,
userDiscoveryAnnouncedUsers,
userDiscoveryUserRelations,
userDiscoveryOtherPromotions,
userDiscoveryOwnPromotions,
userDiscoveryShares,
shortcuts,
shortcutMembers,
];
late final Shape57 contacts = Shape57(
source: i0.VersionedTable(
entityName: 'contacts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(user_id)'],
columns: [
_column_106,
_column_107,
_column_108,
_column_109,
_column_110,
_column_111,
_column_112,
_column_113,
_column_114,
_column_115,
_column_116,
_column_117,
_column_118,
_column_211,
_column_212,
_column_213,
_column_249,
_column_250,
_column_251,
_column_252,
_column_253,
_column_254,
_column_247,
_column_214,
_column_215,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape23 groups = Shape23(
source: i0.VersionedTable(
entityName: 'groups',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id)'],
columns: [
_column_119,
_column_120,
_column_121,
_column_122,
_column_123,
_column_124,
_column_125,
_column_126,
_column_127,
_column_128,
_column_129,
_column_130,
_column_131,
_column_132,
_column_133,
_column_134,
_column_118,
_column_135,
_column_136,
_column_137,
_column_138,
_column_139,
_column_140,
_column_141,
_column_142,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape58 mediaFiles = Shape58(
source: i0.VersionedTable(
entityName: 'media_files',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(media_id)'],
columns: [
_column_143,
_column_144,
_column_145,
_column_255,
_column_256,
_column_146,
_column_147,
_column_148,
_column_149,
_column_239,
_column_240,
_column_207,
_column_150,
_column_151,
_column_152,
_column_153,
_column_154,
_column_155,
_column_156,
_column_157,
_column_244,
_column_245,
_column_118,
_column_241,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape25 messages = Shape25(
source: i0.VersionedTable(
entityName: 'messages',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id)'],
columns: [
_column_158,
_column_159,
_column_160,
_column_144,
_column_161,
_column_162,
_column_163,
_column_164,
_column_165,
_column_153,
_column_166,
_column_167,
_column_168,
_column_169,
_column_118,
_column_170,
_column_171,
_column_172,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape26 messageHistories = Shape26(
source: i0.VersionedTable(
entityName: 'message_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_173,
_column_174,
_column_175,
_column_161,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape27 reactions = Shape27(
source: i0.VersionedTable(
entityName: 'reactions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, sender_id, emoji)'],
columns: [_column_174, _column_176, _column_177, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape38 groupMembers = Shape38(
source: i0.VersionedTable(
entityName: 'group_members',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_id, contact_id)'],
columns: [
_column_158,
_column_178,
_column_179,
_column_180,
_column_209,
_column_210,
_column_181,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape37 receipts = Shape37(
source: i0.VersionedTable(
entityName: 'receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [
_column_182,
_column_183,
_column_184,
_column_185,
_column_186,
_column_208,
_column_187,
_column_188,
_column_189,
_column_190,
_column_191,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape30 receivedReceipts = Shape30(
source: i0.VersionedTable(
entityName: 'received_receipts',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(receipt_id)'],
columns: [_column_182, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape31 signalIdentityKeyStores = Shape31(
source: i0.VersionedTable(
entityName: 'signal_identity_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_194, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape32 signalPreKeyStores = Shape32(
source: i0.VersionedTable(
entityName: 'signal_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(pre_key_id)'],
columns: [_column_195, _column_196, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 signalSenderKeyStores = Shape11(
source: i0.VersionedTable(
entityName: 'signal_sender_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(sender_key_name)'],
columns: [_column_197, _column_198],
attachedDatabase: database,
),
alias: null,
);
late final Shape33 signalSessionStores = Shape33(
source: i0.VersionedTable(
entityName: 'signal_session_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(device_id, name)'],
columns: [_column_192, _column_193, _column_199, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape50 signalSignedPreKeyStores = Shape50(
source: i0.VersionedTable(
entityName: 'signal_signed_pre_key_stores',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(signed_pre_key_id)'],
columns: [_column_242, _column_243, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape34 messageActions = Shape34(
source: i0.VersionedTable(
entityName: 'message_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(message_id, contact_id, type)'],
columns: [_column_174, _column_183, _column_144, _column_200],
attachedDatabase: database,
),
alias: null,
);
late final Shape35 groupHistories = Shape35(
source: i0.VersionedTable(
entityName: 'group_histories',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(group_history_id)'],
columns: [
_column_201,
_column_158,
_column_202,
_column_203,
_column_204,
_column_205,
_column_206,
_column_144,
_column_200,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape54 keyVerifications = Shape54(
source: i0.VersionedTable(
entityName: 'key_verifications',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_216,
_column_183,
_column_144,
_column_248,
_column_118,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape41 verificationTokens = Shape41(
source: i0.VersionedTable(
entityName: 'verification_tokens',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_217, _column_218, _column_118],
attachedDatabase: database,
),
alias: null,
);
late final Shape52 userDiscoveryAnnouncedUsers = Shape52(
source: i0.VersionedTable(
entityName: 'user_discovery_announced_users',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(announced_user_id)'],
columns: [
_column_219,
_column_220,
_column_221,
_column_222,
_column_223,
_column_224,
_column_246,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape43 userDiscoveryUserRelations = Shape43(
source: i0.VersionedTable(
entityName: 'user_discovery_user_relations',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(announced_user_id, from_contact_id)'],
columns: [_column_225, _column_226, _column_227],
attachedDatabase: database,
),
alias: null,
);
late final Shape44 userDiscoveryOtherPromotions = Shape44(
source: i0.VersionedTable(
entityName: 'user_discovery_other_promotions',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(from_contact_id, public_id)'],
columns: [
_column_226,
_column_228,
_column_229,
_column_230,
_column_231,
_column_227,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape45 userDiscoveryOwnPromotions = Shape45(
source: i0.VersionedTable(
entityName: 'user_discovery_own_promotions',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_232, _column_183, _column_233],
attachedDatabase: database,
),
alias: null,
);
late final Shape46 userDiscoveryShares = Shape46(
source: i0.VersionedTable(
entityName: 'user_discovery_shares',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_234, _column_235, _column_175],
attachedDatabase: database,
),
alias: null,
);
late final Shape47 shortcuts = Shape47(
source: i0.VersionedTable(
entityName: 'shortcuts',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_173, _column_236, _column_237],
attachedDatabase: database,
),
alias: null,
);
late final Shape48 shortcutMembers = Shape48(
source: i0.VersionedTable(
entityName: 'shortcut_members',
withoutRowId: false,
isStrict: false,
tableConstraints: ['PRIMARY KEY(shortcut_id, group_id)'],
columns: [_column_238, _column_158],
attachedDatabase: database,
),
alias: null,
);
}
class Shape58 extends i0.VersionedTable {
Shape58({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get mediaId =>
columnsByName['media_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get type =>
columnsByName['type']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get uploadState =>
columnsByName['upload_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get cloudState =>
columnsByName['cloud_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get blurhash =>
columnsByName['blurhash']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get downloadState =>
columnsByName['download_state']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get requiresAuthentication =>
columnsByName['requires_authentication']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get stored =>
columnsByName['stored']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get isDraftMedia =>
columnsByName['is_draft_media']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get hasCropAnalyzed =>
columnsByName['has_crop_analyzed']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get preProgressingProcess =>
columnsByName['pre_progressing_process']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get reuploadRequestedBy =>
columnsByName['reupload_requested_by']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get displayLimitInMilliseconds =>
columnsByName['display_limit_in_milliseconds']!
as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get removeAudio =>
columnsByName['remove_audio']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<i2.Uint8List> get downloadToken =>
columnsByName['download_token']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionKey =>
columnsByName['encryption_key']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionMac =>
columnsByName['encryption_mac']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get encryptionNonce =>
columnsByName['encryption_nonce']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<i2.Uint8List> get storedFileHash =>
columnsByName['stored_file_hash']! as i1.GeneratedColumn<i2.Uint8List>;
i1.GeneratedColumn<int> get hasThumbnail =>
columnsByName['has_thumbnail']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get sizeInBytes =>
columnsByName['size_in_bytes']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get createdAtMonth =>
columnsByName['created_at_month']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<String> _column_255(String aliasedName) =>
i1.GeneratedColumn<String>(
'cloud_state',
aliasedName,
false,
type: i1.DriftSqlType.string,
$customConstraints: 'NOT NULL DEFAULT \'none\'',
defaultValue: const i1.CustomExpression('\'none\''),
);
i1.GeneratedColumn<String> _column_256(String aliasedName) =>
i1.GeneratedColumn<String>(
'blurhash',
aliasedName,
true,
type: i1.DriftSqlType.string,
$customConstraints: 'NULL',
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -11622,6 +12155,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20, required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21, required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22, required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22,
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -11730,6 +12264,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from21To22(migrator, schema); await from21To22(migrator, schema);
return 22; return 22;
case 22:
final schema = Schema23(database: database);
final migrator = i1.Migrator(database, schema);
await from22To23(migrator, schema);
return 23;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -11758,6 +12297,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20, required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21, required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22, required Future<void> Function(i1.Migrator m, Schema22 schema) from21To22,
required Future<void> Function(i1.Migrator m, Schema23 schema) from22To23,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@ -11781,5 +12321,6 @@ i1.OnUpgrade stepByStep({
from19To20: from19To20, from19To20: from19To20,
from20To21: from20To21, from20To21: from20To21,
from21To22: from21To22, from21To22: from21To22,
from22To23: from22To23,
), ),
); );

View file

@ -1265,13 +1265,13 @@ abstract class AppLocalizations {
/// No description provided for @proFeature3. /// No description provided for @proFeature3.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Restore flames'** /// **'25GB Memories storage'**
String get proFeature3; String get proFeature3;
/// No description provided for @proFeature4. /// No description provided for @proFeature4.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Support twonly'** /// **'Restore flames'**
String get proFeature4; String get proFeature4;
/// No description provided for @familyFeature1. /// No description provided for @familyFeature1.
@ -1289,7 +1289,7 @@ abstract class AppLocalizations {
/// No description provided for @familyFeature3. /// No description provided for @familyFeature3.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Restore flames'** /// **'50GB Memories storage'**
String get familyFeature3; String get familyFeature3;
/// No description provided for @familyFeature4. /// No description provided for @familyFeature4.

View file

@ -652,10 +652,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get proFeature2 => '✓ 1 zusätzlicher Plus Benutzer'; String get proFeature2 => '✓ 1 zusätzlicher Plus Benutzer';
@override @override
String get proFeature3 => 'Flammen wiederherstellen'; String get proFeature3 => '25GB Memories Speicher';
@override @override
String get proFeature4 => 'twonly unterstützen'; String get proFeature4 => 'Flammen wiederherstellen';
@override @override
String get familyFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads'; String get familyFeature1 => '✓ Unbegrenzte Medien-Datei-Uploads';
@ -664,10 +664,10 @@ class AppLocalizationsDe extends AppLocalizations {
String get familyFeature2 => '✓ 4 zusätzliche Plus Benutzer'; String get familyFeature2 => '✓ 4 zusätzliche Plus Benutzer';
@override @override
String get familyFeature3 => '✓ Flammen wiederherstellen'; String get familyFeature3 => '50GB Memories Speicher';
@override @override
String get familyFeature4 => 'twonly unterstützen'; String get familyFeature4 => 'Flammen wiederherstellen';
@override @override
String get freeFeature1 => '✓ 10 Medien-Datei-Uploads pro Tag'; String get freeFeature1 => '✓ 10 Medien-Datei-Uploads pro Tag';

View file

@ -647,10 +647,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get proFeature2 => '✓ 1 additional Plus user'; String get proFeature2 => '✓ 1 additional Plus user';
@override @override
String get proFeature3 => 'Restore flames'; String get proFeature3 => '25GB Memories storage';
@override @override
String get proFeature4 => 'Support twonly'; String get proFeature4 => 'Restore flames';
@override @override
String get familyFeature1 => '✓ Unlimited media file uploads'; String get familyFeature1 => '✓ Unlimited media file uploads';
@ -659,7 +659,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get familyFeature2 => '✓ 4 additional Plus user'; String get familyFeature2 => '✓ 4 additional Plus user';
@override @override
String get familyFeature3 => 'Restore flames'; String get familyFeature3 => '50GB Memories storage';
@override @override
String get familyFeature4 => '✓ Support twonly'; String get familyFeature4 => '✓ Support twonly';

@ -1 +1 @@
Subproject commit ef0aed22f39a7453a9dbdbe26a098159b792dbac Subproject commit 499ad4a7b703c1dff414f531c528894ad0c91799

View file

@ -11,9 +11,6 @@ ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson(
) => ReceivedRecoveryShare( ) => ReceivedRecoveryShare(
messageId: (json['messageId'] as num).toInt(), messageId: (json['messageId'] as num).toInt(),
trustedFriendDisplayName: json['trustedFriendDisplayName'] as String, trustedFriendDisplayName: json['trustedFriendDisplayName'] as String,
trustedFriendAvatarSvg: (json['trustedFriendAvatarSvg'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
myDisplayName: json['myDisplayName'] as String, myDisplayName: json['myDisplayName'] as String,
myUserId: (json['myUserId'] as num).toInt(), myUserId: (json['myUserId'] as num).toInt(),
myAvatarSvg: (json['myAvatarSvg'] as List<dynamic>?) myAvatarSvg: (json['myAvatarSvg'] as List<dynamic>?)
@ -23,6 +20,9 @@ ReceivedRecoveryShare _$ReceivedRecoveryShareFromJson(
sharedSecretDataBytes: (json['sharedSecretDataBytes'] as List<dynamic>) sharedSecretDataBytes: (json['sharedSecretDataBytes'] as List<dynamic>)
.map((e) => (e as num).toInt()) .map((e) => (e as num).toInt())
.toList(), .toList(),
trustedFriendAvatarSvg: (json['trustedFriendAvatarSvg'] as List<dynamic>?)
?.map((e) => (e as num).toInt())
.toList(),
); );
Map<String, dynamic> _$ReceivedRecoveryShareToJson( Map<String, dynamic> _$ReceivedRecoveryShareToJson(

View file

@ -150,8 +150,8 @@ class UserData {
@Deprecated('Use the secure storage in rust') @Deprecated('Use the secure storage in rust')
TwonlySafeBackup? twonlySafeBackup; TwonlySafeBackup? twonlySafeBackup;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: true)
bool isBackupEnabled = false; bool isBackupEnabled = true;
PasswordLessRecovery? passwordLessRecovery; PasswordLessRecovery? passwordLessRecovery;

View file

@ -96,7 +96,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
: TwonlySafeBackup.fromJson( : TwonlySafeBackup.fromJson(
json['twonlySafeBackup'] as Map<String, dynamic>, json['twonlySafeBackup'] as Map<String, dynamic>,
) )
..isBackupEnabled = json['isBackupEnabled'] as bool? ?? false ..isBackupEnabled = json['isBackupEnabled'] as bool? ?? true
..passwordLessRecovery = json['passwordLessRecovery'] == null ..passwordLessRecovery = json['passwordLessRecovery'] == null
? null ? null
: PasswordLessRecovery.fromJson( : PasswordLessRecovery.fromJson(

View file

@ -2671,10 +2671,12 @@ class ApplicationData_RequestMemoriesUpload extends $pb.GeneratedMessage {
factory ApplicationData_RequestMemoriesUpload({ factory ApplicationData_RequestMemoriesUpload({
$fixnum.Int64? size, $fixnum.Int64? size,
$fixnum.Int64? originalDate, $fixnum.Int64? originalDate,
$core.String? mediaId,
}) { }) {
final result = create(); final result = create();
if (size != null) result.size = size; if (size != null) result.size = size;
if (originalDate != null) result.originalDate = originalDate; if (originalDate != null) result.originalDate = originalDate;
if (mediaId != null) result.mediaId = mediaId;
return result; return result;
} }
@ -2695,6 +2697,7 @@ class ApplicationData_RequestMemoriesUpload extends $pb.GeneratedMessage {
createEmptyInstance: create) createEmptyInstance: create)
..aInt64(1, _omitFieldNames ? '' : 'size') ..aInt64(1, _omitFieldNames ? '' : 'size')
..aInt64(2, _omitFieldNames ? '' : 'originalDate') ..aInt64(2, _omitFieldNames ? '' : 'originalDate')
..aOS(3, _omitFieldNames ? '' : 'mediaId')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -2737,6 +2740,15 @@ class ApplicationData_RequestMemoriesUpload extends $pb.GeneratedMessage {
$core.bool hasOriginalDate() => $_has(1); $core.bool hasOriginalDate() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearOriginalDate() => $_clearField(2); void clearOriginalDate() => $_clearField(2);
@$pb.TagNumber(3)
$core.String get mediaId => $_getSZ(2);
@$pb.TagNumber(3)
set mediaId($core.String value) => $_setString(2, value);
@$pb.TagNumber(3)
$core.bool hasMediaId() => $_has(2);
@$pb.TagNumber(3)
void clearMediaId() => $_clearField(3);
} }
class ApplicationData_ConfirmMemoriesUpload extends $pb.GeneratedMessage { class ApplicationData_ConfirmMemoriesUpload extends $pb.GeneratedMessage {
@ -2873,9 +2885,11 @@ class ApplicationData_GetMemoriesList extends $pb.GeneratedMessage {
class ApplicationData_GetMemoriesUrl extends $pb.GeneratedMessage { class ApplicationData_GetMemoriesUrl extends $pb.GeneratedMessage {
factory ApplicationData_GetMemoriesUrl({ factory ApplicationData_GetMemoriesUrl({
$core.String? mediaId, $core.String? mediaId,
$core.bool? thumbnail,
}) { }) {
final result = create(); final result = create();
if (mediaId != null) result.mediaId = mediaId; if (mediaId != null) result.mediaId = mediaId;
if (thumbnail != null) result.thumbnail = thumbnail;
return result; return result;
} }
@ -2894,6 +2908,7 @@ class ApplicationData_GetMemoriesUrl extends $pb.GeneratedMessage {
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'), const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create) createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'mediaId') ..aOS(1, _omitFieldNames ? '' : 'mediaId')
..aOB(2, _omitFieldNames ? '' : 'thumbnail')
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -2926,6 +2941,15 @@ class ApplicationData_GetMemoriesUrl extends $pb.GeneratedMessage {
$core.bool hasMediaId() => $_has(0); $core.bool hasMediaId() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearMediaId() => $_clearField(1); void clearMediaId() => $_clearField(1);
@$pb.TagNumber(2)
$core.bool get thumbnail => $_getBF(1);
@$pb.TagNumber(2)
set thumbnail($core.bool value) => $_setBool(1, value);
@$pb.TagNumber(2)
$core.bool hasThumbnail() => $_has(1);
@$pb.TagNumber(2)
void clearThumbnail() => $_clearField(2);
} }
class ApplicationData_GetMemoriesUsage extends $pb.GeneratedMessage { class ApplicationData_GetMemoriesUsage extends $pb.GeneratedMessage {
@ -2972,6 +2996,64 @@ class ApplicationData_GetMemoriesUsage extends $pb.GeneratedMessage {
static ApplicationData_GetMemoriesUsage? _defaultInstance; static ApplicationData_GetMemoriesUsage? _defaultInstance;
} }
class ApplicationData_DeleteMemory extends $pb.GeneratedMessage {
factory ApplicationData_DeleteMemory({
$core.String? mediaId,
}) {
final result = create();
if (mediaId != null) result.mediaId = mediaId;
return result;
}
ApplicationData_DeleteMemory._();
factory ApplicationData_DeleteMemory.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory ApplicationData_DeleteMemory.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'ApplicationData.DeleteMemory',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'client_to_server'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'mediaId')
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_DeleteMemory clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
ApplicationData_DeleteMemory copyWith(
void Function(ApplicationData_DeleteMemory) updates) =>
super.copyWith(
(message) => updates(message as ApplicationData_DeleteMemory))
as ApplicationData_DeleteMemory;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static ApplicationData_DeleteMemory create() =>
ApplicationData_DeleteMemory._();
@$core.override
ApplicationData_DeleteMemory createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static ApplicationData_DeleteMemory getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<ApplicationData_DeleteMemory>(create);
static ApplicationData_DeleteMemory? _defaultInstance;
@$pb.TagNumber(1)
$core.String get mediaId => $_getSZ(0);
@$pb.TagNumber(1)
set mediaId($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasMediaId() => $_has(0);
@$pb.TagNumber(1)
void clearMediaId() => $_clearField(1);
}
enum ApplicationData_ApplicationData { enum ApplicationData_ApplicationData {
textMessage, textMessage,
getUserByUsername, getUserByUsername,
@ -3006,6 +3088,7 @@ enum ApplicationData_ApplicationData {
getMemoriesList, getMemoriesList,
getMemoriesUrl, getMemoriesUrl,
getMemoriesUsage, getMemoriesUsage,
deleteMemory,
notSet notSet
} }
@ -3044,6 +3127,7 @@ class ApplicationData extends $pb.GeneratedMessage {
ApplicationData_GetMemoriesList? getMemoriesList, ApplicationData_GetMemoriesList? getMemoriesList,
ApplicationData_GetMemoriesUrl? getMemoriesUrl, ApplicationData_GetMemoriesUrl? getMemoriesUrl,
ApplicationData_GetMemoriesUsage? getMemoriesUsage, ApplicationData_GetMemoriesUsage? getMemoriesUsage,
ApplicationData_DeleteMemory? deleteMemory,
}) { }) {
final result = create(); final result = create();
if (textMessage != null) result.textMessage = textMessage; if (textMessage != null) result.textMessage = textMessage;
@ -3089,6 +3173,7 @@ class ApplicationData extends $pb.GeneratedMessage {
if (getMemoriesList != null) result.getMemoriesList = getMemoriesList; if (getMemoriesList != null) result.getMemoriesList = getMemoriesList;
if (getMemoriesUrl != null) result.getMemoriesUrl = getMemoriesUrl; if (getMemoriesUrl != null) result.getMemoriesUrl = getMemoriesUrl;
if (getMemoriesUsage != null) result.getMemoriesUsage = getMemoriesUsage; if (getMemoriesUsage != null) result.getMemoriesUsage = getMemoriesUsage;
if (deleteMemory != null) result.deleteMemory = deleteMemory;
return result; return result;
} }
@ -3136,6 +3221,7 @@ class ApplicationData extends $pb.GeneratedMessage {
35: ApplicationData_ApplicationData.getMemoriesList, 35: ApplicationData_ApplicationData.getMemoriesList,
36: ApplicationData_ApplicationData.getMemoriesUrl, 36: ApplicationData_ApplicationData.getMemoriesUrl,
37: ApplicationData_ApplicationData.getMemoriesUsage, 37: ApplicationData_ApplicationData.getMemoriesUsage,
38: ApplicationData_ApplicationData.deleteMemory,
0: ApplicationData_ApplicationData.notSet 0: ApplicationData_ApplicationData.notSet
}; };
static final $pb.BuilderInfo _i = $pb.BuilderInfo( static final $pb.BuilderInfo _i = $pb.BuilderInfo(
@ -3176,7 +3262,8 @@ class ApplicationData extends $pb.GeneratedMessage {
34, 34,
35, 35,
36, 36,
37 37,
38
]) ])
..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage', ..aOM<ApplicationData_TextMessage>(1, _omitFieldNames ? '' : 'textMessage',
protoName: 'textMessage', protoName: 'textMessage',
@ -3289,6 +3376,9 @@ class ApplicationData extends $pb.GeneratedMessage {
..aOM<ApplicationData_GetMemoriesUsage>( ..aOM<ApplicationData_GetMemoriesUsage>(
37, _omitFieldNames ? '' : 'getMemoriesUsage', 37, _omitFieldNames ? '' : 'getMemoriesUsage',
subBuilder: ApplicationData_GetMemoriesUsage.create) subBuilder: ApplicationData_GetMemoriesUsage.create)
..aOM<ApplicationData_DeleteMemory>(
38, _omitFieldNames ? '' : 'deleteMemory',
subBuilder: ApplicationData_DeleteMemory.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -3343,6 +3433,7 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(35) @$pb.TagNumber(35)
@$pb.TagNumber(36) @$pb.TagNumber(36)
@$pb.TagNumber(37) @$pb.TagNumber(37)
@$pb.TagNumber(38)
ApplicationData_ApplicationData whichApplicationData() => ApplicationData_ApplicationData whichApplicationData() =>
_ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!; _ApplicationData_ApplicationDataByTag[$_whichOneof(0)]!;
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -3378,6 +3469,7 @@ class ApplicationData extends $pb.GeneratedMessage {
@$pb.TagNumber(35) @$pb.TagNumber(35)
@$pb.TagNumber(36) @$pb.TagNumber(36)
@$pb.TagNumber(37) @$pb.TagNumber(37)
@$pb.TagNumber(38)
void clearApplicationData() => $_clearField($_whichOneof(0)); void clearApplicationData() => $_clearField($_whichOneof(0));
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -3775,6 +3867,17 @@ class ApplicationData extends $pb.GeneratedMessage {
void clearGetMemoriesUsage() => $_clearField(37); void clearGetMemoriesUsage() => $_clearField(37);
@$pb.TagNumber(37) @$pb.TagNumber(37)
ApplicationData_GetMemoriesUsage ensureGetMemoriesUsage() => $_ensure(32); ApplicationData_GetMemoriesUsage ensureGetMemoriesUsage() => $_ensure(32);
@$pb.TagNumber(38)
ApplicationData_DeleteMemory get deleteMemory => $_getN(33);
@$pb.TagNumber(38)
set deleteMemory(ApplicationData_DeleteMemory value) => $_setField(38, value);
@$pb.TagNumber(38)
$core.bool hasDeleteMemory() => $_has(33);
@$pb.TagNumber(38)
void clearDeleteMemory() => $_clearField(38);
@$pb.TagNumber(38)
ApplicationData_DeleteMemory ensureDeleteMemory() => $_ensure(33);
} }
class Response_PreKey extends $pb.GeneratedMessage { class Response_PreKey extends $pb.GeneratedMessage {

View file

@ -778,6 +778,15 @@ const ApplicationData$json = {
'9': 0, '9': 0,
'10': 'getMemoriesUsage' '10': 'getMemoriesUsage'
}, },
{
'1': 'delete_memory',
'3': 38,
'4': 1,
'5': 11,
'6': '.client_to_server.ApplicationData.DeleteMemory',
'9': 0,
'10': 'deleteMemory'
},
], ],
'3': [ '3': [
ApplicationData_TextMessage$json, ApplicationData_TextMessage$json,
@ -805,7 +814,8 @@ const ApplicationData$json = {
ApplicationData_ConfirmMemoriesUpload$json, ApplicationData_ConfirmMemoriesUpload$json,
ApplicationData_GetMemoriesList$json, ApplicationData_GetMemoriesList$json,
ApplicationData_GetMemoriesUrl$json, ApplicationData_GetMemoriesUrl$json,
ApplicationData_GetMemoriesUsage$json ApplicationData_GetMemoriesUsage$json,
ApplicationData_DeleteMemory$json
], ],
'8': [ '8': [
{'1': 'ApplicationData'}, {'1': 'ApplicationData'},
@ -1026,6 +1036,7 @@ const ApplicationData_RequestMemoriesUpload$json = {
'2': [ '2': [
{'1': 'size', '3': 1, '4': 1, '5': 3, '10': 'size'}, {'1': 'size', '3': 1, '4': 1, '5': 3, '10': 'size'},
{'1': 'original_date', '3': 2, '4': 1, '5': 3, '10': 'originalDate'}, {'1': 'original_date', '3': 2, '4': 1, '5': 3, '10': 'originalDate'},
{'1': 'media_id', '3': 3, '4': 1, '5': 9, '10': 'mediaId'},
], ],
}; };
@ -1051,6 +1062,7 @@ const ApplicationData_GetMemoriesUrl$json = {
'1': 'GetMemoriesUrl', '1': 'GetMemoriesUrl',
'2': [ '2': [
{'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'},
{'1': 'thumbnail', '3': 2, '4': 1, '5': 8, '10': 'thumbnail'},
], ],
}; };
@ -1059,6 +1071,14 @@ const ApplicationData_GetMemoriesUsage$json = {
'1': 'GetMemoriesUsage', '1': 'GetMemoriesUsage',
}; };
@$core.Deprecated('Use applicationDataDescriptor instead')
const ApplicationData_DeleteMemory$json = {
'1': 'DeleteMemory',
'2': [
{'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'},
],
};
/// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `ApplicationData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode( final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm' 'Cg9BcHBsaWNhdGlvbkRhdGESUQoLdGV4dE1lc3NhZ2UYASABKAsyLS5jbGllbnRfdG9fc2Vydm'
@ -1118,36 +1138,39 @@ final $typed_data.Uint8List applicationDataDescriptor = $convert.base64Decode(
'AFIPZ2V0TWVtb3JpZXNMaXN0ElwKEGdldF9tZW1vcmllc191cmwYJCABKAsyMC5jbGllbnRfdG' 'AFIPZ2V0TWVtb3JpZXNMaXN0ElwKEGdldF9tZW1vcmllc191cmwYJCABKAsyMC5jbGllbnRfdG'
'9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRNZW1vcmllc1VybEgAUg5nZXRNZW1vcmllc1Vy' '9fc2VydmVyLkFwcGxpY2F0aW9uRGF0YS5HZXRNZW1vcmllc1VybEgAUg5nZXRNZW1vcmllc1Vy'
'bBJiChJnZXRfbWVtb3JpZXNfdXNhZ2UYJSABKAsyMi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2' 'bBJiChJnZXRfbWVtb3JpZXNfdXNhZ2UYJSABKAsyMi5jbGllbnRfdG9fc2VydmVyLkFwcGxpY2'
'F0aW9uRGF0YS5HZXRNZW1vcmllc1VzYWdlSABSEGdldE1lbW9yaWVzVXNhZ2UaagoLVGV4dE1l' 'F0aW9uRGF0YS5HZXRNZW1vcmllc1VzYWdlSABSEGdldE1lbW9yaWVzVXNhZ2USVQoNZGVsZXRl'
'c3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZHkSIAoJcH' 'X21lbW9yeRgmIAEoCzIuLmNsaWVudF90b19zZXJ2ZXIuQXBwbGljYXRpb25EYXRhLkRlbGV0ZU'
'VzaF9kYXRhGAQgASgMSABSCHB1c2hEYXRhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0VXNlckJ5' '1lbW9yeUgAUgxkZWxldGVNZW1vcnkaagoLVGV4dE1lc3NhZ2USFwoHdXNlcl9pZBgBIAEoA1IG'
'VXNlcm5hbWUSGgoIdXNlcm5hbWUYASABKAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZXJuYW1lEh' 'dXNlcklkEhIKBGJvZHkYAyABKAxSBGJvZHkSIAoJcHVzaF9kYXRhGAQgASgMSABSCHB1c2hEYX'
'oKCHVzZXJuYW1lGAEgASgJUgh1c2VybmFtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tlbhIdCgpn' 'RhiAEBQgwKCl9wdXNoX2RhdGEaLwoRR2V0VXNlckJ5VXNlcm5hbWUSGgoIdXNlcm5hbWUYASAB'
'b29nbGVfZmNtGAEgASgJUglnb29nbGVGY20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl9pZBgBIA' 'KAlSCHVzZXJuYW1lGiwKDkNoYW5nZVVzZXJuYW1lEhoKCHVzZXJuYW1lGAEgASgJUgh1c2Vybm'
'EoA1IGdXNlcklkGhMKEUdldEF2YWlsYWJsZVBsYW5zGhUKE0dldEN1cnJlbnRQbGFuSW5mb3Ma' 'FtZRo1ChRVcGRhdGVHb29nbGVGY21Ub2tlbhIdCgpnb29nbGVfZmNtGAEgASgJUglnb29nbGVG'
'LwoUUmVtb3ZlQWRkaXRpb25hbFVzZXISFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldF' 'Y20aJgoLR2V0VXNlckJ5SWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGhMKEUdldEF2YWlsYW'
'ByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJl' 'JsZVBsYW5zGhUKE0dldEN1cnJlbnRQbGFuSW5mb3MaLwoUUmVtb3ZlQWRkaXRpb25hbFVzZXIS'
'S2V5QnlVc2VySWQSFwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcm' 'FwoHdXNlcl9pZBgBIAEoA1IGdXNlcklkGi0KEkdldFByZWtleXNCeVVzZXJJZBIXCgd1c2VyX2'
'VLZXkSKAoQc2lnbmVkX3ByZWtleV9pZBgBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVk' 'lkGAEgASgDUgZ1c2VySWQaMgoXR2V0U2lnbmVkUHJlS2V5QnlVc2VySWQSFwoHdXNlcl9pZBgB'
'X3ByZWtleRgCIAEoDFIMc2lnbmVkUHJla2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGA' 'IAEoA1IGdXNlcklkGpsBChJVcGRhdGVTaWduZWRQcmVLZXkSKAoQc2lnbmVkX3ByZWtleV9pZB'
'MgASgMUhVzaWduZWRQcmVrZXlTaWduYXR1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2Fk' 'gBIAEoA1IOc2lnbmVkUHJla2V5SWQSIwoNc2lnbmVkX3ByZWtleRgCIAEoDFIMc2lnbmVkUHJl'
'X3Rva2VuGAEgASgMUg1kb3dubG9hZFRva2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdX' 'a2V5EjYKF3NpZ25lZF9wcmVrZXlfc2lnbmF0dXJlGAMgASgMUhVzaWduZWRQcmVrZXlTaWduYX'
'Nlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoL' 'R1cmUaNQoMRG93bmxvYWREb25lEiUKDmRvd25sb2FkX3Rva2VuGAEgASgMUg1kb3dubG9hZFRv'
'SVBBUHVyY2hhc2USHQoKcHJvZHVjdF9pZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIA' 'a2VuGk4KClJlcG9ydFVzZXISKAoQcmVwb3J0ZWRfdXNlcl9pZBgBIAEoA1IOcmVwb3J0ZWRVc2'
'EoCVIGc291cmNlEisKEXZlcmlmaWNhdGlvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRh' 'VySWQSFgoGcmVhc29uGAIgASgJUgZyZWFzb24acQoLSVBBUHVyY2hhc2USHQoKcHJvZHVjdF9p'
'Gg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVsZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlch' 'ZBgBIAEoCVIJcHJvZHVjdElkEhYKBnNvdXJjZRgCIAEoCVIGc291cmNlEisKEXZlcmlmaWNhdG'
'IXCgd1c2VyX2lkGAEgASgDUgZ1c2VySWQaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tl' 'lvbl9kYXRhGAMgASgJUhB2ZXJpZmljYXRpb25EYXRhGg8KDUlQQUZvcmNlQ2hlY2saDwoNRGVs'
'bhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpEZXByZWNhdGVkGo4BChxSZWdpc3RlclBhc3N3b3JkTG' 'ZXRlQWNjb3VudBosChFBZGRBZGRpdGlvbmFsVXNlchIXCgd1c2VyX2lkGAEgASgDUgZ1c2VySW'
'Vzc1JlY292ZXJ5Ei4KEmVuY3J5cHRlZFNlcnZlcktleRgBIAEoDFISZW5jcnlwdGVkU2VydmVy' 'QaMAoNU2V0TG9naW5Ub2tlbhIfCgtsb2dpbl90b2tlbhgBIAEoDFIKbG9naW5Ub2tlbhoMCgpE'
'S2V5EisKDnBpblVubG9ja1Rva2VuGAIgASgMSABSDnBpblVubG9ja1Rva2VuiAEBQhEKD19waW' 'ZXByZWNhdGVkGo4BChxSZWdpc3RlclBhc3N3b3JkTGVzc1JlY292ZXJ5Ei4KEmVuY3J5cHRlZF'
'5VbmxvY2tUb2tlbhpwChhQYXNzd29yZGxlc3NOb3RpZmljYXRpb24SJwoPbm90aWZpY2F0aW9u' 'NlcnZlcktleRgBIAEoDFISZW5jcnlwdGVkU2VydmVyS2V5EisKDnBpblVubG9ja1Rva2VuGAIg'
'X2lkGAEgASgJUg5ub3RpZmljYXRpb25JZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIAEoDFIQZW' 'ASgMSABSDnBpblVubG9ja1Rva2VuiAEBQhEKD19waW5VbmxvY2tUb2tlbhpwChhQYXNzd29yZG'
'5jcnlwdGVkTWVzc2FnZRpQChVSZXF1ZXN0TWVtb3JpZXNVcGxvYWQSEgoEc2l6ZRgBIAEoA1IE' 'xlc3NOb3RpZmljYXRpb24SJwoPbm90aWZpY2F0aW9uX2lkGAEgASgJUg5ub3RpZmljYXRpb25J'
'c2l6ZRIjCg1vcmlnaW5hbF9kYXRlGAIgASgDUgxvcmlnaW5hbERhdGUaMgoVQ29uZmlybU1lbW' 'ZBIrChFlbmNyeXB0ZWRfbWVzc2FnZRgCIAEoDFIQZW5jcnlwdGVkTWVzc2FnZRprChVSZXF1ZX'
'9yaWVzVXBsb2FkEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkGkgKD0dldE1lbW9yaWVzTGlz' 'N0TWVtb3JpZXNVcGxvYWQSEgoEc2l6ZRgBIAEoA1IEc2l6ZRIjCg1vcmlnaW5hbF9kYXRlGAIg'
'dBIfCgtvZmZzZXRfZGF0ZRgBIAEoA1IKb2Zmc2V0RGF0ZRIUCgVsaW1pdBgCIAEoA1IFbGltaX' 'ASgDUgxvcmlnaW5hbERhdGUSGQoIbWVkaWFfaWQYAyABKAlSB21lZGlhSWQaMgoVQ29uZmlybU'
'QaKwoOR2V0TWVtb3JpZXNVcmwSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQaEgoQR2V0TWVt' '1lbW9yaWVzVXBsb2FkEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkGkgKD0dldE1lbW9yaWVz'
'b3JpZXNVc2FnZUIRCg9BcHBsaWNhdGlvbkRhdGE='); 'TGlzdBIfCgtvZmZzZXRfZGF0ZRgBIAEoA1IKb2Zmc2V0RGF0ZRIUCgVsaW1pdBgCIAEoA1IFbG'
'ltaXQaSQoOR2V0TWVtb3JpZXNVcmwSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSHAoJdGh1'
'bWJuYWlsGAIgASgIUgl0aHVtYm5haWwaEgoQR2V0TWVtb3JpZXNVc2FnZRopCgxEZWxldGVNZW'
'1vcnkSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWRCEQoPQXBwbGljYXRpb25EYXRh');
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response$json = { const Response$json = {

View file

@ -1647,17 +1647,82 @@ class Response_PasswordlessNotificationMessages extends $pb.GeneratedMessage {
$_getList(0); $_getList(0);
} }
class Response_PresignedPost extends $pb.GeneratedMessage {
factory Response_PresignedPost({
$core.String? url,
$core.Iterable<$core.MapEntry<$core.String, $core.String>>? fields,
}) {
final result = create();
if (url != null) result.url = url;
if (fields != null) result.fields.addEntries(fields);
return result;
}
Response_PresignedPost._();
factory Response_PresignedPost.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory Response_PresignedPost.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'Response.PresignedPost',
package:
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'url')
..m<$core.String, $core.String>(2, _omitFieldNames ? '' : 'fields',
entryClassName: 'Response.PresignedPost.FieldsEntry',
keyFieldType: $pb.PbFieldType.OS,
valueFieldType: $pb.PbFieldType.OS,
packageName: const $pb.PackageName('server_to_client'))
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PresignedPost clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
Response_PresignedPost copyWith(
void Function(Response_PresignedPost) updates) =>
super.copyWith((message) => updates(message as Response_PresignedPost))
as Response_PresignedPost;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static Response_PresignedPost create() => Response_PresignedPost._();
@$core.override
Response_PresignedPost createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static Response_PresignedPost getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<Response_PresignedPost>(create);
static Response_PresignedPost? _defaultInstance;
@$pb.TagNumber(1)
$core.String get url => $_getSZ(0);
@$pb.TagNumber(1)
set url($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasUrl() => $_has(0);
@$pb.TagNumber(1)
void clearUrl() => $_clearField(1);
@$pb.TagNumber(2)
$pb.PbMap<$core.String, $core.String> get fields => $_getMap(1);
}
class Response_MemoriesUploadUrls extends $pb.GeneratedMessage { class Response_MemoriesUploadUrls extends $pb.GeneratedMessage {
factory Response_MemoriesUploadUrls({ factory Response_MemoriesUploadUrls({
$core.String? mediaId, $core.String? mediaId,
$core.String? thumbnailUploadUrl, Response_PresignedPost? thumbnailUpload,
$core.String? fullUploadUrl, Response_PresignedPost? fullUpload,
}) { }) {
final result = create(); final result = create();
if (mediaId != null) result.mediaId = mediaId; if (mediaId != null) result.mediaId = mediaId;
if (thumbnailUploadUrl != null) if (thumbnailUpload != null) result.thumbnailUpload = thumbnailUpload;
result.thumbnailUploadUrl = thumbnailUploadUrl; if (fullUpload != null) result.fullUpload = fullUpload;
if (fullUploadUrl != null) result.fullUploadUrl = fullUploadUrl;
return result; return result;
} }
@ -1676,8 +1741,10 @@ class Response_MemoriesUploadUrls extends $pb.GeneratedMessage {
const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'), const $pb.PackageName(_omitMessageNames ? '' : 'server_to_client'),
createEmptyInstance: create) createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'mediaId') ..aOS(1, _omitFieldNames ? '' : 'mediaId')
..aOS(2, _omitFieldNames ? '' : 'thumbnailUploadUrl') ..aOM<Response_PresignedPost>(2, _omitFieldNames ? '' : 'thumbnailUpload',
..aOS(3, _omitFieldNames ? '' : 'fullUploadUrl') subBuilder: Response_PresignedPost.create)
..aOM<Response_PresignedPost>(3, _omitFieldNames ? '' : 'fullUpload',
subBuilder: Response_PresignedPost.create)
..hasRequiredFields = false; ..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
@ -1712,22 +1779,26 @@ class Response_MemoriesUploadUrls extends $pb.GeneratedMessage {
void clearMediaId() => $_clearField(1); void clearMediaId() => $_clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.String get thumbnailUploadUrl => $_getSZ(1); Response_PresignedPost get thumbnailUpload => $_getN(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set thumbnailUploadUrl($core.String value) => $_setString(1, value); set thumbnailUpload(Response_PresignedPost value) => $_setField(2, value);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasThumbnailUploadUrl() => $_has(1); $core.bool hasThumbnailUpload() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
void clearThumbnailUploadUrl() => $_clearField(2); void clearThumbnailUpload() => $_clearField(2);
@$pb.TagNumber(2)
Response_PresignedPost ensureThumbnailUpload() => $_ensure(1);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.String get fullUploadUrl => $_getSZ(2); Response_PresignedPost get fullUpload => $_getN(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
set fullUploadUrl($core.String value) => $_setString(2, value); set fullUpload(Response_PresignedPost value) => $_setField(3, value);
@$pb.TagNumber(3) @$pb.TagNumber(3)
$core.bool hasFullUploadUrl() => $_has(2); $core.bool hasFullUpload() => $_has(2);
@$pb.TagNumber(3) @$pb.TagNumber(3)
void clearFullUploadUrl() => $_clearField(3); void clearFullUpload() => $_clearField(3);
@$pb.TagNumber(3)
Response_PresignedPost ensureFullUpload() => $_ensure(2);
} }
class Response_MediaItem extends $pb.GeneratedMessage { class Response_MediaItem extends $pb.GeneratedMessage {

View file

@ -178,6 +178,7 @@ const Response$json = {
Response_ProofOfWork$json, Response_ProofOfWork$json,
Response_PasswordlessNotificationMessage$json, Response_PasswordlessNotificationMessage$json,
Response_PasswordlessNotificationMessages$json, Response_PasswordlessNotificationMessages$json,
Response_PresignedPost$json,
Response_MemoriesUploadUrls$json, Response_MemoriesUploadUrls$json,
Response_MediaItem$json, Response_MediaItem$json,
Response_MemoriesList$json, Response_MemoriesList$json,
@ -553,19 +554,54 @@ const Response_PasswordlessNotificationMessages$json = {
], ],
}; };
@$core.Deprecated('Use responseDescriptor instead')
const Response_PresignedPost$json = {
'1': 'PresignedPost',
'2': [
{'1': 'url', '3': 1, '4': 1, '5': 9, '10': 'url'},
{
'1': 'fields',
'3': 2,
'4': 3,
'5': 11,
'6': '.server_to_client.Response.PresignedPost.FieldsEntry',
'10': 'fields'
},
],
'3': [Response_PresignedPost_FieldsEntry$json],
};
@$core.Deprecated('Use responseDescriptor instead')
const Response_PresignedPost_FieldsEntry$json = {
'1': 'FieldsEntry',
'2': [
{'1': 'key', '3': 1, '4': 1, '5': 9, '10': 'key'},
{'1': 'value', '3': 2, '4': 1, '5': 9, '10': 'value'},
],
'7': {'7': true},
};
@$core.Deprecated('Use responseDescriptor instead') @$core.Deprecated('Use responseDescriptor instead')
const Response_MemoriesUploadUrls$json = { const Response_MemoriesUploadUrls$json = {
'1': 'MemoriesUploadUrls', '1': 'MemoriesUploadUrls',
'2': [ '2': [
{'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'}, {'1': 'media_id', '3': 1, '4': 1, '5': 9, '10': 'mediaId'},
{ {
'1': 'thumbnail_upload_url', '1': 'thumbnail_upload',
'3': 2, '3': 2,
'4': 1, '4': 1,
'5': 9, '5': 11,
'10': 'thumbnailUploadUrl' '6': '.server_to_client.Response.PresignedPost',
'10': 'thumbnailUpload'
},
{
'1': 'full_upload',
'3': 3,
'4': 1,
'5': 11,
'6': '.server_to_client.Response.PresignedPost',
'10': 'fullUpload'
}, },
{'1': 'full_upload_url', '3': 3, '4': 1, '5': 9, '10': 'fullUploadUrl'},
], ],
}; };
@ -843,42 +879,46 @@ final $typed_data.Uint8List responseDescriptor = $convert.base64Decode(
'Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEisKEWVuY3J5' 'Bhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2USDgoCaWQYASABKANSAmlkEisKEWVuY3J5'
'cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3JkbGVzc05vdG' 'cHRlZF9tZXNzYWdlGAIgASgMUhBlbmNyeXB0ZWRNZXNzYWdlGnoKIFBhc3N3b3JkbGVzc05vdG'
'lmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2NsaWVudC5S' 'lmaWNhdGlvbk1lc3NhZ2VzElYKCG1lc3NhZ2VzGAEgAygLMjouc2VydmVyX3RvX2NsaWVudC5S'
'ZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdlcxqJAQoSTW' 'ZXNwb25zZS5QYXNzd29yZGxlc3NOb3RpZmljYXRpb25NZXNzYWdlUghtZXNzYWdlcxqqAQoNUH'
'Vtb3JpZXNVcGxvYWRVcmxzEhkKCG1lZGlhX2lkGAEgASgJUgdtZWRpYUlkEjAKFHRodW1ibmFp' 'Jlc2lnbmVkUG9zdBIQCgN1cmwYASABKAlSA3VybBJMCgZmaWVsZHMYAiADKAsyNC5zZXJ2ZXJf'
'bF91cGxvYWRfdXJsGAIgASgJUhJ0aHVtYm5haWxVcGxvYWRVcmwSJgoPZnVsbF91cGxvYWRfdX' 'dG9fY2xpZW50LlJlc3BvbnNlLlByZXNpZ25lZFBvc3QuRmllbGRzRW50cnlSBmZpZWxkcxo5Cg'
'JsGAMgASgJUg1mdWxsVXBsb2FkVXJsGoEBCglNZWRpYUl0ZW0SGQoIbWVkaWFfaWQYASABKAlS' 'tGaWVsZHNFbnRyeRIQCgNrZXkYASABKAlSA2tleRIUCgV2YWx1ZRgCIAEoCVIFdmFsdWU6AjgB'
'B21lZGlhSWQSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3JpZ2luYWxEYXRlEjQKFnRodW1ibm' 'Gs8BChJNZW1vcmllc1VwbG9hZFVybHMSGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSUwoQdG'
'FpbF9kb3dubG9hZF91cmwYAyABKAlSFHRodW1ibmFpbERvd25sb2FkVXJsGkoKDE1lbW9yaWVz' 'h1bWJuYWlsX3VwbG9hZBgCIAEoCzIoLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJlc2ln'
'TGlzdBI6CgVpdGVtcxgBIAMoCzIkLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuTWVkaWFJdG' 'bmVkUG9zdFIPdGh1bWJuYWlsVXBsb2FkEkkKC2Z1bGxfdXBsb2FkGAMgASgLMiguc2VydmVyX3'
'VtUgVpdGVtcxo5CgtNZW1vcmllc1VybBIqChFmdWxsX2Rvd25sb2FkX3VybBgBIAEoCVIPZnVs' 'RvX2NsaWVudC5SZXNwb25zZS5QcmVzaWduZWRQb3N0UgpmdWxsVXBsb2FkGoEBCglNZWRpYUl0'
'bERvd25sb2FkVXJsGmcKDU1lbW9yaWVzVXNhZ2USGwoJbWF4X2J5dGVzGAEgASgDUghtYXhCeX' 'ZW0SGQoIbWVkaWFfaWQYASABKAlSB21lZGlhSWQSIwoNb3JpZ2luYWxfZGF0ZRgCIAEoA1IMb3'
'RlcxIjCg1jdXJyZW50X2J5dGVzGAIgASgDUgxjdXJyZW50Qnl0ZXMSFAoFY291bnQYAyABKANS' 'JpZ2luYWxEYXRlEjQKFnRodW1ibmFpbF9kb3dubG9hZF91cmwYAyABKAlSFHRodW1ibmFpbERv'
'BWNvdW50GoMMCgJPaxIUCgROb25lGAEgASgISABSBE5vbmUSGAoGdXNlcmlkGAIgASgDSABSBn' 'd25sb2FkVXJsGkoKDE1lbW9yaWVzTGlzdBI6CgVpdGVtcxgBIAMoCzIkLnNlcnZlcl90b19jbG'
'VzZXJpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1dGhjaGFsbGVuZ2USSgoLdXBsb2Fk' 'llbnQuUmVzcG9uc2UuTWVkaWFJdGVtUgVpdGVtcxo5CgtNZW1vcmllc1VybBIqChFmdWxsX2Rv'
'dG9rZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlVwbG9hZFRva2VuSABSC3' 'd25sb2FkX3VybBgBIAEoCVIPZnVsbERvd25sb2FkVXJsGmcKDU1lbW9yaWVzVXNhZ2USGwoJbW'
'VwbG9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2VydmVyX3RvX2NsaWVudC5SZXNwb25z' 'F4X2J5dGVzGAEgASgDUghtYXhCeXRlcxIjCg1jdXJyZW50X2J5dGVzGAIgASgDUgxjdXJyZW50'
'ZS5Vc2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW4YBiABKAxIAFIJYXV0aHRva2VuEk' 'Qnl0ZXMSFAoFY291bnQYAyABKANSBWNvdW50GoMMCgJPaxIUCgROb25lGAEgASgISABSBE5vbm'
'oKDGRlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuRGVwcmVj' 'USGAoGdXNlcmlkGAIgASgDSABSBnVzZXJpZBImCg1hdXRoY2hhbGxlbmdlGAMgASgMSABSDWF1'
'YXRlZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdGVkGAggASgLMiguc2VydmVyX3RvX2' 'dGhjaGFsbGVuZ2USSgoLdXBsb2FkdG9rZW4YBCABKAsyJi5zZXJ2ZXJfdG9fY2xpZW50LlJlc3'
'NsaWVudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1dGhlbnRpY2F0ZWQSOAoFcGxhbnMY' 'BvbnNlLlVwbG9hZFRva2VuSABSC3VwbG9hZHRva2VuEkEKCHVzZXJkYXRhGAUgASgLMiMuc2Vy'
'CSABKAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBsYW5zSABSBXBsYW5zEk0KDHBsYW' 'dmVyX3RvX2NsaWVudC5SZXNwb25zZS5Vc2VyRGF0YUgAUgh1c2VyZGF0YRIeCglhdXRodG9rZW'
'5iYWxsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUGxhbkJhbGxhbmNl' '4YBiABKAxIAFIJYXV0aHRva2VuEkoKDGRlcHJlY2F0ZWRfNxgHIAEoCzIlLnNlcnZlcl90b19j'
'SABSDHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGAsgASgLMiUuc2VydmVyX3RvX2NsaW' 'bGllbnQuUmVzcG9uc2UuRGVwcmVjYXRlZEgAUgtkZXByZWNhdGVkNxJQCg1hdXRoZW50aWNhdG'
'VudC5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0ZWQxMRJfChJhZGRhY2NvdW50c2lu' 'VkGAggASgLMiguc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5BdXRoZW50aWNhdGVkSABSDWF1'
'dml0ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkFkZEFjY291bnRzSW52aX' 'dGhlbnRpY2F0ZWQSOAoFcGxhbnMYCSABKAsyIC5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLl'
'Rlc0gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxvYWR0b2tlbnMYDSABKAsyKS5zZXJ2' 'BsYW5zSABSBXBsYW5zEk0KDHBsYW5iYWxsYW5jZRgKIAEoCzInLnNlcnZlcl90b19jbGllbnQu'
'ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW5zSABSDmRvd25sb2FkdG9rZW5zEk' 'UmVzcG9uc2UuUGxhbkJhbGxhbmNlSABSDHBsYW5iYWxsYW5jZRJMCg1kZXByZWNhdGVkXzExGA'
'0KDHNpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuU2lnbmVk' 'sgASgLMiUuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5EZXByZWNhdGVkSABSDGRlcHJlY2F0'
'UHJlS2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV29yaxgPIAEoCzImLnNlcnZlcl90b1' 'ZWQxMRJfChJhZGRhY2NvdW50c2ludml0ZXMYDCABKAsyLS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3'
'9jbGllbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJvb2ZPZldvcmsSSQogcGFzc3dvcmRs' 'BvbnNlLkFkZEFjY291bnRzSW52aXRlc0gAUhJhZGRhY2NvdW50c2ludml0ZXMSUwoOZG93bmxv'
'ZXNzX3JlY292ZXJ5X3NlcnZlcl9rZXkYECABKAxIAFIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlTZX' 'YWR0b2tlbnMYDSABKAsyKS5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLkRvd25sb2FkVG9rZW'
'J2ZXJLZXkSiwEKInBhc3N3b3JkbGVzc19ub3RpZmljYXRpb25fbWVzc2FnZXMYESABKAsyOy5z' '5zSABSDmRvd25sb2FkdG9rZW5zEk0KDHNpZ25lZHByZWtleRgOIAEoCzInLnNlcnZlcl90b19j'
'ZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2' 'bGllbnQuUmVzcG9uc2UuU2lnbmVkUHJlS2V5SABSDHNpZ25lZHByZWtleRJKCgtwcm9vZk9mV2'
'VzSABSIHBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3NhZ2VzEmEKFG1lbW9yaWVzX3VwbG9h' '9yaxgPIAEoCzImLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuUHJvb2ZPZldvcmtIAFILcHJv'
'ZF91cmxzGBIgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5NZW1vcmllc1VwbG9hZF' 'b2ZPZldvcmsSSQogcGFzc3dvcmRsZXNzX3JlY292ZXJ5X3NlcnZlcl9rZXkYECABKAxIAFIdcG'
'VybHNIAFISbWVtb3JpZXNVcGxvYWRVcmxzEk4KDW1lbW9yaWVzX2xpc3QYEyABKAsyJy5zZXJ2' 'Fzc3dvcmRsZXNzUmVjb3ZlcnlTZXJ2ZXJLZXkSiwEKInBhc3N3b3JkbGVzc19ub3RpZmljYXRp'
'ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk1lbW9yaWVzTGlzdEgAUgxtZW1vcmllc0xpc3QSSwoMbW' 'b25fbWVzc2FnZXMYESABKAsyOy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLlBhc3N3b3JkbG'
'Vtb3JpZXNfdXJsGBQgASgLMiYuc2VydmVyX3RvX2NsaWVudC5SZXNwb25zZS5NZW1vcmllc1Vy' 'Vzc05vdGlmaWNhdGlvbk1lc3NhZ2VzSABSIHBhc3N3b3JkbGVzc05vdGlmaWNhdGlvbk1lc3Nh'
'bEgAUgttZW1vcmllc1VybBJRCg5tZW1vcmllc191c2FnZRgVIAEoCzIoLnNlcnZlcl90b19jbG' 'Z2VzEmEKFG1lbW9yaWVzX3VwbG9hZF91cmxzGBIgASgLMi0uc2VydmVyX3RvX2NsaWVudC5SZX'
'llbnQuUmVzcG9uc2UuTWVtb3JpZXNVc2FnZUgAUg1tZW1vcmllc1VzYWdlQgQKAk9rQgoKCFJl' 'Nwb25zZS5NZW1vcmllc1VwbG9hZFVybHNIAFISbWVtb3JpZXNVcGxvYWRVcmxzEk4KDW1lbW9y'
'c3BvbnNl'); 'aWVzX2xpc3QYEyABKAsyJy5zZXJ2ZXJfdG9fY2xpZW50LlJlc3BvbnNlLk1lbW9yaWVzTGlzdE'
'gAUgxtZW1vcmllc0xpc3QSSwoMbWVtb3JpZXNfdXJsGBQgASgLMiYuc2VydmVyX3RvX2NsaWVu'
'dC5SZXNwb25zZS5NZW1vcmllc1VybEgAUgttZW1vcmllc1VybBJRCg5tZW1vcmllc191c2FnZR'
'gVIAEoCzIoLnNlcnZlcl90b19jbGllbnQuUmVzcG9uc2UuTWVtb3JpZXNVc2FnZUgAUg1tZW1v'
'cmllc1VzYWdlQgQKAk9rQgoKCFJlc3BvbnNl');

View file

@ -9,4 +9,11 @@ message TwonlySafeBackupEncrypted {
bytes mac = 1; bytes mac = 1;
bytes nonce = 2; bytes nonce = 2;
bytes cipher_text = 3; bytes cipher_text = 3;
}
message CloudMediaBackupEncrypted {
string addition = 1;
bytes encrypted_media_key = 2; // Generated by Rust MainKey
bytes media_nonce = 3; // Nonce used for ChaCha20
bytes media_ciphertext = 4; // The actual media_ciphertext (including the Poly1305 MAC)
} }

View file

@ -164,6 +164,99 @@ class TwonlySafeBackupEncrypted extends $pb.GeneratedMessage {
void clearCipherText() => $_clearField(3); void clearCipherText() => $_clearField(3);
} }
class CloudMediaBackupEncrypted extends $pb.GeneratedMessage {
factory CloudMediaBackupEncrypted({
$core.String? addition,
$core.List<$core.int>? encryptedMediaKey,
$core.List<$core.int>? mediaNonce,
$core.List<$core.int>? mediaCiphertext,
}) {
final result = create();
if (addition != null) result.addition = addition;
if (encryptedMediaKey != null) result.encryptedMediaKey = encryptedMediaKey;
if (mediaNonce != null) result.mediaNonce = mediaNonce;
if (mediaCiphertext != null) result.mediaCiphertext = mediaCiphertext;
return result;
}
CloudMediaBackupEncrypted._();
factory CloudMediaBackupEncrypted.fromBuffer($core.List<$core.int> data,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromBuffer(data, registry);
factory CloudMediaBackupEncrypted.fromJson($core.String json,
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
create()..mergeFromJson(json, registry);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
_omitMessageNames ? '' : 'CloudMediaBackupEncrypted',
createEmptyInstance: create)
..aOS(1, _omitFieldNames ? '' : 'addition')
..a<$core.List<$core.int>>(
2, _omitFieldNames ? '' : 'encryptedMediaKey', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
3, _omitFieldNames ? '' : 'mediaNonce', $pb.PbFieldType.OY)
..a<$core.List<$core.int>>(
4, _omitFieldNames ? '' : 'mediaCiphertext', $pb.PbFieldType.OY)
..hasRequiredFields = false;
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
CloudMediaBackupEncrypted clone() => deepCopy();
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
CloudMediaBackupEncrypted copyWith(
void Function(CloudMediaBackupEncrypted) updates) =>
super.copyWith((message) => updates(message as CloudMediaBackupEncrypted))
as CloudMediaBackupEncrypted;
@$core.override
$pb.BuilderInfo get info_ => _i;
@$core.pragma('dart2js:noInline')
static CloudMediaBackupEncrypted create() => CloudMediaBackupEncrypted._();
@$core.override
CloudMediaBackupEncrypted createEmptyInstance() => create();
@$core.pragma('dart2js:noInline')
static CloudMediaBackupEncrypted getDefault() => _defaultInstance ??=
$pb.GeneratedMessage.$_defaultFor<CloudMediaBackupEncrypted>(create);
static CloudMediaBackupEncrypted? _defaultInstance;
@$pb.TagNumber(1)
$core.String get addition => $_getSZ(0);
@$pb.TagNumber(1)
set addition($core.String value) => $_setString(0, value);
@$pb.TagNumber(1)
$core.bool hasAddition() => $_has(0);
@$pb.TagNumber(1)
void clearAddition() => $_clearField(1);
@$pb.TagNumber(2)
$core.List<$core.int> get encryptedMediaKey => $_getN(1);
@$pb.TagNumber(2)
set encryptedMediaKey($core.List<$core.int> value) => $_setBytes(1, value);
@$pb.TagNumber(2)
$core.bool hasEncryptedMediaKey() => $_has(1);
@$pb.TagNumber(2)
void clearEncryptedMediaKey() => $_clearField(2);
@$pb.TagNumber(3)
$core.List<$core.int> get mediaNonce => $_getN(2);
@$pb.TagNumber(3)
set mediaNonce($core.List<$core.int> value) => $_setBytes(2, value);
@$pb.TagNumber(3)
$core.bool hasMediaNonce() => $_has(2);
@$pb.TagNumber(3)
void clearMediaNonce() => $_clearField(3);
@$pb.TagNumber(4)
$core.List<$core.int> get mediaCiphertext => $_getN(3);
@$pb.TagNumber(4)
set mediaCiphertext($core.List<$core.int> value) => $_setBytes(3, value);
@$pb.TagNumber(4)
$core.bool hasMediaCiphertext() => $_has(3);
@$pb.TagNumber(4)
void clearMediaCiphertext() => $_clearField(4);
}
const $core.bool _omitFieldNames = const $core.bool _omitFieldNames =
$core.bool.fromEnvironment('protobuf.omit_field_names'); $core.bool.fromEnvironment('protobuf.omit_field_names');
const $core.bool _omitMessageNames = const $core.bool _omitMessageNames =

View file

@ -51,3 +51,27 @@ final $typed_data.Uint8List twonlySafeBackupEncryptedDescriptor =
$convert.base64Decode( $convert.base64Decode(
'ChlUd29ubHlTYWZlQmFja3VwRW5jcnlwdGVkEhAKA21hYxgBIAEoDFIDbWFjEhQKBW5vbmNlGA' 'ChlUd29ubHlTYWZlQmFja3VwRW5jcnlwdGVkEhAKA21hYxgBIAEoDFIDbWFjEhQKBW5vbmNlGA'
'IgASgMUgVub25jZRIfCgtjaXBoZXJfdGV4dBgDIAEoDFIKY2lwaGVyVGV4dA=='); 'IgASgMUgVub25jZRIfCgtjaXBoZXJfdGV4dBgDIAEoDFIKY2lwaGVyVGV4dA==');
@$core.Deprecated('Use cloudMediaBackupEncryptedDescriptor instead')
const CloudMediaBackupEncrypted$json = {
'1': 'CloudMediaBackupEncrypted',
'2': [
{'1': 'addition', '3': 1, '4': 1, '5': 9, '10': 'addition'},
{
'1': 'encrypted_media_key',
'3': 2,
'4': 1,
'5': 12,
'10': 'encryptedMediaKey'
},
{'1': 'media_nonce', '3': 3, '4': 1, '5': 12, '10': 'mediaNonce'},
{'1': 'media_ciphertext', '3': 4, '4': 1, '5': 12, '10': 'mediaCiphertext'},
],
};
/// Descriptor for `CloudMediaBackupEncrypted`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List cloudMediaBackupEncryptedDescriptor = $convert.base64Decode(
'ChlDbG91ZE1lZGlhQmFja3VwRW5jcnlwdGVkEhoKCGFkZGl0aW9uGAEgASgJUghhZGRpdGlvbh'
'IuChNlbmNyeXB0ZWRfbWVkaWFfa2V5GAIgASgMUhFlbmNyeXB0ZWRNZWRpYUtleRIfCgttZWRp'
'YV9ub25jZRgDIAEoDFIKbWVkaWFOb25jZRIpChBtZWRpYV9jaXBoZXJ0ZXh0GAQgASgMUg9tZW'
'RpYUNpcGhlcnRleHQ=');

View file

@ -32,6 +32,7 @@ import 'package:twonly/src/services/api/server_messages.api.dart';
import 'package:twonly/src/services/api/utils.api.dart'; import 'package:twonly/src/services/api/utils.api.dart';
import 'package:twonly/src/services/flame.service.dart'; import 'package:twonly/src/services/flame.service.dart';
import 'package:twonly/src/services/group.service.dart'; import 'package:twonly/src/services/group.service.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/services/notifications/fcm.notifications.dart'; import 'package:twonly/src/services/notifications/fcm.notifications.dart';
import 'package:twonly/src/services/notifications/pushkeys.notifications.dart'; import 'package:twonly/src/services/notifications/pushkeys.notifications.dart';
import 'package:twonly/src/services/passwordless_recovery.service.dart'; import 'package:twonly/src/services/passwordless_recovery.service.dart';
@ -143,6 +144,7 @@ class ApiService {
unawaited(PasswordlessRecoveryService.performHeartbeat()); unawaited(PasswordlessRecoveryService.performHeartbeat());
unawaited(UserDiscoveryService.checkForNewAnnouncedUsers()); unawaited(UserDiscoveryService.checkForNewAnnouncedUsers());
memoriesCloudService.init();
if (userService.currentUser.userStudyParticipantsToken != null) { if (userService.currentUser.userStudyParticipantsToken != null) {
// In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection // In case the user participates in the user study, call the handler after authenticated, to be sure there is a internet connection
@ -710,6 +712,75 @@ class ApiService {
return sendRequestSync(req); return sendRequestSync(req);
} }
Future<server.Response_MemoriesUploadUrls?> requestMemoriesUpload(
int sizeBytes,
DateTime originalDate,
String mediaId,
) async {
final get = ApplicationData_RequestMemoriesUpload()
..size = Int64(sizeBytes)
..originalDate = Int64(originalDate.millisecondsSinceEpoch)
..mediaId = mediaId;
final appData = ApplicationData()..requestMemoriesUpload = get;
final req = createClientToServerFromApplicationData(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
final ok = res.value as server.Response_Ok;
if (ok.hasMemoriesUploadUrls()) {
return ok.memoriesUploadUrls;
}
}
return null;
}
Future<server.Response_MemoriesUsage?> getMemoriesUsage() async {
final appData = ApplicationData()
..getMemoriesUsage = ApplicationData_GetMemoriesUsage();
final req = createClientToServerFromApplicationData(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
final ok = res.value as server.Response_Ok;
if (ok.hasMemoriesUsage()) {
return ok.memoriesUsage;
}
}
return null;
}
Future<server.Response_MemoriesUrl?> getMemoriesUrl(
String mediaId,
bool thumbnail,
) async {
final appData = ApplicationData()
..getMemoriesUrl = ApplicationData_GetMemoriesUrl(
mediaId: mediaId,
thumbnail: thumbnail,
);
final req = createClientToServerFromApplicationData(appData);
final res = await sendRequestSync(req);
if (res.isSuccess) {
final ok = res.value as server.Response_Ok;
if (ok.hasMemoriesUrl()) {
return ok.memoriesUrl;
}
}
return null;
}
Future<Result> confirmMemoriesUpload(String mediaId) async {
final get = ApplicationData_ConfirmMemoriesUpload()..mediaId = mediaId;
final appData = ApplicationData()..confirmMemoriesUpload = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<Result> deleteMemory(String mediaId) async {
final get = ApplicationData_DeleteMemory()..mediaId = mediaId;
final appData = ApplicationData()..deleteMemory = get;
final req = createClientToServerFromApplicationData(appData);
return sendRequestSync(req);
}
Future<int?> getUserIdFromUsername(String username) async { Future<int?> getUserIdFromUsername(String username) async {
final appData = Handshake( final appData = Handshake(
getUseridByUsername: Handshake_GetUserIdByUsername(username: username), getUseridByUsername: Handshake_GetUserIdByUsername(username: username),

View file

@ -22,6 +22,7 @@ Future<void> initFileDownloader() async {
if (update.task.taskId.contains('download_')) { if (update.task.taskId.contains('download_')) {
await handleDownloadStatusUpdate(update); await handleDownloadStatusUpdate(update);
} }
if (update.task.taskId.contains('backup_')) { if (update.task.taskId.contains('backup_')) {
await BackupService.handleBackupStatusUpdate( await BackupService.handleBackupStatusUpdate(
update.task.taskId, update.task.taskId,

View file

@ -13,6 +13,8 @@ import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/mediafiles/compression.service.dart'; import 'package:twonly/src/services/mediafiles/compression.service.dart';
import 'package:twonly/src/services/mediafiles/thumbnail.service.dart'; import 'package:twonly/src/services/mediafiles/thumbnail.service.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart'
show MemoriesCloudService;
import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
@ -220,6 +222,7 @@ class MediaFileService {
} catch (_) {} } catch (_) {}
} }
if (mediaFile.stored && if (mediaFile.stored &&
mediaFile.cloudState == CloudState.none &&
mediaFile.createdAt.isBefore( mediaFile.createdAt.isBefore(
clock.now().subtract(const Duration(days: 30)), clock.now().subtract(const Duration(days: 30)),
)) { )) {
@ -335,6 +338,7 @@ class MediaFileService {
unawaited(createThumbnail()); unawaited(createThumbnail());
await calculateAndSaveSize(); await calculateAndSaveSize();
await hashMediaFile(); await hashMediaFile();
await MemoriesCloudService().checkUploads();
// updateFromDb is done in hashStoredMedia() // updateFromDb is done in hashStoredMedia()
} }

View file

@ -101,7 +101,7 @@ Future<bool> createThumbnailsForImage(
destinationFile.absolute.path, destinationFile.absolute.path,
minWidth: 300, minWidth: 300,
minHeight: 300, minHeight: 300,
quality: 100, quality: 50,
format: CompressFormat.webp, format: CompressFormat.webp,
); );
stopwatch.stop(); stopwatch.stop();

View file

@ -1,8 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection'; import 'dart:collection';
import 'package:blurhash_dart/blurhash_dart.dart' as bh;
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:image/image.dart' as img;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -190,7 +192,8 @@ class MemoriesService {
return MemoriesState( return MemoriesState(
filesToMigrate: filesToMigrate, filesToMigrate: filesToMigrate,
totalFilesToMigrate: filesToMigrate, // Reset total when computing new state? No, keep existing total if migrating. totalFilesToMigrate:
filesToMigrate, // Reset total when computing new state? No, keep existing total if migrating.
galleryItems: tempGalleryItems, galleryItems: tempGalleryItems,
months: tempMonths, months: tempMonths,
orderedByMonth: tempOrderedByMonth, orderedByMonth: tempOrderedByMonth,
@ -280,6 +283,27 @@ class MemoriesService {
if (mediaService.mediaFile.sizeInBytes == null) { if (mediaService.mediaFile.sizeInBytes == null) {
await mediaService.calculateAndSaveSize(); await mediaService.calculateAndSaveSize();
} }
if (mediaService.mediaFile.blurhash == null) {
try {
final imageFile = mediaService.thumbnailPath.existsSync()
? mediaService.thumbnailPath
: mediaService.originalPath;
if (imageFile.existsSync()) {
final bytes = await imageFile.readAsBytes();
final image = img.decodeImage(bytes);
if (image != null) {
final blurhash = bh.BlurHash.encode(image).hash;
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
MediaFilesCompanion(blurhash: Value(blurhash)),
);
}
}
} catch (e) {
Log.error('Error generating blurhash for ${mediaFile.mediaId}: $e');
}
}
} catch (e) { } catch (e) {
Log.error( Log.error(
'Error in background processing of ${mediaFile.mediaId}: $e', 'Error in background processing of ${mediaFile.mediaId}: $e',

View file

@ -0,0 +1,394 @@
import 'dart:async';
import 'dart:io';
import 'package:cryptography_flutter_plus/cryptography_flutter_plus.dart';
import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:twonly/core/bridge/wrapper/key_manager.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/model/protobuf/client/generated/backup.pb.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/utils/misc.dart';
class MemoriesBackupProgress {
MemoriesBackupProgress({
required this.totalPending,
required this.currentUploaded,
required this.currentUploadProgress,
this.currentMediaId,
});
final int totalPending;
final int currentUploaded;
final double currentUploadProgress;
final String? currentMediaId;
}
class ProgressMultipartRequest extends http.MultipartRequest {
ProgressMultipartRequest(super.method, super.url, {this.onProgress});
final void Function(int bytes, int totalBytes)? onProgress;
@override
http.ByteStream finalize() {
final byteStream = super.finalize();
if (onProgress == null) return byteStream;
final total = contentLength;
var bytes = 0;
final transformer = StreamTransformer<List<int>, List<int>>.fromHandlers(
handleData: (data, sink) {
bytes += data.length;
onProgress!(bytes, total);
sink.add(data);
},
);
return http.ByteStream(byteStream.transform(transformer));
}
}
class MemoriesCloudService {
Timer? _timer;
bool _isProcessing = false;
final _progressController =
StreamController<MemoriesBackupProgress>.broadcast();
Stream<MemoriesBackupProgress> get progressStream =>
_progressController.stream;
MemoriesBackupProgress? _currentProgress;
MemoriesBackupProgress? get currentProgress => _currentProgress;
void init() {
_timer = Timer.periodic(const Duration(minutes: 5), (_) {
checkUploads();
});
// Run immediately
Future.delayed(const Duration(seconds: 10), checkUploads);
}
void dispose() {
_timer?.cancel();
_progressController.close();
}
void _updateProgress({
required int totalPending,
required int currentUploaded,
required double currentUploadProgress,
String? currentMediaId,
}) {
_currentProgress = MemoriesBackupProgress(
totalPending: totalPending,
currentUploaded: currentUploaded,
currentUploadProgress: currentUploadProgress,
currentMediaId: currentMediaId,
);
_progressController.add(_currentProgress!);
}
Future<void> checkUploads() async {
if (_isProcessing || !userService.currentUser.isBackupEnabled) return;
try {
final memories = await twonlyDB.mediaFilesDao.getMemoriesToBackup();
if (memories.isEmpty) {
return;
}
_isProcessing = true;
final total = memories.length;
var uploaded = 0;
_updateProgress(
totalPending: total,
currentUploaded: uploaded,
currentUploadProgress: 0,
);
for (final mediaFile in memories) {
_updateProgress(
totalPending: total,
currentUploaded: uploaded,
currentUploadProgress: 0,
currentMediaId: mediaFile.mediaId,
);
final success = await _backupMemory(mediaFile, (progress) {
_updateProgress(
totalPending: total,
currentUploaded: uploaded,
currentUploadProgress: progress,
currentMediaId: mediaFile.mediaId,
);
});
if (success) {
uploaded++;
}
_updateProgress(
totalPending: total,
currentUploaded: uploaded,
currentUploadProgress: 0,
);
}
} catch (e) {
Log.error('Error in MemoriesCloudService.checkUploads: $e');
} finally {
_currentProgress = null;
_isProcessing = false;
}
}
static Future<bool> downloadThumbnail(MediaFileService media) async {
final urls = await apiService.getMemoriesUrl(media.mediaFile.mediaId, true);
if (urls == null || !urls.hasFullDownloadUrl()) return false;
try {
final response = await http.get(Uri.parse(urls.fullDownloadUrl));
if (response.statusCode == 200) {
return await _decryptFile(response.bodyBytes, media.thumbnailPath);
} else {
Log.warn(
'Failed to download thumbnai statuscode ${response.statusCode}',
);
}
} catch (e) {
Log.warn(e);
}
return false;
}
Future<bool> _backupMemory(
MediaFile mediaFile,
void Function(double progress) onProgress,
) async {
try {
final ms = MediaFileService(mediaFile);
if (!ms.storedPath.existsSync()) return false;
if (!mediaFile.hasThumbnail) {
await MediaFileService(mediaFile).createThumbnail();
}
final sizeBytes = ms.storedPath.lengthSync();
final urls = await apiService.requestMemoriesUpload(
sizeBytes,
mediaFile.createdAt,
mediaFile.mediaId,
);
if (urls == null) {
Log.error('Could not get upload URLs for memory ${mediaFile.mediaId}');
return false;
}
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
cloudState: Value(CloudState.pending),
),
);
final mediaKey = getRandomUint8List(32);
final encryptedMediaKey = await RustKeyManager.encryptCloudMediaKey(
mediaKey: mediaKey,
addition: 'app',
);
final tempDir = await getTemporaryDirectory();
// 1. Upload thumbnail if exists
if (ms.thumbnailPath.existsSync() && urls.hasThumbnailUpload()) {
final thumbFile = await _encryptFile(
ms.thumbnailPath,
mediaKey,
encryptedMediaKey,
tempDir,
'thumb_${mediaFile.mediaId}',
);
try {
await _uploadToS3(urls.thumbnailUpload, thumbFile, (_) {});
} finally {
if (thumbFile.existsSync()) {
thumbFile.deleteSync();
}
}
}
// 2. Upload full media if exists
if (urls.hasFullUpload()) {
final fullFile = await _encryptFile(
ms.storedPath,
mediaKey,
encryptedMediaKey,
tempDir,
'full_${mediaFile.mediaId}',
);
try {
await _uploadToS3(urls.fullUpload, fullFile, onProgress);
} finally {
if (fullFile.existsSync()) {
fullFile.deleteSync();
}
}
}
// 3. Confirm upload
final confirmRes = await apiService.confirmMemoriesUpload(
mediaFile.mediaId,
);
if (confirmRes.isSuccess) {
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
cloudState: Value(CloudState.uploaded),
),
);
Log.info(
'Cloud backup complete and confirmed for ${mediaFile.mediaId}',
);
return true;
} else {
throw Exception('Server confirmation failed: ${confirmRes.error}');
}
} catch (e) {
Log.error('Error backing up memory ${mediaFile.mediaId}: $e');
await twonlyDB.mediaFilesDao.updateMedia(
mediaFile.mediaId,
const MediaFilesCompanion(
cloudState: Value(CloudState.none),
),
);
return false;
}
}
Future<void> _uploadToS3(
server.Response_PresignedPost presignedPost,
File file,
void Function(double progress) onProgress,
) async {
final request = ProgressMultipartRequest(
'POST',
Uri.parse(presignedPost.url),
onProgress: (bytes, total) {
if (total > 0) {
onProgress(bytes / total);
}
},
);
request.fields.addAll(presignedPost.fields);
request.files.add(
await http.MultipartFile.fromPath(
'file',
file.path,
filename: 'file',
),
);
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
if (response.statusCode != 200 && response.statusCode != 204) {
throw Exception(
'S3 upload failed: ${response.statusCode} - ${response.body}',
);
}
}
Future<File> _encryptFile(
File inputFile,
Uint8List mediaKey,
List<int> encryptedMediaKey,
Directory tempDir,
String prefix,
) async {
final dataToEncrypt = await inputFile.readAsBytes();
final chacha20 = FlutterChacha20.poly1305Aead();
final nonce = chacha20.newNonce();
final secretBox = await chacha20.encrypt(
dataToEncrypt,
secretKey: SecretKey(mediaKey),
nonce: nonce,
);
final cipherTextWithMac = Uint8List.fromList(
secretBox.cipherText + secretBox.mac.bytes,
);
final payload = CloudMediaBackupEncrypted()
..addition = 'app'
..encryptedMediaKey = encryptedMediaKey
..mediaNonce = nonce
..mediaCiphertext = cipherTextWithMac;
final outFile = File('${tempDir.path}/$prefix.encrypted');
await outFile.writeAsBytes(payload.writeToBuffer());
return outFile;
}
static Future<bool> _decryptFile(
Uint8List encryptedData,
File outFile,
) async {
try {
// 2. Parse the protobuf payload FIRST to access the key and addition
final payload = CloudMediaBackupEncrypted.fromBuffer(encryptedData);
// 3. Get the media key using the encrypted key and addition from the payload
final mediaKey = await RustKeyManager.decryptCloudMediaKey(
encryptedMediaKey: payload.encryptedMediaKey,
addition: payload.addition,
);
// 4. Extract the concatenated ciphertext + MAC, and the nonce
final nonce = payload.mediaNonce;
final cipherTextWithMac = payload.mediaCiphertext;
// 5. Separate the ciphertext and the MAC (Poly1305 MAC is always 16 bytes)
const macLength = 16;
final cipherTextLength = cipherTextWithMac.length - macLength;
final cipherText = cipherTextWithMac.sublist(0, cipherTextLength);
final macBytes = cipherTextWithMac.sublist(cipherTextLength);
// 6. Reconstruct the SecretBox
final secretBox = SecretBox(
cipherText,
nonce: nonce,
mac: Mac(macBytes),
);
// 7. Decrypt the data using the newly retrieved media key
final chacha20 = FlutterChacha20.poly1305Aead();
final decryptedBytes = await chacha20.decrypt(
secretBox,
secretKey: SecretKey(mediaKey),
);
await outFile.writeAsBytes(decryptedBytes);
return true;
} catch (e) {
Log.error(e);
return false;
}
}
}
final memoriesCloudService = MemoriesCloudService();

View file

@ -1,7 +1,12 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/model/memory_item.model.dart'; import 'package:twonly/src/model/memory_item.model.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/utils/log.dart';
import 'package:twonly/src/visual/components/selectable_thumbnail.comp.dart'; import 'package:twonly/src/visual/components/selectable_thumbnail.comp.dart';
import 'package:twonly/src/visual/views/memories/components/memory_transition_painter.dart'; import 'package:twonly/src/visual/views/memories/components/memory_transition_painter.dart';
@ -31,8 +36,10 @@ class MemoriesThumbnailComp extends StatefulWidget {
class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> { class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
ImageProvider? _imageProvider; ImageProvider? _imageProvider;
File? _selectedImageFile;
ImageStream? _imageStream; ImageStream? _imageStream;
ImageInfo? _imageInfo; ImageInfo? _imageInfo;
int _retries = 0;
late final ImageStreamListener _listener; late final ImageStreamListener _listener;
@override @override
@ -52,6 +59,7 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
setState(() { setState(() {
_imageProvider = null; _imageProvider = null;
_imageInfo = null; _imageInfo = null;
_selectedImageFile = null;
}); });
} }
}, },
@ -60,6 +68,7 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
} }
void _resolveImage() { void _resolveImage() {
if (_retries > 3) return;
final media = widget.galleryItem.mediaService; final media = widget.galleryItem.mediaService;
final hasThumbnail = final hasThumbnail =
media.thumbnailPath.existsSync() && media.thumbnailPath.existsSync() &&
@ -70,17 +79,33 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
media.mediaFile.type == MediaType.image || media.mediaFile.type == MediaType.image ||
media.mediaFile.type == MediaType.gif; media.mediaFile.type == MediaType.gif;
_selectedImageFile = null;
if (hasThumbnail) { if (hasThumbnail) {
_imageProvider = FileImage(media.thumbnailPath); _imageProvider = FileImage(media.thumbnailPath);
_selectedImageFile = media.thumbnailPath;
} else if (hasStored && isImageOrGif) { } else if (hasStored && isImageOrGif) {
_imageProvider = FileImage(media.storedPath); _imageProvider = FileImage(media.storedPath);
_selectedImageFile = media.storedPath;
}
if (!hasThumbnail) {
if (hasStored) {
media.createThumbnail();
} else {
MemoriesCloudService.downloadThumbnail(media).then((success) {
if (mounted && success) {
_resolveImage();
}
});
}
} }
if (_imageProvider != null) { if (_imageProvider != null) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return; if (!mounted) return;
final config = createLocalImageConfiguration(context); final config = createLocalImageConfiguration(context);
_imageStream = _imageProvider!.resolve(config); _imageStream = _imageProvider?.resolve(config);
_imageStream!.addListener(_listener); _imageStream!.addListener(_listener);
}); });
} }
@ -93,6 +118,8 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
widget.galleryItem.mediaService.mediaFile.mediaId) { widget.galleryItem.mediaService.mediaFile.mediaId) {
_imageStream?.removeListener(_listener); _imageStream?.removeListener(_listener);
_imageInfo = null; _imageInfo = null;
_retries = 0;
_selectedImageFile = null;
_resolveImage(); _resolveImage();
} }
} }
@ -148,6 +175,14 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
fit: BoxFit.cover, fit: BoxFit.cover,
gaplessPlayback: true, gaplessPlayback: true,
errorBuilder: (context, error, stackTrace) { errorBuilder: (context, error, stackTrace) {
if (error.toString().contains('Invalid image data')) {
if (_selectedImageFile != null) {
_selectedImageFile?.deleteSync();
_retries++;
_resolveImage();
}
}
Log.warn(error);
return ColoredBox( return ColoredBox(
color: Colors.grey.shade200, color: Colors.grey.shade200,
child: const Center( child: const Center(
@ -159,6 +194,11 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
); );
}, },
) )
else if (media.mediaFile.blurhash != null)
BlurHash(
hash: media.mediaFile.blurhash!,
optimizationMode: BlurHashOptimizationMode.approximation,
)
else else
ColoredBox( ColoredBox(
color: Colors.grey.shade200, color: Colors.grey.shade200,
@ -195,6 +235,32 @@ class _MemoriesThumbnailCompState extends State<MemoriesThumbnailComp> {
], ],
), ),
), ),
if (media.mediaFile.cloudState == CloudState.pending)
const Positioned(
top: 6,
right: 6,
child: Icon(
Icons.cloud_upload_outlined,
color: Colors.white70,
size: 16,
shadows: [
Shadow(color: Colors.black54, blurRadius: 4),
],
),
)
else if (media.mediaFile.cloudState == CloudState.uploaded)
const Positioned(
top: 6,
right: 6,
child: Icon(
Icons.cloud_done_outlined,
color: Colors.white,
size: 16,
shadows: [
Shadow(color: Colors.black54, blurRadius: 4),
],
),
),
], ],
), ),
), ),

View file

@ -21,7 +21,8 @@ class MemoriesView extends StatefulWidget {
State<MemoriesView> createState() => MemoriesViewState(); State<MemoriesView> createState() => MemoriesViewState();
} }
class MemoriesViewState extends State<MemoriesView> with AutomaticKeepAliveClientMixin<MemoriesView> { class MemoriesViewState extends State<MemoriesView>
with AutomaticKeepAliveClientMixin<MemoriesView> {
late final MemoriesService _service; late final MemoriesService _service;
final ValueNotifier<String?> _activeMediaIdNotifier = ValueNotifier(null); final ValueNotifier<String?> _activeMediaIdNotifier = ValueNotifier(null);
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
@ -31,11 +32,27 @@ class MemoriesViewState extends State<MemoriesView> with AutomaticKeepAliveClien
bool _filterFavoritesOnly = false; bool _filterFavoritesOnly = false;
bool get _selectionMode => _selectedMediaIds.isNotEmpty; bool get _selectionMode => _selectedMediaIds.isNotEmpty;
bool _isUsageLimitReached = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_service = MemoriesService(); _service = MemoriesService();
_activeMediaIdNotifier.addListener(_onActiveMediaChanged); _activeMediaIdNotifier.addListener(_onActiveMediaChanged);
_checkUsage();
}
Future<void> _checkUsage() async {
final usage = await apiService.getMemoriesUsage();
if (usage != null &&
usage.maxBytes > 0 &&
usage.currentBytes >= usage.maxBytes) {
if (mounted) {
setState(() {
_isUsageLimitReached = true;
});
}
}
} }
@override @override
@ -255,17 +272,56 @@ class MemoriesViewState extends State<MemoriesView> with AutomaticKeepAliveClien
Future<void> _batchDelete() async { Future<void> _batchDelete() async {
final count = _selectedMediaIds.length; final count = _selectedMediaIds.length;
final confirmed = await showAlertDialog(
context,
context.lang.deleteImageTitle,
context.lang.deleteMemoriesBody(count),
);
if (!confirmed) return;
final items = _service.currentState.galleryItems; final items = _service.currentState.galleryItems;
final selectedList = _selectedMediaIds.toList(); final selectedList = _selectedMediaIds.toList();
var hasCloudBackup = false;
for (final id in selectedList) {
final item = items
.where((e) => e.mediaService.mediaFile.mediaId == id)
.firstOrNull;
if (item != null &&
item.mediaService.mediaFile.cloudState != CloudState.none) {
hasCloudBackup = true;
break;
}
}
bool? deleteCompletely;
if (hasCloudBackup) {
deleteCompletely = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(context.lang.deleteImageTitle),
content: Text(context.lang.deleteMemoriesBody(count)),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(context.lang.galleryCancel),
),
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Local Only'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Completely'),
),
],
),
);
} else {
final confirmed = await showAlertDialog(
context,
context.lang.deleteImageTitle,
context.lang.deleteMemoriesBody(count),
);
if (confirmed) deleteCompletely = true;
}
if (deleteCompletely == null) return;
await _showProgressDialog( await _showProgressDialog(
'Deleting memories...', 'Deleting memories...',
(setProgress) async { (setProgress) async {
@ -275,9 +331,14 @@ class MemoriesViewState extends State<MemoriesView> with AutomaticKeepAliveClien
.where((e) => e.mediaService.mediaFile.mediaId == mediaId) .where((e) => e.mediaService.mediaFile.mediaId == mediaId)
.firstOrNull; .firstOrNull;
if (item != null) { if (item != null) {
item.mediaService.fullMediaRemoval(); if (deleteCompletely!) {
item.mediaService.fullMediaRemoval();
await apiService.deleteMemory(mediaId);
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
} else {
item.mediaService.storedPath.deleteSync();
}
} }
await twonlyDB.mediaFilesDao.deleteMediaFile(mediaId);
setProgress((i + 1) / selectedList.length); setProgress((i + 1) / selectedList.length);
} }
}, },
@ -406,10 +467,8 @@ class MemoriesViewState extends State<MemoriesView> with AutomaticKeepAliveClien
Text( Text(
context.lang.memoriesEmpty, context.lang.memoriesEmpty,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: const TextStyle(
fontSize: 16, color: Colors.grey,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
), ),
), ),
], ],
@ -446,156 +505,201 @@ class MemoriesViewState extends State<MemoriesView> with AutomaticKeepAliveClien
orderedByMonth = filteredOrdered; orderedByMonth = filteredOrdered;
} }
return LayoutBuilder( return Column(
builder: (context, constraints) { children: [
return DraggableScrollbar( if (_isUsageLimitReached)
controller: _scrollController, Container(
labelBuilder: (offset) { color: Colors.redAccent,
final state = _service.currentState; width: double.infinity,
if (state.isEmpty || state.months.isEmpty) return null; padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16,
),
child: const Text(
'Cloud backup limit reached! Please upgrade your plan or free up space.',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
return DraggableScrollbar(
controller: _scrollController,
labelBuilder: (offset) {
final state = _service.currentState;
if (state.isEmpty || state.months.isEmpty) {
return null;
}
// Simple heuristic to find month by offset // Simple heuristic to find month by offset
double currentOffset = 56; double currentOffset = 56;
if (state.galleryItemsLastYears.isNotEmpty) { if (state.galleryItemsLastYears.isNotEmpty) {
currentOffset += 220; currentOffset += 220;
} }
final screenWidth = MediaQuery.sizeOf(context).width; final screenWidth = MediaQuery.sizeOf(
final itemWidth = (screenWidth - 8) / 4; context,
final itemHeight = itemWidth * (16 / 9); ).width;
final rowHeight = itemHeight + 2; final itemWidth = (screenWidth - 8) / 4;
final itemHeight = itemWidth * (16 / 9);
final rowHeight = itemHeight + 2;
for (final month in state.months) { for (final month in state.months) {
final indices = state.orderedByMonth[month]!; final indices = state.orderedByMonth[month]!;
final totalRows = (indices.length + 3) ~/ 4; final totalRows = (indices.length + 3) ~/ 4;
final monthHeight = 44 + (totalRows * rowHeight); final monthHeight = 44 + (totalRows * rowHeight);
if (offset < currentOffset + monthHeight) { if (offset < currentOffset + monthHeight) {
return month; return month;
} }
currentOffset += monthHeight; currentOffset += monthHeight;
} }
return state.months.last; return state.months.last;
}, },
child: CustomScrollView( child: CustomScrollView(
controller: _scrollController, controller: _scrollController,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(),
slivers: [ slivers: [
SliverAppBar( SliverAppBar(
title: const Text( title: const Text(
'Memories', 'Memories',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
),
floating: true,
snap: true,
elevation: 0,
backgroundColor: context.color.surface,
actions: [
if (state.isLoading)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
), ),
child: Center( floating: true,
child: Tooltip( snap: true,
message: context.lang.migrationOfMemories( elevation: 0,
state.filesToMigrate, backgroundColor: context.color.surface,
), actions: [
child: SizedBox( if (state.isLoading)
width: 20, Padding(
height: 20, padding: const EdgeInsets.symmetric(
child: CircularProgressIndicator( horizontal: 16,
value: state.migrationProgress, ),
strokeWidth: 2.5, child: Center(
valueColor: AlwaysStoppedAnimation( child: Tooltip(
context.color.primary, message: context.lang
.migrationOfMemories(
state.filesToMigrate,
),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
value: state.migrationProgress,
strokeWidth: 2.5,
valueColor:
AlwaysStoppedAnimation(
context.color.primary,
),
backgroundColor: context
.color
.primary
.withValues(alpha: 0.2),
),
),
), ),
backgroundColor: context.color.primary ),
.withValues(alpha: 0.2), ),
IconButton(
icon: Icon(
_filterFavoritesOnly
? Icons.favorite
: Icons.favorite_border,
color: _filterFavoritesOnly
? Colors.redAccent
: null,
),
onPressed: () {
setState(() {
_filterFavoritesOnly =
!_filterFavoritesOnly;
});
},
tooltip: _filterFavoritesOnly
? 'Show all'
: 'Show favorites only',
),
],
),
MemoriesFlashbackBannerComp(
lastYears: lastYears,
onOpenFlashback: (items, idx) =>
_openViewer(items, idx, isFlashback: true),
),
for (final month in months) ...[
SliverPadding(
padding: const EdgeInsets.fromLTRB(
8,
12,
8,
6,
),
sliver: SliverToBoxAdapter(
child: Text(
month,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
), ),
), ),
), ),
), ),
), SliverGrid(
IconButton( gridDelegate:
icon: Icon( const SliverGridDelegateWithFixedCrossAxisCount(
_filterFavoritesOnly crossAxisCount: 4,
? Icons.favorite mainAxisSpacing: 2,
: Icons.favorite_border, crossAxisSpacing: 2,
color: _filterFavoritesOnly childAspectRatio: 9 / 16,
? Colors.redAccent ),
: null, delegate: SliverChildBuilderDelegate(
), (context, idx) {
onPressed: () { final globalIndex =
setState(() { orderedByMonth[month]![idx];
_filterFavoritesOnly = !_filterFavoritesOnly; final item =
}); state.galleryItems[globalIndex];
}, final mediaId =
tooltip: _filterFavoritesOnly item.mediaService.mediaFile.mediaId;
? 'Show all' final isSelected = _selectedMediaIds
: 'Show favorites only', .contains(
), mediaId,
], );
),
MemoriesFlashbackBannerComp(
lastYears: lastYears,
onOpenFlashback: (items, idx) =>
_openViewer(items, idx, isFlashback: true),
),
for (final month in months) ...[
SliverPadding(
padding: const EdgeInsets.fromLTRB(8, 12, 8, 6),
sliver: SliverToBoxAdapter(
child: Text(
month,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
SliverGrid(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: 2,
crossAxisSpacing: 2,
childAspectRatio: 9 / 16,
),
delegate: SliverChildBuilderDelegate(
(context, idx) {
final globalIndex = orderedByMonth[month]![idx];
final item = state.galleryItems[globalIndex];
final mediaId =
item.mediaService.mediaFile.mediaId;
final isSelected = _selectedMediaIds.contains(
mediaId,
);
return MemoriesThumbnailComp( return MemoriesThumbnailComp(
galleryItem: item, galleryItem: item,
index: globalIndex, index: globalIndex,
selectionMode: _selectionMode, selectionMode: _selectionMode,
isSelected: isSelected, isSelected: isSelected,
activeMediaIdNotifier: _activeMediaIdNotifier, activeMediaIdNotifier:
onLongPress: () => _onLongPressItem(mediaId), _activeMediaIdNotifier,
onTap: () => _onTapItem(mediaId, globalIndex), onLongPress: () =>
); _onLongPressItem(mediaId),
}, onTap: () =>
childCount: orderedByMonth[month]!.length, _onTapItem(mediaId, globalIndex),
), );
},
childCount: orderedByMonth[month]!.length,
),
),
],
SliverPadding(
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).padding.bottom +
150,
),
),
],
), ),
], );
SliverPadding( },
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 150,
),
),
],
), ),
); ),
}, ],
); );
}, },
), ),

View file

@ -2,6 +2,7 @@ import 'dart:math';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show Value;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_blurhash/flutter_blurhash.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -313,6 +314,9 @@ class _SynchronizedImageViewerScreenState
var filePath = item.mediaService.storedPath; var filePath = item.mediaService.storedPath;
if (!filePath.existsSync()) { if (!filePath.existsSync()) {
filePath = item.mediaService.tempPath; filePath = item.mediaService.tempPath;
if (!filePath.existsSync()) {
filePath = item.mediaService.thumbnailPath;
}
} }
final isVideo = final isVideo =
@ -335,13 +339,23 @@ class _SynchronizedImageViewerScreenState
return childWidget!; return childWidget!;
}, },
child: !filePath.existsSync() child: !filePath.existsSync()
? const Center( ? item.mediaService.mediaFile.blurhash != null
child: Icon( ? BlurHash(
Icons.broken_image_outlined, hash: item
color: Colors.white38, .mediaService
size: 64, .mediaFile
), .blurhash!,
) optimizationMode:
BlurHashOptimizationMode
.approximation,
)
: const Center(
child: Icon(
Icons.broken_image_outlined,
color: Colors.white38,
size: 64,
),
)
: isVideo : isVideo
? VideoPlayerFileHelper(videoPath: filePath) ? VideoPlayerFileHelper(videoPath: filePath)
: PhotoView( : PhotoView(

View file

@ -5,7 +5,10 @@ import 'package:go_router/go_router.dart';
import 'package:twonly/locator.dart'; import 'package:twonly/locator.dart';
import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/constants/routes.keys.dart';
import 'package:twonly/src/model/json/backup.model.dart'; import 'package:twonly/src/model/json/backup.model.dart';
import 'package:twonly/src/model/protobuf/api/websocket/server_to_client.pb.dart'
as server;
import 'package:twonly/src/services/backup.service.dart'; import 'package:twonly/src/services/backup.service.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
import 'package:twonly/src/visual/elements/my_button.element.dart'; import 'package:twonly/src/visual/elements/my_button.element.dart';
import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart'; import 'package:twonly/src/visual/views/settings/backup/passwordless_recovery/components/status.passwordless_recovery.comp.dart';
@ -21,6 +24,7 @@ class BackupView extends StatefulWidget {
class _BackupViewState extends State<BackupView> { class _BackupViewState extends State<BackupView> {
bool _isLoading = false; bool _isLoading = false;
CurrentBackupStatus? _backupStatus; CurrentBackupStatus? _backupStatus;
server.Response_MemoriesUsage? _memoriesUsage;
StreamSubscription<void>? _backupUpdateSub; StreamSubscription<void>? _backupUpdateSub;
@override @override
@ -41,9 +45,11 @@ class _BackupViewState extends State<BackupView> {
Future<void> _loadBackupStatus() async { Future<void> _loadBackupStatus() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
final status = await BackupService.getData(); final status = await BackupService.getData();
final memoriesUsage = await apiService.getMemoriesUsage();
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_backupStatus = status; _backupStatus = status;
_memoriesUsage = memoriesUsage;
_isLoading = false; _isLoading = false;
}); });
} }
@ -182,6 +188,58 @@ class _BackupViewState extends State<BackupView> {
), ),
]), ]),
), ),
const SizedBox(height: 24),
const Center(
child: Text(
'Memories Backup',
style: TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 8),
Table(
defaultVerticalAlignment:
TableCellVerticalAlignment.middle,
children: _buildTableRows([
(
'Usage',
_memoriesUsage != null
? '${formatBytes(_memoriesUsage!.currentBytes.toInt())} / ${formatBytes(_memoriesUsage!.maxBytes.toInt())}'
: '-',
),
(
'Files Backed Up',
_memoriesUsage != null
? '${_memoriesUsage!.count}'
: '-',
),
]),
),
StreamBuilder<MemoriesBackupProgress>(
initialData: memoriesCloudService.currentProgress,
stream: memoriesCloudService.progressStream,
builder: (context, snapshot) {
final progress = snapshot.data;
if (progress == null || progress.totalPending == 0) {
return const SizedBox.shrink();
}
final percent = (progress.currentUploaded / progress.totalPending) +
(progress.currentUploadProgress / progress.totalPending);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 12),
Text(
'Syncing: ${progress.currentUploaded} / ${progress.totalPending} files (${(percent * 100).toStringAsFixed(1)}%)',
style: const TextStyle(fontSize: 14),
),
const SizedBox(height: 6),
LinearProgressIndicator(
value: percent.clamp(0.0, 1.0),
),
],
);
},
),
], ],
), ),

View file

@ -221,6 +221,7 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
isFavorite: false, isFavorite: false,
hasCropAnalyzed: false, hasCropAnalyzed: false,
hasThumbnail: false, hasThumbnail: false,
cloudState: CloudState.none,
createdAt: now, createdAt: now,
); );
final mediaService = MediaFileService(mediaFile); final mediaService = MediaFileService(mediaFile);

112
memories.md Normal file
View file

@ -0,0 +1,112 @@
# Memories API Documentation
The **Memories** feature allows users to back up their media to the server in an end-to-end encrypted format. The client-server communication happens primarily via the existing WebSocket connection using Protocol Buffers (`client_to_server.proto` and `server_to_client.proto`), while the actual binary file uploads/downloads use Amazon S3 presigned URLs via HTTP PUT/GET.
## 1. Checking Storage Quota
Before allowing a user to upload a memory, the client should query the user's current storage usage to ensure they haven't exceeded their limit.
**Request:**
```proto
// client_to_server.proto
message GetMemoriesUsage {}
```
**Response:**
```proto
// server_to_client.proto
message MemoriesUsage {
int64 max_bytes = 1;
int64 current_bytes = 2;
int64 count = 3;
}
```
If `current_bytes` + the new file size > `max_bytes`, the client should block the upload and optionally prompt the user to upgrade their plan or free up space.
## 2. Uploading a Memory
Uploading a memory is a three-step process: requesting presigned URLs, performing the actual HTTP uploads, and finally confirming the upload.
### Step 2.1: Request Upload URLs
The client requests permission to upload a new memory item, declaring its size and original capture date.
**Request:**
```proto
// client_to_server.proto
message RequestMemoriesUpload {
int64 size = 1; // Size of the FULL media file in bytes
int64 original_date = 2; // Unix timestamp (in milliseconds/seconds depending on your app's standard) of when the media was originally created
}
```
**Response:**
```proto
// server_to_client.proto
message MemoriesUploadUrls {
string media_id = 1; // Unique identifier generated by the server for this memory
string thumbnail_upload_url = 2; // S3 presigned URL for uploading the encrypted thumbnail
string full_upload_url = 3; // S3 presigned URL for uploading the encrypted full media
}
```
### Step 2.2: Perform HTTP Uploads
Using the URLs returned in Step 2.1, the client encrypts the thumbnail and the full media locally, and performs an HTTP `PUT` request to the respective URLs.
*Note: The upload URLs are usually valid only for a short time (e.g. 15-60 minutes). Ensure you upload the data promptly.*
### Step 2.3: Confirm the Upload
After both the thumbnail and the full media are successfully uploaded via HTTP, the client **must** notify the server so it can mark the memory as fully uploaded and available.
**Request:**
```proto
// client_to_server.proto
message ConfirmMemoriesUpload {
string media_id = 1; // The media_id obtained in Step 2.1
}
```
*(The server will respond with an empty `Ok::None(true)` acknowledgment upon success).*
## 3. Retrieving Memories (Pagination)
To display a gallery or timeline of backed-up memories, the client requests a paginated list of media items. The endpoint returns a descending list (newest first) based on the original date.
**Request:**
```proto
// client_to_server.proto
message GetMemoriesList {
int64 offset_date = 1; // Unix timestamp to fetch memories older than this date (for pagination). Use MAX_INT64 for the first page.
int64 limit = 2; // Maximum number of items to return in this batch (e.g. 50)
}
```
**Response:**
```proto
// server_to_client.proto
message MemoriesList {
repeated MediaItem items = 1;
}
message MediaItem {
string media_id = 1;
int64 original_date = 2;
string thumbnail_download_url = 3; // S3 presigned URL to download the encrypted thumbnail
}
```
The client can immediately use the `thumbnail_download_url` to fetch and decrypt the thumbnail for the gallery view.
## 4. Downloading Full Media
When a user taps on a memory to view it in full screen, the client requests a temporary download URL for the full-resolution file.
**Request:**
```proto
// client_to_server.proto
message GetMemoriesUrl {
string media_id = 1;
}
```
**Response:**
```proto
// server_to_client.proto
message MemoriesUrl {
string full_download_url = 1; // S3 presigned URL to download the encrypted full media
}
```
The client can then perform an HTTP `GET` request to `full_download_url`, decrypt the payload locally, and display the full-resolution image or video.

View file

@ -112,6 +112,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.5.4" version: "9.5.4"
blurhash_dart:
dependency: "direct main"
description:
name: blurhash_dart
sha256: "43955b6c2e30a7d440028d1af0fa185852f3534b795cc6eb81fbf397b464409f"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:

View file

@ -113,6 +113,7 @@ dependencies:
rust_lib_twonly: rust_lib_twonly:
path: rust_builder path: rust_builder
flutter_rust_bridge: 2.12.0 flutter_rust_bridge: 2.12.0
blurhash_dart: ^1.2.1
dependency_overrides: dependency_overrides:

View file

@ -124,5 +124,21 @@ impl RustKeyManager {
*ctx.key_manager.lock().await = key_manager; *ctx.key_manager.lock().await = key_manager;
Ok(()) Ok(())
} }
pub async fn encrypt_cloud_media_key(media_key: Vec<u8>, addition: String) -> Result<Vec<u8>> {
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
if media_key.len() != 32 {
return Err(TwonlyError::WronKeySize(32, media_key.len()));
}
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&media_key);
Ok(key_manager.main_key.encrypt_cloud_media_key(&key_array, &addition))
}
pub async fn decrypt_cloud_media_key(encrypted_media_key: Vec<u8>, addition: String) -> Result<Vec<u8>> {
let key_manager = get_twonly_flutter()?.key_manager.lock().await;
let decrypted = key_manager.main_key.decrypt_cloud_media_key(&encrypted_media_key, &addition)?;
Ok(decrypted.to_vec())
}
} }

View file

@ -40,7 +40,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueMoi, default_rust_auto_opaque = RustAutoOpaqueMoi,
); );
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 340781866; pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1788847092;
// Section: executor // Section: executor
@ -380,6 +380,38 @@ let api_password = <String>::sse_decode(&mut deserializer);deserializer.end(); m
})().await) })().await)
} }) } })
} }
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_decrypt_cloud_media_key", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_encrypted_media_key = <Vec<u8>>::sse_decode(&mut deserializer);
let api_addition = <String>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::decrypt_cloud_media_key(api_encrypted_media_key, api_addition).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(
port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
rust_vec_len_: i32,
data_len_: i32,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec,_,_,_>(flutter_rust_bridge::for_generated::TaskInfo{ debug_name: "rust_key_manager_encrypt_cloud_media_key", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal }, move || {
let message = unsafe { flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(ptr_, rust_vec_len_, data_len_) };
let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message);
let api_media_key = <Vec<u8>>::sse_decode(&mut deserializer);
let api_addition = <String>::sse_decode(&mut deserializer);deserializer.end(); move |context| async move {
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>((move || async move {
let output_ok = crate::bridge::wrapper::key_manager::RustKeyManager::encrypt_cloud_media_key(api_media_key, api_addition).await?; Ok(output_ok)
})().await)
} })
}
fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl( fn wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
@ -1866,36 +1898,38 @@ fn pde_ffi_dispatcher_primary_impl(
15 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 15 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
16 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len), 16 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
17 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len), 17 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len), 18 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len), 19 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len), 20 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_login_token_impl(port, ptr, rust_vec_len, data_len),
21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len), 21 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len), 22 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len), 23 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len), 24 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len), 25 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len), 26 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_load_signed_prekeys_impl(port, ptr, rust_vec_len, data_len),
27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len), 27 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_key_manager_impl(port, ptr, rust_vec_len, data_len),
28 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len), 28 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
29 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len), 29 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
30 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len), 30 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
31 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len), 31 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_store_signed_prekey_impl(port, ptr, rust_vec_len, data_len),
32 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len), 32 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
33 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len), 33 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
34 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len), 34 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_announced_user_by_public_id_impl(port, ptr, rust_vec_len, data_len),
35 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len), 35 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_config_impl(port, ptr, rust_vec_len, data_len),
36 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len), 36 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_promotion_impl(port, ptr, rust_vec_len, data_len),
37 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len), 37 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_contact_version_impl(port, ptr, rust_vec_len, data_len),
38 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len), 38 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_other_promotions_by_public_id_impl(port, ptr, rust_vec_len, data_len),
39 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len), 39 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_own_promotions_after_version_impl(port, ptr, rust_vec_len, data_len),
40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len), 40 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_get_share_for_contact_impl(port, ptr, rust_vec_len, data_len),
41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len), 41 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_new_user_relation_impl(port, ptr, rust_vec_len, data_len),
42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len), 42 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_push_own_promotion_and_clear_old_version_impl(port, ptr, rust_vec_len, data_len),
43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len), 43 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_contact_version_impl(port, ptr, rust_vec_len, data_len),
44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len), 44 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_set_shares_impl(port, ptr, rust_vec_len, data_len),
45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len), 45 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_store_other_promotion_impl(port, ptr, rust_vec_len, data_len),
46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len), 46 => wire__crate__bridge__callbacks__user_discovery__user_discovery_store_flutter_update_config_impl(port, ptr, rust_vec_len, data_len),
47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len), 47 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_sign_data_impl(port, ptr, rust_vec_len, data_len),
48 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_signature_impl(port, ptr, rust_vec_len, data_len),
49 => wire__crate__bridge__callbacks__user_discovery__user_discovery_utils_flutter_verify_stored_pubkey_impl(port, ptr, rust_vec_len, data_len),
_ => unreachable!(), _ => unreachable!(),
} }
} }

View file

@ -61,23 +61,27 @@ impl MainKey {
self.decrypt_with_info(b"backup_key", encrypted_backup) self.decrypt_with_info(b"backup_key", encrypted_backup)
} }
// Encrypts a newly generated media key using the derived Media Main Key. pub fn encrypt_cloud_media_key(&self, media_key: &[u8; 32], addition: &str) -> Vec<u8> {
// pub fn encrypt_media_key(&self, media_key: &[u8; 32]) -> Vec<u8> { let info = format!("cloud_media_key_{}", addition);
// self.encrypt_with_info(b"media_main_key", media_key) self.encrypt_with_info(info.as_bytes(), media_key)
// } }
// Decrypts a wrapped media key using the derived Media Main Key. pub fn decrypt_cloud_media_key(
// pub fn decrypt_media_key(&self, wrapped_media_key: &[u8]) -> Result<[u8; 32]> { &self,
// let decrypted = self.decrypt_with_info(b"media_main_key", wrapped_media_key)?; encrypted_media_key: &[u8],
addition: &str,
) -> Result<[u8; 32]> {
let info = format!("cloud_media_key_{}", addition);
let decrypted = self.decrypt_with_info(info.as_bytes(), encrypted_media_key)?;
// if decrypted.len() != 32 { if decrypted.len() != 32 {
// return Err("Invalid decrypted key length".to_string())?; return Err("Invalid decrypted key length".to_string())?;
// } }
// let mut result = [0u8; 32]; let mut result = [0u8; 32];
// result.copy_from_slice(&decrypted); result.copy_from_slice(&decrypted);
// Ok(result) Ok(result)
// } }
fn derive_key(&self, info: &[u8]) -> [u8; 32] { fn derive_key(&self, info: &[u8]) -> [u8; 32] {
let hk = Hkdf::<Sha256>::new(None, &self.main_key); let hk = Hkdf::<Sha256>::new(None, &self.main_key);

View file

@ -26,6 +26,7 @@ import 'schema_v19.dart' as v19;
import 'schema_v20.dart' as v20; import 'schema_v20.dart' as v20;
import 'schema_v21.dart' as v21; import 'schema_v21.dart' as v21;
import 'schema_v22.dart' as v22; import 'schema_v22.dart' as v22;
import 'schema_v23.dart' as v23;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -75,6 +76,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v21.DatabaseAtV21(db); return v21.DatabaseAtV21(db);
case 22: case 22:
return v22.DatabaseAtV22(db); return v22.DatabaseAtV22(db);
case 23:
return v23.DatabaseAtV23(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
@ -103,5 +106,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
20, 20,
21, 21,
22, 22,
23,
]; ];
} }

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,151 @@
import 'dart:async';
import 'dart:io';
import 'package:drift/drift.dart' show Value;
import 'package:flutter/services.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:twonly/core/bridge.dart' as bridge;
import 'package:twonly/core/frb_generated.dart';
import 'package:twonly/globals.dart';
import 'package:twonly/locator.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart';
import 'package:twonly/src/database/twonly.db.dart';
import 'package:twonly/src/services/api/api.service.dart';
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
import 'package:twonly/src/services/memories/memories_cloud.service.dart';
import 'package:twonly/src/services/user.service.dart';
import 'package:twonly/src/utils/log.dart';
import '../mocks/platform_channels.dart';
import '../mocks/test_client.dart';
void main() {
group('Memories Cloud Backup Integration', () {
late TestClient client;
late Directory tempDir;
setUpAll(() async {
TestWidgetsFlutterBinding.ensureInitialized();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(
const MethodChannel('dev.fluttercommunity.plus/package_info'),
(call) async {
return {
'appName': 'twonly',
'packageName': 'eu.twonly.app',
'version': '1.0.0',
'buildNumber': '100',
};
},
);
Log.init();
setupPlatformChannelMocks();
HttpOverrides.global = RealHttpOverrides();
final dylibPath =
'${Directory.current.path}/rust/target/debug/librust_lib_twonly.dylib';
if (File(dylibPath).existsSync()) {
await RustLib.init(externalLibrary: ExternalLibrary.open(dylibPath));
} else {
await RustLib.init();
}
tempDir = Directory.systemTemp.createTempSync(
'twonly_cloud_backup_test_',
);
AppEnvironment.initTesting(
customCacheDir: tempDir.path,
customSupportDir: tempDir.path,
);
await bridge.initializeTwonlyFlutter(
config: bridge.InitConfig(
databaseDir: tempDir.path,
dataDir: tempDir.path,
),
);
if (locator.isRegistered<TwonlyDB>()) {
await locator.unregister<TwonlyDB>();
}
if (locator.isRegistered<UserService>()) {
await locator.unregister<UserService>();
}
if (locator.isRegistered<ApiService>()) {
await locator.unregister<ApiService>();
}
locator
..registerFactory<TwonlyDB>(() {
final db = Zone.current[#twonlyDB] as TwonlyDB?;
if (db != null) return db;
throw StateError('No TwonlyDB in active Zone.');
})
..registerFactory<UserService>(() {
final us = Zone.current[#userService] as UserService?;
if (us != null) return us;
throw StateError('No UserService in active Zone.');
})
..registerFactory<ApiService>(() {
final api = Zone.current[#apiService] as ApiService?;
if (api != null) return api;
throw StateError('No ApiService in active Zone.');
});
client = TestClient(777);
await client.init();
});
tearDownAll(() async {
try {
if (tempDir.existsSync()) {
tempDir.deleteSync(recursive: true);
}
} catch (_) {}
});
test('Full Backup Lifecycle', () async {
await client.run(() async {
// 1. Create a dummy media file
final mediaId = 'test_media_${DateTime.now().millisecondsSinceEpoch}';
await twonlyDB.mediaFilesDao.insertOrUpdateMedia(
MediaFilesCompanion(
mediaId: Value(mediaId),
createdAt: Value(DateTime.now()),
stored: const Value(true),
hasThumbnail: const Value(true),
cloudState: const Value(CloudState.none),
type: const Value(MediaType.image),
),
);
final mediaFile = await twonlyDB.mediaFilesDao.getMediaFileById(
mediaId,
);
expect(mediaFile, isNotNull);
expect(mediaFile!.cloudState, CloudState.none);
// Mock original and thumbnail files
final mediaService = MediaFileService(mediaFile);
final originalFile = mediaService.originalPath;
final thumbnailFile = mediaService.thumbnailPath;
await originalFile.create(recursive: true);
await originalFile.writeAsBytes([1, 2, 3, 4, 5]);
await thumbnailFile.create(recursive: true);
await thumbnailFile.writeAsBytes([1, 2, 3]);
// 2. Trigger checkUploads (which performs S3 POST upload and confirmation)
await memoriesCloudService.checkUploads();
// 3. Verify that the media has cloudState == uploaded
final finalMedia = await twonlyDB.mediaFilesDao.getMediaFileById(
mediaId,
);
expect(finalMedia, isNotNull);
expect(finalMedia!.cloudState, CloudState.uploaded);
});
});
});
}