mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-10-09 02:54:14 +00:00
person avatar
This commit is contained in:
parent
7d17ffba1f
commit
854121c8c4
36 changed files with 2663 additions and 527 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8cf0eb53c6955201bea92d4515e0978e383640b3
|
Subproject commit c3d085882368d7269956048bbb69fbd8a69a711c
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
import '../api/server/prekeys.dart';
|
import '../api/server/prekeys.dart';
|
||||||
import '../frb_generated.dart';
|
import '../frb_generated.dart';
|
||||||
|
import '../services/avatars.dart';
|
||||||
import '../services/media_upload.dart';
|
import '../services/media_upload.dart';
|
||||||
import '../services/stickers.dart';
|
import '../services/stickers.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
@ -353,6 +354,14 @@ class RustApi {
|
||||||
static Future<ApiConnectionState> connectionState() =>
|
static Future<ApiConnectionState> connectionState() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiConnectionState();
|
RustLib.instance.api.crateBridgeApiRustApiConnectionState();
|
||||||
|
|
||||||
|
static Future<CustomAvatarInfo> createCustomAvatar({
|
||||||
|
required String imagePath,
|
||||||
|
required bool acceptedContactsOnly,
|
||||||
|
}) => RustLib.instance.api.crateBridgeApiRustApiCreateCustomAvatar(
|
||||||
|
imagePath: imagePath,
|
||||||
|
acceptedContactsOnly: acceptedContactsOnly,
|
||||||
|
);
|
||||||
|
|
||||||
/// Creates a transparent, message-sized sticker from a local image. Model
|
/// Creates a transparent, message-sized sticker from a local image. Model
|
||||||
/// inference and image encoding are CPU-heavy, so neither runs on Tokio's
|
/// inference and image encoding are CPU-heavy, so neither runs on Tokio's
|
||||||
/// async worker threads.
|
/// async worker threads.
|
||||||
|
|
@ -426,6 +435,9 @@ class RustApi {
|
||||||
static Future<void> forceIpaCheck() =>
|
static Future<void> forceIpaCheck() =>
|
||||||
RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck();
|
RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck();
|
||||||
|
|
||||||
|
static Future<CustomAvatarInfo> getCustomAvatar() =>
|
||||||
|
RustLib.instance.api.crateBridgeApiRustApiGetCustomAvatar();
|
||||||
|
|
||||||
static Future<String> getMemoriesUrl({
|
static Future<String> getMemoriesUrl({
|
||||||
required String mediaId,
|
required String mediaId,
|
||||||
required bool thumbnail,
|
required bool thumbnail,
|
||||||
|
|
@ -667,6 +679,9 @@ class RustApi {
|
||||||
userId: userId,
|
userId: userId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static Future<CustomAvatarInfo> removeCustomAvatar() =>
|
||||||
|
RustLib.instance.api.crateBridgeApiRustApiRemoveCustomAvatar();
|
||||||
|
|
||||||
/// Deletes every file of a media item while keeping its row.
|
/// Deletes every file of a media item while keeping its row.
|
||||||
static Future<void> removeMediaFiles({required String mediaId}) => RustLib
|
static Future<void> removeMediaFiles({required String mediaId}) => RustLib
|
||||||
.instance
|
.instance
|
||||||
|
|
@ -800,6 +815,12 @@ class RustApi {
|
||||||
.api
|
.api
|
||||||
.crateBridgeApiRustApiSetBackground(inBackground: inBackground);
|
.crateBridgeApiRustApiSetBackground(inBackground: inBackground);
|
||||||
|
|
||||||
|
static Future<CustomAvatarInfo> setCustomAvatarAudience({
|
||||||
|
required bool acceptedContactsOnly,
|
||||||
|
}) => RustLib.instance.api.crateBridgeApiRustApiSetCustomAvatarAudience(
|
||||||
|
acceptedContactsOnly: acceptedContactsOnly,
|
||||||
|
);
|
||||||
|
|
||||||
static Future<void> setHomeWidgetGroups({
|
static Future<void> setHomeWidgetGroups({
|
||||||
required String widgetId,
|
required String widgetId,
|
||||||
required String platform,
|
required String platform,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -26,6 +26,7 @@ import 'frb_generated.dart';
|
||||||
import 'keys/backup_password_keys.dart';
|
import 'keys/backup_password_keys.dart';
|
||||||
import 'lib.dart';
|
import 'lib.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||||
|
import 'services/avatars.dart';
|
||||||
import 'services/media_upload.dart';
|
import 'services/media_upload.dart';
|
||||||
import 'services/stickers.dart';
|
import 'services/stickers.dart';
|
||||||
import 'signal/engine.dart';
|
import 'signal/engine.dart';
|
||||||
|
|
@ -137,6 +138,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw);
|
WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
CustomAvatarInfo dco_decode_custom_avatar_info(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
double dco_decode_f_64(dynamic raw);
|
double dco_decode_f_64(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -559,6 +563,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
CustomAvatarInfo sse_decode_custom_avatar_info(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
double sse_decode_f_64(SseDeserializer deserializer);
|
double sse_decode_f_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -1078,6 +1085,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_custom_avatar_info(
|
||||||
|
CustomAvatarInfo self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import 'frb_generated.dart';
|
||||||
import 'keys/backup_password_keys.dart';
|
import 'keys/backup_password_keys.dart';
|
||||||
import 'lib.dart';
|
import 'lib.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||||
|
import 'services/avatars.dart';
|
||||||
import 'services/media_upload.dart';
|
import 'services/media_upload.dart';
|
||||||
import 'services/stickers.dart';
|
import 'services/stickers.dart';
|
||||||
import 'signal/engine.dart';
|
import 'signal/engine.dart';
|
||||||
|
|
@ -139,6 +140,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
@protected
|
@protected
|
||||||
WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw);
|
WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
CustomAvatarInfo dco_decode_custom_avatar_info(dynamic raw);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
double dco_decode_f_64(dynamic raw);
|
double dco_decode_f_64(dynamic raw);
|
||||||
|
|
||||||
|
|
@ -561,6 +565,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseDeserializer deserializer,
|
SseDeserializer deserializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
CustomAvatarInfo sse_decode_custom_avatar_info(SseDeserializer deserializer);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
double sse_decode_f_64(SseDeserializer deserializer);
|
double sse_decode_f_64(SseDeserializer deserializer);
|
||||||
|
|
||||||
|
|
@ -1080,6 +1087,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||||
SseSerializer serializer,
|
SseSerializer serializer,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@protected
|
||||||
|
void sse_encode_custom_avatar_info(
|
||||||
|
CustomAvatarInfo self,
|
||||||
|
SseSerializer serializer,
|
||||||
|
);
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||||
|
|
||||||
|
|
|
||||||
46
lib/core/services/avatars.dart
Normal file
46
lib/core/services/avatars.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// This file is automatically generated, so please do not edit it.
|
||||||
|
// @generated by `flutter_rust_bridge`@ 2.12.0.
|
||||||
|
|
||||||
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
|
import '../frb_generated.dart';
|
||||||
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
|
class CustomAvatarInfo {
|
||||||
|
final Uint8List? webp;
|
||||||
|
final Uint8List? sha256;
|
||||||
|
final int? width;
|
||||||
|
final int? height;
|
||||||
|
final bool acceptedContactsOnly;
|
||||||
|
final PlatformInt64 sourceRevision;
|
||||||
|
|
||||||
|
const CustomAvatarInfo({
|
||||||
|
this.webp,
|
||||||
|
this.sha256,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
required this.acceptedContactsOnly,
|
||||||
|
required this.sourceRevision,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
webp.hashCode ^
|
||||||
|
sha256.hashCode ^
|
||||||
|
width.hashCode ^
|
||||||
|
height.hashCode ^
|
||||||
|
acceptedContactsOnly.hashCode ^
|
||||||
|
sourceRevision.hashCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
other is CustomAvatarInfo &&
|
||||||
|
runtimeType == other.runtimeType &&
|
||||||
|
webp == other.webp &&
|
||||||
|
sha256 == other.sha256 &&
|
||||||
|
width == other.width &&
|
||||||
|
height == other.height &&
|
||||||
|
acceptedContactsOnly == other.acceptedContactsOnly &&
|
||||||
|
sourceRevision == other.sourceRevision;
|
||||||
|
}
|
||||||
|
|
@ -2,4 +2,6 @@ class KeyValueKeys {
|
||||||
static const String currentBackupState = 'current_backup_state';
|
static const String currentBackupState = 'current_backup_state';
|
||||||
static const String backupRecoveryState = 'backup_recovery_state';
|
static const String backupRecoveryState = 'backup_recovery_state';
|
||||||
static const String onboardingState = 'onboarding_state';
|
static const String onboardingState = 'onboarding_state';
|
||||||
|
static const String shareImageWidgetExplainer =
|
||||||
|
'share_image_widget_explainer';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2440,6 +2440,78 @@ abstract class AppLocalizations {
|
||||||
/// **'Discard'**
|
/// **'Discard'**
|
||||||
String get avatarSaveChangesDiscard;
|
String get avatarSaveChangesDiscard;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarSvgTab.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Public avatar'**
|
||||||
|
String get customAvatarSvgTab;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarCustomize.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Customize'**
|
||||||
|
String get customAvatarCustomize;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarPhotoTab.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Personal avatar'**
|
||||||
|
String get customAvatarPhotoTab;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarExplanation.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Replaces your public avatar for the selected audience.'**
|
||||||
|
String get customAvatarExplanation;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarCreate.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose photo'**
|
||||||
|
String get customAvatarCreate;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarReplace.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Change photo'**
|
||||||
|
String get customAvatarReplace;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarRemove.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Remove'**
|
||||||
|
String get customAvatarRemove;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarAudience.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Who can see the photo avatar?'**
|
||||||
|
String get customAvatarAudience;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarAcceptedOnly.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Contacts only'**
|
||||||
|
String get customAvatarAcceptedOnly;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarEveryone.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Everyone'**
|
||||||
|
String get customAvatarEveryone;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarChooseSource.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose avatar photo'**
|
||||||
|
String get customAvatarChooseSource;
|
||||||
|
|
||||||
|
/// No description provided for @customAvatarCreateFailed.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The photo avatar could not be created.'**
|
||||||
|
String get customAvatarCreateFailed;
|
||||||
|
|
||||||
/// No description provided for @inProcess.
|
/// No description provided for @inProcess.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -1398,6 +1398,43 @@ class AppLocalizationsAr extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get avatarSaveChangesDiscard => 'تجاهل';
|
String get avatarSaveChangesDiscard => 'تجاهل';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarSvgTab => 'الصورة الرمزية العامة';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCustomize => 'تخصيص';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarPhotoTab => 'الصورة الرمزية الشخصية';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarExplanation =>
|
||||||
|
'تحل محل صورتك الرمزية العامة لدى الجمهور المحدد.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCreate => 'اختيار صورة';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarReplace => 'تغيير الصورة';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarRemove => 'إزالة';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarAudience => 'من يمكنه رؤية الصورة الرمزية؟';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarAcceptedOnly => 'جهات الاتصال فقط';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarEveryone => 'الجميع';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarChooseSource => 'اختر صورة رمزية';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCreateFailed => 'تعذر إنشاء الصورة الرمزية.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get inProcess => 'قيد المعالجة';
|
String get inProcess => 'قيد المعالجة';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1387,6 +1387,44 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get avatarSaveChangesDiscard => 'Verwerfen';
|
String get avatarSaveChangesDiscard => 'Verwerfen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarSvgTab => 'Öffentlicher Avatar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCustomize => 'Anpassen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarPhotoTab => 'Persönlicher Avatar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarExplanation =>
|
||||||
|
'Ersetzt deinen öffentlichen Avatar für den ausgewählten Personenkreis.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCreate => 'Foto auswählen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarReplace => 'Foto ändern';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarRemove => 'Entfernen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarAudience => 'Wer darf den Foto-Avatar sehen?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarAcceptedOnly => 'Nur Kontakte';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarEveryone => 'Alle';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarChooseSource => 'Avatar-Foto auswählen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCreateFailed =>
|
||||||
|
'Der Foto-Avatar konnte nicht erstellt werden.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get inProcess => 'Wird verarbeitet';
|
String get inProcess => 'Wird verarbeitet';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1379,6 +1379,44 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get avatarSaveChangesDiscard => 'Discard';
|
String get avatarSaveChangesDiscard => 'Discard';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarSvgTab => 'Public avatar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCustomize => 'Customize';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarPhotoTab => 'Personal avatar';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarExplanation =>
|
||||||
|
'Replaces your public avatar for the selected audience.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCreate => 'Choose photo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarReplace => 'Change photo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarRemove => 'Remove';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarAudience => 'Who can see the photo avatar?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarAcceptedOnly => 'Contacts only';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarEveryone => 'Everyone';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarChooseSource => 'Choose avatar photo';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get customAvatarCreateFailed =>
|
||||||
|
'The photo avatar could not be created.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get inProcess => 'In process';
|
String get inProcess => 'In process';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -390,6 +390,18 @@
|
||||||
"avatarSaveChanges": "هل تريد حفظ التغييرات؟",
|
"avatarSaveChanges": "هل تريد حفظ التغييرات؟",
|
||||||
"avatarSaveChangesStore": "حفظ",
|
"avatarSaveChangesStore": "حفظ",
|
||||||
"avatarSaveChangesDiscard": "تجاهل",
|
"avatarSaveChangesDiscard": "تجاهل",
|
||||||
|
"customAvatarSvgTab": "الصورة الرمزية العامة",
|
||||||
|
"customAvatarCustomize": "تخصيص",
|
||||||
|
"customAvatarPhotoTab": "الصورة الرمزية الشخصية",
|
||||||
|
"customAvatarExplanation": "تحل محل صورتك الرمزية العامة لدى الجمهور المحدد.",
|
||||||
|
"customAvatarCreate": "اختيار صورة",
|
||||||
|
"customAvatarReplace": "تغيير الصورة",
|
||||||
|
"customAvatarRemove": "إزالة",
|
||||||
|
"customAvatarAudience": "من يمكنه رؤية الصورة الرمزية؟",
|
||||||
|
"customAvatarAcceptedOnly": "جهات الاتصال فقط",
|
||||||
|
"customAvatarEveryone": "الجميع",
|
||||||
|
"customAvatarChooseSource": "اختر صورة رمزية",
|
||||||
|
"customAvatarCreateFailed": "تعذر إنشاء الصورة الرمزية.",
|
||||||
"inProcess": "قيد المعالجة",
|
"inProcess": "قيد المعالجة",
|
||||||
"draftMessage": "مسودة",
|
"draftMessage": "مسودة",
|
||||||
"voiceMessageSlideToCancel": "اسحب للإلغاء",
|
"voiceMessageSlideToCancel": "اسحب للإلغاء",
|
||||||
|
|
|
||||||
|
|
@ -455,6 +455,18 @@
|
||||||
"avatarSaveChanges": "Möchtest du die Änderungen speichern?",
|
"avatarSaveChanges": "Möchtest du die Änderungen speichern?",
|
||||||
"avatarSaveChangesStore": "Speichern",
|
"avatarSaveChangesStore": "Speichern",
|
||||||
"avatarSaveChangesDiscard": "Verwerfen",
|
"avatarSaveChangesDiscard": "Verwerfen",
|
||||||
|
"customAvatarSvgTab": "Öffentlicher Avatar",
|
||||||
|
"customAvatarCustomize": "Anpassen",
|
||||||
|
"customAvatarPhotoTab": "Persönlicher Avatar",
|
||||||
|
"customAvatarExplanation": "Ersetzt deinen öffentlichen Avatar für den ausgewählten Personenkreis.",
|
||||||
|
"customAvatarCreate": "Foto auswählen",
|
||||||
|
"customAvatarReplace": "Foto ändern",
|
||||||
|
"customAvatarRemove": "Entfernen",
|
||||||
|
"customAvatarAudience": "Wer darf den Foto-Avatar sehen?",
|
||||||
|
"customAvatarAcceptedOnly": "Nur Kontakte",
|
||||||
|
"customAvatarEveryone": "Alle",
|
||||||
|
"customAvatarChooseSource": "Avatar-Foto auswählen",
|
||||||
|
"customAvatarCreateFailed": "Der Foto-Avatar konnte nicht erstellt werden.",
|
||||||
"inProcess": "Wird verarbeitet",
|
"inProcess": "Wird verarbeitet",
|
||||||
"draftMessage": "Entwurf",
|
"draftMessage": "Entwurf",
|
||||||
"voiceMessageSlideToCancel": "Zum Abbrechen ziehen",
|
"voiceMessageSlideToCancel": "Zum Abbrechen ziehen",
|
||||||
|
|
|
||||||
|
|
@ -465,6 +465,18 @@
|
||||||
"avatarSaveChanges": "Would you like to save the changes?",
|
"avatarSaveChanges": "Would you like to save the changes?",
|
||||||
"avatarSaveChangesStore": "Save",
|
"avatarSaveChangesStore": "Save",
|
||||||
"avatarSaveChangesDiscard": "Discard",
|
"avatarSaveChangesDiscard": "Discard",
|
||||||
|
"customAvatarSvgTab": "Public avatar",
|
||||||
|
"customAvatarCustomize": "Customize",
|
||||||
|
"customAvatarPhotoTab": "Personal avatar",
|
||||||
|
"customAvatarExplanation": "Replaces your public avatar for the selected audience.",
|
||||||
|
"customAvatarCreate": "Choose photo",
|
||||||
|
"customAvatarReplace": "Change photo",
|
||||||
|
"customAvatarRemove": "Remove",
|
||||||
|
"customAvatarAudience": "Who can see the photo avatar?",
|
||||||
|
"customAvatarAcceptedOnly": "Contacts only",
|
||||||
|
"customAvatarEveryone": "Everyone",
|
||||||
|
"customAvatarChooseSource": "Choose avatar photo",
|
||||||
|
"customAvatarCreateFailed": "The photo avatar could not be created.",
|
||||||
"inProcess": "In process",
|
"inProcess": "In process",
|
||||||
"draftMessage": "Draft",
|
"draftMessage": "Draft",
|
||||||
"voiceMessageSlideToCancel": "Slide to cancel",
|
"voiceMessageSlideToCancel": "Slide to cancel",
|
||||||
|
|
|
||||||
|
|
@ -1174,6 +1174,8 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
||||||
$core.List<$core.int>? avatarSvgCompressed,
|
$core.List<$core.int>? avatarSvgCompressed,
|
||||||
$core.String? username,
|
$core.String? username,
|
||||||
$core.String? displayName,
|
$core.String? displayName,
|
||||||
|
$core.bool? customAvatarRequested,
|
||||||
|
EncryptedContent_CustomAvatar? customAvatar,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (type != null) result.type = type;
|
if (type != null) result.type = type;
|
||||||
|
|
@ -1181,6 +1183,9 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
||||||
result.avatarSvgCompressed = avatarSvgCompressed;
|
result.avatarSvgCompressed = avatarSvgCompressed;
|
||||||
if (username != null) result.username = username;
|
if (username != null) result.username = username;
|
||||||
if (displayName != null) result.displayName = displayName;
|
if (displayName != null) result.displayName = displayName;
|
||||||
|
if (customAvatarRequested != null)
|
||||||
|
result.customAvatarRequested = customAvatarRequested;
|
||||||
|
if (customAvatar != null) result.customAvatar = customAvatar;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1202,6 +1207,10 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
||||||
2, _omitFieldNames ? '' : 'avatarSvgCompressed', $pb.PbFieldType.OY)
|
2, _omitFieldNames ? '' : 'avatarSvgCompressed', $pb.PbFieldType.OY)
|
||||||
..aOS(3, _omitFieldNames ? '' : 'username')
|
..aOS(3, _omitFieldNames ? '' : 'username')
|
||||||
..aOS(4, _omitFieldNames ? '' : 'displayName')
|
..aOS(4, _omitFieldNames ? '' : 'displayName')
|
||||||
|
..aOB(5, _omitFieldNames ? '' : 'customAvatarRequested')
|
||||||
|
..aOM<EncryptedContent_CustomAvatar>(
|
||||||
|
6, _omitFieldNames ? '' : 'customAvatar',
|
||||||
|
subBuilder: EncryptedContent_CustomAvatar.create)
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
|
@ -1261,6 +1270,160 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
||||||
$core.bool hasDisplayName() => $_has(3);
|
$core.bool hasDisplayName() => $_has(3);
|
||||||
@$pb.TagNumber(4)
|
@$pb.TagNumber(4)
|
||||||
void clearDisplayName() => $_clearField(4);
|
void clearDisplayName() => $_clearField(4);
|
||||||
|
|
||||||
|
/// Set on a REQUEST only by clients that understand CustomAvatar.
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.bool get customAvatarRequested => $_getBF(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
set customAvatarRequested($core.bool value) => $_setBool(4, value);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.bool hasCustomAvatarRequested() => $_has(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
void clearCustomAvatarRequested() => $_clearField(5);
|
||||||
|
|
||||||
|
/// Set on UPDATE only in direct response to such a request.
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
EncryptedContent_CustomAvatar get customAvatar => $_getN(5);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
set customAvatar(EncryptedContent_CustomAvatar value) => $_setField(6, value);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
$core.bool hasCustomAvatar() => $_has(5);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
void clearCustomAvatar() => $_clearField(6);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
EncryptedContent_CustomAvatar ensureCustomAvatar() => $_ensure(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EncryptedContent_CustomAvatar extends $pb.GeneratedMessage {
|
||||||
|
factory EncryptedContent_CustomAvatar({
|
||||||
|
$core.int? version,
|
||||||
|
$fixnum.Int64? publicationCounter,
|
||||||
|
EncryptedContent_CustomAvatar_State? state,
|
||||||
|
$core.List<$core.int>? webp,
|
||||||
|
$core.List<$core.int>? sha256,
|
||||||
|
$core.int? width,
|
||||||
|
$core.int? height,
|
||||||
|
}) {
|
||||||
|
final result = create();
|
||||||
|
if (version != null) result.version = version;
|
||||||
|
if (publicationCounter != null)
|
||||||
|
result.publicationCounter = publicationCounter;
|
||||||
|
if (state != null) result.state = state;
|
||||||
|
if (webp != null) result.webp = webp;
|
||||||
|
if (sha256 != null) result.sha256 = sha256;
|
||||||
|
if (width != null) result.width = width;
|
||||||
|
if (height != null) result.height = height;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
EncryptedContent_CustomAvatar._();
|
||||||
|
|
||||||
|
factory EncryptedContent_CustomAvatar.fromBuffer($core.List<$core.int> data,
|
||||||
|
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||||
|
create()..mergeFromBuffer(data, registry);
|
||||||
|
factory EncryptedContent_CustomAvatar.fromJson($core.String json,
|
||||||
|
[$pb.ExtensionRegistry registry = $pb.ExtensionRegistry.EMPTY]) =>
|
||||||
|
create()..mergeFromJson(json, registry);
|
||||||
|
|
||||||
|
static final $pb.BuilderInfo _i = $pb.BuilderInfo(
|
||||||
|
_omitMessageNames ? '' : 'EncryptedContent.CustomAvatar',
|
||||||
|
createEmptyInstance: create)
|
||||||
|
..aI(1, _omitFieldNames ? '' : 'version', fieldType: $pb.PbFieldType.OU3)
|
||||||
|
..aInt64(2, _omitFieldNames ? '' : 'publicationCounter')
|
||||||
|
..aE<EncryptedContent_CustomAvatar_State>(3, _omitFieldNames ? '' : 'state',
|
||||||
|
enumValues: EncryptedContent_CustomAvatar_State.values)
|
||||||
|
..a<$core.List<$core.int>>(
|
||||||
|
4, _omitFieldNames ? '' : 'webp', $pb.PbFieldType.OY)
|
||||||
|
..a<$core.List<$core.int>>(
|
||||||
|
5, _omitFieldNames ? '' : 'sha256', $pb.PbFieldType.OY)
|
||||||
|
..aI(6, _omitFieldNames ? '' : 'width', fieldType: $pb.PbFieldType.OU3)
|
||||||
|
..aI(7, _omitFieldNames ? '' : 'height', fieldType: $pb.PbFieldType.OU3)
|
||||||
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
EncryptedContent_CustomAvatar clone() => deepCopy();
|
||||||
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
EncryptedContent_CustomAvatar copyWith(
|
||||||
|
void Function(EncryptedContent_CustomAvatar) updates) =>
|
||||||
|
super.copyWith(
|
||||||
|
(message) => updates(message as EncryptedContent_CustomAvatar))
|
||||||
|
as EncryptedContent_CustomAvatar;
|
||||||
|
|
||||||
|
@$core.override
|
||||||
|
$pb.BuilderInfo get info_ => _i;
|
||||||
|
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static EncryptedContent_CustomAvatar create() =>
|
||||||
|
EncryptedContent_CustomAvatar._();
|
||||||
|
@$core.override
|
||||||
|
EncryptedContent_CustomAvatar createEmptyInstance() => create();
|
||||||
|
@$core.pragma('dart2js:noInline')
|
||||||
|
static EncryptedContent_CustomAvatar getDefault() => _defaultInstance ??=
|
||||||
|
$pb.GeneratedMessage.$_defaultFor<EncryptedContent_CustomAvatar>(create);
|
||||||
|
static EncryptedContent_CustomAvatar? _defaultInstance;
|
||||||
|
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.int get version => $_getIZ(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
set version($core.int value) => $_setUnsignedInt32(0, value);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
$core.bool hasVersion() => $_has(0);
|
||||||
|
@$pb.TagNumber(1)
|
||||||
|
void clearVersion() => $_clearField(1);
|
||||||
|
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$fixnum.Int64 get publicationCounter => $_getI64(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
set publicationCounter($fixnum.Int64 value) => $_setInt64(1, value);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
$core.bool hasPublicationCounter() => $_has(1);
|
||||||
|
@$pb.TagNumber(2)
|
||||||
|
void clearPublicationCounter() => $_clearField(2);
|
||||||
|
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
EncryptedContent_CustomAvatar_State get state => $_getN(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
set state(EncryptedContent_CustomAvatar_State value) => $_setField(3, value);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
$core.bool hasState() => $_has(2);
|
||||||
|
@$pb.TagNumber(3)
|
||||||
|
void clearState() => $_clearField(3);
|
||||||
|
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.List<$core.int> get webp => $_getN(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
set webp($core.List<$core.int> value) => $_setBytes(3, value);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
$core.bool hasWebp() => $_has(3);
|
||||||
|
@$pb.TagNumber(4)
|
||||||
|
void clearWebp() => $_clearField(4);
|
||||||
|
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.List<$core.int> get sha256 => $_getN(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
set sha256($core.List<$core.int> value) => $_setBytes(4, value);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
$core.bool hasSha256() => $_has(4);
|
||||||
|
@$pb.TagNumber(5)
|
||||||
|
void clearSha256() => $_clearField(5);
|
||||||
|
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
$core.int get width => $_getIZ(5);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
set width($core.int value) => $_setUnsignedInt32(5, value);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
$core.bool hasWidth() => $_has(5);
|
||||||
|
@$pb.TagNumber(6)
|
||||||
|
void clearWidth() => $_clearField(6);
|
||||||
|
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$core.int get height => $_getIZ(6);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
set height($core.int value) => $_setUnsignedInt32(6, value);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$core.bool hasHeight() => $_has(6);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
void clearHeight() => $_clearField(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
|
class EncryptedContent_FlameSync extends $pb.GeneratedMessage {
|
||||||
|
|
@ -1775,6 +1938,8 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
$core.bool? widgetSharingAllowed,
|
$core.bool? widgetSharingAllowed,
|
||||||
EncryptedContent_Story? story,
|
EncryptedContent_Story? story,
|
||||||
$fixnum.Int64? senderTwonlyScore,
|
$fixnum.Int64? senderTwonlyScore,
|
||||||
|
$fixnum.Int64? senderCustomAvatarCounter,
|
||||||
|
$core.int? customAvatarProtocolVersion,
|
||||||
}) {
|
}) {
|
||||||
final result = create();
|
final result = create();
|
||||||
if (groupId != null) result.groupId = groupId;
|
if (groupId != null) result.groupId = groupId;
|
||||||
|
|
@ -1816,6 +1981,10 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
result.widgetSharingAllowed = widgetSharingAllowed;
|
result.widgetSharingAllowed = widgetSharingAllowed;
|
||||||
if (story != null) result.story = story;
|
if (story != null) result.story = story;
|
||||||
if (senderTwonlyScore != null) result.senderTwonlyScore = senderTwonlyScore;
|
if (senderTwonlyScore != null) result.senderTwonlyScore = senderTwonlyScore;
|
||||||
|
if (senderCustomAvatarCounter != null)
|
||||||
|
result.senderCustomAvatarCounter = senderCustomAvatarCounter;
|
||||||
|
if (customAvatarProtocolVersion != null)
|
||||||
|
result.customAvatarProtocolVersion = customAvatarProtocolVersion;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1896,6 +2065,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
..aOM<EncryptedContent_Story>(30, _omitFieldNames ? '' : 'story',
|
..aOM<EncryptedContent_Story>(30, _omitFieldNames ? '' : 'story',
|
||||||
subBuilder: EncryptedContent_Story.create)
|
subBuilder: EncryptedContent_Story.create)
|
||||||
..aInt64(31, _omitFieldNames ? '' : 'senderTwonlyScore')
|
..aInt64(31, _omitFieldNames ? '' : 'senderTwonlyScore')
|
||||||
|
..aInt64(32, _omitFieldNames ? '' : 'senderCustomAvatarCounter')
|
||||||
|
..aI(33, _omitFieldNames ? '' : 'customAvatarProtocolVersion',
|
||||||
|
fieldType: $pb.PbFieldType.OU3)
|
||||||
..hasRequiredFields = false;
|
..hasRequiredFields = false;
|
||||||
|
|
||||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||||
|
|
@ -2226,6 +2398,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
@$pb.TagNumber(30)
|
@$pb.TagNumber(30)
|
||||||
EncryptedContent_Story ensureStory() => $_ensure(26);
|
EncryptedContent_Story ensureStory() => $_ensure(26);
|
||||||
|
|
||||||
|
/// Sum of all exchanged media, omitted when profile sharing is disabled.
|
||||||
@$pb.TagNumber(31)
|
@$pb.TagNumber(31)
|
||||||
$fixnum.Int64 get senderTwonlyScore => $_getI64(27);
|
$fixnum.Int64 get senderTwonlyScore => $_getI64(27);
|
||||||
@$pb.TagNumber(31)
|
@$pb.TagNumber(31)
|
||||||
|
|
@ -2234,6 +2407,27 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
||||||
$core.bool hasSenderTwonlyScore() => $_has(27);
|
$core.bool hasSenderTwonlyScore() => $_has(27);
|
||||||
@$pb.TagNumber(31)
|
@$pb.TagNumber(31)
|
||||||
void clearSenderTwonlyScore() => $_clearField(31);
|
void clearSenderTwonlyScore() => $_clearField(31);
|
||||||
|
|
||||||
|
/// Per-recipient version of the custom photo-avatar state. Receivers request
|
||||||
|
/// the payload only when this advances; the image is never broadcast.
|
||||||
|
@$pb.TagNumber(32)
|
||||||
|
$fixnum.Int64 get senderCustomAvatarCounter => $_getI64(28);
|
||||||
|
@$pb.TagNumber(32)
|
||||||
|
set senderCustomAvatarCounter($fixnum.Int64 value) => $_setInt64(28, value);
|
||||||
|
@$pb.TagNumber(32)
|
||||||
|
$core.bool hasSenderCustomAvatarCounter() => $_has(28);
|
||||||
|
@$pb.TagNumber(32)
|
||||||
|
void clearSenderCustomAvatarCounter() => $_clearField(32);
|
||||||
|
|
||||||
|
@$pb.TagNumber(33)
|
||||||
|
$core.int get customAvatarProtocolVersion => $_getIZ(29);
|
||||||
|
@$pb.TagNumber(33)
|
||||||
|
set customAvatarProtocolVersion($core.int value) =>
|
||||||
|
$_setUnsignedInt32(29, value);
|
||||||
|
@$pb.TagNumber(33)
|
||||||
|
$core.bool hasCustomAvatarProtocolVersion() => $_has(29);
|
||||||
|
@$pb.TagNumber(33)
|
||||||
|
void clearCustomAvatarProtocolVersion() => $_clearField(33);
|
||||||
}
|
}
|
||||||
|
|
||||||
const $core.bool _omitFieldNames =
|
const $core.bool _omitFieldNames =
|
||||||
|
|
|
||||||
|
|
@ -169,5 +169,26 @@ class EncryptedContent_ContactUpdate_Type extends $pb.ProtobufEnum {
|
||||||
const EncryptedContent_ContactUpdate_Type._(super.value, super.name);
|
const EncryptedContent_ContactUpdate_Type._(super.value, super.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EncryptedContent_CustomAvatar_State extends $pb.ProtobufEnum {
|
||||||
|
static const EncryptedContent_CustomAvatar_State SVG_ONLY =
|
||||||
|
EncryptedContent_CustomAvatar_State._(
|
||||||
|
0, _omitEnumNames ? '' : 'SVG_ONLY');
|
||||||
|
static const EncryptedContent_CustomAvatar_State PHOTO =
|
||||||
|
EncryptedContent_CustomAvatar_State._(1, _omitEnumNames ? '' : 'PHOTO');
|
||||||
|
|
||||||
|
static const $core.List<EncryptedContent_CustomAvatar_State> values =
|
||||||
|
<EncryptedContent_CustomAvatar_State>[
|
||||||
|
SVG_ONLY,
|
||||||
|
PHOTO,
|
||||||
|
];
|
||||||
|
|
||||||
|
static final $core.List<EncryptedContent_CustomAvatar_State?> _byValue =
|
||||||
|
$pb.ProtobufEnum.$_initByValueList(values, 1);
|
||||||
|
static EncryptedContent_CustomAvatar_State? valueOf($core.int value) =>
|
||||||
|
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
||||||
|
|
||||||
|
const EncryptedContent_CustomAvatar_State._(super.value, super.name);
|
||||||
|
}
|
||||||
|
|
||||||
const $core.bool _omitEnumNames =
|
const $core.bool _omitEnumNames =
|
||||||
$core.bool.fromEnvironment('protobuf.omit_enum_names');
|
$core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,31 @@ const EncryptedContent$json = {
|
||||||
'10': 'senderTwonlyScore',
|
'10': 'senderTwonlyScore',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'1': 'sender_custom_avatar_counter',
|
||||||
|
'3': 32,
|
||||||
|
'4': 1,
|
||||||
|
'5': 3,
|
||||||
|
'9': 7,
|
||||||
|
'10': 'senderCustomAvatarCounter',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'1': 'custom_avatar_protocol_version',
|
||||||
|
'3': 33,
|
||||||
|
'4': 1,
|
||||||
|
'5': 13,
|
||||||
|
'9': 8,
|
||||||
|
'10': 'customAvatarProtocolVersion',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'1': 'message_update',
|
'1': 'message_update',
|
||||||
'3': 5,
|
'3': 5,
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.MessageUpdate',
|
'6': '.EncryptedContent.MessageUpdate',
|
||||||
'9': 7,
|
'9': 9,
|
||||||
'10': 'messageUpdate',
|
'10': 'messageUpdate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -98,7 +116,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.Media',
|
'6': '.EncryptedContent.Media',
|
||||||
'9': 8,
|
'9': 10,
|
||||||
'10': 'media',
|
'10': 'media',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -108,7 +126,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.MediaUpdate',
|
'6': '.EncryptedContent.MediaUpdate',
|
||||||
'9': 9,
|
'9': 11,
|
||||||
'10': 'mediaUpdate',
|
'10': 'mediaUpdate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -118,7 +136,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.ContactUpdate',
|
'6': '.EncryptedContent.ContactUpdate',
|
||||||
'9': 10,
|
'9': 12,
|
||||||
'10': 'contactUpdate',
|
'10': 'contactUpdate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -128,7 +146,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.ContactRequest',
|
'6': '.EncryptedContent.ContactRequest',
|
||||||
'9': 11,
|
'9': 13,
|
||||||
'10': 'contactRequest',
|
'10': 'contactRequest',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -138,7 +156,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.FlameSync',
|
'6': '.EncryptedContent.FlameSync',
|
||||||
'9': 12,
|
'9': 14,
|
||||||
'10': 'flameSync',
|
'10': 'flameSync',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -148,7 +166,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.Reaction',
|
'6': '.EncryptedContent.Reaction',
|
||||||
'9': 13,
|
'9': 15,
|
||||||
'10': 'reaction',
|
'10': 'reaction',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -158,7 +176,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.TextMessage',
|
'6': '.EncryptedContent.TextMessage',
|
||||||
'9': 14,
|
'9': 16,
|
||||||
'10': 'textMessage',
|
'10': 'textMessage',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -168,7 +186,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.GroupCreate',
|
'6': '.EncryptedContent.GroupCreate',
|
||||||
'9': 15,
|
'9': 17,
|
||||||
'10': 'groupCreate',
|
'10': 'groupCreate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -178,7 +196,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.GroupJoin',
|
'6': '.EncryptedContent.GroupJoin',
|
||||||
'9': 16,
|
'9': 18,
|
||||||
'10': 'groupJoin',
|
'10': 'groupJoin',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -188,7 +206,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.GroupUpdate',
|
'6': '.EncryptedContent.GroupUpdate',
|
||||||
'9': 17,
|
'9': 19,
|
||||||
'10': 'groupUpdate',
|
'10': 'groupUpdate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -198,7 +216,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.ResendGroupPublicKey',
|
'6': '.EncryptedContent.ResendGroupPublicKey',
|
||||||
'9': 18,
|
'9': 20,
|
||||||
'10': 'resendGroupPublicKey',
|
'10': 'resendGroupPublicKey',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -208,7 +226,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.ErrorMessages',
|
'6': '.EncryptedContent.ErrorMessages',
|
||||||
'9': 19,
|
'9': 21,
|
||||||
'10': 'errorMessages',
|
'10': 'errorMessages',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -218,7 +236,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.AdditionalDataMessage',
|
'6': '.EncryptedContent.AdditionalDataMessage',
|
||||||
'9': 20,
|
'9': 22,
|
||||||
'10': 'additionalDataMessage',
|
'10': 'additionalDataMessage',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -228,7 +246,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.TypingIndicator',
|
'6': '.EncryptedContent.TypingIndicator',
|
||||||
'9': 21,
|
'9': 23,
|
||||||
'10': 'typingIndicator',
|
'10': 'typingIndicator',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -238,7 +256,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.UserDiscoveryRequest',
|
'6': '.EncryptedContent.UserDiscoveryRequest',
|
||||||
'9': 22,
|
'9': 24,
|
||||||
'10': 'userDiscoveryRequest',
|
'10': 'userDiscoveryRequest',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -248,7 +266,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.UserDiscoveryUpdate',
|
'6': '.EncryptedContent.UserDiscoveryUpdate',
|
||||||
'9': 23,
|
'9': 25,
|
||||||
'10': 'userDiscoveryUpdate',
|
'10': 'userDiscoveryUpdate',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -258,7 +276,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.KeyVerificationProof',
|
'6': '.EncryptedContent.KeyVerificationProof',
|
||||||
'9': 24,
|
'9': 26,
|
||||||
'10': 'keyVerificationProof',
|
'10': 'keyVerificationProof',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -268,7 +286,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.PasswordLessRecovery',
|
'6': '.EncryptedContent.PasswordLessRecovery',
|
||||||
'9': 25,
|
'9': 27,
|
||||||
'10': 'passwordlessRecovery',
|
'10': 'passwordlessRecovery',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -278,7 +296,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.PasswordLessRecoveryHeartbeat',
|
'6': '.EncryptedContent.PasswordLessRecoveryHeartbeat',
|
||||||
'9': 26,
|
'9': 28,
|
||||||
'10': 'passwordlessRecoveryHeartbeat',
|
'10': 'passwordlessRecoveryHeartbeat',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -288,7 +306,7 @@ const EncryptedContent$json = {
|
||||||
'4': 1,
|
'4': 1,
|
||||||
'5': 11,
|
'5': 11,
|
||||||
'6': '.EncryptedContent.Story',
|
'6': '.EncryptedContent.Story',
|
||||||
'9': 27,
|
'9': 29,
|
||||||
'10': 'story',
|
'10': 'story',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
|
@ -308,6 +326,7 @@ const EncryptedContent$json = {
|
||||||
EncryptedContent_MediaUpdate$json,
|
EncryptedContent_MediaUpdate$json,
|
||||||
EncryptedContent_ContactRequest$json,
|
EncryptedContent_ContactRequest$json,
|
||||||
EncryptedContent_ContactUpdate$json,
|
EncryptedContent_ContactUpdate$json,
|
||||||
|
EncryptedContent_CustomAvatar$json,
|
||||||
EncryptedContent_FlameSync$json,
|
EncryptedContent_FlameSync$json,
|
||||||
EncryptedContent_TypingIndicator$json,
|
EncryptedContent_TypingIndicator$json,
|
||||||
EncryptedContent_UserDiscoveryRequest$json,
|
EncryptedContent_UserDiscoveryRequest$json,
|
||||||
|
|
@ -324,6 +343,8 @@ const EncryptedContent$json = {
|
||||||
{'1': '_ask_for_friend_promotions'},
|
{'1': '_ask_for_friend_promotions'},
|
||||||
{'1': '_widget_sharing_allowed'},
|
{'1': '_widget_sharing_allowed'},
|
||||||
{'1': '_sender_twonly_score'},
|
{'1': '_sender_twonly_score'},
|
||||||
|
{'1': '_sender_custom_avatar_counter'},
|
||||||
|
{'1': '_custom_avatar_protocol_version'},
|
||||||
{'1': '_message_update'},
|
{'1': '_message_update'},
|
||||||
{'1': '_media'},
|
{'1': '_media'},
|
||||||
{'1': '_media_update'},
|
{'1': '_media_update'},
|
||||||
|
|
@ -799,12 +820,33 @@ const EncryptedContent_ContactUpdate$json = {
|
||||||
'10': 'displayName',
|
'10': 'displayName',
|
||||||
'17': true
|
'17': true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
'1': 'custom_avatar_requested',
|
||||||
|
'3': 5,
|
||||||
|
'4': 1,
|
||||||
|
'5': 8,
|
||||||
|
'9': 3,
|
||||||
|
'10': 'customAvatarRequested',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'1': 'custom_avatar',
|
||||||
|
'3': 6,
|
||||||
|
'4': 1,
|
||||||
|
'5': 11,
|
||||||
|
'6': '.EncryptedContent.CustomAvatar',
|
||||||
|
'9': 4,
|
||||||
|
'10': 'customAvatar',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
],
|
],
|
||||||
'4': [EncryptedContent_ContactUpdate_Type$json],
|
'4': [EncryptedContent_ContactUpdate_Type$json],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': '_avatar_svg_compressed'},
|
{'1': '_avatar_svg_compressed'},
|
||||||
{'1': '_username'},
|
{'1': '_username'},
|
||||||
{'1': '_display_name'},
|
{'1': '_display_name'},
|
||||||
|
{'1': '_custom_avatar_requested'},
|
||||||
|
{'1': '_custom_avatar'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -817,6 +859,65 @@ const EncryptedContent_ContactUpdate_Type$json = {
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||||
|
const EncryptedContent_CustomAvatar$json = {
|
||||||
|
'1': 'CustomAvatar',
|
||||||
|
'2': [
|
||||||
|
{'1': 'version', '3': 1, '4': 1, '5': 13, '10': 'version'},
|
||||||
|
{
|
||||||
|
'1': 'publication_counter',
|
||||||
|
'3': 2,
|
||||||
|
'4': 1,
|
||||||
|
'5': 3,
|
||||||
|
'10': 'publicationCounter'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'1': 'state',
|
||||||
|
'3': 3,
|
||||||
|
'4': 1,
|
||||||
|
'5': 14,
|
||||||
|
'6': '.EncryptedContent.CustomAvatar.State',
|
||||||
|
'10': 'state'
|
||||||
|
},
|
||||||
|
{'1': 'webp', '3': 4, '4': 1, '5': 12, '9': 0, '10': 'webp', '17': true},
|
||||||
|
{
|
||||||
|
'1': 'sha256',
|
||||||
|
'3': 5,
|
||||||
|
'4': 1,
|
||||||
|
'5': 12,
|
||||||
|
'9': 1,
|
||||||
|
'10': 'sha256',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
|
{'1': 'width', '3': 6, '4': 1, '5': 13, '9': 2, '10': 'width', '17': true},
|
||||||
|
{
|
||||||
|
'1': 'height',
|
||||||
|
'3': 7,
|
||||||
|
'4': 1,
|
||||||
|
'5': 13,
|
||||||
|
'9': 3,
|
||||||
|
'10': 'height',
|
||||||
|
'17': true
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'4': [EncryptedContent_CustomAvatar_State$json],
|
||||||
|
'8': [
|
||||||
|
{'1': '_webp'},
|
||||||
|
{'1': '_sha256'},
|
||||||
|
{'1': '_width'},
|
||||||
|
{'1': '_height'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||||
|
const EncryptedContent_CustomAvatar_State$json = {
|
||||||
|
'1': 'State',
|
||||||
|
'2': [
|
||||||
|
{'1': 'SVG_ONLY', '2': 0},
|
||||||
|
{'1': 'PHOTO', '2': 1},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
@$core.Deprecated('Use encryptedContentDescriptor instead')
|
||||||
const EncryptedContent_FlameSync$json = {
|
const EncryptedContent_FlameSync$json = {
|
||||||
'1': 'FlameSync',
|
'1': 'FlameSync',
|
||||||
|
|
@ -905,115 +1006,129 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
||||||
'AQESPgoZYXNrX2Zvcl9mcmllbmRfcHJvbW90aW9ucxgZIAEoCEgEUhZhc2tGb3JGcmllbmRQcm'
|
'AQESPgoZYXNrX2Zvcl9mcmllbmRfcHJvbW90aW9ucxgZIAEoCEgEUhZhc2tGb3JGcmllbmRQcm'
|
||||||
'9tb3Rpb25ziAEBEjkKFndpZGdldF9zaGFyaW5nX2FsbG93ZWQYHSABKAhIBVIUd2lkZ2V0U2hh'
|
'9tb3Rpb25ziAEBEjkKFndpZGdldF9zaGFyaW5nX2FsbG93ZWQYHSABKAhIBVIUd2lkZ2V0U2hh'
|
||||||
'cmluZ0FsbG93ZWSIAQESMwoTc2VuZGVyX3R3b25seV9zY29yZRgfIAEoA0gGUhFzZW5kZXJUd2'
|
'cmluZ0FsbG93ZWSIAQESMwoTc2VuZGVyX3R3b25seV9zY29yZRgfIAEoA0gGUhFzZW5kZXJUd2'
|
||||||
'9ubHlTY29yZYgBARJLCg5tZXNzYWdlX3VwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQu'
|
'9ubHlTY29yZYgBARJEChxzZW5kZXJfY3VzdG9tX2F2YXRhcl9jb3VudGVyGCAgASgDSAdSGXNl'
|
||||||
'TWVzc2FnZVVwZGF0ZUgHUg1tZXNzYWdlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcn'
|
'bmRlckN1c3RvbUF2YXRhckNvdW50ZXKIAQESSAoeY3VzdG9tX2F2YXRhcl9wcm90b2NvbF92ZX'
|
||||||
'lwdGVkQ29udGVudC5NZWRpYUgIUgVtZWRpYYgBARJFCgxtZWRpYV91cGRhdGUYByABKAsyHS5F'
|
'JzaW9uGCEgASgNSAhSG2N1c3RvbUF2YXRhclByb3RvY29sVmVyc2lvbogBARJLCg5tZXNzYWdl'
|
||||||
'bmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlSAlSC21lZGlhVXBkYXRliAEBEksKDmNvbnRhY3'
|
'X3VwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZUgJUg1tZXNzYW'
|
||||||
'RfdXBkYXRlGAggASgLMh8uRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlSApSDWNvbnRh'
|
'dlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcnlwdGVkQ29udGVudC5NZWRpYUgKUgVt'
|
||||||
'Y3RVcGRhdGWIAQESTgoPY29udGFjdF9yZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC'
|
'ZWRpYYgBARJFCgxtZWRpYV91cGRhdGUYByABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVX'
|
||||||
'5Db250YWN0UmVxdWVzdEgLUg5jb250YWN0UmVxdWVzdIgBARI/CgpmbGFtZV9zeW5jGAogASgL'
|
'BkYXRlSAtSC21lZGlhVXBkYXRliAEBEksKDmNvbnRhY3RfdXBkYXRlGAggASgLMh8uRW5jcnlw'
|
||||||
'MhsuRW5jcnlwdGVkQ29udGVudC5GbGFtZVN5bmNIDFIJZmxhbWVTeW5jiAEBEjsKCHJlYWN0aW'
|
'dGVkQ29udGVudC5Db250YWN0VXBkYXRlSAxSDWNvbnRhY3RVcGRhdGWIAQESTgoPY29udGFjdF'
|
||||||
'9uGAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgNUghyZWFjdGlvbogBARJFCgx0'
|
'9yZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdEgNUg5jb250'
|
||||||
'ZXh0X21lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb250ZW50LlRleHRNZXNzYWdlSA5SC3RleH'
|
'YWN0UmVxdWVzdIgBARI/CgpmbGFtZV9zeW5jGAogASgLMhsuRW5jcnlwdGVkQ29udGVudC5GbG'
|
||||||
'RNZXNzYWdliAEBEkUKDGdyb3VwX2NyZWF0ZRgOIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuR3Jv'
|
'FtZVN5bmNIDlIJZmxhbWVTeW5jiAEBEjsKCHJlYWN0aW9uGAwgASgLMhouRW5jcnlwdGVkQ29u'
|
||||||
'dXBDcmVhdGVID1ILZ3JvdXBDcmVhdGWIAQESPwoKZ3JvdXBfam9pbhgPIAEoCzIbLkVuY3J5cH'
|
'dGVudC5SZWFjdGlvbkgPUghyZWFjdGlvbogBARJFCgx0ZXh0X21lc3NhZ2UYDSABKAsyHS5Fbm'
|
||||||
'RlZENvbnRlbnQuR3JvdXBKb2luSBBSCWdyb3VwSm9pbogBARJFCgxncm91cF91cGRhdGUYECAB'
|
'NyeXB0ZWRDb250ZW50LlRleHRNZXNzYWdlSBBSC3RleHRNZXNzYWdliAEBEkUKDGdyb3VwX2Ny'
|
||||||
'KAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSBFSC2dyb3VwVXBkYXRliAEBEmIKF3'
|
'ZWF0ZRgOIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuR3JvdXBDcmVhdGVIEVILZ3JvdXBDcmVhdG'
|
||||||
'Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5GBEgASgLMiYuRW5jcnlwdGVkQ29udGVudC5SZXNlbmRH'
|
'WIAQESPwoKZ3JvdXBfam9pbhgPIAEoCzIbLkVuY3J5cHRlZENvbnRlbnQuR3JvdXBKb2luSBJS'
|
||||||
'cm91cFB1YmxpY0tleUgSUhRyZXNlbmRHcm91cFB1YmxpY0tleYgBARJLCg5lcnJvcl9tZXNzYW'
|
'CWdyb3VwSm9pbogBARJFCgxncm91cF91cGRhdGUYECABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||||
'dlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlc0gTUg1lcnJvck1lc3Nh'
|
'dyb3VwVXBkYXRlSBNSC2dyb3VwVXBkYXRliAEBEmIKF3Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5'
|
||||||
'Z2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgASgLMicuRW5jcnlwdGVkQ29udG'
|
'GBEgASgLMiYuRW5jcnlwdGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgUUhRyZXNlbm'
|
||||||
'VudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIFFIVYWRkaXRpb25hbERhdGFNZXNzYWdliAEBElEK'
|
'RHcm91cFB1YmxpY0tleYgBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENv'
|
||||||
'EHR5cGluZ19pbmRpY2F0b3IYFCABKAsyIS5FbmNyeXB0ZWRDb250ZW50LlR5cGluZ0luZGljYX'
|
'bnRlbnQuRXJyb3JNZXNzYWdlc0gVUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZG'
|
||||||
'RvckgVUg90eXBpbmdJbmRpY2F0b3KIAQESYQoWdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdBgWIAEo'
|
'F0YV9tZXNzYWdlGBMgASgLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3Nh'
|
||||||
'CzImLkVuY3J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVJlcXVlc3RIFlIUdXNlckRpc2Nvdm'
|
'Z2VIFlIVYWRkaXRpb25hbERhdGFNZXNzYWdliAEBElEKEHR5cGluZ19pbmRpY2F0b3IYFCABKA'
|
||||||
'VyeVJlcXVlc3SIAQESXgoVdXNlcl9kaXNjb3ZlcnlfdXBkYXRlGBcgASgLMiUuRW5jcnlwdGVk'
|
'syIS5FbmNyeXB0ZWRDb250ZW50LlR5cGluZ0luZGljYXRvckgXUg90eXBpbmdJbmRpY2F0b3KI'
|
||||||
'Q29udGVudC5Vc2VyRGlzY292ZXJ5VXBkYXRlSBdSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQESYQ'
|
'AQESYQoWdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdBgWIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuVX'
|
||||||
'oWa2V5X3ZlcmlmaWNhdGlvbl9wcm9vZhgYIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuS2V5VmVy'
|
'NlckRpc2NvdmVyeVJlcXVlc3RIGFIUdXNlckRpc2NvdmVyeVJlcXVlc3SIAQESXgoVdXNlcl9k'
|
||||||
'aWZpY2F0aW9uUHJvb2ZIGFIUa2V5VmVyaWZpY2F0aW9uUHJvb2aIAQESYAoVcGFzc3dvcmRsZX'
|
'aXNjb3ZlcnlfdXBkYXRlGBcgASgLMiUuRW5jcnlwdGVkQ29udGVudC5Vc2VyRGlzY292ZXJ5VX'
|
||||||
'NzX3JlY292ZXJ5GBogASgLMiYuRW5jcnlwdGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVy'
|
'BkYXRlSBlSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQESYQoWa2V5X3ZlcmlmaWNhdGlvbl9wcm9v'
|
||||||
'eUgZUhRwYXNzd29yZGxlc3NSZWNvdmVyeYgBARJ8Ch9wYXNzd29yZGxlc3NfcmVjb3ZlcnlfaG'
|
'ZhgYIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuS2V5VmVyaWZpY2F0aW9uUHJvb2ZIGlIUa2V5Vm'
|
||||||
'VhcnRiZWF0GBsgASgLMi8uRW5jcnlwdGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVyeUhl'
|
'VyaWZpY2F0aW9uUHJvb2aIAQESYAoVcGFzc3dvcmRsZXNzX3JlY292ZXJ5GBogASgLMiYuRW5j'
|
||||||
'YXJ0YmVhdEgaUh1wYXNzd29yZGxlc3NSZWNvdmVyeUhlYXJ0YmVhdIgBARIyCgVzdG9yeRgeIA'
|
'cnlwdGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVyeUgbUhRwYXNzd29yZGxlc3NSZWNvdm'
|
||||||
'EoCzIXLkVuY3J5cHRlZENvbnRlbnQuU3RvcnlIG1IFc3RvcnmIAQEalgIKDUVycm9yTWVzc2Fn'
|
'VyeYgBARJ8Ch9wYXNzd29yZGxlc3NfcmVjb3ZlcnlfaGVhcnRiZWF0GBsgASgLMi8uRW5jcnlw'
|
||||||
'ZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeXBlUg'
|
'dGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVyeUhlYXJ0YmVhdEgcUh1wYXNzd29yZGxlc3'
|
||||||
'R0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCKcAQoE'
|
'NSZWNvdmVyeUhlYXJ0YmVhdIgBARIyCgVzdG9yeRgeIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQu'
|
||||||
'VHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1'
|
'U3RvcnlIHVIFc3RvcnmIAQEalgIKDUVycm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3'
|
||||||
'RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0Zf'
|
'J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlw'
|
||||||
'U1lOQxADEiMKH0dST1VQX05PVF9GT1VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQoLR3JvdXBDcm'
|
'dF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCKcAQoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU'
|
||||||
'VhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91cF9wdWJsaWNfa2V5GAQg'
|
'5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05f'
|
||||||
'ASgMUg5ncm91cFB1YmxpY0tleRIiCgpncm91cF9uYW1lGAUgASgJSABSCWdyb3VwTmFtZYgBAU'
|
'TUVTU0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0ZfU1lOQxADEiMKH0dST1VQX05PVF9GT1'
|
||||||
'INCgtfZ3JvdXBfbmFtZRo1CglHcm91cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tleRgBIAEoDFIO'
|
'VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQoLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgM'
|
||||||
'Z3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3VwVXBkYXRlEi'
|
'UghzdGF0ZUtleRIoChBncm91cF9wdWJsaWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRIiCg'
|
||||||
'oKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMwoTYWZmZWN0ZWRf'
|
'pncm91cF9uYW1lGAUgASgJSABSCWdyb3VwTmFtZYgBAUINCgtfZ3JvdXBfbmFtZRo1CglHcm91'
|
||||||
'Y29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBARIpCg5uZXdfZ3JvdXBfbm'
|
'cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW'
|
||||||
'FtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2RlbGV0ZV9tZXNzYWdlc19hZnRl'
|
'5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEg'
|
||||||
'cl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb2'
|
'ASgJUg9ncm91cEFjdGlvblR5cGUSMwoTYWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZm'
|
||||||
'5kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbmV3X2dyb3VwX25hbWVCKQonX25l'
|
'ZlY3RlZENvbnRhY3RJZIgBARIpCg5uZXdfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5h'
|
||||||
'd19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGogCCgtUZXh0TWVzc2FnZRIqCh'
|
'bWWIAQESVwombmV3X2RlbGV0ZV9tZXNzYWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAl'
|
||||||
'FzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlS'
|
'IibmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29u'
|
||||||
'BHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZB'
|
'dGFjdF9pZEIRCg9fbmV3X2dyb3VwX25hbWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZX'
|
||||||
'gEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgF'
|
'JfbWlsbGlzZWNvbmRzGogCCgtUZXh0TWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEo'
|
||||||
'IAEoDEgBUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWRCGg'
|
'CVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgAS'
|
||||||
'oYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGuYBChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoR'
|
'gDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJ'
|
||||||
'c2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAi'
|
'ZIgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgFIAEoDEgBUhVhZGRpdGlvbmFsTWVzc2'
|
||||||
'ABKANSCXRpbWVzdGFtcBISCgR0eXBlGAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2Fn'
|
'FnZURhdGGIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWRCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9k'
|
||||||
'ZV9kYXRhGAQgASgMSABSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBARIWCgZoaWRkZW4YBSABKA'
|
'YXRhGuYBChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKA'
|
||||||
'hSBmhpZGRlbkIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEaZAoIUmVhY3Rpb24SKgoRdGFy'
|
'lSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAiABKANSCXRpbWVzdGFtcBISCgR0eXBl'
|
||||||
'Z2V0X21lc3NhZ2VfaWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW'
|
'GAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAQgASgMSABSFWFkZGl0aW'
|
||||||
'1vamkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUavgIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgB'
|
'9uYWxNZXNzYWdlRGF0YYgBARIWCgZoaWRkZW4YBSABKAhSBmhpZGRlbkIaChhfYWRkaXRpb25h'
|
||||||
'IAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi8KEXNlbm'
|
'bF9tZXNzYWdlX2RhdGEaZAoIUmVhY3Rpb24SKgoRdGFyZ2V0X21lc3NhZ2VfaWQYASABKAlSD3'
|
||||||
'Rlcl9tZXNzYWdlX2lkGAIgASgJSABSD3NlbmRlck1lc3NhZ2VJZIgBARI9ChttdWx0aXBsZV90'
|
'RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFgoGcmVtb3ZlGAMgASgIUgZy'
|
||||||
'YXJnZXRfbWVzc2FnZV9pZHMYAyADKAlSGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZX'
|
'ZW1vdmUavgIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbn'
|
||||||
'h0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlw'
|
'QuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi8KEXNlbmRlcl9tZXNzYWdlX2lkGAIgASgJSABS'
|
||||||
'ZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQARIKCgZPUEVORUQQAkIUChJfc2VuZGVyX21lc3'
|
'D3NlbmRlck1lc3NhZ2VJZIgBARI9ChttdWx0aXBsZV90YXJnZXRfbWVzc2FnZV9pZHMYAyADKA'
|
||||||
'NhZ2VfaWRCBwoFX3RleHQauwYKBU1lZGlhEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg9z'
|
'lSGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJ'
|
||||||
'ZW5kZXJNZXNzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVH'
|
'dGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1'
|
||||||
'lwZVIEdHlwZRJGCh1kaXNwbGF5X2xpbWl0X2luX21pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNw'
|
'RFWFQQARIKCgZPUEVORUQQAkIUChJfc2VuZGVyX21lc3NhZ2VfaWRCBwoFX3RleHQauwYKBU1l'
|
||||||
'bGF5TGltaXRJbk1pbGxpc2Vjb25kc4gBARI3ChdyZXF1aXJlc19hdXRoZW50aWNhdGlvbhgEIA'
|
'ZGlhEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMAoEdHlwZR'
|
||||||
'EoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFt'
|
'gCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJGCh1kaXNwbGF5X2xp'
|
||||||
'cBItChBxdW90ZV9tZXNzYWdlX2lkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEioKDmRvd2'
|
'bWl0X2luX21pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc4'
|
||||||
'5sb2FkX3Rva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKgoOZW5jcnlwdGlvbl9rZXkY'
|
'gBARI3ChdyZXF1aXJlc19hdXRoZW50aWNhdGlvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNh'
|
||||||
'CCABKAxIA1INZW5jcnlwdGlvbktleYgBARIqCg5lbmNyeXB0aW9uX21hYxgJIAEoDEgEUg1lbm'
|
'dGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFtcBItChBxdW90ZV9tZXNzYWdlX2lkGA'
|
||||||
'NyeXB0aW9uTWFjiAEBEi4KEGVuY3J5cHRpb25fbm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5v'
|
'YgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEioKDmRvd25sb2FkX3Rva2VuGAcgASgMSAJSDWRv'
|
||||||
'bmNliAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZX'
|
'd25sb2FkVG9rZW6IAQESKgoOZW5jcnlwdGlvbl9rZXkYCCABKAxIA1INZW5jcnlwdGlvbktleY'
|
||||||
'NzYWdlRGF0YYgBARIkCgt3aWRnZXRfb25seRgMIAEoCEgHUgp3aWRnZXRPbmx5iAEBIj4KBFR5'
|
'gBARIqCg5lbmNyeXB0aW9uX21hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi4KEGVuY3J5'
|
||||||
'cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU'
|
'cHRpb25fbm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5vbmNliAEBEjsKF2FkZGl0aW9uYWxfbW'
|
||||||
'8QBEIgCh5fZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHNCEwoRX3F1b3RlX21lc3NhZ2Vf'
|
'Vzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBARIkCgt3aWRnZXRf'
|
||||||
'aWRCEQoPX2Rvd25sb2FkX3Rva2VuQhEKD19lbmNyeXB0aW9uX2tleUIRCg9fZW5jcnlwdGlvbl'
|
'b25seRgMIAEoCEgHUgp3aWRnZXRPbmx5iAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTU'
|
||||||
'9tYWNCEwoRX2VuY3J5cHRpb25fbm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhQg4K'
|
'FHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIgCh5fZGlzcGxheV9saW1pdF9p'
|
||||||
'DF93aWRnZXRfb25seRpOCgVTdG9yeRItCgVtZWRpYRgBIAEoCzIXLkVuY3J5cHRlZENvbnRlbn'
|
'bl9taWxsaXNlY29uZHNCEwoRX3F1b3RlX21lc3NhZ2VfaWRCEQoPX2Rvd25sb2FkX3Rva2VuQh'
|
||||||
'QuTWVkaWFSBW1lZGlhEhYKBm5vdGlmeRgCIAEoCFIGbm90aWZ5GqkBCgtNZWRpYVVwZGF0ZRI2'
|
'EKD19lbmNyeXB0aW9uX2tleUIRCg9fZW5jcnlwdGlvbl9tYWNCEwoRX2VuY3J5cHRpb25fbm9u'
|
||||||
'CgR0eXBlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEi'
|
'Y2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhQg4KDF93aWRnZXRfb25seRpOCgVTdG9yeR'
|
||||||
'oKEXRhcmdldF9tZXNzYWdlX2lkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghS'
|
'ItCgVtZWRpYRgBIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFSBW1lZGlhEhYKBm5vdGlm'
|
||||||
'RU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0Um'
|
'eRgCIAEoCFIGbm90aWZ5GqkBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEgASgOMiIuRW5jcnlwdG'
|
||||||
'VxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5U'
|
'VkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEioKEXRhcmdldF9tZXNzYWdlX2lkGAIg'
|
||||||
'eXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGq'
|
'ASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEh'
|
||||||
'QCCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRh'
|
'QKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUu'
|
||||||
'Y3RVcGRhdGUuVHlwZVIEdHlwZRI3ChVhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYX'
|
'RW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUk'
|
||||||
'ZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARIm'
|
'VRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGtkDCg1Db250YWN0VXBkYXRlEjgKBHR5'
|
||||||
'CgxkaXNwbGF5X25hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRV'
|
'cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI3Ch'
|
||||||
'NUEAASCgoGVVBEQVRFEAFCGAoWX2F2YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbWVC'
|
'VhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIf'
|
||||||
'DwoNX2Rpc3BsYXlfbmFtZRqvAQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDG'
|
'Cgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARImCgxkaXNwbGF5X25hbWUYBCABKAlIAl'
|
||||||
'ZsYW1lQ291bnRlchI5ChlsYXN0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0Rmxh'
|
'ILZGlzcGxheU5hbWWIAQESOwoXY3VzdG9tX2F2YXRhcl9yZXF1ZXN0ZWQYBSABKAhIA1IVY3Vz'
|
||||||
'bWVDb3VudGVyQ2hhbmdlEh8KC2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcm'
|
'dG9tQXZhdGFyUmVxdWVzdGVkiAEBEkgKDWN1c3RvbV9hdmF0YXIYBiABKAsyHi5FbmNyeXB0ZW'
|
||||||
'NlX3VwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5'
|
'RDb250ZW50LkN1c3RvbUF2YXRhckgEUgxjdXN0b21BdmF0YXKIAQEiHwoEVHlwZRILCgdSRVFV'
|
||||||
'cGluZxgBIAEoCFIIaXNUeXBpbmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFF'
|
'RVNUEAASCgoGVVBEQVRFEAFCGAoWX2F2YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbW'
|
||||||
'VzZXJEaXNjb3ZlcnlSZXF1ZXN0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZl'
|
'VCDwoNX2Rpc3BsYXlfbmFtZUIaChhfY3VzdG9tX2F2YXRhcl9yZXF1ZXN0ZWRCEAoOX2N1c3Rv'
|
||||||
'cnNpb24aMQoTVXNlckRpc2NvdmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZX'
|
'bV9hdmF0YXIazgIKDEN1c3RvbUF2YXRhchIYCgd2ZXJzaW9uGAEgASgNUgd2ZXJzaW9uEi8KE3'
|
||||||
'MaPQoUS2V5VmVyaWZpY2F0aW9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1'
|
'B1YmxpY2F0aW9uX2NvdW50ZXIYAiABKANSEnB1YmxpY2F0aW9uQ291bnRlchI6CgVzdGF0ZRgD'
|
||||||
'bGF0ZWRNYWMamwEKFFBhc3N3b3JkTGVzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcm'
|
'IAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ3VzdG9tQXZhdGFyLlN0YXRlUgVzdGF0ZRIXCgR3ZW'
|
||||||
'UYASABKAxIAFITcmVjb3ZlcnlTZWNyZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0'
|
'JwGAQgASgMSABSBHdlYnCIAQESGwoGc2hhMjU2GAUgASgMSAFSBnNoYTI1NogBARIZCgV3aWR0'
|
||||||
'ZRIcCgl0aHJlc2hvbGQYAyABKANSCXRocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZR'
|
'aBgGIAEoDUgCUgV3aWR0aIgBARIbCgZoZWlnaHQYByABKA1IA1IGaGVpZ2h0iAEBIiAKBVN0YX'
|
||||||
'ozCh1QYXNzd29yZExlc3NSZWNvdmVyeUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsK'
|
'RlEgwKCFNWR19PTkxZEAASCQoFUEhPVE8QAUIHCgVfd2VicEIJCgdfc2hhMjU2QggKBl93aWR0'
|
||||||
'CV9ncm91cF9pZEIRCg9faXNfZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZX'
|
'aEIJCgdfaGVpZ2h0Gq8BCglGbGFtZVN5bmMSIwoNZmxhbWVfY291bnRlchgBIAEoA1IMZmxhbW'
|
||||||
'JCIAoeX3NlbmRlcl91c2VyX2Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9w'
|
'VDb3VudGVyEjkKGWxhc3RfZmxhbWVfY291bnRlcl9jaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv'
|
||||||
'cm9tb3Rpb25zQhkKF193aWRnZXRfc2hhcmluZ19hbGxvd2VkQhYKFF9zZW5kZXJfdHdvbmx5X3'
|
'dW50ZXJDaGFuZ2USHwoLYmVzdF9mcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIQoMZm9yY2VfdX'
|
||||||
'Njb3JlQhEKD19tZXNzYWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9f'
|
'BkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZRpNCg9UeXBpbmdJbmRpY2F0b3ISGwoJaXNfdHlwaW5n'
|
||||||
'Y29udGFjdF91cGRhdGVCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0ILCglfcm'
|
'GAEgASgIUghpc1R5cGluZxIdCgpjcmVhdGVkX2F0GAIgASgDUgljcmVhdGVkQXQaPwoUVXNlck'
|
||||||
'VhY3Rpb25CDwoNX3RleHRfbWVzc2FnZUIPCg1fZ3JvdXBfY3JlYXRlQg0KC19ncm91cF9qb2lu'
|
'Rpc2NvdmVyeVJlcXVlc3QSJwoPY3VycmVudF92ZXJzaW9uGAEgASgMUg5jdXJyZW50VmVyc2lv'
|
||||||
'Qg8KDV9ncm91cF91cGRhdGVCGgoYX3Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5QhEKD19lcnJvcl'
|
'bhoxChNVc2VyRGlzY292ZXJ5VXBkYXRlEhoKCG1lc3NhZ2VzGAEgAygMUghtZXNzYWdlcxo9Ch'
|
||||||
'9tZXNzYWdlc0IaChhfYWRkaXRpb25hbF9kYXRhX21lc3NhZ2VCEwoRX3R5cGluZ19pbmRpY2F0'
|
'RLZXlWZXJpZmljYXRpb25Qcm9vZhIlCg5jYWxjdWxhdGVkX21hYxgBIAEoDFINY2FsY3VsYXRl'
|
||||||
'b3JCGQoXX3VzZXJfZGlzY292ZXJ5X3JlcXVlc3RCGAoWX3VzZXJfZGlzY292ZXJ5X3VwZGF0ZU'
|
'ZE1hYxqbAQoUUGFzc3dvcmRMZXNzUmVjb3ZlcnkSNQoTcmVjb3ZlcnlTZWNyZXRTaGFyZRgBIA'
|
||||||
'IZChdfa2V5X3ZlcmlmaWNhdGlvbl9wcm9vZkIYChZfcGFzc3dvcmRsZXNzX3JlY292ZXJ5QiIK'
|
'EoDEgAUhNyZWNvdmVyeVNlY3JldFNoYXJliAEBEhYKBmRlbGV0ZRgCIAEoCFIGZGVsZXRlEhwK'
|
||||||
'IF9wYXNzd29yZGxlc3NfcmVjb3ZlcnlfaGVhcnRiZWF0QggKBl9zdG9yeUoECBwQHQ==');
|
'CXRocmVzaG9sZBgDIAEoA1IJdGhyZXNob2xkQhYKFF9yZWNvdmVyeVNlY3JldFNoYXJlGjMKHV'
|
||||||
|
'Bhc3N3b3JkTGVzc1JlY292ZXJ5SGVhcnRiZWF0EhIKBGhhc2gYASABKAxSBGhhc2hCCwoJX2dy'
|
||||||
|
'b3VwX2lkQhEKD19pc19kaXJlY3RfY2hhdEIZChdfc2VuZGVyX3Byb2ZpbGVfY291bnRlckIgCh'
|
||||||
|
'5fc2VuZGVyX3VzZXJfZGlzY292ZXJ5X3ZlcnNpb25CHAoaX2Fza19mb3JfZnJpZW5kX3Byb21v'
|
||||||
|
'dGlvbnNCGQoXX3dpZGdldF9zaGFyaW5nX2FsbG93ZWRCFgoUX3NlbmRlcl90d29ubHlfc2Nvcm'
|
||||||
|
'VCHwodX3NlbmRlcl9jdXN0b21fYXZhdGFyX2NvdW50ZXJCIQofX2N1c3RvbV9hdmF0YXJfcHJv'
|
||||||
|
'dG9jb2xfdmVyc2lvbkIRCg9fbWVzc2FnZV91cGRhdGVCCAoGX21lZGlhQg8KDV9tZWRpYV91cG'
|
||||||
|
'RhdGVCEQoPX2NvbnRhY3RfdXBkYXRlQhIKEF9jb250YWN0X3JlcXVlc3RCDQoLX2ZsYW1lX3N5'
|
||||||
|
'bmNCCwoJX3JlYWN0aW9uQg8KDV90ZXh0X21lc3NhZ2VCDwoNX2dyb3VwX2NyZWF0ZUINCgtfZ3'
|
||||||
|
'JvdXBfam9pbkIPCg1fZ3JvdXBfdXBkYXRlQhoKGF9yZXNlbmRfZ3JvdXBfcHVibGljX2tleUIR'
|
||||||
|
'Cg9fZXJyb3JfbWVzc2FnZXNCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlQhMKEV90eXBpbm'
|
||||||
|
'dfaW5kaWNhdG9yQhkKF191c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0QhgKFl91c2VyX2Rpc2NvdmVy'
|
||||||
|
'eV91cGRhdGVCGQoXX2tleV92ZXJpZmljYXRpb25fcHJvb2ZCGAoWX3Bhc3N3b3JkbGVzc19yZW'
|
||||||
|
'NvdmVyeUIiCiBfcGFzc3dvcmRsZXNzX3JlY292ZXJ5X2hlYXJ0YmVhdEIICgZfc3RvcnlKBAgc'
|
||||||
|
'EB0=');
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart' show listEquals, setEquals;
|
import 'package:flutter/foundation.dart' show listEquals;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.dart';
|
import 'package:flutter_svg/svg.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
|
@ -80,6 +80,7 @@ String _avatarPngPathFor(Contact contact) {
|
||||||
class _AvatarIconState extends State<AvatarIcon> {
|
class _AvatarIconState extends State<AvatarIcon> {
|
||||||
List<Contact> _avatarContacts = [];
|
List<Contact> _avatarContacts = [];
|
||||||
Set<int> _contactsWithPngAvatar = {};
|
Set<int> _contactsWithPngAvatar = {};
|
||||||
|
final Map<int, String> _resolvedAvatarPaths = {};
|
||||||
String? _myAvatarPath;
|
String? _myAvatarPath;
|
||||||
|
|
||||||
StreamSubscription<List<Contact>>? groupStream;
|
StreamSubscription<List<Contact>>? groupStream;
|
||||||
|
|
@ -105,35 +106,20 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
}
|
}
|
||||||
|
|
||||||
void _setAvatarContacts(List<Contact> contacts) {
|
void _setAvatarContacts(List<Contact> contacts) {
|
||||||
_avatarContacts = contacts
|
_avatarContacts = contacts;
|
||||||
.where((contact) => contact.avatarSvgCompressed != null)
|
|
||||||
.toList();
|
|
||||||
unawaited(_refreshAvatarFiles());
|
unawaited(_refreshAvatarFiles());
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _refreshAvatarFiles() async {
|
Future<void> _refreshAvatarFiles() async {
|
||||||
final available = <int>{};
|
|
||||||
for (final contact in _avatarContacts) {
|
for (final contact in _avatarContacts) {
|
||||||
final path = _avatarPngPathFor(contact);
|
if (!_unrenderableAvatars.contains(_avatarCacheKey(contact))) {
|
||||||
var exists = _existingAvatarPngPaths.contains(path);
|
// Rust resolves a received custom avatar before the SVG fallback. Do
|
||||||
if (!exists) {
|
// not trust the old profile-counter cache here: a custom-avatar update
|
||||||
// Async file access keeps avatar discovery off the UI thread.
|
// deliberately leaves that public counter unchanged.
|
||||||
// ignore: avoid_slow_async_io
|
|
||||||
exists = await File(path).exists();
|
|
||||||
if (exists) _existingAvatarPngPaths.add(path);
|
|
||||||
}
|
|
||||||
if (exists) {
|
|
||||||
available.add(contact.userId);
|
|
||||||
} else if (!_unrenderableAvatars.contains(_avatarCacheKey(contact))) {
|
|
||||||
// No PNG yet: render one rather than falling back to the SVG. The
|
|
||||||
// default avatar is shown until it lands.
|
|
||||||
unawaited(_renderAvatarPng(contact));
|
unawaited(_renderAvatarPng(contact));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!mounted) return;
|
|
||||||
if (setEquals(_contactsWithPngAvatar, available)) return;
|
|
||||||
setState(() => _contactsWithPngAvatar = available);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _renderAvatarPng(Contact contact) async {
|
Future<void> _renderAvatarPng(Contact contact) async {
|
||||||
|
|
@ -167,6 +153,7 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
if (_contactsWithPngAvatar.contains(contactId)) return;
|
if (_contactsWithPngAvatar.contains(contactId)) return;
|
||||||
if (!_avatarContacts.any((entry) => entry.userId == contactId)) return;
|
if (!_avatarContacts.any((entry) => entry.userId == contactId)) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
|
_resolvedAvatarPaths[contactId] = path!;
|
||||||
_contactsWithPngAvatar = {..._contactsWithPngAvatar, contactId};
|
_contactsWithPngAvatar = {..._contactsWithPngAvatar, contactId};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -189,7 +176,9 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
Widget getAvatarForContact(Contact contact) {
|
Widget getAvatarForContact(Contact contact) {
|
||||||
if (_contactsWithPngAvatar.contains(contact.userId)) {
|
if (_contactsWithPngAvatar.contains(contact.userId)) {
|
||||||
return Image.file(
|
return Image.file(
|
||||||
File(_avatarPngPathFor(contact)),
|
File(
|
||||||
|
_resolvedAvatarPaths[contact.userId] ?? _avatarPngPathFor(contact),
|
||||||
|
),
|
||||||
errorBuilder: errorBuilder,
|
errorBuilder: errorBuilder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -206,16 +195,13 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
.watchGroupContact(widget.group!.groupId)
|
.watchGroupContact(widget.group!.groupId)
|
||||||
.listen((contacts) {
|
.listen((contacts) {
|
||||||
_avatarContacts = [];
|
_avatarContacts = [];
|
||||||
if (contacts.length == 1) {
|
|
||||||
if (contacts.first.avatarSvgCompressed != null) {
|
|
||||||
_avatarContacts.add(contacts.first);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (final contact in contacts) {
|
for (final contact in contacts) {
|
||||||
if (contact.avatarSvgCompressed != null) {
|
_unrenderableAvatars.remove(_avatarCacheKey(contact));
|
||||||
_avatarContacts.add(contact);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (contacts.length == 1) {
|
||||||
|
_avatarContacts.add(contacts.first);
|
||||||
|
} else {
|
||||||
|
_avatarContacts.addAll(contacts);
|
||||||
}
|
}
|
||||||
unawaited(_refreshAvatarFiles());
|
unawaited(_refreshAvatarFiles());
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
@ -229,7 +215,8 @@ class _AvatarIconState extends State<AvatarIcon> {
|
||||||
contactStream = twonlyDB.contactsDao
|
contactStream = twonlyDB.contactsDao
|
||||||
.watchContact(widget.contactId!)
|
.watchContact(widget.contactId!)
|
||||||
.listen((contact) {
|
.listen((contact) {
|
||||||
if (contact != null && contact.avatarSvgCompressed != null) {
|
if (contact != null) {
|
||||||
|
_unrenderableAvatars.remove(_avatarCacheKey(contact));
|
||||||
_avatarContacts = [contact];
|
_avatarContacts = [contact];
|
||||||
unawaited(_refreshAvatarFiles());
|
unawaited(_refreshAvatarFiles());
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,10 @@ class _StoryTargetSelectorState extends State<StoryTargetSelector> {
|
||||||
);
|
);
|
||||||
if (before != widget.target.contactGroupIds.length) _changed();
|
if (before != widget.target.contactGroupIds.length) _changed();
|
||||||
});
|
});
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
unawaited(_updateAudience());
|
unawaited(_updateAudience());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,13 @@ import 'dart:math' as math;
|
||||||
import 'package:drift/drift.dart' show Value;
|
import 'package:drift/drift.dart' show Value;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
|
import 'package:twonly/src/constants/keyvalue.keys.dart';
|
||||||
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
import 'package:twonly/src/database/tables/mediafiles.table.dart';
|
||||||
import 'package:twonly/src/database/twonly.db.dart';
|
import 'package:twonly/src/database/twonly.db.dart';
|
||||||
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
import 'package:twonly/src/model/protobuf/client/generated/data.pb.dart';
|
||||||
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
import 'package:twonly/src/services/mediafiles/mediafile.service.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
import 'package:twonly/src/utils/keyvalue.dart';
|
||||||
import 'package:twonly/src/utils/log.dart';
|
import 'package:twonly/src/utils/log.dart';
|
||||||
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
|
import 'package:twonly/src/visual/helpers/media_view_sizing.helper.dart';
|
||||||
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
import 'package:twonly/src/visual/helpers/screenshot.helper.dart';
|
||||||
|
|
@ -96,6 +98,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
bool _widgetRecipientAvailable = false;
|
bool _widgetRecipientAvailable = false;
|
||||||
bool _sendToWidget = false;
|
bool _sendToWidget = false;
|
||||||
bool _updatingWidgetMode = false;
|
bool _updatingWidgetMode = false;
|
||||||
|
bool _widgetExplainerPreferenceLoaded = false;
|
||||||
bool _widgetExplainerDismissed =
|
bool _widgetExplainerDismissed =
|
||||||
userService.currentUser.hideWidgetShareExplainer;
|
userService.currentUser.hideWidgetShareExplainer;
|
||||||
bool _previousMediaSettingsCaptured = false;
|
bool _previousMediaSettingsCaptured = false;
|
||||||
|
|
@ -103,7 +106,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
bool _requiresAuthBeforeWidget = false;
|
bool _requiresAuthBeforeWidget = false;
|
||||||
|
|
||||||
bool get _showWidgetExplainer =>
|
bool get _showWidgetExplainer =>
|
||||||
_widgetRecipientAvailable && !_widgetExplainerDismissed;
|
_widgetRecipientAvailable &&
|
||||||
|
_widgetExplainerPreferenceLoaded &&
|
||||||
|
!_widgetExplainerDismissed;
|
||||||
|
|
||||||
MediaFileService get mediaService => widget.mediaFileService;
|
MediaFileService get mediaService => widget.mediaFileService;
|
||||||
MediaFile get media => widget.mediaFileService.mediaFile;
|
MediaFile get media => widget.mediaFileService.mediaFile;
|
||||||
|
|
@ -138,6 +143,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
_widgetGroupsSubscription = twonlyDB.groupsDao
|
_widgetGroupsSubscription = twonlyDB.groupsDao
|
||||||
.watchGroupsAllowedForWidgetShare()
|
.watchGroupsAllowedForWidgetShare()
|
||||||
.listen(_updateWidgetRecipientAvailability);
|
.listen(_updateWidgetRecipientAvailability);
|
||||||
|
unawaited(_loadWidgetExplainerPreference());
|
||||||
if (media.type == MediaType.image || media.type == MediaType.gif) {
|
if (media.type == MediaType.image || media.type == MediaType.gif) {
|
||||||
_loadInitialImage();
|
_loadInitialImage();
|
||||||
}
|
}
|
||||||
|
|
@ -162,9 +168,27 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadWidgetExplainerPreference() async {
|
||||||
|
final preference = await KeyValueStore.get(
|
||||||
|
KeyValueKeys.shareImageWidgetExplainer,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_widgetExplainerDismissed =
|
||||||
|
_widgetExplainerDismissed || preference?['dismissed'] == true;
|
||||||
|
_widgetExplainerPreferenceLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _dismissWidgetExplainer() async {
|
Future<void> _dismissWidgetExplainer() async {
|
||||||
setState(() => _widgetExplainerDismissed = true);
|
setState(() => _widgetExplainerDismissed = true);
|
||||||
await UserService.update((u) => u.hideWidgetShareExplainer = true);
|
await Future.wait([
|
||||||
|
KeyValueStore.put(
|
||||||
|
KeyValueKeys.shareImageWidgetExplainer,
|
||||||
|
const {'dismissed': true},
|
||||||
|
),
|
||||||
|
UserService.update((u) => u.hideWidgetShareExplainer = true),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateWidgetRecipientAvailability(List<Group> widgetGroups) {
|
void _updateWidgetRecipientAvailability(List<Group> widgetGroups) {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,9 @@ import 'package:twonly/src/visual/views/memories/components/memory_thumbnail.com
|
||||||
/// Selects a full-resolution image from twonly Memories, with an explicit
|
/// Selects a full-resolution image from twonly Memories, with an explicit
|
||||||
/// escape hatch to the device gallery in the top app bar.
|
/// escape hatch to the device gallery in the top app bar.
|
||||||
class StickerSourcePickerView extends StatefulWidget {
|
class StickerSourcePickerView extends StatefulWidget {
|
||||||
const StickerSourcePickerView({super.key});
|
const StickerSourcePickerView({this.forAvatar = false, super.key});
|
||||||
|
|
||||||
|
final bool forAvatar;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<StickerSourcePickerView> createState() =>
|
State<StickerSourcePickerView> createState() =>
|
||||||
|
|
@ -63,7 +65,11 @@ class _StickerSourcePickerViewState extends State<StickerSourcePickerView> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(context.lang.createSticker),
|
title: Text(
|
||||||
|
widget.forAvatar
|
||||||
|
? context.lang.customAvatarChooseSource
|
||||||
|
: context.lang.createSticker,
|
||||||
|
),
|
||||||
actions: [
|
actions: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: _resolvingMediaId == null ? _pickFromGallery : null,
|
onPressed: _resolvingMediaId == null ? _pickFromGallery : null,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:twonly/core/services/avatars.dart';
|
||||||
import 'package:twonly/core/user_config.dart';
|
import 'package:twonly/core/user_config.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
|
|
@ -13,6 +14,38 @@ class PublicProfilePrivacyView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PublicProfilePrivacyViewState extends State<PublicProfilePrivacyView> {
|
class _PublicProfilePrivacyViewState extends State<PublicProfilePrivacyView> {
|
||||||
|
CustomAvatarInfo? _customAvatar;
|
||||||
|
bool _updatingAvatarVisibility = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadCustomAvatarVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCustomAvatarVisibility() async {
|
||||||
|
try {
|
||||||
|
final avatar = await RustApi.getCustomAvatar();
|
||||||
|
if (mounted) setState(() => _customAvatar = avatar);
|
||||||
|
} on Object {
|
||||||
|
// Keep the setting disabled when storage is unavailable. Other public
|
||||||
|
// profile settings remain usable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setCustomAvatarVisibility(bool acceptedOnly) async {
|
||||||
|
if (_updatingAvatarVisibility) return;
|
||||||
|
setState(() => _updatingAvatarVisibility = true);
|
||||||
|
try {
|
||||||
|
final avatar = await RustApi.setCustomAvatarAudience(
|
||||||
|
acceptedContactsOnly: acceptedOnly,
|
||||||
|
);
|
||||||
|
if (mounted) setState(() => _customAvatar = avatar);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _updatingAvatarVisibility = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _setTwonlyScoreVisibility(
|
Future<void> _setTwonlyScoreVisibility(
|
||||||
TwonlyScoreVisibility visibility,
|
TwonlyScoreVisibility visibility,
|
||||||
) async {
|
) async {
|
||||||
|
|
@ -69,6 +102,36 @@ class _PublicProfilePrivacyViewState extends State<PublicProfilePrivacyView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(context.lang.customAvatarPhotoTab),
|
||||||
|
subtitle: Text(context.lang.customAvatarExplanation),
|
||||||
|
trailing: SizedBox(
|
||||||
|
width: 180,
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<bool>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: _customAvatar?.acceptedContactsOnly ?? true,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: true,
|
||||||
|
child: Text(context.lang.customAvatarAcceptedOnly),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: false,
|
||||||
|
child: Text(context.lang.privacyVisibilityEveryone),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: _customAvatar == null || _updatingAvatarVisibility
|
||||||
|
? null
|
||||||
|
: (acceptedOnly) {
|
||||||
|
if (acceptedOnly != null) {
|
||||||
|
_setCustomAvatarVisibility(acceptedOnly);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,15 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:avatar_maker/avatar_maker.dart';
|
import 'package:avatar_maker/avatar_maker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:twonly/core/services/avatars.dart';
|
||||||
import 'package:twonly/locator.dart';
|
import 'package:twonly/locator.dart';
|
||||||
import 'package:twonly/src/services/user.service.dart';
|
import 'package:twonly/src/services/user.service.dart';
|
||||||
import 'package:twonly/src/utils/misc.dart';
|
import 'package:twonly/src/utils/misc.dart';
|
||||||
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
import 'package:twonly/src/visual/elements/my_button.element.dart';
|
||||||
|
import 'package:twonly/src/visual/loader/three_rotating_dots.loader.dart';
|
||||||
|
import 'package:twonly/src/visual/views/memories/sticker_source_picker.view.dart';
|
||||||
|
|
||||||
class ModifyAvatarView extends StatefulWidget {
|
class ModifyAvatarView extends StatefulWidget {
|
||||||
const ModifyAvatarView({super.key});
|
const ModifyAvatarView({super.key});
|
||||||
|
|
@ -16,30 +20,340 @@ class ModifyAvatarView extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
|
CustomAvatarInfo? _customAvatar;
|
||||||
|
bool _loadingCustomAvatar = true;
|
||||||
|
bool _processingCustomAvatar = false;
|
||||||
|
bool _updatingAudience = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadCustomAvatar();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCustomAvatar() async {
|
||||||
|
try {
|
||||||
|
final avatar = await RustApi.getCustomAvatar();
|
||||||
|
if (mounted) setState(() => _customAvatar = avatar);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _loadingCustomAvatar = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _chooseCustomAvatar() async {
|
||||||
|
if (_processingCustomAvatar) return;
|
||||||
|
final path = await Navigator.of(context).push<String>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => const StickerSourcePickerView(forAvatar: true),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!mounted || path == null) return;
|
||||||
|
setState(() => _processingCustomAvatar = true);
|
||||||
|
try {
|
||||||
|
final avatar = await RustApi.createCustomAvatar(
|
||||||
|
imagePath: path,
|
||||||
|
acceptedContactsOnly: _customAvatar?.acceptedContactsOnly ?? true,
|
||||||
|
);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _customAvatar = avatar);
|
||||||
|
userService.triggerUserUpdate();
|
||||||
|
} on Object {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.lang.customAvatarCreateFailed)),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _processingCustomAvatar = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeCustomAvatar() async {
|
||||||
|
if (_processingCustomAvatar) return;
|
||||||
|
setState(() => _processingCustomAvatar = true);
|
||||||
|
try {
|
||||||
|
final avatar = await RustApi.removeCustomAvatar();
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _customAvatar = avatar);
|
||||||
|
userService.triggerUserUpdate();
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _processingCustomAvatar = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _setCustomAvatarAudience(bool acceptedContactsOnly) async {
|
||||||
|
if (_updatingAudience) return;
|
||||||
|
setState(() => _updatingAudience = true);
|
||||||
|
try {
|
||||||
|
final avatar = await RustApi.setCustomAvatarAudience(
|
||||||
|
acceptedContactsOnly: acceptedContactsOnly,
|
||||||
|
);
|
||||||
|
if (mounted) setState(() => _customAvatar = avatar);
|
||||||
|
} on Object {
|
||||||
|
if (!mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(context.lang.errorInternalError)),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _updatingAudience = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openAvatarMaker() async {
|
||||||
|
await Navigator.of(context).push<void>(
|
||||||
|
MaterialPageRoute(builder: (context) => const _SvgAvatarEditorView()),
|
||||||
|
);
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(context.lang.settingsProfileCustomizeAvatar),
|
||||||
|
),
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
_buildAvatarMakerCard(context),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildPhotoAvatarCard(context),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (_processingCustomAvatar)
|
||||||
|
const Positioned.fill(
|
||||||
|
child: ColoredBox(
|
||||||
|
color: Color(0x66000000),
|
||||||
|
child: Center(child: ThreeRotatingDots(size: 36)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAvatarMakerCard(BuildContext context) {
|
||||||
|
final svg = userService.currentUser.avatarSvg;
|
||||||
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.customAvatarSvgTab,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: ClipOval(
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 150,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: context.color.surfaceContainerHigh,
|
||||||
|
child: svg != null && svg.isNotEmpty
|
||||||
|
? SvgPicture.string(svg)
|
||||||
|
: const Icon(Icons.person_outline, size: 72),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryMiddle,
|
||||||
|
onPressed: _openAvatarMaker,
|
||||||
|
child: Text(context.lang.customAvatarCustomize),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPhotoAvatarCard(BuildContext context) {
|
||||||
|
if (_loadingCustomAvatar) {
|
||||||
|
return const Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: SizedBox(
|
||||||
|
height: 220,
|
||||||
|
child: Center(child: ThreeRotatingDots(size: 32)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final avatar = _customAvatar;
|
||||||
|
final hasPhoto = avatar?.webp != null;
|
||||||
|
return Card(
|
||||||
|
margin: EdgeInsets.zero,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
context.lang.customAvatarPhotoTab,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
context.lang.customAvatarExplanation,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: context.color.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Center(
|
||||||
|
child: ClipOval(
|
||||||
|
child: SizedBox.square(
|
||||||
|
dimension: 220,
|
||||||
|
child: ColoredBox(
|
||||||
|
color: context.color.surfaceContainerHigh,
|
||||||
|
child: hasPhoto
|
||||||
|
? Image.memory(avatar!.webp!, fit: BoxFit.contain)
|
||||||
|
: const Icon(Icons.person_outline, size: 100),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Text(
|
||||||
|
context.lang.customAvatarAudience,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Center(
|
||||||
|
child: DropdownButton<bool>(
|
||||||
|
value: avatar?.acceptedContactsOnly ?? true,
|
||||||
|
alignment: AlignmentDirectional.center,
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: true,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
context.lang.customAvatarAcceptedOnly,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: false,
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
context.lang.customAvatarEveryone,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: _updatingAudience
|
||||||
|
? null
|
||||||
|
: (acceptedContactsOnly) {
|
||||||
|
if (acceptedContactsOnly != null) {
|
||||||
|
_setCustomAvatarAudience(acceptedContactsOnly);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 12,
|
||||||
|
children: [
|
||||||
|
MyButton(
|
||||||
|
variant: MyButtonVariant.secondaryMiddle,
|
||||||
|
onPressed: _processingCustomAvatar
|
||||||
|
? null
|
||||||
|
: _chooseCustomAvatar,
|
||||||
|
child: Text(
|
||||||
|
hasPhoto
|
||||||
|
? context.lang.customAvatarReplace
|
||||||
|
: context.lang.customAvatarCreate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasPhoto)
|
||||||
|
MyButton(
|
||||||
|
variant: MyButtonVariant.errorMiddle,
|
||||||
|
onPressed: _processingCustomAvatar
|
||||||
|
? null
|
||||||
|
: _removeCustomAvatar,
|
||||||
|
child: Text(context.lang.customAvatarRemove),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SvgAvatarEditorView extends StatefulWidget {
|
||||||
|
const _SvgAvatarEditorView();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SvgAvatarEditorView> createState() => _SvgAvatarEditorViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SvgAvatarEditorViewState extends State<_SvgAvatarEditorView> {
|
||||||
late final _CustomAvatarMakerController _avatarMakerController;
|
late final _CustomAvatarMakerController _avatarMakerController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final svg = userService.currentUser.avatarSvg;
|
final svg = userService.currentUser.avatarSvg;
|
||||||
if (svg != null && svg.isNotEmpty) {
|
_avatarMakerController = svg != null && svg.isNotEmpty
|
||||||
_avatarMakerController = _CustomAvatarMakerController(
|
? _CustomAvatarMakerController(svg: svg)
|
||||||
svg: svg,
|
: _CustomAvatarMakerController.defaultAvatar();
|
||||||
);
|
|
||||||
} else {
|
|
||||||
_avatarMakerController = _CustomAvatarMakerController.defaultAvatar();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> updateUserAvatar(String svg) async {
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_avatarMakerController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _updateUserAvatar(String svg) async {
|
||||||
await UserService.update(
|
await UserService.update(
|
||||||
(u) => u
|
(user) => user
|
||||||
..avatarSvg = svg
|
..avatarSvg = svg
|
||||||
..avatarCounter = u.avatarCounter + 1,
|
..avatarCounter = user.avatarCounter + 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
AvatarMakerThemeData getAvatarMakerTheme(BuildContext context) {
|
Future<void> _storeAvatarAndExit() async {
|
||||||
|
await _avatarMakerController.saveAvatarSVG();
|
||||||
|
final svg = _avatarMakerController.getAvatarSVGSync();
|
||||||
|
await _updateUserAvatar(svg);
|
||||||
|
if (mounted) Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool?> _showBackDialog() {
|
||||||
|
return showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text(context.lang.avatarSaveChanges),
|
||||||
|
actions: [
|
||||||
|
FilledButton(
|
||||||
|
onPressed: _storeAvatarAndExit,
|
||||||
|
child: Text(context.lang.avatarSaveChangesStore),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
child: Text(context.lang.avatarSaveChangesDiscard),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
AvatarMakerThemeData _theme(BuildContext context) {
|
||||||
final colors = context.color;
|
final colors = context.color;
|
||||||
return AvatarMakerThemeData(
|
return AvatarMakerThemeData(
|
||||||
boxDecoration: BoxDecoration(
|
boxDecoration: BoxDecoration(
|
||||||
|
|
@ -73,42 +387,6 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool?> _showBackDialog() {
|
|
||||||
return showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text(
|
|
||||||
context.lang.avatarSaveChanges,
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
FilledButton(
|
|
||||||
child: Text(context.lang.avatarSaveChangesStore),
|
|
||||||
onPressed: () async {
|
|
||||||
await storeAvatarAndExit();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
child: Text(context.lang.avatarSaveChangesDiscard),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> storeAvatarAndExit() async {
|
|
||||||
await _avatarMakerController.saveAvatarSVG();
|
|
||||||
final svg = _avatarMakerController.getAvatarSVGSync();
|
|
||||||
await updateUserAvatar(svg);
|
|
||||||
if (mounted) {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return PopScope<bool?>(
|
return PopScope<bool?>(
|
||||||
|
|
@ -117,31 +395,23 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
if (didPop) return;
|
if (didPop) return;
|
||||||
if (_avatarMakerController.getAvatarSVGSync() !=
|
if (_avatarMakerController.getAvatarSVGSync() !=
|
||||||
userService.currentUser.avatarSvg) {
|
userService.currentUser.avatarSvg) {
|
||||||
// there where changes
|
|
||||||
final shouldPop = await _showBackDialog() ?? false;
|
final shouldPop = await _showBackDialog() ?? false;
|
||||||
if (context.mounted && shouldPop) {
|
if (context.mounted && shouldPop) Navigator.pop(context);
|
||||||
Navigator.pop(context);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(title: Text(context.lang.customAvatarSvgTab)),
|
||||||
title: Text(context.lang.settingsProfileCustomizeAvatar),
|
|
||||||
),
|
|
||||||
body: Center(
|
body: Center(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
AvatarMakerAvatar(
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
child: AvatarMakerAvatar(
|
|
||||||
radius: 130,
|
radius: 130,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
controller: _avatarMakerController,
|
controller: _avatarMakerController,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
|
|
@ -151,11 +421,14 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
children: [
|
children: [
|
||||||
MyButton(
|
MyButton(
|
||||||
variant: MyButtonVariant.primaryDense,
|
variant: MyButtonVariant.primaryDense,
|
||||||
onPressed: storeAvatarAndExit,
|
onPressed: _storeAvatarAndExit,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const FaIcon(FontAwesomeIcons.floppyDisk, size: 16),
|
const FaIcon(
|
||||||
|
FontAwesomeIcons.floppyDisk,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(context.lang.avatarSaveChangesStore),
|
Text(context.lang.avatarSaveChangesStore),
|
||||||
],
|
],
|
||||||
|
|
@ -177,13 +450,14 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
MyButton(
|
MyButton(
|
||||||
variant: MyButtonVariant.secondaryDense,
|
variant: MyButtonVariant.secondaryDense,
|
||||||
onPressed: _avatarMakerController.restoreState,
|
onPressed: _avatarMakerController.restoreState,
|
||||||
onLongPress: () {
|
onLongPress: _avatarMakerController.clearCustomizations,
|
||||||
_avatarMakerController.clearCustomizations();
|
|
||||||
},
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const FaIcon(FontAwesomeIcons.rotateLeft, size: 16),
|
const FaIcon(
|
||||||
|
FontAwesomeIcons.rotateLeft,
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(context.lang.avatarCustomizeReset),
|
Text(context.lang.avatarCustomizeReset),
|
||||||
],
|
],
|
||||||
|
|
@ -193,15 +467,13 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(vertical: 30),
|
||||||
vertical: 30,
|
|
||||||
),
|
|
||||||
child: AvatarMakerCustomizer(
|
child: AvatarMakerCustomizer(
|
||||||
scaffoldWidth: min(
|
scaffoldWidth: min(
|
||||||
600,
|
600,
|
||||||
MediaQuery.of(context).size.width * 0.95,
|
MediaQuery.of(context).size.width * 0.95,
|
||||||
),
|
),
|
||||||
theme: getAvatarMakerTheme(context),
|
theme: _theme(context),
|
||||||
controller: _avatarMakerController,
|
controller: _avatarMakerController,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -776,6 +776,7 @@ async fn handle_encrypted_inner(
|
||||||
}
|
}
|
||||||
|
|
||||||
contact::check_for_profile_update(t, from_user_id, &content).await?;
|
contact::check_for_profile_update(t, from_user_id, &content).await?;
|
||||||
|
contact::check_for_custom_avatar_update(t, from_user_id, &content).await?;
|
||||||
|
|
||||||
if content.ask_for_friend_promotions == Some(true) {
|
if content.ask_for_friend_promotions == Some(true) {
|
||||||
Contact::update_ask_for_friend_promotions(t, from_user_id).await?;
|
Contact::update_ask_for_friend_promotions(t, from_user_id).await?;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,72 @@ use std::io::Write as _;
|
||||||
use std::sync::{Arc, Mutex, OnceLock};
|
use std::sync::{Arc, Mutex, OnceLock};
|
||||||
|
|
||||||
static REQUESTED_PROFILES: OnceLock<Mutex<HashMap<i64, i64>>> = OnceLock::new();
|
static REQUESTED_PROFILES: OnceLock<Mutex<HashMap<i64, i64>>> = OnceLock::new();
|
||||||
|
static REQUESTED_CUSTOM_AVATARS: OnceLock<Mutex<HashMap<i64, (i64, std::time::Instant)>>> =
|
||||||
|
OnceLock::new();
|
||||||
|
|
||||||
|
pub(crate) async fn check_for_custom_avatar_update(
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
from_user_id: i64,
|
||||||
|
content: &EncryptedContent,
|
||||||
|
) -> Result<()> {
|
||||||
|
if content.custom_avatar_protocol_version.unwrap_or_default()
|
||||||
|
< crate::services::avatars::CUSTOM_AVATAR_PROTOCOL_VERSION
|
||||||
|
|| content
|
||||||
|
.contact_update
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|update| update.custom_avatar.is_some())
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let Some(counter) = content.sender_custom_avatar_counter else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if counter <= crate::services::avatars::applied_counter(t, from_user_id).await? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let should_request = {
|
||||||
|
let mut requested = REQUESTED_CUSTOM_AVATARS
|
||||||
|
.get_or_init(|| Mutex::new(HashMap::new()))
|
||||||
|
.lock()
|
||||||
|
.unwrap();
|
||||||
|
requested.retain(|_, (_, requested_at)| {
|
||||||
|
requested_at.elapsed() < std::time::Duration::from_secs(120)
|
||||||
|
});
|
||||||
|
let previous = requested
|
||||||
|
.get(&from_user_id)
|
||||||
|
.map(|(counter, _)| *counter)
|
||||||
|
.unwrap_or(-1);
|
||||||
|
if counter > previous {
|
||||||
|
requested.insert(from_user_id, (counter, std::time::Instant::now()));
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if should_request {
|
||||||
|
queue_encrypted_content(
|
||||||
|
t,
|
||||||
|
from_user_id,
|
||||||
|
EncryptedContent {
|
||||||
|
contact_update: Some(encrypted_content::ContactUpdate {
|
||||||
|
r#type: encrypted_content::contact_update::Type::Request as i32,
|
||||||
|
username: None,
|
||||||
|
display_name: None,
|
||||||
|
avatar_svg_compressed: None,
|
||||||
|
custom_avatar_requested: Some(true),
|
||||||
|
custom_avatar: None,
|
||||||
|
}),
|
||||||
|
custom_avatar_protocol_version: Some(
|
||||||
|
crate::services::avatars::CUSTOM_AVATAR_PROTOCOL_VERSION,
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn check_for_profile_update(
|
pub(crate) async fn check_for_profile_update(
|
||||||
t: &mut Transaction<'_, Sqlite>,
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
|
@ -68,6 +134,8 @@ pub(crate) async fn check_for_profile_update(
|
||||||
username: None,
|
username: None,
|
||||||
display_name: None,
|
display_name: None,
|
||||||
avatar_svg_compressed: None,
|
avatar_svg_compressed: None,
|
||||||
|
custom_avatar_requested: None,
|
||||||
|
custom_avatar: None,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
|
@ -216,6 +284,11 @@ pub(crate) async fn handle_contact_update(
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
|
|
||||||
|
let custom_avatar = if update.custom_avatar_requested == Some(true) {
|
||||||
|
Some(crate::services::avatars::publication_payload(tr, from_user_id).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
queue_encrypted_content(
|
queue_encrypted_content(
|
||||||
tr,
|
tr,
|
||||||
from_user_id,
|
from_user_id,
|
||||||
|
|
@ -225,8 +298,16 @@ pub(crate) async fn handle_contact_update(
|
||||||
username: Some(user.username),
|
username: Some(user.username),
|
||||||
display_name: Some(user.display_name),
|
display_name: Some(user.display_name),
|
||||||
avatar_svg_compressed,
|
avatar_svg_compressed,
|
||||||
|
custom_avatar_requested: None,
|
||||||
|
custom_avatar: custom_avatar.clone(),
|
||||||
}),
|
}),
|
||||||
sender_profile_counter: Some(user.avatar_counter),
|
sender_profile_counter: Some(user.avatar_counter),
|
||||||
|
sender_custom_avatar_counter: custom_avatar
|
||||||
|
.as_ref()
|
||||||
|
.map(|avatar| avatar.publication_counter),
|
||||||
|
custom_avatar_protocol_version: Some(
|
||||||
|
crate::services::avatars::CUSTOM_AVATAR_PROTOCOL_VERSION,
|
||||||
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
|
|
@ -240,6 +321,21 @@ pub(crate) async fn handle_contact_update(
|
||||||
}
|
}
|
||||||
|
|
||||||
let avatar_svg_compressed = update.avatar_svg_compressed.clone();
|
let avatar_svg_compressed = update.avatar_svg_compressed.clone();
|
||||||
|
if let Some(custom_avatar) = update.custom_avatar.clone() {
|
||||||
|
match crate::services::avatars::apply_received(tr, from_user_id, custom_avatar).await {
|
||||||
|
Ok(_) => {
|
||||||
|
if let Some(requested) = REQUESTED_CUSTOM_AVATARS.get() {
|
||||||
|
requested.lock().unwrap().remove(&from_user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
// A malformed optional avatar must not keep an otherwise valid
|
||||||
|
// reliable-mailbox profile response retrying forever. The
|
||||||
|
// advertised counter stays stale and can be requested again.
|
||||||
|
tracing::warn!(from_user_id, %error, "discarding invalid custom avatar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let previous = sqlx::query!(
|
let previous = sqlx::query!(
|
||||||
"SELECT username, display_name FROM contacts WHERE user_id = ?",
|
"SELECT username, display_name FROM contacts WHERE user_id = ?",
|
||||||
|
|
|
||||||
|
|
@ -509,6 +509,9 @@ async fn prepare_queued_receipt_from_row(
|
||||||
let plaintext = message.encrypted_content.take().ok_or_else(|| {
|
let plaintext = message.encrypted_content.take().ok_or_else(|| {
|
||||||
TwonlyError::Generic("queued encrypted message has no content".into())
|
TwonlyError::Generic("queued encrypted message has no content".into())
|
||||||
})?;
|
})?;
|
||||||
|
let plaintext =
|
||||||
|
crate::services::avatars::refresh_queued_response(ctx, row.contact_id, plaintext)
|
||||||
|
.await?;
|
||||||
|
|
||||||
message.encrypted_content =
|
message.encrypted_content =
|
||||||
Some(encrypt_v2_with_session_recovery(ctx, row.contact_id, plaintext).await?);
|
Some(encrypt_v2_with_session_recovery(ctx, row.contact_id, plaintext).await?);
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,10 @@ pub(crate) async fn decorate_content(
|
||||||
};
|
};
|
||||||
|
|
||||||
content.sender_profile_counter = Some(config.avatar_counter);
|
content.sender_profile_counter = Some(config.avatar_counter);
|
||||||
|
content.sender_custom_avatar_counter =
|
||||||
|
Some(crate::services::avatars::prepare_publication(t, contact_id).await?);
|
||||||
|
content.custom_avatar_protocol_version =
|
||||||
|
Some(crate::services::avatars::CUSTOM_AVATAR_PROTOCOL_VERSION);
|
||||||
content.widget_sharing_allowed = Some(
|
content.widget_sharing_allowed = Some(
|
||||||
sqlx::query_scalar!(
|
sqlx::query_scalar!(
|
||||||
"SELECT widget_sharing_granted FROM contacts WHERE user_id = ?",
|
"SELECT widget_sharing_granted FROM contacts WHERE user_id = ?",
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ message EncryptedContent {
|
||||||
optional bool widget_sharing_allowed = 29;
|
optional bool widget_sharing_allowed = 29;
|
||||||
// Sum of all exchanged media, omitted when profile sharing is disabled.
|
// Sum of all exchanged media, omitted when profile sharing is disabled.
|
||||||
optional int64 sender_twonly_score = 31;
|
optional int64 sender_twonly_score = 31;
|
||||||
|
// Per-recipient version of the custom photo-avatar state. Receivers request
|
||||||
|
// the payload only when this advances; the image is never broadcast.
|
||||||
|
optional int64 sender_custom_avatar_counter = 32;
|
||||||
|
optional uint32 custom_avatar_protocol_version = 33;
|
||||||
|
|
||||||
optional MessageUpdate message_update = 5;
|
optional MessageUpdate message_update = 5;
|
||||||
optional Media media = 6;
|
optional Media media = 6;
|
||||||
|
|
@ -177,6 +181,25 @@ message EncryptedContent {
|
||||||
optional bytes avatar_svg_compressed = 2;
|
optional bytes avatar_svg_compressed = 2;
|
||||||
optional string username = 3;
|
optional string username = 3;
|
||||||
optional string display_name = 4;
|
optional string display_name = 4;
|
||||||
|
// Set on a REQUEST only by clients that understand CustomAvatar.
|
||||||
|
optional bool custom_avatar_requested = 5;
|
||||||
|
// Set on UPDATE only in direct response to such a request.
|
||||||
|
optional CustomAvatar custom_avatar = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CustomAvatar {
|
||||||
|
enum State {
|
||||||
|
SVG_ONLY = 0;
|
||||||
|
PHOTO = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32 version = 1;
|
||||||
|
int64 publication_counter = 2;
|
||||||
|
State state = 3;
|
||||||
|
optional bytes webp = 4;
|
||||||
|
optional bytes sha256 = 5;
|
||||||
|
optional uint32 width = 6;
|
||||||
|
optional uint32 height = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
message FlameSync {
|
message FlameSync {
|
||||||
|
|
|
||||||
|
|
@ -198,6 +198,9 @@ impl RustApi {
|
||||||
|
|
||||||
pub async fn current_user_avatar_path() -> Result<Option<String>> {
|
pub async fn current_user_avatar_path() -> Result<Option<String>> {
|
||||||
let ctx = Context::get_static()?.clone();
|
let ctx = Context::get_static()?.clone();
|
||||||
|
if let Some(path) = avatars::current_custom_avatar_path(&ctx).await? {
|
||||||
|
return Ok(Some(path.display().to_string()));
|
||||||
|
}
|
||||||
let path = tokio::task::spawn_blocking(move || avatars::current_user_avatar_path(&ctx))
|
let path = tokio::task::spawn_blocking(move || avatars::current_user_avatar_path(&ctx))
|
||||||
.await
|
.await
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
|
|
@ -206,6 +209,33 @@ impl RustApi {
|
||||||
Ok(path.map(|path| path.display().to_string()))
|
Ok(path.map(|path| path.display().to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_custom_avatar() -> Result<avatars::CustomAvatarInfo> {
|
||||||
|
avatars::own_custom_avatar(Context::get_static()?).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_custom_avatar(
|
||||||
|
image_path: String,
|
||||||
|
accepted_contacts_only: bool,
|
||||||
|
) -> Result<avatars::CustomAvatarInfo> {
|
||||||
|
let avatar = tokio::task::spawn_blocking(move || {
|
||||||
|
avatars::create_custom_avatar(Path::new(&image_path))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("avatar creation task failed: {error}")))??;
|
||||||
|
avatars::store_own_custom_avatar(Context::get_static()?, avatar, accepted_contacts_only)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_custom_avatar_audience(
|
||||||
|
accepted_contacts_only: bool,
|
||||||
|
) -> Result<avatars::CustomAvatarInfo> {
|
||||||
|
avatars::set_custom_avatar_audience(Context::get_static()?, accepted_contacts_only).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_custom_avatar() -> Result<avatars::CustomAvatarInfo> {
|
||||||
|
avatars::remove_own_custom_avatar(Context::get_static()?).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Settles every background transfer this device believes is still in
|
/// Settles every background transfer this device believes is still in
|
||||||
/// flight and resumes any upload a terminated process left behind.
|
/// flight and resumes any upload a terminated process left behind.
|
||||||
pub async fn finish_started_media_uploads() -> Result<()> {
|
pub async fn finish_started_media_uploads() -> Result<()> {
|
||||||
|
|
|
||||||
31
rust/src/database/app/migrations/0031_custom_avatars.sql
Normal file
31
rust/src/database/app/migrations/0031_custom_avatars.sql
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
CREATE TABLE own_custom_avatar (
|
||||||
|
singleton INTEGER NOT NULL PRIMARY KEY CHECK(singleton = 1),
|
||||||
|
webp BLOB CHECK(webp IS NULL OR length(webp) BETWEEN 1 AND 131072),
|
||||||
|
sha256 BLOB CHECK(sha256 IS NULL OR length(sha256) = 32),
|
||||||
|
width INTEGER CHECK(width IS NULL OR width BETWEEN 1 AND 300),
|
||||||
|
height INTEGER CHECK(height IS NULL OR height BETWEEN 1 AND 300),
|
||||||
|
accepted_contacts_only INTEGER NOT NULL DEFAULT 1 CHECK(accepted_contacts_only IN (0, 1)),
|
||||||
|
source_revision INTEGER NOT NULL DEFAULT 0 CHECK(source_revision >= 0),
|
||||||
|
CHECK((webp IS NULL AND sha256 IS NULL AND width IS NULL AND height IS NULL) OR
|
||||||
|
(webp IS NOT NULL AND sha256 IS NOT NULL AND width IS NOT NULL AND height IS NOT NULL))
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO own_custom_avatar(singleton) VALUES (1);
|
||||||
|
|
||||||
|
CREATE TABLE custom_avatar_publications (
|
||||||
|
contact_id INTEGER NOT NULL PRIMARY KEY REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
publication_counter INTEGER NOT NULL DEFAULT 0 CHECK(publication_counter >= 0),
|
||||||
|
visible_source_revision INTEGER,
|
||||||
|
shows_photo INTEGER NOT NULL DEFAULT 0 CHECK(shows_photo IN (0, 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE received_custom_avatars (
|
||||||
|
contact_id INTEGER NOT NULL PRIMARY KEY REFERENCES contacts(user_id) ON DELETE CASCADE,
|
||||||
|
applied_counter INTEGER NOT NULL DEFAULT 0 CHECK(applied_counter >= 0),
|
||||||
|
webp BLOB CHECK(webp IS NULL OR length(webp) BETWEEN 1 AND 131072),
|
||||||
|
sha256 BLOB CHECK(sha256 IS NULL OR length(sha256) = 32),
|
||||||
|
width INTEGER CHECK(width IS NULL OR width BETWEEN 1 AND 300),
|
||||||
|
height INTEGER CHECK(height IS NULL OR height BETWEEN 1 AND 300),
|
||||||
|
CHECK((webp IS NULL AND sha256 IS NULL AND width IS NULL AND height IS NULL) OR
|
||||||
|
(webp IS NOT NULL AND sha256 IS NOT NULL AND width IS NOT NULL AND height IS NOT NULL))
|
||||||
|
);
|
||||||
|
|
@ -16,7 +16,7 @@ mod legacy_import;
|
||||||
pub mod tables;
|
pub mod tables;
|
||||||
|
|
||||||
pub const APP_DATABASE_FILE: &str = "app_db.sqlite";
|
pub const APP_DATABASE_FILE: &str = "app_db.sqlite";
|
||||||
pub const APP_SCHEMA_VERSION: i64 = 15;
|
pub const APP_SCHEMA_VERSION: i64 = 16;
|
||||||
|
|
||||||
/// User-owned application tables in the current Rust schema. Rust-only outbox
|
/// User-owned application tables in the current Rust schema. Rust-only outbox
|
||||||
/// tables are deliberately absent because they are reconstructed locally.
|
/// tables are deliberately absent because they are reconstructed locally.
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||||
);
|
);
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1901082359;
|
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 926647182;
|
||||||
|
|
||||||
// Section: executor
|
// Section: executor
|
||||||
|
|
||||||
|
|
@ -1880,6 +1880,47 @@ fn wire__crate__bridge__api__rust_api_connection_state_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__api__rust_api_create_custom_avatar_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_api_create_custom_avatar",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_image_path = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_accepted_contacts_only = <bool>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok = crate::bridge::api::RustApi::create_custom_avatar(
|
||||||
|
api_image_path,
|
||||||
|
api_accepted_contacts_only,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__api__rust_api_create_sticker_impl(
|
fn wire__crate__bridge__api__rust_api_create_sticker_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -2468,6 +2509,41 @@ fn wire__crate__bridge__api__rust_api_force_ipa_check_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__api__rust_api_get_custom_avatar_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_api_get_custom_avatar",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok = crate::bridge::api::RustApi::get_custom_avatar().await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__api__rust_api_get_memories_url_impl(
|
fn wire__crate__bridge__api__rust_api_get_memories_url_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -3798,6 +3874,41 @@ fn wire__crate__bridge__api__rust_api_remove_additional_user_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__api__rust_api_remove_custom_avatar_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_api_remove_custom_avatar",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok = crate::bridge::api::RustApi::remove_custom_avatar().await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__api__rust_api_remove_media_files_impl(
|
fn wire__crate__bridge__api__rust_api_remove_media_files_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -4548,6 +4659,45 @@ fn wire__crate__bridge__api__rust_api_set_background_impl(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
fn wire__crate__bridge__api__rust_api_set_custom_avatar_audience_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
rust_vec_len_: i32,
|
||||||
|
data_len_: i32,
|
||||||
|
) {
|
||||||
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
|
debug_name: "rust_api_set_custom_avatar_audience",
|
||||||
|
port: Some(port_),
|
||||||
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
|
},
|
||||||
|
move || {
|
||||||
|
let message = unsafe {
|
||||||
|
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||||
|
ptr_,
|
||||||
|
rust_vec_len_,
|
||||||
|
data_len_,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut deserializer =
|
||||||
|
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||||
|
let api_accepted_contacts_only = <bool>::sse_decode(&mut deserializer);
|
||||||
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
|
(move || async move {
|
||||||
|
let output_ok = crate::bridge::api::RustApi::set_custom_avatar_audience(
|
||||||
|
api_accepted_contacts_only,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
fn wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(
|
fn wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(
|
||||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
|
|
@ -6908,6 +7058,26 @@ impl SseDecode for bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseDecode for crate::services::avatars::CustomAvatarInfo {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
let mut var_webp = <Option<Vec<u8>>>::sse_decode(deserializer);
|
||||||
|
let mut var_sha256 = <Option<Vec<u8>>>::sse_decode(deserializer);
|
||||||
|
let mut var_width = <Option<u32>>::sse_decode(deserializer);
|
||||||
|
let mut var_height = <Option<u32>>::sse_decode(deserializer);
|
||||||
|
let mut var_acceptedContactsOnly = <bool>::sse_decode(deserializer);
|
||||||
|
let mut var_sourceRevision = <i64>::sse_decode(deserializer);
|
||||||
|
return crate::services::avatars::CustomAvatarInfo {
|
||||||
|
webp: var_webp,
|
||||||
|
sha256: var_sha256,
|
||||||
|
width: var_width,
|
||||||
|
height: var_height,
|
||||||
|
accepted_contacts_only: var_acceptedContactsOnly,
|
||||||
|
source_revision: var_sourceRevision,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseDecode for f64 {
|
impl SseDecode for f64 {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||||
|
|
@ -8245,134 +8415,138 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||||
49 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len),
|
49 => wire__crate__bridge__api__rust_api_confirm_memories_upload_impl(port, ptr, rust_vec_len, data_len),
|
||||||
50 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len),
|
50 => wire__crate__bridge__api__rust_api_connect_impl(port, ptr, rust_vec_len, data_len),
|
||||||
51 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len),
|
51 => wire__crate__bridge__api__rust_api_connection_state_impl(port, ptr, rust_vec_len, data_len),
|
||||||
52 => wire__crate__bridge__api__rust_api_create_sticker_impl(port, ptr, rust_vec_len, data_len),
|
52 => wire__crate__bridge__api__rust_api_create_custom_avatar_impl(port, ptr, rust_vec_len, data_len),
|
||||||
53 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len),
|
53 => wire__crate__bridge__api__rust_api_create_sticker_impl(port, ptr, rust_vec_len, data_len),
|
||||||
54 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len),
|
54 => wire__crate__bridge__api__rust_api_crop_media_transparent_borders_impl(port, ptr, rust_vec_len, data_len),
|
||||||
56 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len),
|
55 => wire__crate__bridge__api__rust_api_current_user_avatar_path_impl(port, ptr, rust_vec_len, data_len),
|
||||||
57 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len),
|
57 => wire__crate__bridge__api__rust_api_delete_account_impl(port, ptr, rust_vec_len, data_len),
|
||||||
58 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len),
|
58 => wire__crate__bridge__api__rust_api_delete_memory_impl(port, ptr, rust_vec_len, data_len),
|
||||||
59 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len),
|
59 => wire__crate__bridge__api__rust_api_delete_widget_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
60 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len),
|
60 => wire__crate__bridge__api__rust_api_disable_memories_backup_impl(port, ptr, rust_vec_len, data_len),
|
||||||
61 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len),
|
61 => wire__crate__bridge__api__rust_api_download_done_impl(port, ptr, rust_vec_len, data_len),
|
||||||
62 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len),
|
62 => wire__crate__bridge__api__rust_api_download_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
63 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len),
|
63 => wire__crate__bridge__api__rust_api_download_pending_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
64 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len),
|
64 => wire__crate__bridge__api__rust_api_ensure_avatar_png_impl(port, ptr, rust_vec_len, data_len),
|
||||||
65 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len),
|
65 => wire__crate__bridge__api__rust_api_establish_signal_session_impl(port, ptr, rust_vec_len, data_len),
|
||||||
66 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len),
|
66 => wire__crate__bridge__api__rust_api_events_impl(port, ptr, rust_vec_len, data_len),
|
||||||
67 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len),
|
67 => wire__crate__bridge__api__rust_api_finish_started_media_uploads_impl(port, ptr, rust_vec_len, data_len),
|
||||||
68 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len),
|
68 => wire__crate__bridge__api__rust_api_force_ipa_check_impl(port, ptr, rust_vec_len, data_len),
|
||||||
69 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len),
|
69 => wire__crate__bridge__api__rust_api_get_custom_avatar_impl(port, ptr, rust_vec_len, data_len),
|
||||||
70 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len),
|
70 => wire__crate__bridge__api__rust_api_get_memories_url_impl(port, ptr, rust_vec_len, data_len),
|
||||||
71 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len),
|
71 => wire__crate__bridge__api__rust_api_get_memories_usage_impl(port, ptr, rust_vec_len, data_len),
|
||||||
72 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
|
72 => wire__crate__bridge__api__rust_api_get_plan_balance_impl(port, ptr, rust_vec_len, data_len),
|
||||||
73 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len),
|
73 => wire__crate__bridge__api__rust_api_get_proof_of_work_impl(port, ptr, rust_vec_len, data_len),
|
||||||
74 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len),
|
74 => wire__crate__bridge__api__rust_api_get_server_key_for_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
|
||||||
75 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
|
75 => wire__crate__bridge__api__rust_api_get_user_by_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
76 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len),
|
76 => wire__crate__bridge__api__rust_api_get_user_data_impl(port, ptr, rust_vec_len, data_len),
|
||||||
77 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len),
|
77 => wire__crate__bridge__api__rust_api_get_user_id_from_username_impl(port, ptr, rust_vec_len, data_len),
|
||||||
78 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len),
|
78 => wire__crate__bridge__api__rust_api_hand_outbox_to_os_impl(port, ptr, rust_vec_len, data_len),
|
||||||
79 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
|
79 => wire__crate__bridge__api__rust_api_initialize_media_upload_impl(port, ptr, rust_vec_len, data_len),
|
||||||
80 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
|
80 => wire__crate__bridge__api__rust_api_initialize_memory_location_impl(port, ptr, rust_vec_len, data_len),
|
||||||
81 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
|
81 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_impl(port, ptr, rust_vec_len, data_len),
|
||||||
82 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
|
82 => wire__crate__bridge__api__rust_api_insert_and_send_ask_about_user_impl(port, ptr, rust_vec_len, data_len),
|
||||||
83 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
|
83 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_impl(port, ptr, rust_vec_len, data_len),
|
||||||
84 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
|
84 => wire__crate__bridge__api__rust_api_insert_and_send_text_impl(port, ptr, rust_vec_len, data_len),
|
||||||
85 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len),
|
85 => wire__crate__bridge__api__rust_api_ipa_purchase_impl(port, ptr, rust_vec_len, data_len),
|
||||||
86 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len),
|
86 => wire__crate__bridge__api__rust_api_load_plan_balance_impl(port, ptr, rust_vec_len, data_len),
|
||||||
87 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len),
|
87 => wire__crate__bridge__api__rust_api_media_size_limit_report_impl(port, ptr, rust_vec_len, data_len),
|
||||||
88 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
|
88 => wire__crate__bridge__api__rust_api_media_step_finished_impl(port, ptr, rust_vec_len, data_len),
|
||||||
89 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
|
89 => wire__crate__bridge__api__rust_api_notification_badge_count_impl(port, ptr, rust_vec_len, data_len),
|
||||||
90 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len),
|
90 => wire__crate__bridge__api__rust_api_notify_messages_opened_impl(port, ptr, rust_vec_len, data_len),
|
||||||
91 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len),
|
91 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_impl(port, ptr, rust_vec_len, data_len),
|
||||||
92 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len),
|
92 => wire__crate__bridge__api__rust_api_prerender_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
93 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len),
|
93 => wire__crate__bridge__api__rust_api_prewarm_memory_location_impl(port, ptr, rust_vec_len, data_len),
|
||||||
94 => wire__crate__bridge__api__rust_api_refresh_contact_joined_dates_impl(port, ptr, rust_vec_len, data_len),
|
94 => wire__crate__bridge__api__rust_api_purge_media_temp_folder_impl(port, ptr, rust_vec_len, data_len),
|
||||||
95 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
|
95 => wire__crate__bridge__api__rust_api_purge_widget_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
96 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
|
96 => wire__crate__bridge__api__rust_api_refresh_contact_joined_dates_impl(port, ptr, rust_vec_len, data_len),
|
||||||
97 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len),
|
97 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
|
||||||
98 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
|
98 => wire__crate__bridge__api__rust_api_register_impl(port, ptr, rust_vec_len, data_len),
|
||||||
99 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
|
99 => wire__crate__bridge__api__rust_api_register_home_widget_impl(port, ptr, rust_vec_len, data_len),
|
||||||
100 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
|
100 => wire__crate__bridge__api__rust_api_register_passwordless_notification_impl(port, ptr, rust_vec_len, data_len),
|
||||||
101 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
|
101 => wire__crate__bridge__api__rust_api_register_passwordless_recovery_impl(port, ptr, rust_vec_len, data_len),
|
||||||
102 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len),
|
102 => wire__crate__bridge__api__rust_api_reload_configuration_impl(port, ptr, rust_vec_len, data_len),
|
||||||
103 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
|
103 => wire__crate__bridge__api__rust_api_remove_additional_user_impl(port, ptr, rust_vec_len, data_len),
|
||||||
104 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
|
104 => wire__crate__bridge__api__rust_api_remove_custom_avatar_impl(port, ptr, rust_vec_len, data_len),
|
||||||
105 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
|
105 => wire__crate__bridge__api__rust_api_remove_media_files_impl(port, ptr, rust_vec_len, data_len),
|
||||||
106 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
|
106 => wire__crate__bridge__api__rust_api_report_user_impl(port, ptr, rust_vec_len, data_len),
|
||||||
107 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
|
107 => wire__crate__bridge__api__rust_api_request_binary_impl(port, ptr, rust_vec_len, data_len),
|
||||||
108 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
|
108 => wire__crate__bridge__api__rust_api_request_contact_by_username_impl(port, ptr, rust_vec_len, data_len),
|
||||||
109 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len),
|
109 => wire__crate__bridge__api__rust_api_request_media_reupload_impl(port, ptr, rust_vec_len, data_len),
|
||||||
110 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len),
|
110 => wire__crate__bridge__api__rust_api_request_memories_upload_impl(port, ptr, rust_vec_len, data_len),
|
||||||
111 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len),
|
111 => wire__crate__bridge__api__rust_api_retransmit_all_messages_impl(port, ptr, rust_vec_len, data_len),
|
||||||
112 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
|
112 => wire__crate__bridge__api__rust_api_retry_pending_media_reuploads_impl(port, ptr, rust_vec_len, data_len),
|
||||||
113 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
|
113 => wire__crate__bridge__api__rust_api_reupload_pending_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
114 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
|
114 => wire__crate__bridge__api__rust_api_save_media_to_gallery_impl(port, ptr, rust_vec_len, data_len),
|
||||||
115 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
|
115 => wire__crate__bridge__api__rust_api_send_binary_impl(port, ptr, rust_vec_len, data_len),
|
||||||
116 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len),
|
116 => wire__crate__bridge__api__rust_api_send_contact_profile_impl(port, ptr, rust_vec_len, data_len),
|
||||||
117 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
|
117 => wire__crate__bridge__api__rust_api_send_encrypted_content_impl(port, ptr, rust_vec_len, data_len),
|
||||||
118 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
|
118 => wire__crate__bridge__api__rust_api_send_encrypted_content_to_group_impl(port, ptr, rust_vec_len, data_len),
|
||||||
119 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
|
119 => wire__crate__bridge__api__rust_api_send_media_to_groups_impl(port, ptr, rust_vec_len, data_len),
|
||||||
120 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
|
120 => wire__crate__bridge__api__rust_api_send_queued_message_impl(port, ptr, rust_vec_len, data_len),
|
||||||
121 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len),
|
121 => wire__crate__bridge__api__rust_api_send_text_message_impl(port, ptr, rust_vec_len, data_len),
|
||||||
122 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
|
122 => wire__crate__bridge__api__rust_api_send_typing_impl(port, ptr, rust_vec_len, data_len),
|
||||||
123 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len),
|
123 => wire__crate__bridge__api__rust_api_set_background_impl(port, ptr, rust_vec_len, data_len),
|
||||||
124 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len),
|
124 => wire__crate__bridge__api__rust_api_set_custom_avatar_audience_impl(port, ptr, rust_vec_len, data_len),
|
||||||
125 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len),
|
125 => wire__crate__bridge__api__rust_api_set_home_widget_groups_impl(port, ptr, rust_vec_len, data_len),
|
||||||
126 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
|
126 => wire__crate__bridge__api__rust_api_set_login_token_impl(port, ptr, rust_vec_len, data_len),
|
||||||
127 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len),
|
127 => wire__crate__bridge__api__rust_api_set_media_display_limit_impl(port, ptr, rust_vec_len, data_len),
|
||||||
128 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
|
128 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_impl(port, ptr, rust_vec_len, data_len),
|
||||||
129 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len),
|
129 => wire__crate__bridge__api__rust_api_set_media_trim_impl(port, ptr, rust_vec_len, data_len),
|
||||||
130 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len),
|
130 => wire__crate__bridge__api__rust_api_set_network_available_impl(port, ptr, rust_vec_len, data_len),
|
||||||
131 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len),
|
131 => wire__crate__bridge__api__rust_api_store_media_impl(port, ptr, rust_vec_len, data_len),
|
||||||
132 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len),
|
132 => wire__crate__bridge__api__rust_api_submit_recovery_share_impl(port, ptr, rust_vec_len, data_len),
|
||||||
133 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
|
133 => wire__crate__bridge__api__rust_api_sync_widget_permissions_impl(port, ptr, rust_vec_len, data_len),
|
||||||
134 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
|
134 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_impl(port, ptr, rust_vec_len, data_len),
|
||||||
135 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
|
135 => wire__crate__bridge__api__rust_api_try_request_contact_by_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
136 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len),
|
136 => wire__crate__bridge__api__rust_api_unregister_home_widget_impl(port, ptr, rust_vec_len, data_len),
|
||||||
137 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
|
137 => wire__crate__bridge__api__rust_api_update_fcm_token_impl(port, ptr, rust_vec_len, data_len),
|
||||||
138 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
|
138 => wire__crate__bridge__api__rust_api_update_signed_pre_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
139 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
|
139 => wire__crate__bridge__api__rust_api_upload_pqc_pre_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
140 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
|
140 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_impl(port, ptr, rust_vec_len, data_len),
|
||||||
141 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
141 => wire__crate__bridge__wrapper__app_database__rust_app_database_execute_impl(port, ptr, rust_vec_len, data_len),
|
||||||
142 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
|
142 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_impl(port, ptr, rust_vec_len, data_len),
|
||||||
143 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
143 => wire__crate__bridge__wrapper__app_database__rust_app_database_migrate_legacy_database_impl(port, ptr, rust_vec_len, data_len),
|
||||||
144 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
|
144 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_impl(port, ptr, rust_vec_len, data_len),
|
||||||
145 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
145 => wire__crate__bridge__wrapper__backup__rust_backup_archive_create_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
||||||
146 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
|
146 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_impl(port, ptr, rust_vec_len, data_len),
|
||||||
147 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
147 => wire__crate__bridge__wrapper__backup__rust_backup_archive_restore_backup_archive_impl(port, ptr, rust_vec_len, data_len),
|
||||||
148 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
|
148 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
149 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
149 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
150 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
150 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_impl(port, ptr, rust_vec_len, data_len),
|
||||||
151 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
151 => wire__crate__bridge__wrapper__backup__rust_backup_identity_import_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
152 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
152 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_impl(port, ptr, rust_vec_len, data_len),
|
||||||
153 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
153 => wire__crate__bridge__wrapper__backup__rust_backup_identity_set_backup_password_keys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
154 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
|
154 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
155 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_encrypt_cloud_media_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
|
156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||||
157 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
|
157 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
158 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
158 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_impl(port, ptr, rust_vec_len, data_len),
|
||||||
159 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
|
159 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_signal_identity_impl(port, ptr, rust_vec_len, data_len),
|
||||||
160 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
|
160 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_impl(port, ptr, rust_vec_len, data_len),
|
||||||
161 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
161 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_serialize_impl(port, ptr, rust_vec_len, data_len),
|
||||||
162 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
162 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_impl(port, ptr, rust_vec_len, data_len),
|
||||||
163 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
163 => wire__crate__bridge__wrapper__signal__rust_signal_decrypt_impl(port, ptr, rust_vec_len, data_len),
|
||||||
164 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len),
|
164 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_impl(port, ptr, rust_vec_len, data_len),
|
||||||
165 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
165 => wire__crate__bridge__wrapper__signal__rust_signal_generate_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||||
166 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
166 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_impl(port, ptr, rust_vec_len, data_len),
|
||||||
167 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len),
|
167 => wire__crate__bridge__wrapper__signal__rust_signal_get_contact_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
168 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
168 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_impl(port, ptr, rust_vec_len, data_len),
|
||||||
169 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
169 => wire__crate__bridge__wrapper__signal__rust_signal_get_user_public_key_impl(port, ptr, rust_vec_len, data_len),
|
||||||
170 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
170 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_impl(port, ptr, rust_vec_len, data_len),
|
||||||
171 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
171 => wire__crate__bridge__wrapper__signal__rust_signal_reset_contact_session_impl(port, ptr, rust_vec_len, data_len),
|
||||||
172 => wire__crate__bridge__stories__story_audience_chats_impl(port, ptr, rust_vec_len, data_len),
|
172 => wire__crate__bridge__wrapper__rust_utils_generate_shares_impl(port, ptr, rust_vec_len, data_len),
|
||||||
173 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
173 => wire__crate__bridge__wrapper__rust_utils_recover_secret_impl(port, ptr, rust_vec_len, data_len),
|
||||||
174 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
174 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||||
175 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
175 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||||
177 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
176 => wire__crate__bridge__stories__story_audience_chats_impl(port, ptr, rust_vec_len, data_len),
|
||||||
178 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
177 => wire__crate__bridge__groups__update_chat_deletion_time_impl(port, ptr, rust_vec_len, data_len),
|
||||||
179 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
178 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||||
180 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
179 => wire__crate__bridge__webxdc__updates_after_impl(port, ptr, rust_vec_len, data_len),
|
||||||
181 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
181 => wire__crate__bridge__user_config__user_config_api_create_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
182 => wire__crate__bridge__user_config__user_config_api_import_json_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
183 => wire__crate__bridge__user_config__user_config_api_load_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
184 => wire__crate__bridge__user_config__user_config_api_save_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
185 => wire__crate__bridge__user_config__user_config_api_update_impl(port, ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8388,15 +8562,15 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||||
28 => wire__crate__bridge__stories__max_media_recipients_impl(ptr, rust_vec_len, data_len),
|
28 => wire__crate__bridge__stories__max_media_recipients_impl(ptr, rust_vec_len, data_len),
|
||||||
40 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len),
|
40 => wire__crate__bridge__api__rust_api_api_base_url_impl(ptr, rust_vec_len, data_len),
|
||||||
42 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len),
|
42 => wire__crate__bridge__api__rust_api_avatar_png_path_impl(ptr, rust_vec_len, data_len),
|
||||||
55 => {
|
56 => {
|
||||||
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
wire__crate__bridge__api__rust_api_decode_avatar_svg_impl(ptr, rust_vec_len, data_len)
|
||||||
}
|
}
|
||||||
176 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
180 => wire__crate__bridge__user_config__user_config_api_clone_impl(
|
||||||
ptr,
|
ptr,
|
||||||
rust_vec_len,
|
rust_vec_len,
|
||||||
data_len,
|
data_len,
|
||||||
),
|
),
|
||||||
182 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
186 => wire__crate__bridge__logging__write_log_impl(ptr, rust_vec_len, data_len),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8498,6 +8672,31 @@ impl flutter_rust_bridge::IntoIntoDart<crate::keys::backup_password_keys::Backup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
|
impl flutter_rust_bridge::IntoDart for crate::services::avatars::CustomAvatarInfo {
|
||||||
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
|
[
|
||||||
|
self.webp.into_into_dart().into_dart(),
|
||||||
|
self.sha256.into_into_dart().into_dart(),
|
||||||
|
self.width.into_into_dart().into_dart(),
|
||||||
|
self.height.into_into_dart().into_dart(),
|
||||||
|
self.accepted_contacts_only.into_into_dart().into_dart(),
|
||||||
|
self.source_revision.into_into_dart().into_dart(),
|
||||||
|
]
|
||||||
|
.into_dart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive
|
||||||
|
for crate::services::avatars::CustomAvatarInfo
|
||||||
|
{
|
||||||
|
}
|
||||||
|
impl flutter_rust_bridge::IntoIntoDart<crate::services::avatars::CustomAvatarInfo>
|
||||||
|
for crate::services::avatars::CustomAvatarInfo
|
||||||
|
{
|
||||||
|
fn into_into_dart(self) -> crate::services::avatars::CustomAvatarInfo {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||||
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::bridge::FlameState> {
|
impl flutter_rust_bridge::IntoDart for FrbWrapper<crate::bridge::FlameState> {
|
||||||
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi {
|
||||||
[
|
[
|
||||||
|
|
@ -9677,6 +9876,18 @@ impl SseEncode for bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SseEncode for crate::services::avatars::CustomAvatarInfo {
|
||||||
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
<Option<Vec<u8>>>::sse_encode(self.webp, serializer);
|
||||||
|
<Option<Vec<u8>>>::sse_encode(self.sha256, serializer);
|
||||||
|
<Option<u32>>::sse_encode(self.width, serializer);
|
||||||
|
<Option<u32>>::sse_encode(self.height, serializer);
|
||||||
|
<bool>::sse_encode(self.accepted_contacts_only, serializer);
|
||||||
|
<i64>::sse_encode(self.source_revision, serializer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl SseEncode for f64 {
|
impl SseEncode for f64 {
|
||||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,18 @@ use crate::context::Context;
|
||||||
use crate::error::{Result, TwonlyError};
|
use crate::error::{Result, TwonlyError};
|
||||||
use crate::user_config::UserConfig;
|
use crate::user_config::UserConfig;
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
use std::io::Read;
|
use image::imageops;
|
||||||
|
use image::{DynamicImage, ImageDecoder, Rgba, RgbaImage};
|
||||||
|
use prost::Message as _;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use sqlx::{Row, Sqlite, Transaction};
|
||||||
|
use std::io::{Cursor, Read};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::api::proto::client::encrypted_content;
|
||||||
|
use crate::services::{media_codec, stickers};
|
||||||
|
|
||||||
const DEFAULT_AVATAR_SVG: &str = r##"<!-- Taken from: https://getavataaars.com/ -->
|
const DEFAULT_AVATAR_SVG: &str = r##"<!-- Taken from: https://getavataaars.com/ -->
|
||||||
<svg width="264px" height="280px" viewBox="0 0 264 280" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
<svg width="264px" height="280px" viewBox="0 0 264 280" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
|
@ -36,6 +44,367 @@ const DEFAULT_AVATAR_SVG: &str = r##"<!-- Taken from: https://getavataaars.com/
|
||||||
|
|
||||||
static AVATAR_RENDER_LOCK: Mutex<()> = Mutex::new(());
|
static AVATAR_RENDER_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
|
pub(crate) const CUSTOM_AVATAR_PROTOCOL_VERSION: u32 = 1;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CustomAvatarInfo {
|
||||||
|
pub webp: Option<Vec<u8>>,
|
||||||
|
pub sha256: Option<Vec<u8>>,
|
||||||
|
pub width: Option<u32>,
|
||||||
|
pub height: Option<u32>,
|
||||||
|
pub accepted_contacts_only: bool,
|
||||||
|
pub source_revision: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create_custom_avatar(source: &Path) -> Result<stickers::StickerOutput> {
|
||||||
|
let cutout = stickers::create(source)?;
|
||||||
|
let decoded = image::load_from_memory_with_format(&cutout.webp, image::ImageFormat::WebP)
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("invalid generated avatar: {error}")))?;
|
||||||
|
let fitted = decoded.thumbnail(270, 270).to_rgba8();
|
||||||
|
let mut canvas = RgbaImage::from_pixel(300, 300, Rgba([0, 0, 0, 0]));
|
||||||
|
imageops::overlay(
|
||||||
|
&mut canvas,
|
||||||
|
&fitted,
|
||||||
|
i64::from((300 - fitted.width()) / 2),
|
||||||
|
i64::from((300 - fitted.height()) / 2),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Bake the circular preview into the payload, including a one-pixel soft
|
||||||
|
// edge. Consumers still clip defensively, but the file itself is circular.
|
||||||
|
let center = 149.5_f32;
|
||||||
|
for (x, y, pixel) in canvas.enumerate_pixels_mut() {
|
||||||
|
let distance = ((x as f32 - center).powi(2) + (y as f32 - center).powi(2)).sqrt();
|
||||||
|
let circle_alpha = (150.0 - distance).clamp(0.0, 1.0);
|
||||||
|
pixel[3] = (f32::from(pixel[3]) * circle_alpha).round() as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = DynamicImage::ImageRgba8(canvas);
|
||||||
|
let mut webp = Vec::new();
|
||||||
|
for quality in [90.0_f32, 82.0, 74.0, 66.0, 58.0] {
|
||||||
|
webp = media_codec::encode_webp(&image, quality)?;
|
||||||
|
if webp.len() <= stickers::MAX_STICKER_BYTES {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if webp.len() > stickers::MAX_STICKER_BYTES {
|
||||||
|
return Err(TwonlyError::Generic(
|
||||||
|
"custom avatar is too large after compression".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let sha256 = Sha256::digest(&webp).to_vec();
|
||||||
|
Ok(stickers::StickerOutput {
|
||||||
|
content_hash: hex::encode(&sha256),
|
||||||
|
sha256,
|
||||||
|
webp,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn own_custom_avatar(ctx: &Context) -> Result<CustomAvatarInfo> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let row = sqlx::query(
|
||||||
|
"SELECT webp, sha256, width, height, accepted_contacts_only, source_revision \
|
||||||
|
FROM own_custom_avatar WHERE singleton = 1",
|
||||||
|
)
|
||||||
|
.fetch_one(&database.pool)
|
||||||
|
.await?;
|
||||||
|
Ok(CustomAvatarInfo {
|
||||||
|
webp: row.try_get("webp")?,
|
||||||
|
sha256: row.try_get("sha256")?,
|
||||||
|
width: row
|
||||||
|
.try_get::<Option<i64>, _>("width")?
|
||||||
|
.map(|value| value as u32),
|
||||||
|
height: row
|
||||||
|
.try_get::<Option<i64>, _>("height")?
|
||||||
|
.map(|value| value as u32),
|
||||||
|
accepted_contacts_only: row.try_get::<i64, _>("accepted_contacts_only")? != 0,
|
||||||
|
source_revision: row.try_get("source_revision")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn store_own_custom_avatar(
|
||||||
|
ctx: &Context,
|
||||||
|
avatar: stickers::StickerOutput,
|
||||||
|
accepted_contacts_only: bool,
|
||||||
|
) -> Result<CustomAvatarInfo> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE own_custom_avatar SET webp = ?, sha256 = ?, width = ?, height = ?, \
|
||||||
|
accepted_contacts_only = ?, source_revision = source_revision + 1 WHERE singleton = 1",
|
||||||
|
)
|
||||||
|
.bind(avatar.webp)
|
||||||
|
.bind(avatar.sha256)
|
||||||
|
.bind(i64::from(avatar.width))
|
||||||
|
.bind(i64::from(avatar.height))
|
||||||
|
.bind(accepted_contacts_only)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
own_custom_avatar(ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn set_custom_avatar_audience(
|
||||||
|
ctx: &Context,
|
||||||
|
accepted_contacts_only: bool,
|
||||||
|
) -> Result<CustomAvatarInfo> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE own_custom_avatar SET accepted_contacts_only = ?, \
|
||||||
|
source_revision = source_revision + CASE WHEN accepted_contacts_only != ? THEN 1 ELSE 0 END \
|
||||||
|
WHERE singleton = 1",
|
||||||
|
)
|
||||||
|
.bind(accepted_contacts_only)
|
||||||
|
.bind(accepted_contacts_only)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
own_custom_avatar(ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn remove_own_custom_avatar(ctx: &Context) -> Result<CustomAvatarInfo> {
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE own_custom_avatar SET webp = NULL, sha256 = NULL, width = NULL, height = NULL, \
|
||||||
|
source_revision = source_revision + CASE WHEN webp IS NULL THEN 0 ELSE 1 END WHERE singleton = 1",
|
||||||
|
)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await?;
|
||||||
|
own_custom_avatar(ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Materializes the avatar state visible to one recipient and returns its
|
||||||
|
/// counter. Private photo changes never advance excluded recipients' counters.
|
||||||
|
pub(crate) async fn prepare_publication(
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
contact_id: i64,
|
||||||
|
) -> Result<i64> {
|
||||||
|
let own = sqlx::query(
|
||||||
|
"SELECT webp IS NOT NULL AS has_photo, accepted_contacts_only, source_revision \
|
||||||
|
FROM own_custom_avatar WHERE singleton = 1",
|
||||||
|
)
|
||||||
|
.fetch_one(&mut **t)
|
||||||
|
.await?;
|
||||||
|
let contact =
|
||||||
|
sqlx::query("SELECT accepted, blocked, deleted_by_user FROM contacts WHERE user_id = ?")
|
||||||
|
.bind(contact_id)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
|
.await?;
|
||||||
|
let accepted = contact
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|row| row.get::<i64, _>("accepted") != 0);
|
||||||
|
let eligible = contact.as_ref().is_some_and(|row| {
|
||||||
|
row.get::<i64, _>("blocked") == 0 && row.get::<i64, _>("deleted_by_user") == 0
|
||||||
|
});
|
||||||
|
let accepted_only = own.get::<i64, _>("accepted_contacts_only") != 0;
|
||||||
|
let shows_photo =
|
||||||
|
own.get::<i64, _>("has_photo") != 0 && eligible && (!accepted_only || accepted);
|
||||||
|
let visible_revision = shows_photo.then(|| own.get::<i64, _>("source_revision"));
|
||||||
|
|
||||||
|
let current = sqlx::query(
|
||||||
|
"SELECT publication_counter, visible_source_revision, shows_photo \
|
||||||
|
FROM custom_avatar_publications WHERE contact_id = ?",
|
||||||
|
)
|
||||||
|
.bind(contact_id)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
|
.await?;
|
||||||
|
if let Some(row) = current {
|
||||||
|
let old_photo = row.get::<i64, _>("shows_photo") != 0;
|
||||||
|
let old_revision: Option<i64> = row.try_get("visible_source_revision")?;
|
||||||
|
let counter: i64 = row.get("publication_counter");
|
||||||
|
if old_photo == shows_photo && old_revision == visible_revision {
|
||||||
|
return Ok(counter);
|
||||||
|
}
|
||||||
|
let counter = counter + 1;
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE custom_avatar_publications SET publication_counter = ?, \
|
||||||
|
visible_source_revision = ?, shows_photo = ? WHERE contact_id = ?",
|
||||||
|
)
|
||||||
|
.bind(counter)
|
||||||
|
.bind(visible_revision)
|
||||||
|
.bind(shows_photo)
|
||||||
|
.bind(contact_id)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
return Ok(counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
let counter = i64::from(shows_photo);
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO custom_avatar_publications(contact_id, publication_counter, \
|
||||||
|
visible_source_revision, shows_photo) VALUES (?, ?, ?, ?)",
|
||||||
|
)
|
||||||
|
.bind(contact_id)
|
||||||
|
.bind(counter)
|
||||||
|
.bind(visible_revision)
|
||||||
|
.bind(shows_photo)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
Ok(counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn publication_payload(
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
contact_id: i64,
|
||||||
|
) -> Result<encrypted_content::CustomAvatar> {
|
||||||
|
let counter = prepare_publication(t, contact_id).await?;
|
||||||
|
let publication =
|
||||||
|
sqlx::query("SELECT shows_photo FROM custom_avatar_publications WHERE contact_id = ?")
|
||||||
|
.bind(contact_id)
|
||||||
|
.fetch_one(&mut **t)
|
||||||
|
.await?;
|
||||||
|
let shows_photo = publication.get::<i64, _>("shows_photo") != 0;
|
||||||
|
if !shows_photo {
|
||||||
|
return Ok(encrypted_content::CustomAvatar {
|
||||||
|
version: CUSTOM_AVATAR_PROTOCOL_VERSION,
|
||||||
|
publication_counter: counter,
|
||||||
|
state: encrypted_content::custom_avatar::State::SvgOnly as i32,
|
||||||
|
webp: None,
|
||||||
|
sha256: None,
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let own = sqlx::query(
|
||||||
|
"SELECT webp, sha256, width, height FROM own_custom_avatar WHERE singleton = 1",
|
||||||
|
)
|
||||||
|
.fetch_one(&mut **t)
|
||||||
|
.await?;
|
||||||
|
Ok(encrypted_content::CustomAvatar {
|
||||||
|
version: CUSTOM_AVATAR_PROTOCOL_VERSION,
|
||||||
|
publication_counter: counter,
|
||||||
|
state: encrypted_content::custom_avatar::State::Photo as i32,
|
||||||
|
webp: own.try_get("webp")?,
|
||||||
|
sha256: own.try_get("sha256")?,
|
||||||
|
width: own
|
||||||
|
.try_get::<Option<i64>, _>("width")?
|
||||||
|
.map(|value| value as u32),
|
||||||
|
height: own
|
||||||
|
.try_get::<Option<i64>, _>("height")?
|
||||||
|
.map(|value| value as u32),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn applied_counter(
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
contact_id: i64,
|
||||||
|
) -> Result<i64> {
|
||||||
|
Ok(sqlx::query_scalar::<_, i64>(
|
||||||
|
"SELECT applied_counter FROM received_custom_avatars WHERE contact_id = ?",
|
||||||
|
)
|
||||||
|
.bind(contact_id)
|
||||||
|
.fetch_optional(&mut **t)
|
||||||
|
.await?
|
||||||
|
.unwrap_or(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn apply_received(
|
||||||
|
t: &mut Transaction<'_, Sqlite>,
|
||||||
|
contact_id: i64,
|
||||||
|
avatar: encrypted_content::CustomAvatar,
|
||||||
|
) -> Result<bool> {
|
||||||
|
if avatar.version != CUSTOM_AVATAR_PROTOCOL_VERSION || avatar.publication_counter < 0 {
|
||||||
|
return Err(TwonlyError::Generic(
|
||||||
|
"invalid custom avatar version or counter".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if avatar.publication_counter <= applied_counter(t, contact_id).await? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let state = encrypted_content::custom_avatar::State::try_from(avatar.state)?;
|
||||||
|
let (webp, sha256, width, height) = match state {
|
||||||
|
encrypted_content::custom_avatar::State::SvgOnly => (None, None, None, None),
|
||||||
|
encrypted_content::custom_avatar::State::Photo => {
|
||||||
|
let webp = avatar
|
||||||
|
.webp
|
||||||
|
.ok_or_else(|| TwonlyError::Generic("custom avatar has no image".into()))?;
|
||||||
|
let sha256 = avatar
|
||||||
|
.sha256
|
||||||
|
.ok_or_else(|| TwonlyError::Generic("custom avatar has no digest".into()))?;
|
||||||
|
let width = avatar.width.unwrap_or_default();
|
||||||
|
let height = avatar.height.unwrap_or_default();
|
||||||
|
validate_custom_webp(&webp, &sha256, width, height)?;
|
||||||
|
(Some(webp), Some(sha256), Some(width), Some(height))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO received_custom_avatars(contact_id, applied_counter, webp, sha256, width, height) \
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(contact_id) DO UPDATE SET \
|
||||||
|
applied_counter = excluded.applied_counter, webp = excluded.webp, sha256 = excluded.sha256, \
|
||||||
|
width = excluded.width, height = excluded.height",
|
||||||
|
)
|
||||||
|
.bind(contact_id)
|
||||||
|
.bind(avatar.publication_counter)
|
||||||
|
.bind(webp)
|
||||||
|
.bind(sha256)
|
||||||
|
.bind(width.map(i64::from))
|
||||||
|
.bind(height.map(i64::from))
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
// Notify existing contact streams without adding protocol fields to Drift.
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE contacts SET sender_profile_counter = sender_profile_counter WHERE user_id = ?",
|
||||||
|
)
|
||||||
|
.bind(contact_id)
|
||||||
|
.execute(&mut **t)
|
||||||
|
.await?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuilds an avatar response immediately before encryption. A photo queued
|
||||||
|
/// while a contact was authorized can therefore become an SVG-only withdrawal
|
||||||
|
/// if acceptance or audience changed before transmission.
|
||||||
|
pub(crate) async fn refresh_queued_response(
|
||||||
|
ctx: &Context,
|
||||||
|
contact_id: i64,
|
||||||
|
bytes: Vec<u8>,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let mut content = crate::api::proto::client::EncryptedContent::decode(bytes.as_slice())?;
|
||||||
|
let is_avatar_response = content
|
||||||
|
.contact_update
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|update| update.custom_avatar.is_some());
|
||||||
|
if !is_avatar_response {
|
||||||
|
return Ok(bytes);
|
||||||
|
}
|
||||||
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
let mut transaction = database.pool.begin().await?;
|
||||||
|
let avatar = publication_payload(&mut transaction, contact_id).await?;
|
||||||
|
content.sender_custom_avatar_counter = Some(avatar.publication_counter);
|
||||||
|
content.custom_avatar_protocol_version = Some(CUSTOM_AVATAR_PROTOCOL_VERSION);
|
||||||
|
if let Some(update) = content.contact_update.as_mut() {
|
||||||
|
update.custom_avatar = Some(avatar);
|
||||||
|
}
|
||||||
|
transaction.commit().await?;
|
||||||
|
Ok(content.encode_to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_custom_webp(webp: &[u8], digest: &[u8], width: u32, height: u32) -> Result<()> {
|
||||||
|
if webp.is_empty()
|
||||||
|
|| webp.len() > stickers::MAX_STICKER_BYTES
|
||||||
|
|| digest.len() != 32
|
||||||
|
|| Sha256::digest(webp).as_slice() != digest
|
||||||
|
|| width != 300
|
||||||
|
|| height != 300
|
||||||
|
|| image::guess_format(webp).ok() != Some(image::ImageFormat::WebP)
|
||||||
|
{
|
||||||
|
return Err(TwonlyError::Generic("invalid custom avatar payload".into()));
|
||||||
|
}
|
||||||
|
let decoder = image::codecs::webp::WebPDecoder::new(Cursor::new(webp))
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("invalid custom avatar WebP: {error}")))?;
|
||||||
|
if decoder.dimensions() != (width, height) || decoder.has_animation() {
|
||||||
|
return Err(TwonlyError::Generic(
|
||||||
|
"invalid or animated custom avatar WebP".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let decoded = DynamicImage::from_decoder(decoder)
|
||||||
|
.map_err(|error| TwonlyError::Generic(format!("invalid custom avatar WebP: {error}")))?;
|
||||||
|
if !decoded.color().has_alpha() || !decoded.to_rgba8().pixels().any(|pixel| pixel[3] > 0) {
|
||||||
|
return Err(TwonlyError::Generic(
|
||||||
|
"invalid custom avatar dimensions".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_svg_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
|
fn decode_svg_bytes(bytes: &[u8]) -> Result<Vec<u8>> {
|
||||||
if bytes.starts_with(&[0x1f, 0x8b]) {
|
if bytes.starts_with(&[0x1f, 0x8b]) {
|
||||||
let mut decoded = Vec::new();
|
let mut decoded = Vec::new();
|
||||||
|
|
@ -85,6 +454,22 @@ pub(crate) async fn ensure_contact_avatar_png(
|
||||||
contact_id: i64,
|
contact_id: i64,
|
||||||
) -> Result<Option<PathBuf>> {
|
) -> Result<Option<PathBuf>> {
|
||||||
let database = ctx.app_db.read().await.clone();
|
let database = ctx.app_db.read().await.clone();
|
||||||
|
if let Some(custom) = sqlx::query(
|
||||||
|
"SELECT applied_counter, webp FROM received_custom_avatars \
|
||||||
|
WHERE contact_id = ? AND webp IS NOT NULL",
|
||||||
|
)
|
||||||
|
.bind(contact_id)
|
||||||
|
.fetch_optional(&database.pool)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
let counter: i64 = custom.get("applied_counter");
|
||||||
|
let webp: Vec<u8> = custom.get("webp");
|
||||||
|
let output = Path::new(&ctx.config.data_dir)
|
||||||
|
.join("avatars")
|
||||||
|
.join(format!("contact_{contact_id}_custom_{counter}.webp"));
|
||||||
|
write_cached_bytes(&output, &webp)?;
|
||||||
|
return Ok(Some(output));
|
||||||
|
}
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!(
|
||||||
r#"SELECT avatar_svg_compressed, sender_profile_counter FROM contacts WHERE user_id = ?"#,
|
r#"SELECT avatar_svg_compressed, sender_profile_counter FROM contacts WHERE user_id = ?"#,
|
||||||
contact_id,
|
contact_id,
|
||||||
|
|
@ -115,6 +500,35 @@ pub(crate) async fn ensure_contact_avatar_png(
|
||||||
Ok(Some(output))
|
Ok(Some(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn current_custom_avatar_path(ctx: &Context) -> Result<Option<PathBuf>> {
|
||||||
|
let avatar = own_custom_avatar(ctx).await?;
|
||||||
|
let Some(webp) = avatar.webp else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let output = Path::new(&ctx.config.data_dir)
|
||||||
|
.join("avatars")
|
||||||
|
.join(format!("user_custom_{}.webp", avatar.source_revision));
|
||||||
|
write_cached_bytes(&output, &webp)?;
|
||||||
|
Ok(Some(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_cached_bytes(output: &Path, bytes: &[u8]) -> Result<()> {
|
||||||
|
let _guard = AVATAR_RENDER_LOCK
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| TwonlyError::Generic("avatar render lock was poisoned".into()))?;
|
||||||
|
if output.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let directory = output
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| TwonlyError::Generic("avatar output has no parent directory".into()))?;
|
||||||
|
std::fs::create_dir_all(directory)?;
|
||||||
|
let temporary = output.with_extension("tmp");
|
||||||
|
std::fs::write(&temporary, bytes)?;
|
||||||
|
std::fs::rename(temporary, output)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn notification_avatar_path(
|
pub(crate) fn notification_avatar_path(
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
sender_id: i64,
|
sender_id: i64,
|
||||||
|
|
@ -281,4 +695,113 @@ mod tests {
|
||||||
let image = image::load_from_memory(&png).unwrap();
|
let image = image::load_from_memory(&png).unwrap();
|
||||||
assert_eq!((image.width(), image.height()), (270, 300));
|
assert_eq!((image.width(), image.height()), (270, 300));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn publication_counter_only_changes_when_the_recipient_view_changes() {
|
||||||
|
let temporary_directory = tempfile::tempdir().unwrap();
|
||||||
|
let context = Context::init_for_testing(
|
||||||
|
temporary_directory.path().join("database"),
|
||||||
|
temporary_directory.path().join("data"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let database = context.app_db.read().await.clone();
|
||||||
|
sqlx::query("INSERT INTO contacts(user_id, username) VALUES (7, 'peer')")
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
sqlx::query(
|
||||||
|
"UPDATE own_custom_avatar SET webp = x'01', sha256 = zeroblob(32), width = 300, \
|
||||||
|
height = 300, source_revision = 1, accepted_contacts_only = 1 WHERE singleton = 1",
|
||||||
|
)
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut transaction = database.pool.begin().await.unwrap();
|
||||||
|
assert_eq!(prepare_publication(&mut transaction, 7).await.unwrap(), 0);
|
||||||
|
transaction.commit().await.unwrap();
|
||||||
|
|
||||||
|
// A new private photo is invisible to an unaccepted peer.
|
||||||
|
sqlx::query("UPDATE own_custom_avatar SET source_revision = 2 WHERE singleton = 1")
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut transaction = database.pool.begin().await.unwrap();
|
||||||
|
assert_eq!(prepare_publication(&mut transaction, 7).await.unwrap(), 0);
|
||||||
|
transaction.commit().await.unwrap();
|
||||||
|
|
||||||
|
sqlx::query("UPDATE contacts SET accepted = 1 WHERE user_id = 7")
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut transaction = database.pool.begin().await.unwrap();
|
||||||
|
assert_eq!(prepare_publication(&mut transaction, 7).await.unwrap(), 1);
|
||||||
|
transaction.commit().await.unwrap();
|
||||||
|
|
||||||
|
// Revoking acceptance creates a versioned SVG-only withdrawal.
|
||||||
|
sqlx::query("UPDATE contacts SET accepted = 0 WHERE user_id = 7")
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let mut transaction = database.pool.begin().await.unwrap();
|
||||||
|
assert_eq!(prepare_publication(&mut transaction, 7).await.unwrap(), 2);
|
||||||
|
let payload = publication_payload(&mut transaction, 7).await.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
encrypted_content::custom_avatar::State::try_from(payload.state).unwrap(),
|
||||||
|
encrypted_content::custom_avatar::State::SvgOnly
|
||||||
|
);
|
||||||
|
transaction.commit().await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn received_avatar_is_validated_and_ordered_by_publication_counter() {
|
||||||
|
let temporary_directory = tempfile::tempdir().unwrap();
|
||||||
|
let context = Context::init_for_testing(
|
||||||
|
temporary_directory.path().join("database"),
|
||||||
|
temporary_directory.path().join("data"),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let database = context.app_db.read().await.clone();
|
||||||
|
sqlx::query("INSERT INTO contacts(user_id, username) VALUES (9, 'peer')")
|
||||||
|
.execute(&database.pool)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let image = DynamicImage::ImageRgba8(RgbaImage::from_fn(300, 300, |x, y| {
|
||||||
|
if x < 4 || y < 4 {
|
||||||
|
Rgba([0, 0, 0, 0])
|
||||||
|
} else {
|
||||||
|
Rgba([10, 20, 30, 255])
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
let webp = media_codec::encode_webp(&image, 80.0).unwrap();
|
||||||
|
let digest = Sha256::digest(&webp).to_vec();
|
||||||
|
let photo = encrypted_content::CustomAvatar {
|
||||||
|
version: CUSTOM_AVATAR_PROTOCOL_VERSION,
|
||||||
|
publication_counter: 2,
|
||||||
|
state: encrypted_content::custom_avatar::State::Photo as i32,
|
||||||
|
webp: Some(webp),
|
||||||
|
sha256: Some(digest),
|
||||||
|
width: Some(300),
|
||||||
|
height: Some(300),
|
||||||
|
};
|
||||||
|
let mut transaction = database.pool.begin().await.unwrap();
|
||||||
|
assert!(apply_received(&mut transaction, 9, photo).await.unwrap());
|
||||||
|
transaction.commit().await.unwrap();
|
||||||
|
|
||||||
|
let stale = encrypted_content::CustomAvatar {
|
||||||
|
version: CUSTOM_AVATAR_PROTOCOL_VERSION,
|
||||||
|
publication_counter: 1,
|
||||||
|
state: encrypted_content::custom_avatar::State::SvgOnly as i32,
|
||||||
|
webp: None,
|
||||||
|
sha256: None,
|
||||||
|
width: None,
|
||||||
|
height: None,
|
||||||
|
};
|
||||||
|
let mut transaction = database.pool.begin().await.unwrap();
|
||||||
|
assert!(!apply_received(&mut transaction, 9, stale).await.unwrap());
|
||||||
|
assert_eq!(applied_counter(&mut transaction, 9).await.unwrap(), 2);
|
||||||
|
transaction.commit().await.unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,8 @@ impl ContactService {
|
||||||
username: Some(config.username),
|
username: Some(config.username),
|
||||||
display_name: Some(config.display_name),
|
display_name: Some(config.display_name),
|
||||||
avatar_svg_compressed,
|
avatar_svg_compressed,
|
||||||
|
custom_avatar_requested: None,
|
||||||
|
custom_avatar: None,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -571,7 +571,6 @@ pub async fn pending_batch(ctx: &Arc<Context>, locale: &str) -> Result<Notificat
|
||||||
r#"
|
r#"
|
||||||
SELECT n.event_id, n.notification_id, n.conversation_id, n.sender_id,
|
SELECT n.event_id, n.notification_id, n.conversation_id, n.sender_id,
|
||||||
COALESCE(c.display_name, c.username) AS "sender_name!: String",
|
COALESCE(c.display_name, c.username) AS "sender_name!: String",
|
||||||
c.avatar_svg_compressed, c.sender_profile_counter,
|
|
||||||
g.group_name AS conversation_name,
|
g.group_name AS conversation_name,
|
||||||
COALESCE(g.is_direct_chat, 0) AS "is_direct_chat!: i64",
|
COALESCE(g.is_direct_chat, 0) AS "is_direct_chat!: i64",
|
||||||
n.message_id, n.kind, n.content, n.created_at,
|
n.message_id, n.kind, n.content, n.created_at,
|
||||||
|
|
@ -602,12 +601,8 @@ pub async fn pending_batch(ctx: &Arc<Context>, locale: &str) -> Result<Notificat
|
||||||
let title = row.sender_name.clone();
|
let title = row.sender_name.clone();
|
||||||
let body = localized_body(locale, &row);
|
let body = localized_body(locale, &row);
|
||||||
let is_group = row.conversation_id.is_some() && row.is_direct_chat == 0;
|
let is_group = row.conversation_id.is_some() && row.is_direct_chat == 0;
|
||||||
let avatar_path = match notification_avatar_path(
|
let avatar_path =
|
||||||
ctx,
|
match crate::services::avatars::ensure_contact_avatar_png(ctx, row.sender_id).await {
|
||||||
row.sender_id,
|
|
||||||
row.sender_profile_counter,
|
|
||||||
row.avatar_svg_compressed.as_deref(),
|
|
||||||
) {
|
|
||||||
Ok(path) => path.map(|path| path.display().to_string()),
|
Ok(path) => path.map(|path| path.display().to_string()),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|
@ -873,8 +868,6 @@ struct PendingRow {
|
||||||
conversation_id: Option<String>,
|
conversation_id: Option<String>,
|
||||||
sender_id: i64,
|
sender_id: i64,
|
||||||
sender_name: String,
|
sender_name: String,
|
||||||
avatar_svg_compressed: Option<Vec<u8>>,
|
|
||||||
sender_profile_counter: i64,
|
|
||||||
conversation_name: Option<String>,
|
conversation_name: Option<String>,
|
||||||
is_direct_chat: i64,
|
is_direct_chat: i64,
|
||||||
message_id: Option<String>,
|
message_id: Option<String>,
|
||||||
|
|
@ -1091,8 +1084,6 @@ mod tests {
|
||||||
conversation_id: None,
|
conversation_id: None,
|
||||||
sender_id: 7,
|
sender_id: 7,
|
||||||
sender_name: "Alice".into(),
|
sender_name: "Alice".into(),
|
||||||
avatar_svg_compressed: None,
|
|
||||||
sender_profile_counter: 0,
|
|
||||||
conversation_name: None,
|
conversation_name: None,
|
||||||
is_direct_chat: 1,
|
is_direct_chat: 1,
|
||||||
message_id: None,
|
message_id: None,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:avatar_maker/avatar_maker.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('detaches from an external controller when disposed', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final controller = NonPersistentAvatarMakerController();
|
||||||
|
addTearDown(controller.dispose);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: AvatarMakerCustomizer(
|
||||||
|
controller: controller,
|
||||||
|
scaffoldHeight: 500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const MaterialApp(home: SizedBox()));
|
||||||
|
controller.updatePreview(newAvatarMakerSVG: '<svg></svg>');
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue