diff --git a/.gitignore b/.gitignore index 13a35b50..01c4b072 100644 --- a/.gitignore +++ b/.gitignore @@ -60,4 +60,4 @@ fastlane/repo/status/running.json # Widget Preview related .widget_preview/ ios/build/ -APKS/*.apk +offline/ \ No newline at end of file diff --git a/lib/core/frb_generated.dart b/lib/core/frb_generated.dart index 71640a8f..58ee8023 100644 --- a/lib/core/frb_generated.dart +++ b/lib/core/frb_generated.dart @@ -8082,6 +8082,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + TwonlyScoreVisibility dco_decode_twonly_score_visibility(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return TwonlyScoreVisibility.values[raw as int]; + } + @protected int dco_decode_u_16(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -8122,8 +8128,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { UserConfig dco_decode_user_config(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 64) - throw Exception('unexpected arr length: expect 64 but see ${arr.length}'); + if (arr.length != 65) + throw Exception('unexpected arr length: expect 65 but see ${arr.length}'); return UserConfig( userId: dco_decode_i_64(arr[0]), username: dco_decode_String(arr[1]), @@ -8169,31 +8175,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { allowErrorTrackingViaSentry: dco_decode_bool(arr[37]), screenLockEnabled: dco_decode_bool(arr[38]), isCloudBackupEnabled: dco_decode_bool(arr[39]), - isUserDiscoveryEnabled: dco_decode_bool(arr[40]), - requiredSendImages: dco_decode_i_64(arr[41]), - userDiscoveryThreshold: dco_decode_u_8(arr[42]), - userDiscoveryRequiresManualApproval: dco_decode_bool(arr[43]), - userDiscoverySharePromotion: dco_decode_bool(arr[44]), - userDiscoveryInitializationError: dco_decode_bool(arr[45]), - askForFriendPromotions: dco_decode_bool(arr[46]), - currentPreKeyIndexStart: dco_decode_i_64(arr[47]), - currentSignedPreKeyIndexStart: dco_decode_i_64(arr[48]), - lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[49]), - hideChangeLog: dco_decode_bool(arr[50]), - hideMemoriesBackupPromo: dco_decode_bool(arr[51]), - hideWidgetShareExplainer: dco_decode_bool(arr[52]), - updateFcmToken: dco_decode_bool(arr[53]), - canUseLoginTokenForAuth: dco_decode_bool(arr[54]), - twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[55]), - isBackupEnabled: dco_decode_bool(arr[56]), + twonlyScoreVisibility: dco_decode_twonly_score_visibility(arr[40]), + isUserDiscoveryEnabled: dco_decode_bool(arr[41]), + requiredSendImages: dco_decode_i_64(arr[42]), + userDiscoveryThreshold: dco_decode_u_8(arr[43]), + userDiscoveryRequiresManualApproval: dco_decode_bool(arr[44]), + userDiscoverySharePromotion: dco_decode_bool(arr[45]), + userDiscoveryInitializationError: dco_decode_bool(arr[46]), + askForFriendPromotions: dco_decode_bool(arr[47]), + currentPreKeyIndexStart: dco_decode_i_64(arr[48]), + currentSignedPreKeyIndexStart: dco_decode_i_64(arr[49]), + lastChangeLogHash: dco_decode_opt_list_prim_u_8_strict(arr[50]), + hideChangeLog: dco_decode_bool(arr[51]), + hideMemoriesBackupPromo: dco_decode_bool(arr[52]), + hideWidgetShareExplainer: dco_decode_bool(arr[53]), + updateFcmToken: dco_decode_bool(arr[54]), + canUseLoginTokenForAuth: dco_decode_bool(arr[55]), + twonlySafeBackup: dco_decode_opt_box_autoadd_twonly_safe_backup(arr[56]), + isBackupEnabled: dco_decode_bool(arr[57]), passwordLessRecovery: - dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[57]), - fcmToken: dco_decode_opt_String(arr[58]), - lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[59]), - lastServerMessageAt: dco_decode_opt_box_autoadd_i_64(arr[60]), - currentSetupPage: dco_decode_opt_String(arr[61]), - skipSetupPages: dco_decode_bool(arr[62]), - hasZoomed: dco_decode_bool(arr[63]), + dco_decode_opt_box_autoadd_passwordless_recovery_config(arr[58]), + fcmToken: dco_decode_opt_String(arr[59]), + lastFcmWakeupAt: dco_decode_opt_box_autoadd_i_64(arr[60]), + lastServerMessageAt: dco_decode_opt_box_autoadd_i_64(arr[61]), + currentSetupPage: dco_decode_opt_String(arr[62]), + skipSetupPages: dco_decode_bool(arr[63]), + hasZoomed: dco_decode_bool(arr[64]), ); } @@ -9449,6 +9456,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); } + @protected + TwonlyScoreVisibility sse_decode_twonly_score_visibility( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return TwonlyScoreVisibility.values[inner]; + } + @protected int sse_decode_u_16(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -9534,6 +9550,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { var var_allowErrorTrackingViaSentry = sse_decode_bool(deserializer); var var_screenLockEnabled = sse_decode_bool(deserializer); var var_isCloudBackupEnabled = sse_decode_bool(deserializer); + var var_twonlyScoreVisibility = sse_decode_twonly_score_visibility( + deserializer, + ); var var_isUserDiscoveryEnabled = sse_decode_bool(deserializer); var var_requiredSendImages = sse_decode_i_64(deserializer); var var_userDiscoveryThreshold = sse_decode_u_8(deserializer); @@ -9606,6 +9625,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { allowErrorTrackingViaSentry: var_allowErrorTrackingViaSentry, screenLockEnabled: var_screenLockEnabled, isCloudBackupEnabled: var_isCloudBackupEnabled, + twonlyScoreVisibility: var_twonlyScoreVisibility, isUserDiscoveryEnabled: var_isUserDiscoveryEnabled, requiredSendImages: var_requiredSendImages, userDiscoveryThreshold: var_userDiscoveryThreshold, @@ -10856,6 +10876,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_prim_u_8_strict(self.encryptionKey, serializer); } + @protected + void sse_encode_twonly_score_visibility( + TwonlyScoreVisibility self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + @protected void sse_encode_u_16(int self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -10943,6 +10972,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_bool(self.allowErrorTrackingViaSentry, serializer); sse_encode_bool(self.screenLockEnabled, serializer); sse_encode_bool(self.isCloudBackupEnabled, serializer); + sse_encode_twonly_score_visibility(self.twonlyScoreVisibility, serializer); sse_encode_bool(self.isUserDiscoveryEnabled, serializer); sse_encode_i_64(self.requiredSendImages, serializer); sse_encode_u_8(self.userDiscoveryThreshold, serializer); diff --git a/lib/core/frb_generated.io.dart b/lib/core/frb_generated.io.dart index d5bd0fda..da5089ec 100644 --- a/lib/core/frb_generated.io.dart +++ b/lib/core/frb_generated.io.dart @@ -396,6 +396,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw); + @protected + TwonlyScoreVisibility dco_decode_twonly_score_visibility(dynamic raw); + @protected int dco_decode_u_16(dynamic raw); @@ -863,6 +866,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer); + @protected + TwonlyScoreVisibility sse_decode_twonly_score_visibility( + SseDeserializer deserializer, + ); + @protected int sse_decode_u_16(SseDeserializer deserializer); @@ -1454,6 +1462,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_twonly_score_visibility( + TwonlyScoreVisibility self, + SseSerializer serializer, + ); + @protected void sse_encode_u_16(int self, SseSerializer serializer); diff --git a/lib/core/frb_generated.web.dart b/lib/core/frb_generated.web.dart index b83b111d..ef90dbf4 100644 --- a/lib/core/frb_generated.web.dart +++ b/lib/core/frb_generated.web.dart @@ -398,6 +398,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected TwonlySafeBackup dco_decode_twonly_safe_backup(dynamic raw); + @protected + TwonlyScoreVisibility dco_decode_twonly_score_visibility(dynamic raw); + @protected int dco_decode_u_16(dynamic raw); @@ -865,6 +868,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected TwonlySafeBackup sse_decode_twonly_safe_backup(SseDeserializer deserializer); + @protected + TwonlyScoreVisibility sse_decode_twonly_score_visibility( + SseDeserializer deserializer, + ); + @protected int sse_decode_u_16(SseDeserializer deserializer); @@ -1456,6 +1464,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_twonly_score_visibility( + TwonlyScoreVisibility self, + SseSerializer serializer, + ); + @protected void sse_encode_u_16(int self, SseSerializer serializer); diff --git a/lib/core/user_config.dart b/lib/core/user_config.dart index 3c864cbc..af31267d 100644 --- a/lib/core/user_config.dart +++ b/lib/core/user_config.dart @@ -102,6 +102,12 @@ class TwonlySafeBackup { encryptionKey == other.encryptionKey; } +enum TwonlyScoreVisibility { + nobody, + onlyContacts, + everyone, +} + class UserConfig { PlatformInt64 userId; String username; @@ -155,6 +161,9 @@ class UserConfig { bool allowErrorTrackingViaSentry; bool screenLockEnabled; bool isCloudBackupEnabled; + + /// Controls which recipients receive the current twonly Score in sender metadata. + TwonlyScoreVisibility twonlyScoreVisibility; bool isUserDiscoveryEnabled; PlatformInt64 requiredSendImages; int userDiscoveryThreshold; @@ -229,6 +238,7 @@ class UserConfig { required this.allowErrorTrackingViaSentry, required this.screenLockEnabled, required this.isCloudBackupEnabled, + required this.twonlyScoreVisibility, required this.isUserDiscoveryEnabled, required this.requiredSendImages, required this.userDiscoveryThreshold, @@ -297,6 +307,7 @@ class UserConfig { allowErrorTrackingViaSentry.hashCode ^ screenLockEnabled.hashCode ^ isCloudBackupEnabled.hashCode ^ + twonlyScoreVisibility.hashCode ^ isUserDiscoveryEnabled.hashCode ^ requiredSendImages.hashCode ^ userDiscoveryThreshold.hashCode ^ @@ -371,6 +382,7 @@ class UserConfig { allowErrorTrackingViaSentry == other.allowErrorTrackingViaSentry && screenLockEnabled == other.screenLockEnabled && isCloudBackupEnabled == other.isCloudBackupEnabled && + twonlyScoreVisibility == other.twonlyScoreVisibility && isUserDiscoveryEnabled == other.isUserDiscoveryEnabled && requiredSendImages == other.requiredSendImages && userDiscoveryThreshold == other.userDiscoveryThreshold && diff --git a/lib/src/constants/routes.keys.dart b/lib/src/constants/routes.keys.dart index a4cadfe0..02c6e6ee 100644 --- a/lib/src/constants/routes.keys.dart +++ b/lib/src/constants/routes.keys.dart @@ -42,6 +42,8 @@ class Routes { '/settings/privacy/block_users'; static const String settingsPrivacyUserDiscovery = '/settings/privacy/user_discovery'; + static const String settingsPrivacyPublicProfile = + '/settings/privacy/public_profile'; static const String settingsNotification = '/settings/notification'; static const String settingsStorage = '/settings/storage_data'; static const String settingsStorageManage = '/settings/storage_data/manage'; diff --git a/lib/src/database/tables/contacts.table.dart b/lib/src/database/tables/contacts.table.dart index 8976a5a1..a3ef9c51 100644 --- a/lib/src/database/tables/contacts.table.dart +++ b/lib/src/database/tables/contacts.table.dart @@ -53,6 +53,8 @@ class Contacts extends Table { BoolColumn get widgetSharingGranted => boolean().withDefault(const Constant(false))(); + IntColumn get twonlyScore => integer().nullable()(); + IntColumn get mediaSendCounter => integer().withDefault(const Constant(0))(); IntColumn get mediaReceivedCounter => integer().withDefault(const Constant(0))(); diff --git a/lib/src/database/twonly.db.g.dart b/lib/src/database/twonly.db.g.dart index 1d1e57c0..c0f0da24 100644 --- a/lib/src/database/twonly.db.g.dart +++ b/lib/src/database/twonly.db.g.dart @@ -337,6 +337,17 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { ), defaultValue: const Constant(false), ); + static const VerificationMeta _twonlyScoreMeta = const VerificationMeta( + 'twonlyScore', + ); + @override + late final GeneratedColumn twonlyScore = GeneratedColumn( + 'twonly_score', + aliasedName, + true, + type: DriftSqlType.int, + requiredDuringInsert: false, + ); static const VerificationMeta _mediaSendCounterMeta = const VerificationMeta( 'mediaSendCounter', ); @@ -388,6 +399,7 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { askForFriendPromotions, widgetSharingAllowed, widgetSharingGranted, + twonlyScore, mediaSendCounter, mediaReceivedCounter, ]; @@ -606,6 +618,15 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { ), ); } + if (data.containsKey('twonly_score')) { + context.handle( + _twonlyScoreMeta, + twonlyScore.isAcceptableOrUnknown( + data['twonly_score']!, + _twonlyScoreMeta, + ), + ); + } if (data.containsKey('media_send_counter')) { context.handle( _mediaSendCounterMeta, @@ -739,6 +760,10 @@ class $ContactsTable extends Contacts with TableInfo<$ContactsTable, Contact> { DriftSqlType.bool, data['${effectivePrefix}widget_sharing_granted'], )!, + twonlyScore: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}twonly_score'], + ), mediaSendCounter: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}media_send_counter'], @@ -788,6 +813,7 @@ class Contact extends DataClass implements Insertable { final bool? askForFriendPromotions; final bool widgetSharingAllowed; final bool widgetSharingGranted; + final int? twonlyScore; final int mediaSendCounter; final int mediaReceivedCounter; const Contact({ @@ -817,6 +843,7 @@ class Contact extends DataClass implements Insertable { this.askForFriendPromotions, required this.widgetSharingAllowed, required this.widgetSharingGranted, + this.twonlyScore, required this.mediaSendCounter, required this.mediaReceivedCounter, }); @@ -885,6 +912,9 @@ class Contact extends DataClass implements Insertable { } map['widget_sharing_allowed'] = Variable(widgetSharingAllowed); map['widget_sharing_granted'] = Variable(widgetSharingGranted); + if (!nullToAbsent || twonlyScore != null) { + map['twonly_score'] = Variable(twonlyScore); + } map['media_send_counter'] = Variable(mediaSendCounter); map['media_received_counter'] = Variable(mediaReceivedCounter); return map; @@ -944,6 +974,9 @@ class Contact extends DataClass implements Insertable { : Value(askForFriendPromotions), widgetSharingAllowed: Value(widgetSharingAllowed), widgetSharingGranted: Value(widgetSharingGranted), + twonlyScore: twonlyScore == null && nullToAbsent + ? const Value.absent() + : Value(twonlyScore), mediaSendCounter: Value(mediaSendCounter), mediaReceivedCounter: Value(mediaReceivedCounter), ); @@ -1011,6 +1044,7 @@ class Contact extends DataClass implements Insertable { widgetSharingGranted: serializer.fromJson( json['widgetSharingGranted'], ), + twonlyScore: serializer.fromJson(json['twonlyScore']), mediaSendCounter: serializer.fromJson(json['mediaSendCounter']), mediaReceivedCounter: serializer.fromJson( json['mediaReceivedCounter'], @@ -1065,6 +1099,7 @@ class Contact extends DataClass implements Insertable { ), 'widgetSharingAllowed': serializer.toJson(widgetSharingAllowed), 'widgetSharingGranted': serializer.toJson(widgetSharingGranted), + 'twonlyScore': serializer.toJson(twonlyScore), 'mediaSendCounter': serializer.toJson(mediaSendCounter), 'mediaReceivedCounter': serializer.toJson(mediaReceivedCounter), }; @@ -1097,6 +1132,7 @@ class Contact extends DataClass implements Insertable { Value askForFriendPromotions = const Value.absent(), bool? widgetSharingAllowed, bool? widgetSharingGranted, + Value twonlyScore = const Value.absent(), int? mediaSendCounter, int? mediaReceivedCounter, }) => Contact( @@ -1145,6 +1181,7 @@ class Contact extends DataClass implements Insertable { : this.askForFriendPromotions, widgetSharingAllowed: widgetSharingAllowed ?? this.widgetSharingAllowed, widgetSharingGranted: widgetSharingGranted ?? this.widgetSharingGranted, + twonlyScore: twonlyScore.present ? twonlyScore.value : this.twonlyScore, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -1212,6 +1249,9 @@ class Contact extends DataClass implements Insertable { widgetSharingGranted: data.widgetSharingGranted.present ? data.widgetSharingGranted.value : this.widgetSharingGranted, + twonlyScore: data.twonlyScore.present + ? data.twonlyScore.value + : this.twonlyScore, mediaSendCounter: data.mediaSendCounter.present ? data.mediaSendCounter.value : this.mediaSendCounter, @@ -1252,6 +1292,7 @@ class Contact extends DataClass implements Insertable { ..write('askForFriendPromotions: $askForFriendPromotions, ') ..write('widgetSharingAllowed: $widgetSharingAllowed, ') ..write('widgetSharingGranted: $widgetSharingGranted, ') + ..write('twonlyScore: $twonlyScore, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -1286,6 +1327,7 @@ class Contact extends DataClass implements Insertable { askForFriendPromotions, widgetSharingAllowed, widgetSharingGranted, + twonlyScore, mediaSendCounter, mediaReceivedCounter, ]); @@ -1333,6 +1375,7 @@ class Contact extends DataClass implements Insertable { other.askForFriendPromotions == this.askForFriendPromotions && other.widgetSharingAllowed == this.widgetSharingAllowed && other.widgetSharingGranted == this.widgetSharingGranted && + other.twonlyScore == this.twonlyScore && other.mediaSendCounter == this.mediaSendCounter && other.mediaReceivedCounter == this.mediaReceivedCounter); } @@ -1364,6 +1407,7 @@ class ContactsCompanion extends UpdateCompanion { final Value askForFriendPromotions; final Value widgetSharingAllowed; final Value widgetSharingGranted; + final Value twonlyScore; final Value mediaSendCounter; final Value mediaReceivedCounter; const ContactsCompanion({ @@ -1393,6 +1437,7 @@ class ContactsCompanion extends UpdateCompanion { this.askForFriendPromotions = const Value.absent(), this.widgetSharingAllowed = const Value.absent(), this.widgetSharingGranted = const Value.absent(), + this.twonlyScore = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }); @@ -1423,6 +1468,7 @@ class ContactsCompanion extends UpdateCompanion { this.askForFriendPromotions = const Value.absent(), this.widgetSharingAllowed = const Value.absent(), this.widgetSharingGranted = const Value.absent(), + this.twonlyScore = const Value.absent(), this.mediaSendCounter = const Value.absent(), this.mediaReceivedCounter = const Value.absent(), }) : username = Value(username); @@ -1453,6 +1499,7 @@ class ContactsCompanion extends UpdateCompanion { Expression? askForFriendPromotions, Expression? widgetSharingAllowed, Expression? widgetSharingGranted, + Expression? twonlyScore, Expression? mediaSendCounter, Expression? mediaReceivedCounter, }) { @@ -1497,6 +1544,7 @@ class ContactsCompanion extends UpdateCompanion { 'widget_sharing_allowed': widgetSharingAllowed, if (widgetSharingGranted != null) 'widget_sharing_granted': widgetSharingGranted, + if (twonlyScore != null) 'twonly_score': twonlyScore, if (mediaSendCounter != null) 'media_send_counter': mediaSendCounter, if (mediaReceivedCounter != null) 'media_received_counter': mediaReceivedCounter, @@ -1530,6 +1578,7 @@ class ContactsCompanion extends UpdateCompanion { Value? askForFriendPromotions, Value? widgetSharingAllowed, Value? widgetSharingGranted, + Value? twonlyScore, Value? mediaSendCounter, Value? mediaReceivedCounter, }) { @@ -1568,6 +1617,7 @@ class ContactsCompanion extends UpdateCompanion { askForFriendPromotions ?? this.askForFriendPromotions, widgetSharingAllowed: widgetSharingAllowed ?? this.widgetSharingAllowed, widgetSharingGranted: widgetSharingGranted ?? this.widgetSharingGranted, + twonlyScore: twonlyScore ?? this.twonlyScore, mediaSendCounter: mediaSendCounter ?? this.mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter ?? this.mediaReceivedCounter, ); @@ -1682,6 +1732,9 @@ class ContactsCompanion extends UpdateCompanion { widgetSharingGranted.value, ); } + if (twonlyScore.present) { + map['twonly_score'] = Variable(twonlyScore.value); + } if (mediaSendCounter.present) { map['media_send_counter'] = Variable(mediaSendCounter.value); } @@ -1722,6 +1775,7 @@ class ContactsCompanion extends UpdateCompanion { ..write('askForFriendPromotions: $askForFriendPromotions, ') ..write('widgetSharingAllowed: $widgetSharingAllowed, ') ..write('widgetSharingGranted: $widgetSharingGranted, ') + ..write('twonlyScore: $twonlyScore, ') ..write('mediaSendCounter: $mediaSendCounter, ') ..write('mediaReceivedCounter: $mediaReceivedCounter') ..write(')')) @@ -15757,6 +15811,7 @@ typedef $$ContactsTableCreateCompanionBuilder = Value askForFriendPromotions, Value widgetSharingAllowed, Value widgetSharingGranted, + Value twonlyScore, Value mediaSendCounter, Value mediaReceivedCounter, }); @@ -15788,6 +15843,7 @@ typedef $$ContactsTableUpdateCompanionBuilder = Value askForFriendPromotions, Value widgetSharingAllowed, Value widgetSharingGranted, + Value twonlyScore, Value mediaSendCounter, Value mediaReceivedCounter, }); @@ -16207,6 +16263,11 @@ class $$ContactsTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get twonlyScore => $composableBuilder( + column: $table.twonlyScore, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get mediaSendCounter => $composableBuilder( column: $table.mediaSendCounter, builder: (column) => ColumnFilters(column), @@ -16667,6 +16728,11 @@ class $$ContactsTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get twonlyScore => $composableBuilder( + column: $table.twonlyScore, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get mediaSendCounter => $composableBuilder( column: $table.mediaSendCounter, builder: (column) => ColumnOrderings(column), @@ -16804,6 +16870,11 @@ class $$ContactsTableAnnotationComposer builder: (column) => column, ); + GeneratedColumn get twonlyScore => $composableBuilder( + column: $table.twonlyScore, + builder: (column) => column, + ); + GeneratedColumn get mediaSendCounter => $composableBuilder( column: $table.mediaSendCounter, builder: (column) => column, @@ -17198,6 +17269,7 @@ class $$ContactsTableTableManager Value askForFriendPromotions = const Value.absent(), Value widgetSharingAllowed = const Value.absent(), Value widgetSharingGranted = const Value.absent(), + Value twonlyScore = const Value.absent(), Value mediaSendCounter = const Value.absent(), Value mediaReceivedCounter = const Value.absent(), }) => ContactsCompanion( @@ -17227,6 +17299,7 @@ class $$ContactsTableTableManager askForFriendPromotions: askForFriendPromotions, widgetSharingAllowed: widgetSharingAllowed, widgetSharingGranted: widgetSharingGranted, + twonlyScore: twonlyScore, mediaSendCounter: mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter, ), @@ -17260,6 +17333,7 @@ class $$ContactsTableTableManager Value askForFriendPromotions = const Value.absent(), Value widgetSharingAllowed = const Value.absent(), Value widgetSharingGranted = const Value.absent(), + Value twonlyScore = const Value.absent(), Value mediaSendCounter = const Value.absent(), Value mediaReceivedCounter = const Value.absent(), }) => ContactsCompanion.insert( @@ -17289,6 +17363,7 @@ class $$ContactsTableTableManager askForFriendPromotions: askForFriendPromotions, widgetSharingAllowed: widgetSharingAllowed, widgetSharingGranted: widgetSharingGranted, + twonlyScore: twonlyScore, mediaSendCounter: mediaSendCounter, mediaReceivedCounter: mediaReceivedCounter, ), diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index b04f82fb..04c01584 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -658,6 +658,48 @@ abstract class AppLocalizations { /// **'{len} contact(s)'** String settingsPrivacyBlockUsersCount(Object len); + /// No description provided for @publicProfilePrivacyTitle. + /// + /// In en, this message translates to: + /// **'Public Profile'** + String get publicProfilePrivacyTitle; + + /// No description provided for @publicProfilePrivacyDescription. + /// + /// In en, this message translates to: + /// **'Choose who can see information about your profile.'** + String get publicProfilePrivacyDescription; + + /// No description provided for @publicProfileTwonlyScore. + /// + /// In en, this message translates to: + /// **'twonly Score'** + String get publicProfileTwonlyScore; + + /// No description provided for @contactTwonlyScoreDescription. + /// + /// In en, this message translates to: + /// **'The twonly Score reflects a person\'s activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.'** + String get contactTwonlyScoreDescription; + + /// No description provided for @privacyVisibilityNobody. + /// + /// In en, this message translates to: + /// **'Nobody'** + String get privacyVisibilityNobody; + + /// No description provided for @privacyVisibilityOnlyContacts. + /// + /// In en, this message translates to: + /// **'Only Contacts'** + String get privacyVisibilityOnlyContacts; + + /// No description provided for @privacyVisibilityEveryone. + /// + /// In en, this message translates to: + /// **'Everyone'** + String get privacyVisibilityEveryone; + /// No description provided for @settingsNotification. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_ar.dart b/lib/src/localization/generated/app_localizations_ar.dart index 96948475..b874f888 100644 --- a/lib/src/localization/generated/app_localizations_ar.dart +++ b/lib/src/localization/generated/app_localizations_ar.dart @@ -302,6 +302,29 @@ class AppLocalizationsAr extends AppLocalizations { return 'جهات الاتصال: $len'; } + @override + String get publicProfilePrivacyTitle => 'الملف الشخصي العام'; + + @override + String get publicProfilePrivacyDescription => + 'اختر من يمكنه رؤية معلومات ملفك الشخصي.'; + + @override + String get publicProfileTwonlyScore => 'نقاط twonly'; + + @override + String get contactTwonlyScoreDescription => + 'تعكس نقاط twonly نشاط الشخص على twonly. ويتم احتسابها استنادًا إلى عدة عوامل، مثل عدد الصور ومقاطع الفيديو التي يرسلها ويستلمها.'; + + @override + String get privacyVisibilityNobody => 'لا أحد'; + + @override + String get privacyVisibilityOnlyContacts => 'جهات الاتصال فقط'; + + @override + String get privacyVisibilityEveryone => 'الجميع'; + @override String get settingsNotification => 'الإشعارات'; diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 8e08eb4d..06d2f468 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -305,6 +305,29 @@ class AppLocalizationsDe extends AppLocalizations { return '$len Kontakt(e)'; } + @override + String get publicProfilePrivacyTitle => 'Öffentliches Profil'; + + @override + String get publicProfilePrivacyDescription => + 'Wähle aus, wer Informationen über dein Profil sehen kann.'; + + @override + String get publicProfileTwonlyScore => 'twonly Score'; + + @override + String get contactTwonlyScoreDescription => + 'Der twonly Score spiegelt die Aktivität einer Person auf twonly wider. Er wird aus verschiedenen Faktoren berechnet, zum Beispiel daraus, wie viele Fotos und Videos sie sendet und empfängt.'; + + @override + String get privacyVisibilityNobody => 'Niemand'; + + @override + String get privacyVisibilityOnlyContacts => 'Nur Kontakte'; + + @override + String get privacyVisibilityEveryone => 'Alle'; + @override String get settingsNotification => 'Benachrichtigung'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 7cc49fa0..fc5a2ee7 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -302,6 +302,29 @@ class AppLocalizationsEn extends AppLocalizations { return '$len contact(s)'; } + @override + String get publicProfilePrivacyTitle => 'Public Profile'; + + @override + String get publicProfilePrivacyDescription => + 'Choose who can see information about your profile.'; + + @override + String get publicProfileTwonlyScore => 'twonly Score'; + + @override + String get contactTwonlyScoreDescription => + 'The twonly Score reflects a person\'s activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.'; + + @override + String get privacyVisibilityNobody => 'Nobody'; + + @override + String get privacyVisibilityOnlyContacts => 'Only Contacts'; + + @override + String get privacyVisibilityEveryone => 'Everyone'; + @override String get settingsNotification => 'Notification'; diff --git a/lib/src/localization/translations/ar.arb b/lib/src/localization/translations/ar.arb index 3e5d5ca4..144bf5c8 100644 --- a/lib/src/localization/translations/ar.arb +++ b/lib/src/localization/translations/ar.arb @@ -93,6 +93,13 @@ "settingsPrivacyBlockUsers": "حظر جهات الاتصال", "settingsPrivacyBlockUsersDesc": "لن تتمكن جهات الاتصال المحظورة من التواصل معك. يمكنك إلغاء حظر أي جهة اتصال في أي وقت.", "settingsPrivacyBlockUsersCount": "جهات الاتصال: {len}", + "publicProfilePrivacyTitle": "الملف الشخصي العام", + "publicProfilePrivacyDescription": "اختر من يمكنه رؤية معلومات ملفك الشخصي.", + "publicProfileTwonlyScore": "نقاط twonly", + "contactTwonlyScoreDescription": "تعكس نقاط twonly نشاط الشخص على twonly. ويتم احتسابها استنادًا إلى عدة عوامل، مثل عدد الصور ومقاطع الفيديو التي يرسلها ويستلمها.", + "privacyVisibilityNobody": "لا أحد", + "privacyVisibilityOnlyContacts": "جهات الاتصال فقط", + "privacyVisibilityEveryone": "الجميع", "settingsNotification": "الإشعارات", "settingsNotifyPermission": "أذونات الإشعارات", "settingsNotifyPermissionDesc": "افتح إعدادات النظام للسماح بالإشعارات الفورية.", diff --git a/lib/src/localization/translations/de.arb b/lib/src/localization/translations/de.arb index 58926b19..f3a9b78a 100644 --- a/lib/src/localization/translations/de.arb +++ b/lib/src/localization/translations/de.arb @@ -102,6 +102,13 @@ "settingsPrivacyBlockUsers": "Kontakte blockieren", "settingsPrivacyBlockUsersDesc": "Blockierte Kontakte können nicht mit dir kommunizieren. Du kannst einen blockierten Kontakt jederzeit wieder entsperren.", "settingsPrivacyBlockUsersCount": "{len} Kontakt(e)", + "publicProfilePrivacyTitle": "Öffentliches Profil", + "publicProfilePrivacyDescription": "Wähle aus, wer Informationen über dein Profil sehen kann.", + "publicProfileTwonlyScore": "twonly Score", + "contactTwonlyScoreDescription": "Der twonly Score spiegelt die Aktivität einer Person auf twonly wider. Er wird aus verschiedenen Faktoren berechnet, zum Beispiel daraus, wie viele Fotos und Videos sie sendet und empfängt.", + "privacyVisibilityNobody": "Niemand", + "privacyVisibilityOnlyContacts": "Nur Kontakte", + "privacyVisibilityEveryone": "Alle", "settingsNotification": "Benachrichtigung", "settingsNotifyPermission": "Benachrichtigungsberechtigung", "settingsNotifyPermissionDesc": "Systemeinstellungen öffnen, um Push-Benachrichtigungen zu erlauben.", diff --git a/lib/src/localization/translations/en.arb b/lib/src/localization/translations/en.arb index c6101cff..6b209619 100644 --- a/lib/src/localization/translations/en.arb +++ b/lib/src/localization/translations/en.arb @@ -98,6 +98,13 @@ "len": {} } }, + "publicProfilePrivacyTitle": "Public Profile", + "publicProfilePrivacyDescription": "Choose who can see information about your profile.", + "publicProfileTwonlyScore": "twonly Score", + "contactTwonlyScoreDescription": "The twonly Score reflects a person's activity on twonly. It is calculated from several factors, such as how many photos and videos they send and receive.", + "privacyVisibilityNobody": "Nobody", + "privacyVisibilityOnlyContacts": "Only Contacts", + "privacyVisibilityEveryone": "Everyone", "settingsNotification": "Notification", "settingsNotifyPermission": "Notification permissions", "settingsNotifyPermissionDesc": "Open system settings to allow push notifications.", diff --git a/lib/src/model/protobuf/client/generated/messages.pb.dart b/lib/src/model/protobuf/client/generated/messages.pb.dart index 4636be61..6acc457d 100644 --- a/lib/src/model/protobuf/client/generated/messages.pb.dart +++ b/lib/src/model/protobuf/client/generated/messages.pb.dart @@ -1774,6 +1774,7 @@ class EncryptedContent extends $pb.GeneratedMessage { passwordlessRecoveryHeartbeat, $core.bool? widgetSharingAllowed, EncryptedContent_Story? story, + $fixnum.Int64? senderTwonlyScore, }) { final result = create(); if (groupId != null) result.groupId = groupId; @@ -1814,6 +1815,7 @@ class EncryptedContent extends $pb.GeneratedMessage { if (widgetSharingAllowed != null) result.widgetSharingAllowed = widgetSharingAllowed; if (story != null) result.story = story; + if (senderTwonlyScore != null) result.senderTwonlyScore = senderTwonlyScore; return result; } @@ -1893,6 +1895,7 @@ class EncryptedContent extends $pb.GeneratedMessage { ..aOB(29, _omitFieldNames ? '' : 'widgetSharingAllowed') ..aOM(30, _omitFieldNames ? '' : 'story', subBuilder: EncryptedContent_Story.create) + ..aInt64(31, _omitFieldNames ? '' : 'senderTwonlyScore') ..hasRequiredFields = false; @$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.') @@ -2222,6 +2225,15 @@ class EncryptedContent extends $pb.GeneratedMessage { void clearStory() => $_clearField(30); @$pb.TagNumber(30) EncryptedContent_Story ensureStory() => $_ensure(26); + + @$pb.TagNumber(31) + $fixnum.Int64 get senderTwonlyScore => $_getI64(27); + @$pb.TagNumber(31) + set senderTwonlyScore($fixnum.Int64 value) => $_setInt64(27, value); + @$pb.TagNumber(31) + $core.bool hasSenderTwonlyScore() => $_has(27); + @$pb.TagNumber(31) + void clearSenderTwonlyScore() => $_clearField(31); } const $core.bool _omitFieldNames = diff --git a/lib/src/model/protobuf/client/generated/messages.pbjson.dart b/lib/src/model/protobuf/client/generated/messages.pbjson.dart index 9c9fa61f..d71b1d35 100644 --- a/lib/src/model/protobuf/client/generated/messages.pbjson.dart +++ b/lib/src/model/protobuf/client/generated/messages.pbjson.dart @@ -73,13 +73,22 @@ const EncryptedContent$json = { '10': 'widgetSharingAllowed', '17': true }, + { + '1': 'sender_twonly_score', + '3': 31, + '4': 1, + '5': 3, + '9': 6, + '10': 'senderTwonlyScore', + '17': true + }, { '1': 'message_update', '3': 5, '4': 1, '5': 11, '6': '.EncryptedContent.MessageUpdate', - '9': 6, + '9': 7, '10': 'messageUpdate', '17': true }, @@ -89,7 +98,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.Media', - '9': 7, + '9': 8, '10': 'media', '17': true }, @@ -99,7 +108,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.MediaUpdate', - '9': 8, + '9': 9, '10': 'mediaUpdate', '17': true }, @@ -109,7 +118,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.ContactUpdate', - '9': 9, + '9': 10, '10': 'contactUpdate', '17': true }, @@ -119,7 +128,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.ContactRequest', - '9': 10, + '9': 11, '10': 'contactRequest', '17': true }, @@ -129,7 +138,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.FlameSync', - '9': 11, + '9': 12, '10': 'flameSync', '17': true }, @@ -139,7 +148,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.Reaction', - '9': 12, + '9': 13, '10': 'reaction', '17': true }, @@ -149,7 +158,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.TextMessage', - '9': 13, + '9': 14, '10': 'textMessage', '17': true }, @@ -159,7 +168,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.GroupCreate', - '9': 14, + '9': 15, '10': 'groupCreate', '17': true }, @@ -169,7 +178,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.GroupJoin', - '9': 15, + '9': 16, '10': 'groupJoin', '17': true }, @@ -179,7 +188,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.GroupUpdate', - '9': 16, + '9': 17, '10': 'groupUpdate', '17': true }, @@ -189,7 +198,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.ResendGroupPublicKey', - '9': 17, + '9': 18, '10': 'resendGroupPublicKey', '17': true }, @@ -199,7 +208,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.ErrorMessages', - '9': 18, + '9': 19, '10': 'errorMessages', '17': true }, @@ -209,7 +218,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.AdditionalDataMessage', - '9': 19, + '9': 20, '10': 'additionalDataMessage', '17': true }, @@ -219,7 +228,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.TypingIndicator', - '9': 20, + '9': 21, '10': 'typingIndicator', '17': true }, @@ -229,7 +238,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.UserDiscoveryRequest', - '9': 21, + '9': 22, '10': 'userDiscoveryRequest', '17': true }, @@ -239,7 +248,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.UserDiscoveryUpdate', - '9': 22, + '9': 23, '10': 'userDiscoveryUpdate', '17': true }, @@ -249,7 +258,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.KeyVerificationProof', - '9': 23, + '9': 24, '10': 'keyVerificationProof', '17': true }, @@ -259,7 +268,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.PasswordLessRecovery', - '9': 24, + '9': 25, '10': 'passwordlessRecovery', '17': true }, @@ -269,7 +278,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.PasswordLessRecoveryHeartbeat', - '9': 25, + '9': 26, '10': 'passwordlessRecoveryHeartbeat', '17': true }, @@ -279,7 +288,7 @@ const EncryptedContent$json = { '4': 1, '5': 11, '6': '.EncryptedContent.Story', - '9': 26, + '9': 27, '10': 'story', '17': true }, @@ -314,6 +323,7 @@ const EncryptedContent$json = { {'1': '_sender_user_discovery_version'}, {'1': '_ask_for_friend_promotions'}, {'1': '_widget_sharing_allowed'}, + {'1': '_sender_twonly_score'}, {'1': '_message_update'}, {'1': '_media'}, {'1': '_media_update'}, @@ -894,115 +904,116 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode( 'JfZGlzY292ZXJ5X3ZlcnNpb24YFSABKAxIA1Iac2VuZGVyVXNlckRpc2NvdmVyeVZlcnNpb26I' 'AQESPgoZYXNrX2Zvcl9mcmllbmRfcHJvbW90aW9ucxgZIAEoCEgEUhZhc2tGb3JGcmllbmRQcm' '9tb3Rpb25ziAEBEjkKFndpZGdldF9zaGFyaW5nX2FsbG93ZWQYHSABKAhIBVIUd2lkZ2V0U2hh' - 'cmluZ0FsbG93ZWSIAQESSwoObWVzc2FnZV91cGRhdGUYBSABKAsyHy5FbmNyeXB0ZWRDb250ZW' - '50Lk1lc3NhZ2VVcGRhdGVIBlINbWVzc2FnZVVwZGF0ZYgBARIyCgVtZWRpYRgGIAEoCzIXLkVu' - 'Y3J5cHRlZENvbnRlbnQuTWVkaWFIB1IFbWVkaWGIAQESRQoMbWVkaWFfdXBkYXRlGAcgASgLMh' - '0uRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZUgIUgttZWRpYVVwZGF0ZYgBARJLCg5jb250' - 'YWN0X3VwZGF0ZRgIIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFVwZGF0ZUgJUg1jb2' - '50YWN0VXBkYXRliAEBEk4KD2NvbnRhY3RfcmVxdWVzdBgJIAEoCzIgLkVuY3J5cHRlZENvbnRl' - 'bnQuQ29udGFjdFJlcXVlc3RIClIOY29udGFjdFJlcXVlc3SIAQESPwoKZmxhbWVfc3luYxgKIA' - 'EoCzIbLkVuY3J5cHRlZENvbnRlbnQuRmxhbWVTeW5jSAtSCWZsYW1lU3luY4gBARI7CghyZWFj' - 'dGlvbhgMIAEoCzIaLkVuY3J5cHRlZENvbnRlbnQuUmVhY3Rpb25IDFIIcmVhY3Rpb26IAQESRQ' - 'oMdGV4dF9tZXNzYWdlGA0gASgLMh0uRW5jcnlwdGVkQ29udGVudC5UZXh0TWVzc2FnZUgNUgt0' - 'ZXh0TWVzc2FnZYgBARJFCgxncm91cF9jcmVhdGUYDiABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk' - 'dyb3VwQ3JlYXRlSA5SC2dyb3VwQ3JlYXRliAEBEj8KCmdyb3VwX2pvaW4YDyABKAsyGy5FbmNy' - 'eXB0ZWRDb250ZW50Lkdyb3VwSm9pbkgPUglncm91cEpvaW6IAQESRQoMZ3JvdXBfdXBkYXRlGB' - 'AgASgLMh0uRW5jcnlwdGVkQ29udGVudC5Hcm91cFVwZGF0ZUgQUgtncm91cFVwZGF0ZYgBARJi' - 'ChdyZXNlbmRfZ3JvdXBfcHVibGljX2tleRgRIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUmVzZW' - '5kR3JvdXBQdWJsaWNLZXlIEVIUcmVzZW5kR3JvdXBQdWJsaWNLZXmIAQESSwoOZXJyb3JfbWVz' - 'c2FnZXMYEiABKAsyHy5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXNIElINZXJyb3JNZX' - 'NzYWdlc4gBARJkChdhZGRpdGlvbmFsX2RhdGFfbWVzc2FnZRgTIAEoCzInLkVuY3J5cHRlZENv' - 'bnRlbnQuQWRkaXRpb25hbERhdGFNZXNzYWdlSBNSFWFkZGl0aW9uYWxEYXRhTWVzc2FnZYgBAR' - 'JRChB0eXBpbmdfaW5kaWNhdG9yGBQgASgLMiEuRW5jcnlwdGVkQ29udGVudC5UeXBpbmdJbmRp' - 'Y2F0b3JIFFIPdHlwaW5nSW5kaWNhdG9yiAEBEmEKFnVzZXJfZGlzY292ZXJ5X3JlcXVlc3QYFi' - 'ABKAsyJi5FbmNyeXB0ZWRDb250ZW50LlVzZXJEaXNjb3ZlcnlSZXF1ZXN0SBVSFHVzZXJEaXNj' - 'b3ZlcnlSZXF1ZXN0iAEBEl4KFXVzZXJfZGlzY292ZXJ5X3VwZGF0ZRgXIAEoCzIlLkVuY3J5cH' - 'RlZENvbnRlbnQuVXNlckRpc2NvdmVyeVVwZGF0ZUgWUhN1c2VyRGlzY292ZXJ5VXBkYXRliAEB' - 'EmEKFmtleV92ZXJpZmljYXRpb25fcHJvb2YYGCABKAsyJi5FbmNyeXB0ZWRDb250ZW50LktleV' - 'ZlcmlmaWNhdGlvblByb29mSBdSFGtleVZlcmlmaWNhdGlvblByb29miAEBEmAKFXBhc3N3b3Jk' - 'bGVzc19yZWNvdmVyeRgaIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUmVjb3' - 'ZlcnlIGFIUcGFzc3dvcmRsZXNzUmVjb3ZlcnmIAQESfAofcGFzc3dvcmRsZXNzX3JlY292ZXJ5' - 'X2hlYXJ0YmVhdBgbIAEoCzIvLkVuY3J5cHRlZENvbnRlbnQuUGFzc3dvcmRMZXNzUmVjb3Zlcn' - 'lIZWFydGJlYXRIGVIdcGFzc3dvcmRsZXNzUmVjb3ZlcnlIZWFydGJlYXSIAQESMgoFc3RvcnkY' - 'HiABKAsyFy5FbmNyeXB0ZWRDb250ZW50LlN0b3J5SBpSBXN0b3J5iAEBGpYCCg1FcnJvck1lc3' - 'NhZ2VzEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkVycm9yTWVzc2FnZXMuVHlw' - 'ZVIEdHlwZRIsChJyZWxhdGVkX3JlY2VpcHRfaWQYAiABKAlSEHJlbGF0ZWRSZWNlaXB0SWQinA' - 'EKBFR5cGUSPAo4RVJST1JfUFJPQ0VTU0lOR19NRVNTQUdFX0NSRUFURURfQUNDT1VOVF9SRVFV' - 'RVNUX0lOU1RFQUQQABIYChRVTktOT1dOX01FU1NBR0VfVFlQRRACEhcKE1NFU1NJT05fT1VUX0' - '9GX1NZTkMQAxIjCh9HUk9VUF9OT1RfRk9VTkRfT1JfTk9UX0FfTUVNQkVSEAQahwEKC0dyb3Vw' - 'Q3JlYXRlEhsKCXN0YXRlX2tleRgDIAEoDFIIc3RhdGVLZXkSKAoQZ3JvdXBfcHVibGljX2tleR' - 'gEIAEoDFIOZ3JvdXBQdWJsaWNLZXkSIgoKZ3JvdXBfbmFtZRgFIAEoCUgAUglncm91cE5hbWWI' - 'AQFCDQoLX2dyb3VwX25hbWUaNQoJR3JvdXBKb2luEigKEGdyb3VwX3B1YmxpY19rZXkYASABKA' - 'xSDmdyb3VwUHVibGljS2V5GhYKFFJlc2VuZEdyb3VwUHVibGljS2V5GsgCCgtHcm91cFVwZGF0' - 'ZRIqChFncm91cF9hY3Rpb25fdHlwZRgBIAEoCVIPZ3JvdXBBY3Rpb25UeXBlEjMKE2FmZmVjdG' - 'VkX2NvbnRhY3RfaWQYAiABKANIAFIRYWZmZWN0ZWRDb250YWN0SWSIAQESKQoObmV3X2dyb3Vw' - 'X25hbWUYAyABKAlIAVIMbmV3R3JvdXBOYW1liAEBElcKJm5ld19kZWxldGVfbWVzc2FnZXNfYW' - 'Z0ZXJfbWlsbGlzZWNvbmRzGAQgASgDSAJSIm5ld0RlbGV0ZU1lc3NhZ2VzQWZ0ZXJNaWxsaXNl' - 'Y29uZHOIAQFCFgoUX2FmZmVjdGVkX2NvbnRhY3RfaWRCEQoPX25ld19ncm91cF9uYW1lQikKJ1' - '9uZXdfZGVsZXRlX21lc3NhZ2VzX2FmdGVyX21pbGxpc2Vjb25kcxqIAgoLVGV4dE1lc3NhZ2US' - 'KgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBISCgR0ZXh0GAIgAS' - 'gJUgR0ZXh0EhwKCXRpbWVzdGFtcBgDIAEoA1IJdGltZXN0YW1wEi0KEHF1b3RlX21lc3NhZ2Vf' - 'aWQYBCABKAlIAFIOcXVvdGVNZXNzYWdlSWSIAQESOwoXYWRkaXRpb25hbF9tZXNzYWdlX2RhdG' - 'EYBSABKAxIAVIVYWRkaXRpb25hbE1lc3NhZ2VEYXRhiAEBQhMKEV9xdW90ZV9tZXNzYWdlX2lk' - 'QhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRrmAQoVQWRkaXRpb25hbERhdGFNZXNzYWdlEi' - 'oKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSHAoJdGltZXN0YW1w' - 'GAIgASgDUgl0aW1lc3RhbXASEgoEdHlwZRgDIAEoCVIEdHlwZRI7ChdhZGRpdGlvbmFsX21lc3' - 'NhZ2VfZGF0YRgEIAEoDEgAUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQESFgoGaGlkZGVuGAUg' - 'ASgIUgZoaWRkZW5CGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGmQKCFJlYWN0aW9uEioKEX' - 'RhcmdldF9tZXNzYWdlX2lkGAEgASgJUg90YXJnZXRNZXNzYWdlSWQSFAoFZW1vamkYAiABKAlS' - 'BWVtb2ppEhYKBnJlbW92ZRgDIAEoCFIGcmVtb3ZlGr4CCg1NZXNzYWdlVXBkYXRlEjgKBHR5cG' - 'UYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50Lk1lc3NhZ2VVcGRhdGUuVHlwZVIEdHlwZRIvChFz' - 'ZW5kZXJfbWVzc2FnZV9pZBgCIAEoCUgAUg9zZW5kZXJNZXNzYWdlSWSIAQESPQobbXVsdGlwbG' - 'VfdGFyZ2V0X21lc3NhZ2VfaWRzGAMgAygJUhhtdWx0aXBsZVRhcmdldE1lc3NhZ2VJZHMSFwoE' - 'dGV4dBgEIAEoCUgBUgR0ZXh0iAEBEhwKCXRpbWVzdGFtcBgFIAEoA1IJdGltZXN0YW1wIi0KBF' - 'R5cGUSCgoGREVMRVRFEAASDQoJRURJVF9URVhUEAESCgoGT1BFTkVEEAJCFAoSX3NlbmRlcl9t' - 'ZXNzYWdlX2lkQgcKBV90ZXh0GrsGCgVNZWRpYRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCV' - 'IPc2VuZGVyTWVzc2FnZUlkEjAKBHR5cGUYAiABKA4yHC5FbmNyeXB0ZWRDb250ZW50Lk1lZGlh' - 'LlR5cGVSBHR5cGUSRgodZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHMYAyABKANIAFIaZG' - 'lzcGxheUxpbWl0SW5NaWxsaXNlY29uZHOIAQESNwoXcmVxdWlyZXNfYXV0aGVudGljYXRpb24Y' - 'BCABKAhSFnJlcXVpcmVzQXV0aGVudGljYXRpb24SHAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3' - 'RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgGIAEoCUgBUg5xdW90ZU1lc3NhZ2VJZIgBARIqCg5k' - 'b3dubG9hZF90b2tlbhgHIAEoDEgCUg1kb3dubG9hZFRva2VuiAEBEioKDmVuY3J5cHRpb25fa2' - 'V5GAggASgMSANSDWVuY3J5cHRpb25LZXmIAQESKgoOZW5jcnlwdGlvbl9tYWMYCSABKAxIBFIN' - 'ZW5jcnlwdGlvbk1hY4gBARIuChBlbmNyeXB0aW9uX25vbmNlGAogASgMSAVSD2VuY3J5cHRpb2' - '5Ob25jZYgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgLIAEoDEgGUhVhZGRpdGlvbmFs' - 'TWVzc2FnZURhdGGIAQESJAoLd2lkZ2V0X29ubHkYDCABKAhIB1IKd2lkZ2V0T25seYgBASI+Cg' - 'RUeXBlEgwKCFJFVVBMT0FEEAASCQoFSU1BR0UQARIJCgVWSURFTxACEgcKA0dJRhADEgkKBUFV' - 'RElPEARCIAoeX2Rpc3BsYXlfbGltaXRfaW5fbWlsbGlzZWNvbmRzQhMKEV9xdW90ZV9tZXNzYW' - 'dlX2lkQhEKD19kb3dubG9hZF90b2tlbkIRCg9fZW5jcnlwdGlvbl9rZXlCEQoPX2VuY3J5cHRp' - 'b25fbWFjQhMKEV9lbmNyeXB0aW9uX25vbmNlQhoKGF9hZGRpdGlvbmFsX21lc3NhZ2VfZGF0YU' - 'IOCgxfd2lkZ2V0X29ubHkaTgoFU3RvcnkSLQoFbWVkaWEYASABKAsyFy5FbmNyeXB0ZWRDb250' - 'ZW50Lk1lZGlhUgVtZWRpYRIWCgZub3RpZnkYAiABKAhSBm5vdGlmeRqpAQoLTWVkaWFVcGRhdG' - 'USNgoEdHlwZRgBIAEoDjIiLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFVcGRhdGUuVHlwZVIEdHlw' - 'ZRIqChF0YXJnZXRfbWVzc2FnZV9pZBgCIAEoCVIPdGFyZ2V0TWVzc2FnZUlkIjYKBFR5cGUSDA' - 'oIUkVPUEVORUQQABIKCgZTVE9SRUQQARIUChBERUNSWVBUSU9OX0VSUk9SEAIaeAoOQ29udGFj' - 'dFJlcXVlc3QSOQoEdHlwZRgBIAEoDjIlLkVuY3J5cHRlZENvbnRlbnQuQ29udGFjdFJlcXVlc3' - 'QuVHlwZVIEdHlwZSIrCgRUeXBlEgsKB1JFUVVFU1QQABIKCgZSRUpFQ1QQARIKCgZBQ0NFUFQQ' - 'AhqkAgoNQ29udGFjdFVwZGF0ZRI4CgR0eXBlGAEgASgOMiQuRW5jcnlwdGVkQ29udGVudC5Db2' - '50YWN0VXBkYXRlLlR5cGVSBHR5cGUSNwoVYXZhdGFyX3N2Z19jb21wcmVzc2VkGAIgASgMSABS' - 'E2F2YXRhclN2Z0NvbXByZXNzZWSIAQESHwoIdXNlcm5hbWUYAyABKAlIAVIIdXNlcm5hbWWIAQ' - 'ESJgoMZGlzcGxheV9uYW1lGAQgASgJSAJSC2Rpc3BsYXlOYW1liAEBIh8KBFR5cGUSCwoHUkVR' - 'VUVTVBAAEgoKBlVQREFURRABQhgKFl9hdmF0YXJfc3ZnX2NvbXByZXNzZWRCCwoJX3VzZXJuYW' - '1lQg8KDV9kaXNwbGF5X25hbWUarwEKCUZsYW1lU3luYxIjCg1mbGFtZV9jb3VudGVyGAEgASgD' - 'UgxmbGFtZUNvdW50ZXISOQoZbGFzdF9mbGFtZV9jb3VudGVyX2NoYW5nZRgCIAEoA1IWbGFzdE' - 'ZsYW1lQ291bnRlckNoYW5nZRIfCgtiZXN0X2ZyaWVuZBgDIAEoCFIKYmVzdEZyaWVuZBIhCgxm' - 'b3JjZV91cGRhdGUYBCABKAhSC2ZvcmNlVXBkYXRlGk0KD1R5cGluZ0luZGljYXRvchIbCglpc1' - '90eXBpbmcYASABKAhSCGlzVHlwaW5nEh0KCmNyZWF0ZWRfYXQYAiABKANSCWNyZWF0ZWRBdBo/' - 'ChRVc2VyRGlzY292ZXJ5UmVxdWVzdBInCg9jdXJyZW50X3ZlcnNpb24YASABKAxSDmN1cnJlbn' - 'RWZXJzaW9uGjEKE1VzZXJEaXNjb3ZlcnlVcGRhdGUSGgoIbWVzc2FnZXMYASADKAxSCG1lc3Nh' - 'Z2VzGj0KFEtleVZlcmlmaWNhdGlvblByb29mEiUKDmNhbGN1bGF0ZWRfbWFjGAEgASgMUg1jYW' - 'xjdWxhdGVkTWFjGpsBChRQYXNzd29yZExlc3NSZWNvdmVyeRI1ChNyZWNvdmVyeVNlY3JldFNo' - 'YXJlGAEgASgMSABSE3JlY292ZXJ5U2VjcmV0U2hhcmWIAQESFgoGZGVsZXRlGAIgASgIUgZkZW' - 'xldGUSHAoJdGhyZXNob2xkGAMgASgDUgl0aHJlc2hvbGRCFgoUX3JlY292ZXJ5U2VjcmV0U2hh' - 'cmUaMwodUGFzc3dvcmRMZXNzUmVjb3ZlcnlIZWFydGJlYXQSEgoEaGFzaBgBIAEoDFIEaGFzaE' - 'ILCglfZ3JvdXBfaWRCEQoPX2lzX2RpcmVjdF9jaGF0QhkKF19zZW5kZXJfcHJvZmlsZV9jb3Vu' - 'dGVyQiAKHl9zZW5kZXJfdXNlcl9kaXNjb3ZlcnlfdmVyc2lvbkIcChpfYXNrX2Zvcl9mcmllbm' - 'RfcHJvbW90aW9uc0IZChdfd2lkZ2V0X3NoYXJpbmdfYWxsb3dlZEIRCg9fbWVzc2FnZV91cGRh' - 'dGVCCAoGX21lZGlhQg8KDV9tZWRpYV91cGRhdGVCEQoPX2NvbnRhY3RfdXBkYXRlQhIKEF9jb2' - '50YWN0X3JlcXVlc3RCDQoLX2ZsYW1lX3N5bmNCCwoJX3JlYWN0aW9uQg8KDV90ZXh0X21lc3Nh' - 'Z2VCDwoNX2dyb3VwX2NyZWF0ZUINCgtfZ3JvdXBfam9pbkIPCg1fZ3JvdXBfdXBkYXRlQhoKGF' - '9yZXNlbmRfZ3JvdXBfcHVibGljX2tleUIRCg9fZXJyb3JfbWVzc2FnZXNCGgoYX2FkZGl0aW9u' - 'YWxfZGF0YV9tZXNzYWdlQhMKEV90eXBpbmdfaW5kaWNhdG9yQhkKF191c2VyX2Rpc2NvdmVyeV' - '9yZXF1ZXN0QhgKFl91c2VyX2Rpc2NvdmVyeV91cGRhdGVCGQoXX2tleV92ZXJpZmljYXRpb25f' - 'cHJvb2ZCGAoWX3Bhc3N3b3JkbGVzc19yZWNvdmVyeUIiCiBfcGFzc3dvcmRsZXNzX3JlY292ZX' - 'J5X2hlYXJ0YmVhdEIICgZfc3RvcnlKBAgcEB0='); + 'cmluZ0FsbG93ZWSIAQESMwoTc2VuZGVyX3R3b25seV9zY29yZRgfIAEoA0gGUhFzZW5kZXJUd2' + '9ubHlTY29yZYgBARJLCg5tZXNzYWdlX3VwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQu' + 'TWVzc2FnZVVwZGF0ZUgHUg1tZXNzYWdlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcn' + 'lwdGVkQ29udGVudC5NZWRpYUgIUgVtZWRpYYgBARJFCgxtZWRpYV91cGRhdGUYByABKAsyHS5F' + 'bmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlSAlSC21lZGlhVXBkYXRliAEBEksKDmNvbnRhY3' + 'RfdXBkYXRlGAggASgLMh8uRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlSApSDWNvbnRh' + 'Y3RVcGRhdGWIAQESTgoPY29udGFjdF9yZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC' + '5Db250YWN0UmVxdWVzdEgLUg5jb250YWN0UmVxdWVzdIgBARI/CgpmbGFtZV9zeW5jGAogASgL' + 'MhsuRW5jcnlwdGVkQ29udGVudC5GbGFtZVN5bmNIDFIJZmxhbWVTeW5jiAEBEjsKCHJlYWN0aW' + '9uGAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgNUghyZWFjdGlvbogBARJFCgx0' + 'ZXh0X21lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb250ZW50LlRleHRNZXNzYWdlSA5SC3RleH' + 'RNZXNzYWdliAEBEkUKDGdyb3VwX2NyZWF0ZRgOIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuR3Jv' + 'dXBDcmVhdGVID1ILZ3JvdXBDcmVhdGWIAQESPwoKZ3JvdXBfam9pbhgPIAEoCzIbLkVuY3J5cH' + 'RlZENvbnRlbnQuR3JvdXBKb2luSBBSCWdyb3VwSm9pbogBARJFCgxncm91cF91cGRhdGUYECAB' + 'KAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSBFSC2dyb3VwVXBkYXRliAEBEmIKF3' + 'Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5GBEgASgLMiYuRW5jcnlwdGVkQ29udGVudC5SZXNlbmRH' + 'cm91cFB1YmxpY0tleUgSUhRyZXNlbmRHcm91cFB1YmxpY0tleYgBARJLCg5lcnJvcl9tZXNzYW' + 'dlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlc0gTUg1lcnJvck1lc3Nh' + 'Z2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgASgLMicuRW5jcnlwdGVkQ29udG' + 'VudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIFFIVYWRkaXRpb25hbERhdGFNZXNzYWdliAEBElEK' + 'EHR5cGluZ19pbmRpY2F0b3IYFCABKAsyIS5FbmNyeXB0ZWRDb250ZW50LlR5cGluZ0luZGljYX' + 'RvckgVUg90eXBpbmdJbmRpY2F0b3KIAQESYQoWdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdBgWIAEo' + 'CzImLkVuY3J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVJlcXVlc3RIFlIUdXNlckRpc2Nvdm' + 'VyeVJlcXVlc3SIAQESXgoVdXNlcl9kaXNjb3ZlcnlfdXBkYXRlGBcgASgLMiUuRW5jcnlwdGVk' + 'Q29udGVudC5Vc2VyRGlzY292ZXJ5VXBkYXRlSBdSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQESYQ' + 'oWa2V5X3ZlcmlmaWNhdGlvbl9wcm9vZhgYIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuS2V5VmVy' + 'aWZpY2F0aW9uUHJvb2ZIGFIUa2V5VmVyaWZpY2F0aW9uUHJvb2aIAQESYAoVcGFzc3dvcmRsZX' + 'NzX3JlY292ZXJ5GBogASgLMiYuRW5jcnlwdGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVy' + 'eUgZUhRwYXNzd29yZGxlc3NSZWNvdmVyeYgBARJ8Ch9wYXNzd29yZGxlc3NfcmVjb3ZlcnlfaG' + 'VhcnRiZWF0GBsgASgLMi8uRW5jcnlwdGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVyeUhl' + 'YXJ0YmVhdEgaUh1wYXNzd29yZGxlc3NSZWNvdmVyeUhlYXJ0YmVhdIgBARIyCgVzdG9yeRgeIA' + 'EoCzIXLkVuY3J5cHRlZENvbnRlbnQuU3RvcnlIG1IFc3RvcnmIAQEalgIKDUVycm9yTWVzc2Fn' + 'ZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeXBlUg' + 'R0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCKcAQoE' + 'VHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1' + 'RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0Zf' + 'U1lOQxADEiMKH0dST1VQX05PVF9GT1VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQoLR3JvdXBDcm' + 'VhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91cF9wdWJsaWNfa2V5GAQg' + 'ASgMUg5ncm91cFB1YmxpY0tleRIiCgpncm91cF9uYW1lGAUgASgJSABSCWdyb3VwTmFtZYgBAU' + 'INCgtfZ3JvdXBfbmFtZRo1CglHcm91cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tleRgBIAEoDFIO' + 'Z3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3VwVXBkYXRlEi' + 'oKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMwoTYWZmZWN0ZWRf' + 'Y29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBARIpCg5uZXdfZ3JvdXBfbm' + 'FtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2RlbGV0ZV9tZXNzYWdlc19hZnRl' + 'cl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb2' + '5kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbmV3X2dyb3VwX25hbWVCKQonX25l' + 'd19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGogCCgtUZXh0TWVzc2FnZRIqCh' + 'FzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlS' + 'BHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZB' + 'gEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgF' + 'IAEoDEgBUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWRCGg' + 'oYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGuYBChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoR' + 'c2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAi' + 'ABKANSCXRpbWVzdGFtcBISCgR0eXBlGAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2Fn' + 'ZV9kYXRhGAQgASgMSABSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBARIWCgZoaWRkZW4YBSABKA' + 'hSBmhpZGRlbkIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEaZAoIUmVhY3Rpb24SKgoRdGFy' + 'Z2V0X21lc3NhZ2VfaWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW' + '1vamkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUavgIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgB' + 'IAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi8KEXNlbm' + 'Rlcl9tZXNzYWdlX2lkGAIgASgJSABSD3NlbmRlck1lc3NhZ2VJZIgBARI9ChttdWx0aXBsZV90' + 'YXJnZXRfbWVzc2FnZV9pZHMYAyADKAlSGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZX' + 'h0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlw' + 'ZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQARIKCgZPUEVORUQQAkIUChJfc2VuZGVyX21lc3' + 'NhZ2VfaWRCBwoFX3RleHQauwYKBU1lZGlhEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg9z' + 'ZW5kZXJNZXNzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVH' + 'lwZVIEdHlwZRJGCh1kaXNwbGF5X2xpbWl0X2luX21pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNw' + 'bGF5TGltaXRJbk1pbGxpc2Vjb25kc4gBARI3ChdyZXF1aXJlc19hdXRoZW50aWNhdGlvbhgEIA' + 'EoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFt' + 'cBItChBxdW90ZV9tZXNzYWdlX2lkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEioKDmRvd2' + '5sb2FkX3Rva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKgoOZW5jcnlwdGlvbl9rZXkY' + 'CCABKAxIA1INZW5jcnlwdGlvbktleYgBARIqCg5lbmNyeXB0aW9uX21hYxgJIAEoDEgEUg1lbm' + 'NyeXB0aW9uTWFjiAEBEi4KEGVuY3J5cHRpb25fbm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5v' + 'bmNliAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZX' + 'NzYWdlRGF0YYgBARIkCgt3aWRnZXRfb25seRgMIAEoCEgHUgp3aWRnZXRPbmx5iAEBIj4KBFR5' + 'cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU' + '8QBEIgCh5fZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHNCEwoRX3F1b3RlX21lc3NhZ2Vf' + 'aWRCEQoPX2Rvd25sb2FkX3Rva2VuQhEKD19lbmNyeXB0aW9uX2tleUIRCg9fZW5jcnlwdGlvbl' + '9tYWNCEwoRX2VuY3J5cHRpb25fbm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhQg4K' + 'DF93aWRnZXRfb25seRpOCgVTdG9yeRItCgVtZWRpYRgBIAEoCzIXLkVuY3J5cHRlZENvbnRlbn' + 'QuTWVkaWFSBW1lZGlhEhYKBm5vdGlmeRgCIAEoCFIGbm90aWZ5GqkBCgtNZWRpYVVwZGF0ZRI2' + 'CgR0eXBlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEi' + 'oKEXRhcmdldF9tZXNzYWdlX2lkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghS' + 'RU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0Um' + 'VxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5U' + 'eXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGq' + 'QCCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRh' + 'Y3RVcGRhdGUuVHlwZVIEdHlwZRI3ChVhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYX' + 'ZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARIm' + 'CgxkaXNwbGF5X25hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRV' + 'NUEAASCgoGVVBEQVRFEAFCGAoWX2F2YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbWVC' + 'DwoNX2Rpc3BsYXlfbmFtZRqvAQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDG' + 'ZsYW1lQ291bnRlchI5ChlsYXN0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0Rmxh' + 'bWVDb3VudGVyQ2hhbmdlEh8KC2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcm' + 'NlX3VwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5' + 'cGluZxgBIAEoCFIIaXNUeXBpbmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFF' + 'VzZXJEaXNjb3ZlcnlSZXF1ZXN0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZl' + 'cnNpb24aMQoTVXNlckRpc2NvdmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZX' + 'MaPQoUS2V5VmVyaWZpY2F0aW9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1' + 'bGF0ZWRNYWMamwEKFFBhc3N3b3JkTGVzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcm' + 'UYASABKAxIAFITcmVjb3ZlcnlTZWNyZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0' + 'ZRIcCgl0aHJlc2hvbGQYAyABKANSCXRocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZR' + 'ozCh1QYXNzd29yZExlc3NSZWNvdmVyeUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsK' + 'CV9ncm91cF9pZEIRCg9faXNfZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZX' + 'JCIAoeX3NlbmRlcl91c2VyX2Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9w' + 'cm9tb3Rpb25zQhkKF193aWRnZXRfc2hhcmluZ19hbGxvd2VkQhYKFF9zZW5kZXJfdHdvbmx5X3' + 'Njb3JlQhEKD19tZXNzYWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9f' + 'Y29udGFjdF91cGRhdGVCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0ILCglfcm' + 'VhY3Rpb25CDwoNX3RleHRfbWVzc2FnZUIPCg1fZ3JvdXBfY3JlYXRlQg0KC19ncm91cF9qb2lu' + 'Qg8KDV9ncm91cF91cGRhdGVCGgoYX3Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5QhEKD19lcnJvcl' + '9tZXNzYWdlc0IaChhfYWRkaXRpb25hbF9kYXRhX21lc3NhZ2VCEwoRX3R5cGluZ19pbmRpY2F0' + 'b3JCGQoXX3VzZXJfZGlzY292ZXJ5X3JlcXVlc3RCGAoWX3VzZXJfZGlzY292ZXJ5X3VwZGF0ZU' + 'IZChdfa2V5X3ZlcmlmaWNhdGlvbl9wcm9vZkIYChZfcGFzc3dvcmRsZXNzX3JlY292ZXJ5QiIK' + 'IF9wYXNzd29yZGxlc3NfcmVjb3ZlcnlfaGVhcnRiZWF0QggKBl9zdG9yeUoECBwQHQ=='); diff --git a/lib/src/providers/routing.provider.dart b/lib/src/providers/routing.provider.dart index 50176376..5c9429cc 100644 --- a/lib/src/providers/routing.provider.dart +++ b/lib/src/providers/routing.provider.dart @@ -42,6 +42,7 @@ import 'package:twonly/src/visual/views/settings/help/news.view.dart'; import 'package:twonly/src/visual/views/settings/notification.view.dart'; import 'package:twonly/src/visual/views/settings/privacy.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/block_users.view.dart'; +import 'package:twonly/src/visual/views/settings/privacy/public_profile.view.dart'; import 'package:twonly/src/visual/views/settings/privacy/user_discovery.view.dart'; import 'package:twonly/src/visual/views/settings/profile/modify_avatar.view.dart'; import 'package:twonly/src/visual/views/settings/profile/profile.view.dart'; @@ -241,6 +242,10 @@ final routerProvider = GoRouter( path: 'user_discovery', builder: (context, state) => const UserDiscoverySettingsView(), ), + GoRoute( + path: 'public_profile', + builder: (context, state) => const PublicProfilePrivacyView(), + ), ], ), GoRoute( diff --git a/lib/src/visual/views/contact/contact.view.dart b/lib/src/visual/views/contact/contact.view.dart index 7ae43b05..f1c104a9 100644 --- a/lib/src/visual/views/contact/contact.view.dart +++ b/lib/src/visual/views/contact/contact.view.dart @@ -19,6 +19,7 @@ import 'package:twonly/src/visual/components/select_chat_deletion_time.comp.dart import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/components/verification_badge.comp.dart'; import 'package:twonly/src/visual/elements/better_list_title.element.dart'; +import 'package:twonly/src/visual/elements/my_chip.element.dart'; import 'package:twonly/src/visual/views/contact/contact_components/mutual_groups_expansion_tile.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/restore_flame.comp.dart'; import 'package:twonly/src/visual/views/contact/contact_components/verification_expansion_tile.comp.dart'; @@ -225,6 +226,39 @@ class _ContactViewState extends State { ), if (getContactDisplayName(contact) != contact.username) Center(child: Text('(${contact.username})')), + if (contact.twonlyScore != null) + Padding( + padding: const EdgeInsets.only(top: 10), + child: Center( + child: IntrinsicWidth( + child: MyChip( + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const FaIcon(FontAwesomeIcons.trophy, size: 14), + const SizedBox(width: 7), + Text('${contact.twonlyScore}'), + ], + ), + onTap: () => showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.lang.publicProfileTwonlyScore), + content: Text( + context.lang.contactTwonlyScoreDescription, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(context.lang.ok), + ), + ], + ), + ), + ), + ), + ), + ), const SizedBox(height: 50), BetterListTile( icon: FontAwesomeIcons.solidComments, diff --git a/lib/src/visual/views/settings/privacy.view.dart b/lib/src/visual/views/settings/privacy.view.dart index e2514595..e8b93222 100644 --- a/lib/src/visual/views/settings/privacy.view.dart +++ b/lib/src/visual/views/settings/privacy.view.dart @@ -47,6 +47,10 @@ class _PrivacyViewState extends State { ), body: ListView( children: [ + ListTile( + title: Text(context.lang.publicProfilePrivacyTitle), + onTap: () => context.push(Routes.settingsPrivacyPublicProfile), + ), ListTile( title: Text(context.lang.settingsPrivacyBlockUsers), subtitle: StreamBuilder( diff --git a/lib/src/visual/views/settings/privacy/public_profile.view.dart b/lib/src/visual/views/settings/privacy/public_profile.view.dart new file mode 100644 index 00000000..e5ba9f7a --- /dev/null +++ b/lib/src/visual/views/settings/privacy/public_profile.view.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:twonly/core/user_config.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/services/user.service.dart'; +import 'package:twonly/src/utils/misc.dart'; + +class PublicProfilePrivacyView extends StatefulWidget { + const PublicProfilePrivacyView({super.key}); + + @override + State createState() => + _PublicProfilePrivacyViewState(); +} + +class _PublicProfilePrivacyViewState extends State { + Future _setTwonlyScoreVisibility( + TwonlyScoreVisibility visibility, + ) async { + await UserService.update((user) { + user.twonlyScoreVisibility = visibility; + }); + if (mounted) setState(() {}); + } + + String _visibilityLabel(TwonlyScoreVisibility visibility) { + return switch (visibility) { + TwonlyScoreVisibility.nobody => context.lang.privacyVisibilityNobody, + TwonlyScoreVisibility.onlyContacts => + context.lang.privacyVisibilityOnlyContacts, + TwonlyScoreVisibility.everyone => context.lang.privacyVisibilityEveryone, + }; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(context.lang.publicProfilePrivacyTitle)), + body: ListView( + padding: const EdgeInsets.only(top: 16), + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Text( + context.lang.publicProfilePrivacyDescription, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: context.color.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 12), + ListTile( + title: Text(context.lang.publicProfileTwonlyScore), + trailing: DropdownButtonHideUnderline( + child: DropdownButton( + value: userService.currentUser.twonlyScoreVisibility, + items: TwonlyScoreVisibility.values + .map( + (visibility) => DropdownMenuItem( + value: visibility, + child: Text(_visibilityLabel(visibility)), + ), + ) + .toList(), + onChanged: (visibility) { + if (visibility != null) { + _setTwonlyScoreVisibility(visibility); + } + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/rust/.sqlx/query-1b8401b1ec6cbb52dfc4ce32b2bf73900eec826b2ca2029a05a0125024ee071d.json b/rust/.sqlx/query-1b8401b1ec6cbb52dfc4ce32b2bf73900eec826b2ca2029a05a0125024ee071d.json new file mode 100644 index 00000000..95b72a9e --- /dev/null +++ b/rust/.sqlx/query-1b8401b1ec6cbb52dfc4ce32b2bf73900eec826b2ca2029a05a0125024ee071d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE contacts SET twonly_score = ? WHERE user_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "1b8401b1ec6cbb52dfc4ce32b2bf73900eec826b2ca2029a05a0125024ee071d" +} diff --git a/rust/.sqlx/query-c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f.json b/rust/.sqlx/query-c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f.json index e7819962..10912e94 100644 --- a/rust/.sqlx/query-c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f.json +++ b/rust/.sqlx/query-c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f.json @@ -321,6 +321,17 @@ "name": "widget_sharing_granted" } } + }, + { + "name": "twonly_score", + "ordinal": 29, + "type_info": "Integer", + "origin": { + "Table": { + "table": "contacts", + "name": "twonly_score" + } + } } ], "parameters": { @@ -355,7 +366,8 @@ false, false, false, - false + false, + true ] }, "hash": "c618a305e1355fcefa5c8420544779b5fe4f003a16ba01ee2834f028fabf213f" diff --git a/rust/src/api/messages/incoming.rs b/rust/src/api/messages/incoming.rs index 022e98cf..ee6c918c 100644 --- a/rust/src/api/messages/incoming.rs +++ b/rust/src/api/messages/incoming.rs @@ -794,6 +794,8 @@ async fn handle_encrypted_inner( return contact::handle_contact_request(ctx, t, from_user_id, request).await; } + Contact::update_twonly_score(t, from_user_id, content.sender_twonly_score).await?; + if let Some(update) = content.contact_update { return contact::handle_contact_update( ctx, @@ -970,6 +972,57 @@ async fn handle_encrypted_inner( mod tests { use super::*; + #[tokio::test] + async fn incoming_twonly_score_is_stored_and_missing_score_clears_it() -> Result<()> { + let directory = tempfile::tempdir()?; + let ctx = Context::init_for_testing( + directory.path().join("database"), + directory.path().join("data"), + ) + .await?; + let database = ctx.app_db.read().await.clone(); + sqlx::query("INSERT INTO contacts(user_id, username) VALUES (7, 'alice')") + .execute(&database.pool) + .await?; + + let mut transaction = database.pool.begin().await?; + handle_encrypted_inner( + &ctx, + &mut transaction, + 7, + "score-present", + proto::EncryptedContent { + sender_twonly_score: Some(23), + ..Default::default() + }, + ) + .await?; + transaction.commit().await?; + let score: Option = + sqlx::query_scalar("SELECT twonly_score FROM contacts WHERE user_id = 7") + .fetch_one(&database.pool) + .await?; + assert_eq!(score, Some(23)); + + let mut transaction = database.pool.begin().await?; + handle_encrypted_inner( + &ctx, + &mut transaction, + 7, + "score-missing", + proto::EncryptedContent::default(), + ) + .await?; + transaction.commit().await?; + let score: Option = + sqlx::query_scalar("SELECT twonly_score FROM contacts WHERE user_id = 7") + .fetch_one(&database.pool) + .await?; + assert_eq!(score, None); + + Ok(()) + } + #[test] fn copies_of_one_receipt_share_a_lock_until_the_last_one_goes() { let receipt_id = "in-flight-registry-test"; diff --git a/rust/src/api/messages/outgoing.rs b/rust/src/api/messages/outgoing.rs index 467220fe..96ca5440 100644 --- a/rust/src/api/messages/outgoing.rs +++ b/rust/src/api/messages/outgoing.rs @@ -8,7 +8,7 @@ use crate::api::messages::incoming::messages; use crate::api::proto::client as proto; use crate::context::Context; use crate::error::Result; -use crate::user_config::UserConfig; +use crate::user_config::{TwonlyScoreVisibility, UserConfig}; use prost::Message as _; use sqlx::{Sqlite, Transaction}; use std::sync::Arc; @@ -66,6 +66,31 @@ pub(crate) async fn decorate_content( .unwrap_or(0) != 0, ); + let share_twonly_score = match config.twonly_score_visibility { + TwonlyScoreVisibility::Nobody => false, + TwonlyScoreVisibility::OnlyContacts => { + sqlx::query_scalar!( + "SELECT accepted FROM contacts WHERE user_id = ?", + contact_id, + ) + .fetch_optional(&mut **t) + .await? + .unwrap_or(0) + != 0 + } + TwonlyScoreVisibility::Everyone => true, + }; + content.sender_twonly_score = if share_twonly_score { + Some( + sqlx::query_scalar::<_, i64>( + "SELECT COALESCE(SUM(total_media_counter), 0) FROM groups", + ) + .fetch_one(&mut **t) + .await?, + ) + } else { + None + }; if config.ask_for_friend_promotions { let accepted = sqlx::query_scalar!("SELECT COUNT(*) FROM contacts WHERE accepted = 1") .fetch_one(&mut **t) @@ -203,3 +228,73 @@ pub async fn send_c2c_message_to_contact( } Ok(None) } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn twonly_score_respects_recipient_visibility() -> Result<()> { + let directory = tempfile::tempdir()?; + let ctx = Context::init_for_testing( + directory.path().join("database"), + directory.path().join("data"), + ) + .await?; + UserConfig::save_json( + &ctx, + r#"{"userId":42,"username":"ben","displayName":"Ben"}"#, + )?; + + let database = ctx.app_db.read().await.clone(); + sqlx::query("INSERT INTO contacts(user_id, username) VALUES (7, 'anna')") + .execute(&database.pool) + .await?; + sqlx::query( + "INSERT INTO groups(group_id, group_name, total_media_counter) VALUES ('a', 'A', 4), ('b', 'B', 7)", + ) + .execute(&database.pool) + .await?; + + let mut content = proto::EncryptedContent::default(); + let mut transaction = database.pool.begin().await?; + decorate_content(&ctx, &mut transaction, 7, &mut content, false).await?; + transaction.rollback().await?; + assert_eq!(content.sender_twonly_score, Some(11)); + + UserConfig::update(&ctx, |config| { + config.twonly_score_visibility = TwonlyScoreVisibility::OnlyContacts; + })?; + let mut hidden_content = proto::EncryptedContent { + sender_twonly_score: Some(999), + ..Default::default() + }; + let mut transaction = database.pool.begin().await?; + decorate_content(&ctx, &mut transaction, 7, &mut hidden_content, false).await?; + transaction.rollback().await?; + assert_eq!(hidden_content.sender_twonly_score, None); + + sqlx::query("UPDATE contacts SET accepted = 1 WHERE user_id = 7") + .execute(&database.pool) + .await?; + let mut contact_content = proto::EncryptedContent::default(); + let mut transaction = database.pool.begin().await?; + decorate_content(&ctx, &mut transaction, 7, &mut contact_content, false).await?; + transaction.rollback().await?; + assert_eq!(contact_content.sender_twonly_score, Some(11)); + + UserConfig::update(&ctx, |config| { + config.twonly_score_visibility = TwonlyScoreVisibility::Nobody; + })?; + let mut nobody_content = proto::EncryptedContent { + sender_twonly_score: Some(999), + ..Default::default() + }; + let mut transaction = database.pool.begin().await?; + decorate_content(&ctx, &mut transaction, 7, &mut nobody_content, false).await?; + transaction.rollback().await?; + assert_eq!(nobody_content.sender_twonly_score, None); + + Ok(()) + } +} diff --git a/rust/src/api/proto/client/messages.proto b/rust/src/api/proto/client/messages.proto index 2ae09263..69bd09e6 100644 --- a/rust/src/api/proto/client/messages.proto +++ b/rust/src/api/proto/client/messages.proto @@ -11,6 +11,8 @@ message EncryptedContent { optional bool ask_for_friend_promotions = 25; reserved 28; optional bool widget_sharing_allowed = 29; + // Sum of all exchanged media, omitted when profile sharing is disabled. + optional int64 sender_twonly_score = 31; optional MessageUpdate message_update = 5; optional Media media = 6; diff --git a/rust/src/database/app/migrations/0029_public_profile_score.sql b/rust/src/database/app/migrations/0029_public_profile_score.sql new file mode 100644 index 00000000..3ed3d24a --- /dev/null +++ b/rust/src/database/app/migrations/0029_public_profile_score.sql @@ -0,0 +1,3 @@ +-- A contact's latest publicly shared twonly Score. NULL means that the +-- contact does not currently share it. +ALTER TABLE contacts ADD COLUMN twonly_score INTEGER; diff --git a/rust/src/database/app/mod.rs b/rust/src/database/app/mod.rs index ea39b97e..02dc6535 100644 --- a/rust/src/database/app/mod.rs +++ b/rust/src/database/app/mod.rs @@ -16,7 +16,7 @@ mod legacy_import; pub mod tables; pub const APP_DATABASE_FILE: &str = "app_db.sqlite"; -pub const APP_SCHEMA_VERSION: i64 = 13; +pub const APP_SCHEMA_VERSION: i64 = 14; /// User-owned application tables in the current Rust schema. Rust-only outbox /// tables are deliberately absent because they are reconstructed locally. diff --git a/rust/src/database/app/tables/contact.rs b/rust/src/database/app/tables/contact.rs index 64be6246..dd1cf852 100644 --- a/rust/src/database/app/tables/contact.rs +++ b/rust/src/database/app/tables/contact.rs @@ -41,6 +41,7 @@ pub struct Contact { pub media_received_counter: i64, pub widget_sharing_allowed: i64, pub widget_sharing_granted: i64, + pub twonly_score: Option, } #[derive(bon::Builder)] @@ -206,6 +207,21 @@ impl Contact { Ok(()) } + pub async fn update_twonly_score( + t: &mut Transaction<'_, Sqlite>, + user_id: i64, + score: Option, + ) -> Result<()> { + sqlx::query!( + "UPDATE contacts SET twonly_score = ? WHERE user_id = ?", + score, + user_id, + ) + .execute(&mut **t) + .await?; + Ok(()) + } + pub async fn is_user_discovery_allowed( context: &Context, t: &mut Transaction<'_, Sqlite>, diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 4270374f..e3959f2e 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -7830,6 +7830,19 @@ impl SseDecode for crate::user_config::TwonlySafeBackup { } } +impl SseDecode for crate::user_config::TwonlyScoreVisibility { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::user_config::TwonlyScoreVisibility::Nobody, + 1 => crate::user_config::TwonlyScoreVisibility::OnlyContacts, + 2 => crate::user_config::TwonlyScoreVisibility::Everyone, + _ => unreachable!("Invalid variant for TwonlyScoreVisibility: {}", inner), + }; + } +} + impl SseDecode for u16 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -7918,6 +7931,8 @@ impl SseDecode for crate::user_config::UserConfig { let mut var_allowErrorTrackingViaSentry = ::sse_decode(deserializer); let mut var_screenLockEnabled = ::sse_decode(deserializer); let mut var_isCloudBackupEnabled = ::sse_decode(deserializer); + let mut var_twonlyScoreVisibility = + ::sse_decode(deserializer); let mut var_isUserDiscoveryEnabled = ::sse_decode(deserializer); let mut var_requiredSendImages = ::sse_decode(deserializer); let mut var_userDiscoveryThreshold = ::sse_decode(deserializer); @@ -7986,6 +8001,7 @@ impl SseDecode for crate::user_config::UserConfig { allow_error_tracking_via_sentry: var_allowErrorTrackingViaSentry, screen_lock_enabled: var_screenLockEnabled, is_cloud_backup_enabled: var_isCloudBackupEnabled, + twonly_score_visibility: var_twonlyScoreVisibility, is_user_discovery_enabled: var_isUserDiscoveryEnabled, required_send_images: var_requiredSendImages, user_discovery_threshold: var_userDiscoveryThreshold, @@ -9202,6 +9218,28 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::user_config::TwonlyScoreVisibility { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Nobody => 0.into_dart(), + Self::OnlyContacts => 1.into_dart(), + Self::Everyone => 2.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::user_config::TwonlyScoreVisibility +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::user_config::TwonlyScoreVisibility +{ + fn into_into_dart(self) -> crate::user_config::TwonlyScoreVisibility { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -9261,6 +9299,7 @@ impl flutter_rust_bridge::IntoDart for crate::user_config::UserConfig { .into_dart(), self.screen_lock_enabled.into_into_dart().into_dart(), self.is_cloud_backup_enabled.into_into_dart().into_dart(), + self.twonly_score_visibility.into_into_dart().into_dart(), self.is_user_discovery_enabled.into_into_dart().into_dart(), self.required_send_images.into_into_dart().into_dart(), self.user_discovery_threshold.into_into_dart().into_dart(), @@ -10344,6 +10383,23 @@ impl SseEncode for crate::user_config::TwonlySafeBackup { } } +impl SseEncode for crate::user_config::TwonlyScoreVisibility { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::user_config::TwonlyScoreVisibility::Nobody => 0, + crate::user_config::TwonlyScoreVisibility::OnlyContacts => 1, + crate::user_config::TwonlyScoreVisibility::Everyone => 2, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + impl SseEncode for u16 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -10445,6 +10501,10 @@ impl SseEncode for crate::user_config::UserConfig { ::sse_encode(self.allow_error_tracking_via_sentry, serializer); ::sse_encode(self.screen_lock_enabled, serializer); ::sse_encode(self.is_cloud_backup_enabled, serializer); + ::sse_encode( + self.twonly_score_visibility, + serializer, + ); ::sse_encode(self.is_user_discovery_enabled, serializer); ::sse_encode(self.required_send_images, serializer); ::sse_encode(self.user_discovery_threshold, serializer); diff --git a/rust/src/user_config.rs b/rust/src/user_config.rs index f528b4a1..572938f9 100644 --- a/rust/src/user_config.rs +++ b/rust/src/user_config.rs @@ -166,6 +166,41 @@ pub enum ThemeMode { Dark, } +#[derive(Clone, Copy, Debug, Default, Serialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum TwonlyScoreVisibility { + Nobody, + OnlyContacts, + #[default] + Everyone, +} + +impl<'de> Deserialize<'de> for TwonlyScoreVisibility { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + let value = serde_json::Value::deserialize(deserializer)?; + match value { + serde_json::Value::String(value) => match value.as_str() { + "nobody" => Ok(Self::Nobody), + "onlyContacts" => Ok(Self::OnlyContacts), + "everyone" => Ok(Self::Everyone), + _ => Err(serde::de::Error::custom(format!( + "unknown twonly Score visibility: {value}" + ))), + }, + // Migrate the boolean setting used by the first version of this + // feature without changing an existing privacy choice. + serde_json::Value::Bool(true) => Ok(Self::Everyone), + serde_json::Value::Bool(false) => Ok(Self::Nobody), + _ => Err(serde::de::Error::custom( + "twonly Score visibility must be a visibility name", + )), + } + } +} + #[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq)] #[serde(rename_all = "camelCase")] #[flutter_rust_bridge::frb(non_final)] @@ -288,6 +323,11 @@ pub struct UserConfig { #[frb(non_final)] pub is_cloud_backup_enabled: bool, + /// Controls which recipients receive the current twonly Score in sender metadata. + #[serde(default, alias = "shareTwonlyScore")] + #[frb(non_final)] + pub twonly_score_visibility: TwonlyScoreVisibility, + #[serde(default)] #[frb(non_final)] pub is_user_discovery_enabled: bool, @@ -480,7 +520,9 @@ impl UserConfig { #[cfg(test)] mod tests { - use super::{merge_changed_fields, UserConfig, MIN_USER_DISCOVERY_THRESHOLD}; + use super::{ + merge_changed_fields, TwonlyScoreVisibility, UserConfig, MIN_USER_DISCOVERY_THRESHOLD, + }; #[test] fn parses_only_rust_fields_and_applies_defaults() { @@ -503,10 +545,45 @@ mod tests { ); assert!(config.can_use_login_token_for_auth); + assert_eq!( + config.twonly_score_visibility, + TwonlyScoreVisibility::Everyone + ); assert!(!config.is_user_discovery_enabled); assert_eq!(config.last_server_message_at, None); } + #[test] + fn migrates_legacy_twonly_score_boolean() { + let hidden: UserConfig = serde_json::from_str( + r#"{ + "userId": 42, + "username": "alice", + "displayName": "Alice", + "shareTwonlyScore": false + }"#, + ) + .unwrap(); + assert_eq!( + hidden.twonly_score_visibility, + TwonlyScoreVisibility::Nobody + ); + + let shared: UserConfig = serde_json::from_str( + r#"{ + "userId": 42, + "username": "alice", + "displayName": "Alice", + "shareTwonlyScore": true + }"#, + ) + .unwrap(); + assert_eq!( + shared.twonly_score_visibility, + TwonlyScoreVisibility::Everyone + ); + } + #[test] fn normalizes_legacy_user_discovery_threshold() { let mut config: UserConfig = serde_json::from_str( diff --git a/test/mocks/user_config.dart b/test/mocks/user_config.dart index 6e308d1f..39166880 100644 --- a/test/mocks/user_config.dart +++ b/test/mocks/user_config.dart @@ -35,6 +35,7 @@ UserConfig testUserConfig({ allowErrorTrackingViaSentry: false, screenLockEnabled: false, isCloudBackupEnabled: false, + twonlyScoreVisibility: TwonlyScoreVisibility.everyone, isUserDiscoveryEnabled: false, requiredSendImages: 4, userDiscoveryThreshold: 3,