share twonly score with friends

This commit is contained in:
otsmr 2026-10-05 22:15:25 +02:00
parent 4337db58be
commit e76eead183
32 changed files with 918 additions and 164 deletions

2
.gitignore vendored
View file

@ -60,4 +60,4 @@ fastlane/repo/status/running.json
# Widget Preview related
.widget_preview/
ios/build/
APKS/*.apk
offline/

View file

@ -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<dynamic>;
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);

View file

@ -396,6 +396,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@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<RustLibWire> {
@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<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_twonly_score_visibility(
TwonlyScoreVisibility self,
SseSerializer serializer,
);
@protected
void sse_encode_u_16(int self, SseSerializer serializer);

View file

@ -398,6 +398,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@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<RustLibWire> {
@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<RustLibWire> {
SseSerializer serializer,
);
@protected
void sse_encode_twonly_score_visibility(
TwonlyScoreVisibility self,
SseSerializer serializer,
);
@protected
void sse_encode_u_16(int self, SseSerializer serializer);

View file

@ -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 &&

View file

@ -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';

View file

@ -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))();

View file

@ -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<int> twonlyScore = GeneratedColumn<int>(
'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<Contact> {
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<Contact> {
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<Contact> {
}
map['widget_sharing_allowed'] = Variable<bool>(widgetSharingAllowed);
map['widget_sharing_granted'] = Variable<bool>(widgetSharingGranted);
if (!nullToAbsent || twonlyScore != null) {
map['twonly_score'] = Variable<int>(twonlyScore);
}
map['media_send_counter'] = Variable<int>(mediaSendCounter);
map['media_received_counter'] = Variable<int>(mediaReceivedCounter);
return map;
@ -944,6 +974,9 @@ class Contact extends DataClass implements Insertable<Contact> {
: 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<Contact> {
widgetSharingGranted: serializer.fromJson<bool>(
json['widgetSharingGranted'],
),
twonlyScore: serializer.fromJson<int?>(json['twonlyScore']),
mediaSendCounter: serializer.fromJson<int>(json['mediaSendCounter']),
mediaReceivedCounter: serializer.fromJson<int>(
json['mediaReceivedCounter'],
@ -1065,6 +1099,7 @@ class Contact extends DataClass implements Insertable<Contact> {
),
'widgetSharingAllowed': serializer.toJson<bool>(widgetSharingAllowed),
'widgetSharingGranted': serializer.toJson<bool>(widgetSharingGranted),
'twonlyScore': serializer.toJson<int?>(twonlyScore),
'mediaSendCounter': serializer.toJson<int>(mediaSendCounter),
'mediaReceivedCounter': serializer.toJson<int>(mediaReceivedCounter),
};
@ -1097,6 +1132,7 @@ class Contact extends DataClass implements Insertable<Contact> {
Value<bool?> askForFriendPromotions = const Value.absent(),
bool? widgetSharingAllowed,
bool? widgetSharingGranted,
Value<int?> twonlyScore = const Value.absent(),
int? mediaSendCounter,
int? mediaReceivedCounter,
}) => Contact(
@ -1145,6 +1181,7 @@ class Contact extends DataClass implements Insertable<Contact> {
: 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<Contact> {
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<Contact> {
..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<Contact> {
askForFriendPromotions,
widgetSharingAllowed,
widgetSharingGranted,
twonlyScore,
mediaSendCounter,
mediaReceivedCounter,
]);
@ -1333,6 +1375,7 @@ class Contact extends DataClass implements Insertable<Contact> {
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<Contact> {
final Value<bool?> askForFriendPromotions;
final Value<bool> widgetSharingAllowed;
final Value<bool> widgetSharingGranted;
final Value<int?> twonlyScore;
final Value<int> mediaSendCounter;
final Value<int> mediaReceivedCounter;
const ContactsCompanion({
@ -1393,6 +1437,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
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<Contact> {
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<Contact> {
Expression<bool>? askForFriendPromotions,
Expression<bool>? widgetSharingAllowed,
Expression<bool>? widgetSharingGranted,
Expression<int>? twonlyScore,
Expression<int>? mediaSendCounter,
Expression<int>? mediaReceivedCounter,
}) {
@ -1497,6 +1544,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
'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<Contact> {
Value<bool?>? askForFriendPromotions,
Value<bool>? widgetSharingAllowed,
Value<bool>? widgetSharingGranted,
Value<int?>? twonlyScore,
Value<int>? mediaSendCounter,
Value<int>? mediaReceivedCounter,
}) {
@ -1568,6 +1617,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
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<Contact> {
widgetSharingGranted.value,
);
}
if (twonlyScore.present) {
map['twonly_score'] = Variable<int>(twonlyScore.value);
}
if (mediaSendCounter.present) {
map['media_send_counter'] = Variable<int>(mediaSendCounter.value);
}
@ -1722,6 +1775,7 @@ class ContactsCompanion extends UpdateCompanion<Contact> {
..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<bool?> askForFriendPromotions,
Value<bool> widgetSharingAllowed,
Value<bool> widgetSharingGranted,
Value<int?> twonlyScore,
Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter,
});
@ -15788,6 +15843,7 @@ typedef $$ContactsTableUpdateCompanionBuilder =
Value<bool?> askForFriendPromotions,
Value<bool> widgetSharingAllowed,
Value<bool> widgetSharingGranted,
Value<int?> twonlyScore,
Value<int> mediaSendCounter,
Value<int> mediaReceivedCounter,
});
@ -16207,6 +16263,11 @@ class $$ContactsTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get twonlyScore => $composableBuilder(
column: $table.twonlyScore,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter,
builder: (column) => ColumnFilters(column),
@ -16667,6 +16728,11 @@ class $$ContactsTableOrderingComposer
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get twonlyScore => $composableBuilder(
column: $table.twonlyScore,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter,
builder: (column) => ColumnOrderings(column),
@ -16804,6 +16870,11 @@ class $$ContactsTableAnnotationComposer
builder: (column) => column,
);
GeneratedColumn<int> get twonlyScore => $composableBuilder(
column: $table.twonlyScore,
builder: (column) => column,
);
GeneratedColumn<int> get mediaSendCounter => $composableBuilder(
column: $table.mediaSendCounter,
builder: (column) => column,
@ -17198,6 +17269,7 @@ class $$ContactsTableTableManager
Value<bool?> askForFriendPromotions = const Value.absent(),
Value<bool> widgetSharingAllowed = const Value.absent(),
Value<bool> widgetSharingGranted = const Value.absent(),
Value<int?> twonlyScore = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(),
Value<int> 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<bool?> askForFriendPromotions = const Value.absent(),
Value<bool> widgetSharingAllowed = const Value.absent(),
Value<bool> widgetSharingGranted = const Value.absent(),
Value<int?> twonlyScore = const Value.absent(),
Value<int> mediaSendCounter = const Value.absent(),
Value<int> mediaReceivedCounter = const Value.absent(),
}) => ContactsCompanion.insert(
@ -17289,6 +17363,7 @@ class $$ContactsTableTableManager
askForFriendPromotions: askForFriendPromotions,
widgetSharingAllowed: widgetSharingAllowed,
widgetSharingGranted: widgetSharingGranted,
twonlyScore: twonlyScore,
mediaSendCounter: mediaSendCounter,
mediaReceivedCounter: mediaReceivedCounter,
),

View file

@ -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:

View file

@ -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 => 'الإشعارات';

View file

@ -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';

View file

@ -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';

View file

@ -93,6 +93,13 @@
"settingsPrivacyBlockUsers": "حظر جهات الاتصال",
"settingsPrivacyBlockUsersDesc": "لن تتمكن جهات الاتصال المحظورة من التواصل معك. يمكنك إلغاء حظر أي جهة اتصال في أي وقت.",
"settingsPrivacyBlockUsersCount": "جهات الاتصال: {len}",
"publicProfilePrivacyTitle": "الملف الشخصي العام",
"publicProfilePrivacyDescription": "اختر من يمكنه رؤية معلومات ملفك الشخصي.",
"publicProfileTwonlyScore": "نقاط twonly",
"contactTwonlyScoreDescription": "تعكس نقاط twonly نشاط الشخص على twonly. ويتم احتسابها استنادًا إلى عدة عوامل، مثل عدد الصور ومقاطع الفيديو التي يرسلها ويستلمها.",
"privacyVisibilityNobody": "لا أحد",
"privacyVisibilityOnlyContacts": "جهات الاتصال فقط",
"privacyVisibilityEveryone": "الجميع",
"settingsNotification": "الإشعارات",
"settingsNotifyPermission": "أذونات الإشعارات",
"settingsNotifyPermissionDesc": "افتح إعدادات النظام للسماح بالإشعارات الفورية.",

View file

@ -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.",

View file

@ -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.",

View file

@ -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<EncryptedContent_Story>(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 =

View file

@ -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==');

View file

@ -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(

View file

@ -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<ContactView> {
),
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<void>(
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,

View file

@ -47,6 +47,10 @@ class _PrivacyViewState extends State<PrivacyView> {
),
body: ListView(
children: [
ListTile(
title: Text(context.lang.publicProfilePrivacyTitle),
onTap: () => context.push(Routes.settingsPrivacyPublicProfile),
),
ListTile(
title: Text(context.lang.settingsPrivacyBlockUsers),
subtitle: StreamBuilder(

View file

@ -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<PublicProfilePrivacyView> createState() =>
_PublicProfilePrivacyViewState();
}
class _PublicProfilePrivacyViewState extends State<PublicProfilePrivacyView> {
Future<void> _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<TwonlyScoreVisibility>(
value: userService.currentUser.twonlyScoreVisibility,
items: TwonlyScoreVisibility.values
.map(
(visibility) => DropdownMenuItem(
value: visibility,
child: Text(_visibilityLabel(visibility)),
),
)
.toList(),
onChanged: (visibility) {
if (visibility != null) {
_setTwonlyScoreVisibility(visibility);
}
},
),
),
),
],
),
);
}
}

View file

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "UPDATE contacts SET twonly_score = ? WHERE user_id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "1b8401b1ec6cbb52dfc4ce32b2bf73900eec826b2ca2029a05a0125024ee071d"
}

View file

@ -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"

View file

@ -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<i64> =
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<i64> =
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";

View file

@ -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(())
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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.

View file

@ -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<i64>,
}
#[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<i64>,
) -> 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>,

View file

@ -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 = <i32>::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 = <bool>::sse_decode(deserializer);
let mut var_screenLockEnabled = <bool>::sse_decode(deserializer);
let mut var_isCloudBackupEnabled = <bool>::sse_decode(deserializer);
let mut var_twonlyScoreVisibility =
<crate::user_config::TwonlyScoreVisibility>::sse_decode(deserializer);
let mut var_isUserDiscoveryEnabled = <bool>::sse_decode(deserializer);
let mut var_requiredSendImages = <i64>::sse_decode(deserializer);
let mut var_userDiscoveryThreshold = <u8>::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<crate::user_config::TwonlySafeBackup>
}
}
// 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<crate::user_config::TwonlyScoreVisibility>
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) {
<i32>::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 {
<bool>::sse_encode(self.allow_error_tracking_via_sentry, serializer);
<bool>::sse_encode(self.screen_lock_enabled, serializer);
<bool>::sse_encode(self.is_cloud_backup_enabled, serializer);
<crate::user_config::TwonlyScoreVisibility>::sse_encode(
self.twonly_score_visibility,
serializer,
);
<bool>::sse_encode(self.is_user_discovery_enabled, serializer);
<i64>::sse_encode(self.required_send_images, serializer);
<u8>::sse_encode(self.user_discovery_threshold, serializer);

View file

@ -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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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(

View file

@ -35,6 +35,7 @@ UserConfig testUserConfig({
allowErrorTrackingViaSentry: false,
screenLockEnabled: false,
isCloudBackupEnabled: false,
twonlyScoreVisibility: TwonlyScoreVisibility.everyone,
isUserDiscoveryEnabled: false,
requiredSendImages: 4,
userDiscoveryThreshold: 3,