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 '../frb_generated.dart';
|
||||
import '../services/avatars.dart';
|
||||
import '../services/media_upload.dart';
|
||||
import '../services/stickers.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
|
|
@ -353,6 +354,14 @@ class RustApi {
|
|||
static Future<ApiConnectionState> connectionState() =>
|
||||
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
|
||||
/// inference and image encoding are CPU-heavy, so neither runs on Tokio's
|
||||
/// async worker threads.
|
||||
|
|
@ -426,6 +435,9 @@ class RustApi {
|
|||
static Future<void> forceIpaCheck() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiForceIpaCheck();
|
||||
|
||||
static Future<CustomAvatarInfo> getCustomAvatar() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiGetCustomAvatar();
|
||||
|
||||
static Future<String> getMemoriesUrl({
|
||||
required String mediaId,
|
||||
required bool thumbnail,
|
||||
|
|
@ -667,6 +679,9 @@ class RustApi {
|
|||
userId: userId,
|
||||
);
|
||||
|
||||
static Future<CustomAvatarInfo> removeCustomAvatar() =>
|
||||
RustLib.instance.api.crateBridgeApiRustApiRemoveCustomAvatar();
|
||||
|
||||
/// Deletes every file of a media item while keeping its row.
|
||||
static Future<void> removeMediaFiles({required String mediaId}) => RustLib
|
||||
.instance
|
||||
|
|
@ -800,6 +815,12 @@ class RustApi {
|
|||
.api
|
||||
.crateBridgeApiRustApiSetBackground(inBackground: inBackground);
|
||||
|
||||
static Future<CustomAvatarInfo> setCustomAvatarAudience({
|
||||
required bool acceptedContactsOnly,
|
||||
}) => RustLib.instance.api.crateBridgeApiRustApiSetCustomAvatarAudience(
|
||||
acceptedContactsOnly: acceptedContactsOnly,
|
||||
);
|
||||
|
||||
static Future<void> setHomeWidgetGroups({
|
||||
required String widgetId,
|
||||
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 'lib.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||
import 'services/avatars.dart';
|
||||
import 'services/media_upload.dart';
|
||||
import 'services/stickers.dart';
|
||||
import 'signal/engine.dart';
|
||||
|
|
@ -137,6 +138,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
CustomAvatarInfo dco_decode_custom_avatar_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw);
|
||||
|
||||
|
|
@ -559,6 +563,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
CustomAvatarInfo sse_decode_custom_avatar_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -1078,6 +1085,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_custom_avatar_info(
|
||||
CustomAvatarInfo self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_64(double self, SseSerializer serializer);
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import 'frb_generated.dart';
|
|||
import 'keys/backup_password_keys.dart';
|
||||
import 'lib.dart';
|
||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_web.dart';
|
||||
import 'services/avatars.dart';
|
||||
import 'services/media_upload.dart';
|
||||
import 'services/stickers.dart';
|
||||
import 'signal/engine.dart';
|
||||
|
|
@ -139,6 +140,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
@protected
|
||||
WebxdcInstanceInfo dco_decode_box_autoadd_webxdc_instance_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
CustomAvatarInfo dco_decode_custom_avatar_info(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_64(dynamic raw);
|
||||
|
||||
|
|
@ -561,6 +565,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
CustomAvatarInfo sse_decode_custom_avatar_info(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_64(SseDeserializer deserializer);
|
||||
|
||||
|
|
@ -1080,6 +1087,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
|||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_custom_avatar_info(
|
||||
CustomAvatarInfo self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
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 backupRecoveryState = 'backup_recovery_state';
|
||||
static const String onboardingState = 'onboarding_state';
|
||||
static const String shareImageWidgetExplainer =
|
||||
'share_image_widget_explainer';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2440,6 +2440,78 @@ abstract class AppLocalizations {
|
|||
/// **'Discard'**
|
||||
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.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -1398,6 +1398,43 @@ class AppLocalizationsAr extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get inProcess => 'قيد المعالجة';
|
||||
|
||||
|
|
|
|||
|
|
@ -1387,6 +1387,44 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get inProcess => 'Wird verarbeitet';
|
||||
|
||||
|
|
|
|||
|
|
@ -1379,6 +1379,44 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
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
|
||||
String get inProcess => 'In process';
|
||||
|
||||
|
|
|
|||
|
|
@ -390,6 +390,18 @@
|
|||
"avatarSaveChanges": "هل تريد حفظ التغييرات؟",
|
||||
"avatarSaveChangesStore": "حفظ",
|
||||
"avatarSaveChangesDiscard": "تجاهل",
|
||||
"customAvatarSvgTab": "الصورة الرمزية العامة",
|
||||
"customAvatarCustomize": "تخصيص",
|
||||
"customAvatarPhotoTab": "الصورة الرمزية الشخصية",
|
||||
"customAvatarExplanation": "تحل محل صورتك الرمزية العامة لدى الجمهور المحدد.",
|
||||
"customAvatarCreate": "اختيار صورة",
|
||||
"customAvatarReplace": "تغيير الصورة",
|
||||
"customAvatarRemove": "إزالة",
|
||||
"customAvatarAudience": "من يمكنه رؤية الصورة الرمزية؟",
|
||||
"customAvatarAcceptedOnly": "جهات الاتصال فقط",
|
||||
"customAvatarEveryone": "الجميع",
|
||||
"customAvatarChooseSource": "اختر صورة رمزية",
|
||||
"customAvatarCreateFailed": "تعذر إنشاء الصورة الرمزية.",
|
||||
"inProcess": "قيد المعالجة",
|
||||
"draftMessage": "مسودة",
|
||||
"voiceMessageSlideToCancel": "اسحب للإلغاء",
|
||||
|
|
|
|||
|
|
@ -455,6 +455,18 @@
|
|||
"avatarSaveChanges": "Möchtest du die Änderungen speichern?",
|
||||
"avatarSaveChangesStore": "Speichern",
|
||||
"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",
|
||||
"draftMessage": "Entwurf",
|
||||
"voiceMessageSlideToCancel": "Zum Abbrechen ziehen",
|
||||
|
|
|
|||
|
|
@ -465,6 +465,18 @@
|
|||
"avatarSaveChanges": "Would you like to save the changes?",
|
||||
"avatarSaveChangesStore": "Save",
|
||||
"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",
|
||||
"draftMessage": "Draft",
|
||||
"voiceMessageSlideToCancel": "Slide to cancel",
|
||||
|
|
|
|||
|
|
@ -1174,6 +1174,8 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
|||
$core.List<$core.int>? avatarSvgCompressed,
|
||||
$core.String? username,
|
||||
$core.String? displayName,
|
||||
$core.bool? customAvatarRequested,
|
||||
EncryptedContent_CustomAvatar? customAvatar,
|
||||
}) {
|
||||
final result = create();
|
||||
if (type != null) result.type = type;
|
||||
|
|
@ -1181,6 +1183,9 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
|||
result.avatarSvgCompressed = avatarSvgCompressed;
|
||||
if (username != null) result.username = username;
|
||||
if (displayName != null) result.displayName = displayName;
|
||||
if (customAvatarRequested != null)
|
||||
result.customAvatarRequested = customAvatarRequested;
|
||||
if (customAvatar != null) result.customAvatar = customAvatar;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1202,6 +1207,10 @@ class EncryptedContent_ContactUpdate extends $pb.GeneratedMessage {
|
|||
2, _omitFieldNames ? '' : 'avatarSvgCompressed', $pb.PbFieldType.OY)
|
||||
..aOS(3, _omitFieldNames ? '' : 'username')
|
||||
..aOS(4, _omitFieldNames ? '' : 'displayName')
|
||||
..aOB(5, _omitFieldNames ? '' : 'customAvatarRequested')
|
||||
..aOM<EncryptedContent_CustomAvatar>(
|
||||
6, _omitFieldNames ? '' : 'customAvatar',
|
||||
subBuilder: EncryptedContent_CustomAvatar.create)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$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);
|
||||
@$pb.TagNumber(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 {
|
||||
|
|
@ -1775,6 +1938,8 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
$core.bool? widgetSharingAllowed,
|
||||
EncryptedContent_Story? story,
|
||||
$fixnum.Int64? senderTwonlyScore,
|
||||
$fixnum.Int64? senderCustomAvatarCounter,
|
||||
$core.int? customAvatarProtocolVersion,
|
||||
}) {
|
||||
final result = create();
|
||||
if (groupId != null) result.groupId = groupId;
|
||||
|
|
@ -1816,6 +1981,10 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
result.widgetSharingAllowed = widgetSharingAllowed;
|
||||
if (story != null) result.story = story;
|
||||
if (senderTwonlyScore != null) result.senderTwonlyScore = senderTwonlyScore;
|
||||
if (senderCustomAvatarCounter != null)
|
||||
result.senderCustomAvatarCounter = senderCustomAvatarCounter;
|
||||
if (customAvatarProtocolVersion != null)
|
||||
result.customAvatarProtocolVersion = customAvatarProtocolVersion;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1896,6 +2065,9 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
..aOM<EncryptedContent_Story>(30, _omitFieldNames ? '' : 'story',
|
||||
subBuilder: EncryptedContent_Story.create)
|
||||
..aInt64(31, _omitFieldNames ? '' : 'senderTwonlyScore')
|
||||
..aInt64(32, _omitFieldNames ? '' : 'senderCustomAvatarCounter')
|
||||
..aI(33, _omitFieldNames ? '' : 'customAvatarProtocolVersion',
|
||||
fieldType: $pb.PbFieldType.OU3)
|
||||
..hasRequiredFields = false;
|
||||
|
||||
@$core.Deprecated('See https://github.com/google/protobuf.dart/issues/998.')
|
||||
|
|
@ -2226,6 +2398,7 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
@$pb.TagNumber(30)
|
||||
EncryptedContent_Story ensureStory() => $_ensure(26);
|
||||
|
||||
/// Sum of all exchanged media, omitted when profile sharing is disabled.
|
||||
@$pb.TagNumber(31)
|
||||
$fixnum.Int64 get senderTwonlyScore => $_getI64(27);
|
||||
@$pb.TagNumber(31)
|
||||
|
|
@ -2234,6 +2407,27 @@ class EncryptedContent extends $pb.GeneratedMessage {
|
|||
$core.bool hasSenderTwonlyScore() => $_has(27);
|
||||
@$pb.TagNumber(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 =
|
||||
|
|
|
|||
|
|
@ -169,5 +169,26 @@ class EncryptedContent_ContactUpdate_Type extends $pb.ProtobufEnum {
|
|||
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 =
|
||||
$core.bool.fromEnvironment('protobuf.omit_enum_names');
|
||||
|
|
|
|||
|
|
@ -82,13 +82,31 @@ const EncryptedContent$json = {
|
|||
'10': 'senderTwonlyScore',
|
||||
'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',
|
||||
'3': 5,
|
||||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.MessageUpdate',
|
||||
'9': 7,
|
||||
'9': 9,
|
||||
'10': 'messageUpdate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -98,7 +116,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.Media',
|
||||
'9': 8,
|
||||
'9': 10,
|
||||
'10': 'media',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -108,7 +126,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.MediaUpdate',
|
||||
'9': 9,
|
||||
'9': 11,
|
||||
'10': 'mediaUpdate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -118,7 +136,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.ContactUpdate',
|
||||
'9': 10,
|
||||
'9': 12,
|
||||
'10': 'contactUpdate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -128,7 +146,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.ContactRequest',
|
||||
'9': 11,
|
||||
'9': 13,
|
||||
'10': 'contactRequest',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -138,7 +156,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.FlameSync',
|
||||
'9': 12,
|
||||
'9': 14,
|
||||
'10': 'flameSync',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -148,7 +166,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.Reaction',
|
||||
'9': 13,
|
||||
'9': 15,
|
||||
'10': 'reaction',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -158,7 +176,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.TextMessage',
|
||||
'9': 14,
|
||||
'9': 16,
|
||||
'10': 'textMessage',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -168,7 +186,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.GroupCreate',
|
||||
'9': 15,
|
||||
'9': 17,
|
||||
'10': 'groupCreate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -178,7 +196,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.GroupJoin',
|
||||
'9': 16,
|
||||
'9': 18,
|
||||
'10': 'groupJoin',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -188,7 +206,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.GroupUpdate',
|
||||
'9': 17,
|
||||
'9': 19,
|
||||
'10': 'groupUpdate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -198,7 +216,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.ResendGroupPublicKey',
|
||||
'9': 18,
|
||||
'9': 20,
|
||||
'10': 'resendGroupPublicKey',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -208,7 +226,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.ErrorMessages',
|
||||
'9': 19,
|
||||
'9': 21,
|
||||
'10': 'errorMessages',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -218,7 +236,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.AdditionalDataMessage',
|
||||
'9': 20,
|
||||
'9': 22,
|
||||
'10': 'additionalDataMessage',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -228,7 +246,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.TypingIndicator',
|
||||
'9': 21,
|
||||
'9': 23,
|
||||
'10': 'typingIndicator',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -238,7 +256,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.UserDiscoveryRequest',
|
||||
'9': 22,
|
||||
'9': 24,
|
||||
'10': 'userDiscoveryRequest',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -248,7 +266,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.UserDiscoveryUpdate',
|
||||
'9': 23,
|
||||
'9': 25,
|
||||
'10': 'userDiscoveryUpdate',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -258,7 +276,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.KeyVerificationProof',
|
||||
'9': 24,
|
||||
'9': 26,
|
||||
'10': 'keyVerificationProof',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -268,7 +286,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.PasswordLessRecovery',
|
||||
'9': 25,
|
||||
'9': 27,
|
||||
'10': 'passwordlessRecovery',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -278,7 +296,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.PasswordLessRecoveryHeartbeat',
|
||||
'9': 26,
|
||||
'9': 28,
|
||||
'10': 'passwordlessRecoveryHeartbeat',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -288,7 +306,7 @@ const EncryptedContent$json = {
|
|||
'4': 1,
|
||||
'5': 11,
|
||||
'6': '.EncryptedContent.Story',
|
||||
'9': 27,
|
||||
'9': 29,
|
||||
'10': 'story',
|
||||
'17': true
|
||||
},
|
||||
|
|
@ -308,6 +326,7 @@ const EncryptedContent$json = {
|
|||
EncryptedContent_MediaUpdate$json,
|
||||
EncryptedContent_ContactRequest$json,
|
||||
EncryptedContent_ContactUpdate$json,
|
||||
EncryptedContent_CustomAvatar$json,
|
||||
EncryptedContent_FlameSync$json,
|
||||
EncryptedContent_TypingIndicator$json,
|
||||
EncryptedContent_UserDiscoveryRequest$json,
|
||||
|
|
@ -324,6 +343,8 @@ const EncryptedContent$json = {
|
|||
{'1': '_ask_for_friend_promotions'},
|
||||
{'1': '_widget_sharing_allowed'},
|
||||
{'1': '_sender_twonly_score'},
|
||||
{'1': '_sender_custom_avatar_counter'},
|
||||
{'1': '_custom_avatar_protocol_version'},
|
||||
{'1': '_message_update'},
|
||||
{'1': '_media'},
|
||||
{'1': '_media_update'},
|
||||
|
|
@ -799,12 +820,33 @@ const EncryptedContent_ContactUpdate$json = {
|
|||
'10': 'displayName',
|
||||
'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],
|
||||
'8': [
|
||||
{'1': '_avatar_svg_compressed'},
|
||||
{'1': '_username'},
|
||||
{'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')
|
||||
const EncryptedContent_FlameSync$json = {
|
||||
'1': 'FlameSync',
|
||||
|
|
@ -905,115 +1006,129 @@ final $typed_data.Uint8List encryptedContentDescriptor = $convert.base64Decode(
|
|||
'AQESPgoZYXNrX2Zvcl9mcmllbmRfcHJvbW90aW9ucxgZIAEoCEgEUhZhc2tGb3JGcmllbmRQcm'
|
||||
'9tb3Rpb25ziAEBEjkKFndpZGdldF9zaGFyaW5nX2FsbG93ZWQYHSABKAhIBVIUd2lkZ2V0U2hh'
|
||||
'cmluZ0FsbG93ZWSIAQESMwoTc2VuZGVyX3R3b25seV9zY29yZRgfIAEoA0gGUhFzZW5kZXJUd2'
|
||||
'9ubHlTY29yZYgBARJLCg5tZXNzYWdlX3VwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQu'
|
||||
'TWVzc2FnZVVwZGF0ZUgHUg1tZXNzYWdlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcn'
|
||||
'lwdGVkQ29udGVudC5NZWRpYUgIUgVtZWRpYYgBARJFCgxtZWRpYV91cGRhdGUYByABKAsyHS5F'
|
||||
'bmNyeXB0ZWRDb250ZW50Lk1lZGlhVXBkYXRlSAlSC21lZGlhVXBkYXRliAEBEksKDmNvbnRhY3'
|
||||
'RfdXBkYXRlGAggASgLMh8uRW5jcnlwdGVkQ29udGVudC5Db250YWN0VXBkYXRlSApSDWNvbnRh'
|
||||
'Y3RVcGRhdGWIAQESTgoPY29udGFjdF9yZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC'
|
||||
'5Db250YWN0UmVxdWVzdEgLUg5jb250YWN0UmVxdWVzdIgBARI/CgpmbGFtZV9zeW5jGAogASgL'
|
||||
'MhsuRW5jcnlwdGVkQ29udGVudC5GbGFtZVN5bmNIDFIJZmxhbWVTeW5jiAEBEjsKCHJlYWN0aW'
|
||||
'9uGAwgASgLMhouRW5jcnlwdGVkQ29udGVudC5SZWFjdGlvbkgNUghyZWFjdGlvbogBARJFCgx0'
|
||||
'ZXh0X21lc3NhZ2UYDSABKAsyHS5FbmNyeXB0ZWRDb250ZW50LlRleHRNZXNzYWdlSA5SC3RleH'
|
||||
'RNZXNzYWdliAEBEkUKDGdyb3VwX2NyZWF0ZRgOIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuR3Jv'
|
||||
'dXBDcmVhdGVID1ILZ3JvdXBDcmVhdGWIAQESPwoKZ3JvdXBfam9pbhgPIAEoCzIbLkVuY3J5cH'
|
||||
'RlZENvbnRlbnQuR3JvdXBKb2luSBBSCWdyb3VwSm9pbogBARJFCgxncm91cF91cGRhdGUYECAB'
|
||||
'KAsyHS5FbmNyeXB0ZWRDb250ZW50Lkdyb3VwVXBkYXRlSBFSC2dyb3VwVXBkYXRliAEBEmIKF3'
|
||||
'Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5GBEgASgLMiYuRW5jcnlwdGVkQ29udGVudC5SZXNlbmRH'
|
||||
'cm91cFB1YmxpY0tleUgSUhRyZXNlbmRHcm91cFB1YmxpY0tleYgBARJLCg5lcnJvcl9tZXNzYW'
|
||||
'dlcxgSIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlc0gTUg1lcnJvck1lc3Nh'
|
||||
'Z2VziAEBEmQKF2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlGBMgASgLMicuRW5jcnlwdGVkQ29udG'
|
||||
'VudC5BZGRpdGlvbmFsRGF0YU1lc3NhZ2VIFFIVYWRkaXRpb25hbERhdGFNZXNzYWdliAEBElEK'
|
||||
'EHR5cGluZ19pbmRpY2F0b3IYFCABKAsyIS5FbmNyeXB0ZWRDb250ZW50LlR5cGluZ0luZGljYX'
|
||||
'RvckgVUg90eXBpbmdJbmRpY2F0b3KIAQESYQoWdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdBgWIAEo'
|
||||
'CzImLkVuY3J5cHRlZENvbnRlbnQuVXNlckRpc2NvdmVyeVJlcXVlc3RIFlIUdXNlckRpc2Nvdm'
|
||||
'VyeVJlcXVlc3SIAQESXgoVdXNlcl9kaXNjb3ZlcnlfdXBkYXRlGBcgASgLMiUuRW5jcnlwdGVk'
|
||||
'Q29udGVudC5Vc2VyRGlzY292ZXJ5VXBkYXRlSBdSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQESYQ'
|
||||
'oWa2V5X3ZlcmlmaWNhdGlvbl9wcm9vZhgYIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuS2V5VmVy'
|
||||
'aWZpY2F0aW9uUHJvb2ZIGFIUa2V5VmVyaWZpY2F0aW9uUHJvb2aIAQESYAoVcGFzc3dvcmRsZX'
|
||||
'NzX3JlY292ZXJ5GBogASgLMiYuRW5jcnlwdGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVy'
|
||||
'eUgZUhRwYXNzd29yZGxlc3NSZWNvdmVyeYgBARJ8Ch9wYXNzd29yZGxlc3NfcmVjb3ZlcnlfaG'
|
||||
'VhcnRiZWF0GBsgASgLMi8uRW5jcnlwdGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVyeUhl'
|
||||
'YXJ0YmVhdEgaUh1wYXNzd29yZGxlc3NSZWNvdmVyeUhlYXJ0YmVhdIgBARIyCgVzdG9yeRgeIA'
|
||||
'EoCzIXLkVuY3J5cHRlZENvbnRlbnQuU3RvcnlIG1IFc3RvcnmIAQEalgIKDUVycm9yTWVzc2Fn'
|
||||
'ZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeXBlUg'
|
||||
'R0eXBlEiwKEnJlbGF0ZWRfcmVjZWlwdF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCKcAQoE'
|
||||
'VHlwZRI8CjhFUlJPUl9QUk9DRVNTSU5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1'
|
||||
'RfSU5TVEVBRBAAEhgKFFVOS05PV05fTUVTU0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0Zf'
|
||||
'U1lOQxADEiMKH0dST1VQX05PVF9GT1VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQoLR3JvdXBDcm'
|
||||
'VhdGUSGwoJc3RhdGVfa2V5GAMgASgMUghzdGF0ZUtleRIoChBncm91cF9wdWJsaWNfa2V5GAQg'
|
||||
'ASgMUg5ncm91cFB1YmxpY0tleRIiCgpncm91cF9uYW1lGAUgASgJSABSCWdyb3VwTmFtZYgBAU'
|
||||
'INCgtfZ3JvdXBfbmFtZRo1CglHcm91cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tleRgBIAEoDFIO'
|
||||
'Z3JvdXBQdWJsaWNLZXkaFgoUUmVzZW5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3VwVXBkYXRlEi'
|
||||
'oKEWdyb3VwX2FjdGlvbl90eXBlGAEgASgJUg9ncm91cEFjdGlvblR5cGUSMwoTYWZmZWN0ZWRf'
|
||||
'Y29udGFjdF9pZBgCIAEoA0gAUhFhZmZlY3RlZENvbnRhY3RJZIgBARIpCg5uZXdfZ3JvdXBfbm'
|
||||
'FtZRgDIAEoCUgBUgxuZXdHcm91cE5hbWWIAQESVwombmV3X2RlbGV0ZV9tZXNzYWdlc19hZnRl'
|
||||
'cl9taWxsaXNlY29uZHMYBCABKANIAlIibmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb2'
|
||||
'5kc4gBAUIWChRfYWZmZWN0ZWRfY29udGFjdF9pZEIRCg9fbmV3X2dyb3VwX25hbWVCKQonX25l'
|
||||
'd19kZWxldGVfbWVzc2FnZXNfYWZ0ZXJfbWlsbGlzZWNvbmRzGogCCgtUZXh0TWVzc2FnZRIqCh'
|
||||
'FzZW5kZXJfbWVzc2FnZV9pZBgBIAEoCVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlS'
|
||||
'BHRleHQSHAoJdGltZXN0YW1wGAMgASgDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZB'
|
||||
'gEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJZIgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgF'
|
||||
'IAEoDEgBUhVhZGRpdGlvbmFsTWVzc2FnZURhdGGIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWRCGg'
|
||||
'oYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGuYBChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoR'
|
||||
'c2VuZGVyX21lc3NhZ2VfaWQYASABKAlSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAi'
|
||||
'ABKANSCXRpbWVzdGFtcBISCgR0eXBlGAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2Fn'
|
||||
'ZV9kYXRhGAQgASgMSABSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBARIWCgZoaWRkZW4YBSABKA'
|
||||
'hSBmhpZGRlbkIaChhfYWRkaXRpb25hbF9tZXNzYWdlX2RhdGEaZAoIUmVhY3Rpb24SKgoRdGFy'
|
||||
'Z2V0X21lc3NhZ2VfaWQYASABKAlSD3RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW'
|
||||
'1vamkSFgoGcmVtb3ZlGAMgASgIUgZyZW1vdmUavgIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgB'
|
||||
'IAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi8KEXNlbm'
|
||||
'Rlcl9tZXNzYWdlX2lkGAIgASgJSABSD3NlbmRlck1lc3NhZ2VJZIgBARI9ChttdWx0aXBsZV90'
|
||||
'YXJnZXRfbWVzc2FnZV9pZHMYAyADKAlSGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZX'
|
||||
'h0GAQgASgJSAFSBHRleHSIAQESHAoJdGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlw'
|
||||
'ZRIKCgZERUxFVEUQABINCglFRElUX1RFWFQQARIKCgZPUEVORUQQAkIUChJfc2VuZGVyX21lc3'
|
||||
'NhZ2VfaWRCBwoFX3RleHQauwYKBU1lZGlhEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg9z'
|
||||
'ZW5kZXJNZXNzYWdlSWQSMAoEdHlwZRgCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVH'
|
||||
'lwZVIEdHlwZRJGCh1kaXNwbGF5X2xpbWl0X2luX21pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNw'
|
||||
'bGF5TGltaXRJbk1pbGxpc2Vjb25kc4gBARI3ChdyZXF1aXJlc19hdXRoZW50aWNhdGlvbhgEIA'
|
||||
'EoCFIWcmVxdWlyZXNBdXRoZW50aWNhdGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFt'
|
||||
'cBItChBxdW90ZV9tZXNzYWdlX2lkGAYgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEioKDmRvd2'
|
||||
'5sb2FkX3Rva2VuGAcgASgMSAJSDWRvd25sb2FkVG9rZW6IAQESKgoOZW5jcnlwdGlvbl9rZXkY'
|
||||
'CCABKAxIA1INZW5jcnlwdGlvbktleYgBARIqCg5lbmNyeXB0aW9uX21hYxgJIAEoDEgEUg1lbm'
|
||||
'NyeXB0aW9uTWFjiAEBEi4KEGVuY3J5cHRpb25fbm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5v'
|
||||
'bmNliAEBEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZX'
|
||||
'NzYWdlRGF0YYgBARIkCgt3aWRnZXRfb25seRgMIAEoCEgHUgp3aWRnZXRPbmx5iAEBIj4KBFR5'
|
||||
'cGUSDAoIUkVVUExPQUQQABIJCgVJTUFHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU'
|
||||
'8QBEIgCh5fZGlzcGxheV9saW1pdF9pbl9taWxsaXNlY29uZHNCEwoRX3F1b3RlX21lc3NhZ2Vf'
|
||||
'aWRCEQoPX2Rvd25sb2FkX3Rva2VuQhEKD19lbmNyeXB0aW9uX2tleUIRCg9fZW5jcnlwdGlvbl'
|
||||
'9tYWNCEwoRX2VuY3J5cHRpb25fbm9uY2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhQg4K'
|
||||
'DF93aWRnZXRfb25seRpOCgVTdG9yeRItCgVtZWRpYRgBIAEoCzIXLkVuY3J5cHRlZENvbnRlbn'
|
||||
'QuTWVkaWFSBW1lZGlhEhYKBm5vdGlmeRgCIAEoCFIGbm90aWZ5GqkBCgtNZWRpYVVwZGF0ZRI2'
|
||||
'CgR0eXBlGAEgASgOMiIuRW5jcnlwdGVkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEi'
|
||||
'oKEXRhcmdldF9tZXNzYWdlX2lkGAIgASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghS'
|
||||
'RU9QRU5FRBAAEgoKBlNUT1JFRBABEhQKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0Um'
|
||||
'VxdWVzdBI5CgR0eXBlGAEgASgOMiUuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5U'
|
||||
'eXBlUgR0eXBlIisKBFR5cGUSCwoHUkVRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGq'
|
||||
'QCCg1Db250YWN0VXBkYXRlEjgKBHR5cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRh'
|
||||
'Y3RVcGRhdGUuVHlwZVIEdHlwZRI3ChVhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYX'
|
||||
'ZhdGFyU3ZnQ29tcHJlc3NlZIgBARIfCgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARIm'
|
||||
'CgxkaXNwbGF5X25hbWUYBCABKAlIAlILZGlzcGxheU5hbWWIAQEiHwoEVHlwZRILCgdSRVFVRV'
|
||||
'NUEAASCgoGVVBEQVRFEAFCGAoWX2F2YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbWVC'
|
||||
'DwoNX2Rpc3BsYXlfbmFtZRqvAQoJRmxhbWVTeW5jEiMKDWZsYW1lX2NvdW50ZXIYASABKANSDG'
|
||||
'ZsYW1lQ291bnRlchI5ChlsYXN0X2ZsYW1lX2NvdW50ZXJfY2hhbmdlGAIgASgDUhZsYXN0Rmxh'
|
||||
'bWVDb3VudGVyQ2hhbmdlEh8KC2Jlc3RfZnJpZW5kGAMgASgIUgpiZXN0RnJpZW5kEiEKDGZvcm'
|
||||
'NlX3VwZGF0ZRgEIAEoCFILZm9yY2VVcGRhdGUaTQoPVHlwaW5nSW5kaWNhdG9yEhsKCWlzX3R5'
|
||||
'cGluZxgBIAEoCFIIaXNUeXBpbmcSHQoKY3JlYXRlZF9hdBgCIAEoA1IJY3JlYXRlZEF0Gj8KFF'
|
||||
'VzZXJEaXNjb3ZlcnlSZXF1ZXN0EicKD2N1cnJlbnRfdmVyc2lvbhgBIAEoDFIOY3VycmVudFZl'
|
||||
'cnNpb24aMQoTVXNlckRpc2NvdmVyeVVwZGF0ZRIaCghtZXNzYWdlcxgBIAMoDFIIbWVzc2FnZX'
|
||||
'MaPQoUS2V5VmVyaWZpY2F0aW9uUHJvb2YSJQoOY2FsY3VsYXRlZF9tYWMYASABKAxSDWNhbGN1'
|
||||
'bGF0ZWRNYWMamwEKFFBhc3N3b3JkTGVzc1JlY292ZXJ5EjUKE3JlY292ZXJ5U2VjcmV0U2hhcm'
|
||||
'UYASABKAxIAFITcmVjb3ZlcnlTZWNyZXRTaGFyZYgBARIWCgZkZWxldGUYAiABKAhSBmRlbGV0'
|
||||
'ZRIcCgl0aHJlc2hvbGQYAyABKANSCXRocmVzaG9sZEIWChRfcmVjb3ZlcnlTZWNyZXRTaGFyZR'
|
||||
'ozCh1QYXNzd29yZExlc3NSZWNvdmVyeUhlYXJ0YmVhdBISCgRoYXNoGAEgASgMUgRoYXNoQgsK'
|
||||
'CV9ncm91cF9pZEIRCg9faXNfZGlyZWN0X2NoYXRCGQoXX3NlbmRlcl9wcm9maWxlX2NvdW50ZX'
|
||||
'JCIAoeX3NlbmRlcl91c2VyX2Rpc2NvdmVyeV92ZXJzaW9uQhwKGl9hc2tfZm9yX2ZyaWVuZF9w'
|
||||
'cm9tb3Rpb25zQhkKF193aWRnZXRfc2hhcmluZ19hbGxvd2VkQhYKFF9zZW5kZXJfdHdvbmx5X3'
|
||||
'Njb3JlQhEKD19tZXNzYWdlX3VwZGF0ZUIICgZfbWVkaWFCDwoNX21lZGlhX3VwZGF0ZUIRCg9f'
|
||||
'Y29udGFjdF91cGRhdGVCEgoQX2NvbnRhY3RfcmVxdWVzdEINCgtfZmxhbWVfc3luY0ILCglfcm'
|
||||
'VhY3Rpb25CDwoNX3RleHRfbWVzc2FnZUIPCg1fZ3JvdXBfY3JlYXRlQg0KC19ncm91cF9qb2lu'
|
||||
'Qg8KDV9ncm91cF91cGRhdGVCGgoYX3Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5QhEKD19lcnJvcl'
|
||||
'9tZXNzYWdlc0IaChhfYWRkaXRpb25hbF9kYXRhX21lc3NhZ2VCEwoRX3R5cGluZ19pbmRpY2F0'
|
||||
'b3JCGQoXX3VzZXJfZGlzY292ZXJ5X3JlcXVlc3RCGAoWX3VzZXJfZGlzY292ZXJ5X3VwZGF0ZU'
|
||||
'IZChdfa2V5X3ZlcmlmaWNhdGlvbl9wcm9vZkIYChZfcGFzc3dvcmRsZXNzX3JlY292ZXJ5QiIK'
|
||||
'IF9wYXNzd29yZGxlc3NfcmVjb3ZlcnlfaGVhcnRiZWF0QggKBl9zdG9yeUoECBwQHQ==');
|
||||
'9ubHlTY29yZYgBARJEChxzZW5kZXJfY3VzdG9tX2F2YXRhcl9jb3VudGVyGCAgASgDSAdSGXNl'
|
||||
'bmRlckN1c3RvbUF2YXRhckNvdW50ZXKIAQESSAoeY3VzdG9tX2F2YXRhcl9wcm90b2NvbF92ZX'
|
||||
'JzaW9uGCEgASgNSAhSG2N1c3RvbUF2YXRhclByb3RvY29sVmVyc2lvbogBARJLCg5tZXNzYWdl'
|
||||
'X3VwZGF0ZRgFIAEoCzIfLkVuY3J5cHRlZENvbnRlbnQuTWVzc2FnZVVwZGF0ZUgJUg1tZXNzYW'
|
||||
'dlVXBkYXRliAEBEjIKBW1lZGlhGAYgASgLMhcuRW5jcnlwdGVkQ29udGVudC5NZWRpYUgKUgVt'
|
||||
'ZWRpYYgBARJFCgxtZWRpYV91cGRhdGUYByABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk1lZGlhVX'
|
||||
'BkYXRlSAtSC21lZGlhVXBkYXRliAEBEksKDmNvbnRhY3RfdXBkYXRlGAggASgLMh8uRW5jcnlw'
|
||||
'dGVkQ29udGVudC5Db250YWN0VXBkYXRlSAxSDWNvbnRhY3RVcGRhdGWIAQESTgoPY29udGFjdF'
|
||||
'9yZXF1ZXN0GAkgASgLMiAuRW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdEgNUg5jb250'
|
||||
'YWN0UmVxdWVzdIgBARI/CgpmbGFtZV9zeW5jGAogASgLMhsuRW5jcnlwdGVkQ29udGVudC5GbG'
|
||||
'FtZVN5bmNIDlIJZmxhbWVTeW5jiAEBEjsKCHJlYWN0aW9uGAwgASgLMhouRW5jcnlwdGVkQ29u'
|
||||
'dGVudC5SZWFjdGlvbkgPUghyZWFjdGlvbogBARJFCgx0ZXh0X21lc3NhZ2UYDSABKAsyHS5Fbm'
|
||||
'NyeXB0ZWRDb250ZW50LlRleHRNZXNzYWdlSBBSC3RleHRNZXNzYWdliAEBEkUKDGdyb3VwX2Ny'
|
||||
'ZWF0ZRgOIAEoCzIdLkVuY3J5cHRlZENvbnRlbnQuR3JvdXBDcmVhdGVIEVILZ3JvdXBDcmVhdG'
|
||||
'WIAQESPwoKZ3JvdXBfam9pbhgPIAEoCzIbLkVuY3J5cHRlZENvbnRlbnQuR3JvdXBKb2luSBJS'
|
||||
'CWdyb3VwSm9pbogBARJFCgxncm91cF91cGRhdGUYECABKAsyHS5FbmNyeXB0ZWRDb250ZW50Lk'
|
||||
'dyb3VwVXBkYXRlSBNSC2dyb3VwVXBkYXRliAEBEmIKF3Jlc2VuZF9ncm91cF9wdWJsaWNfa2V5'
|
||||
'GBEgASgLMiYuRW5jcnlwdGVkQ29udGVudC5SZXNlbmRHcm91cFB1YmxpY0tleUgUUhRyZXNlbm'
|
||||
'RHcm91cFB1YmxpY0tleYgBARJLCg5lcnJvcl9tZXNzYWdlcxgSIAEoCzIfLkVuY3J5cHRlZENv'
|
||||
'bnRlbnQuRXJyb3JNZXNzYWdlc0gVUg1lcnJvck1lc3NhZ2VziAEBEmQKF2FkZGl0aW9uYWxfZG'
|
||||
'F0YV9tZXNzYWdlGBMgASgLMicuRW5jcnlwdGVkQ29udGVudC5BZGRpdGlvbmFsRGF0YU1lc3Nh'
|
||||
'Z2VIFlIVYWRkaXRpb25hbERhdGFNZXNzYWdliAEBElEKEHR5cGluZ19pbmRpY2F0b3IYFCABKA'
|
||||
'syIS5FbmNyeXB0ZWRDb250ZW50LlR5cGluZ0luZGljYXRvckgXUg90eXBpbmdJbmRpY2F0b3KI'
|
||||
'AQESYQoWdXNlcl9kaXNjb3ZlcnlfcmVxdWVzdBgWIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuVX'
|
||||
'NlckRpc2NvdmVyeVJlcXVlc3RIGFIUdXNlckRpc2NvdmVyeVJlcXVlc3SIAQESXgoVdXNlcl9k'
|
||||
'aXNjb3ZlcnlfdXBkYXRlGBcgASgLMiUuRW5jcnlwdGVkQ29udGVudC5Vc2VyRGlzY292ZXJ5VX'
|
||||
'BkYXRlSBlSE3VzZXJEaXNjb3ZlcnlVcGRhdGWIAQESYQoWa2V5X3ZlcmlmaWNhdGlvbl9wcm9v'
|
||||
'ZhgYIAEoCzImLkVuY3J5cHRlZENvbnRlbnQuS2V5VmVyaWZpY2F0aW9uUHJvb2ZIGlIUa2V5Vm'
|
||||
'VyaWZpY2F0aW9uUHJvb2aIAQESYAoVcGFzc3dvcmRsZXNzX3JlY292ZXJ5GBogASgLMiYuRW5j'
|
||||
'cnlwdGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVyeUgbUhRwYXNzd29yZGxlc3NSZWNvdm'
|
||||
'VyeYgBARJ8Ch9wYXNzd29yZGxlc3NfcmVjb3ZlcnlfaGVhcnRiZWF0GBsgASgLMi8uRW5jcnlw'
|
||||
'dGVkQ29udGVudC5QYXNzd29yZExlc3NSZWNvdmVyeUhlYXJ0YmVhdEgcUh1wYXNzd29yZGxlc3'
|
||||
'NSZWNvdmVyeUhlYXJ0YmVhdIgBARIyCgVzdG9yeRgeIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQu'
|
||||
'U3RvcnlIHVIFc3RvcnmIAQEalgIKDUVycm9yTWVzc2FnZXMSOAoEdHlwZRgBIAEoDjIkLkVuY3'
|
||||
'J5cHRlZENvbnRlbnQuRXJyb3JNZXNzYWdlcy5UeXBlUgR0eXBlEiwKEnJlbGF0ZWRfcmVjZWlw'
|
||||
'dF9pZBgCIAEoCVIQcmVsYXRlZFJlY2VpcHRJZCKcAQoEVHlwZRI8CjhFUlJPUl9QUk9DRVNTSU'
|
||||
'5HX01FU1NBR0VfQ1JFQVRFRF9BQ0NPVU5UX1JFUVVFU1RfSU5TVEVBRBAAEhgKFFVOS05PV05f'
|
||||
'TUVTU0FHRV9UWVBFEAISFwoTU0VTU0lPTl9PVVRfT0ZfU1lOQxADEiMKH0dST1VQX05PVF9GT1'
|
||||
'VORF9PUl9OT1RfQV9NRU1CRVIQBBqHAQoLR3JvdXBDcmVhdGUSGwoJc3RhdGVfa2V5GAMgASgM'
|
||||
'UghzdGF0ZUtleRIoChBncm91cF9wdWJsaWNfa2V5GAQgASgMUg5ncm91cFB1YmxpY0tleRIiCg'
|
||||
'pncm91cF9uYW1lGAUgASgJSABSCWdyb3VwTmFtZYgBAUINCgtfZ3JvdXBfbmFtZRo1CglHcm91'
|
||||
'cEpvaW4SKAoQZ3JvdXBfcHVibGljX2tleRgBIAEoDFIOZ3JvdXBQdWJsaWNLZXkaFgoUUmVzZW'
|
||||
'5kR3JvdXBQdWJsaWNLZXkayAIKC0dyb3VwVXBkYXRlEioKEWdyb3VwX2FjdGlvbl90eXBlGAEg'
|
||||
'ASgJUg9ncm91cEFjdGlvblR5cGUSMwoTYWZmZWN0ZWRfY29udGFjdF9pZBgCIAEoA0gAUhFhZm'
|
||||
'ZlY3RlZENvbnRhY3RJZIgBARIpCg5uZXdfZ3JvdXBfbmFtZRgDIAEoCUgBUgxuZXdHcm91cE5h'
|
||||
'bWWIAQESVwombmV3X2RlbGV0ZV9tZXNzYWdlc19hZnRlcl9taWxsaXNlY29uZHMYBCABKANIAl'
|
||||
'IibmV3RGVsZXRlTWVzc2FnZXNBZnRlck1pbGxpc2Vjb25kc4gBAUIWChRfYWZmZWN0ZWRfY29u'
|
||||
'dGFjdF9pZEIRCg9fbmV3X2dyb3VwX25hbWVCKQonX25ld19kZWxldGVfbWVzc2FnZXNfYWZ0ZX'
|
||||
'JfbWlsbGlzZWNvbmRzGogCCgtUZXh0TWVzc2FnZRIqChFzZW5kZXJfbWVzc2FnZV9pZBgBIAEo'
|
||||
'CVIPc2VuZGVyTWVzc2FnZUlkEhIKBHRleHQYAiABKAlSBHRleHQSHAoJdGltZXN0YW1wGAMgAS'
|
||||
'gDUgl0aW1lc3RhbXASLQoQcXVvdGVfbWVzc2FnZV9pZBgEIAEoCUgAUg5xdW90ZU1lc3NhZ2VJ'
|
||||
'ZIgBARI7ChdhZGRpdGlvbmFsX21lc3NhZ2VfZGF0YRgFIAEoDEgBUhVhZGRpdGlvbmFsTWVzc2'
|
||||
'FnZURhdGGIAQFCEwoRX3F1b3RlX21lc3NhZ2VfaWRCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9k'
|
||||
'YXRhGuYBChVBZGRpdGlvbmFsRGF0YU1lc3NhZ2USKgoRc2VuZGVyX21lc3NhZ2VfaWQYASABKA'
|
||||
'lSD3NlbmRlck1lc3NhZ2VJZBIcCgl0aW1lc3RhbXAYAiABKANSCXRpbWVzdGFtcBISCgR0eXBl'
|
||||
'GAMgASgJUgR0eXBlEjsKF2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhGAQgASgMSABSFWFkZGl0aW'
|
||||
'9uYWxNZXNzYWdlRGF0YYgBARIWCgZoaWRkZW4YBSABKAhSBmhpZGRlbkIaChhfYWRkaXRpb25h'
|
||||
'bF9tZXNzYWdlX2RhdGEaZAoIUmVhY3Rpb24SKgoRdGFyZ2V0X21lc3NhZ2VfaWQYASABKAlSD3'
|
||||
'RhcmdldE1lc3NhZ2VJZBIUCgVlbW9qaRgCIAEoCVIFZW1vamkSFgoGcmVtb3ZlGAMgASgIUgZy'
|
||||
'ZW1vdmUavgIKDU1lc3NhZ2VVcGRhdGUSOAoEdHlwZRgBIAEoDjIkLkVuY3J5cHRlZENvbnRlbn'
|
||||
'QuTWVzc2FnZVVwZGF0ZS5UeXBlUgR0eXBlEi8KEXNlbmRlcl9tZXNzYWdlX2lkGAIgASgJSABS'
|
||||
'D3NlbmRlck1lc3NhZ2VJZIgBARI9ChttdWx0aXBsZV90YXJnZXRfbWVzc2FnZV9pZHMYAyADKA'
|
||||
'lSGG11bHRpcGxlVGFyZ2V0TWVzc2FnZUlkcxIXCgR0ZXh0GAQgASgJSAFSBHRleHSIAQESHAoJ'
|
||||
'dGltZXN0YW1wGAUgASgDUgl0aW1lc3RhbXAiLQoEVHlwZRIKCgZERUxFVEUQABINCglFRElUX1'
|
||||
'RFWFQQARIKCgZPUEVORUQQAkIUChJfc2VuZGVyX21lc3NhZ2VfaWRCBwoFX3RleHQauwYKBU1l'
|
||||
'ZGlhEioKEXNlbmRlcl9tZXNzYWdlX2lkGAEgASgJUg9zZW5kZXJNZXNzYWdlSWQSMAoEdHlwZR'
|
||||
'gCIAEoDjIcLkVuY3J5cHRlZENvbnRlbnQuTWVkaWEuVHlwZVIEdHlwZRJGCh1kaXNwbGF5X2xp'
|
||||
'bWl0X2luX21pbGxpc2Vjb25kcxgDIAEoA0gAUhpkaXNwbGF5TGltaXRJbk1pbGxpc2Vjb25kc4'
|
||||
'gBARI3ChdyZXF1aXJlc19hdXRoZW50aWNhdGlvbhgEIAEoCFIWcmVxdWlyZXNBdXRoZW50aWNh'
|
||||
'dGlvbhIcCgl0aW1lc3RhbXAYBSABKANSCXRpbWVzdGFtcBItChBxdW90ZV9tZXNzYWdlX2lkGA'
|
||||
'YgASgJSAFSDnF1b3RlTWVzc2FnZUlkiAEBEioKDmRvd25sb2FkX3Rva2VuGAcgASgMSAJSDWRv'
|
||||
'd25sb2FkVG9rZW6IAQESKgoOZW5jcnlwdGlvbl9rZXkYCCABKAxIA1INZW5jcnlwdGlvbktleY'
|
||||
'gBARIqCg5lbmNyeXB0aW9uX21hYxgJIAEoDEgEUg1lbmNyeXB0aW9uTWFjiAEBEi4KEGVuY3J5'
|
||||
'cHRpb25fbm9uY2UYCiABKAxIBVIPZW5jcnlwdGlvbk5vbmNliAEBEjsKF2FkZGl0aW9uYWxfbW'
|
||||
'Vzc2FnZV9kYXRhGAsgASgMSAZSFWFkZGl0aW9uYWxNZXNzYWdlRGF0YYgBARIkCgt3aWRnZXRf'
|
||||
'b25seRgMIAEoCEgHUgp3aWRnZXRPbmx5iAEBIj4KBFR5cGUSDAoIUkVVUExPQUQQABIJCgVJTU'
|
||||
'FHRRABEgkKBVZJREVPEAISBwoDR0lGEAMSCQoFQVVESU8QBEIgCh5fZGlzcGxheV9saW1pdF9p'
|
||||
'bl9taWxsaXNlY29uZHNCEwoRX3F1b3RlX21lc3NhZ2VfaWRCEQoPX2Rvd25sb2FkX3Rva2VuQh'
|
||||
'EKD19lbmNyeXB0aW9uX2tleUIRCg9fZW5jcnlwdGlvbl9tYWNCEwoRX2VuY3J5cHRpb25fbm9u'
|
||||
'Y2VCGgoYX2FkZGl0aW9uYWxfbWVzc2FnZV9kYXRhQg4KDF93aWRnZXRfb25seRpOCgVTdG9yeR'
|
||||
'ItCgVtZWRpYRgBIAEoCzIXLkVuY3J5cHRlZENvbnRlbnQuTWVkaWFSBW1lZGlhEhYKBm5vdGlm'
|
||||
'eRgCIAEoCFIGbm90aWZ5GqkBCgtNZWRpYVVwZGF0ZRI2CgR0eXBlGAEgASgOMiIuRW5jcnlwdG'
|
||||
'VkQ29udGVudC5NZWRpYVVwZGF0ZS5UeXBlUgR0eXBlEioKEXRhcmdldF9tZXNzYWdlX2lkGAIg'
|
||||
'ASgJUg90YXJnZXRNZXNzYWdlSWQiNgoEVHlwZRIMCghSRU9QRU5FRBAAEgoKBlNUT1JFRBABEh'
|
||||
'QKEERFQ1JZUFRJT05fRVJST1IQAhp4Cg5Db250YWN0UmVxdWVzdBI5CgR0eXBlGAEgASgOMiUu'
|
||||
'RW5jcnlwdGVkQ29udGVudC5Db250YWN0UmVxdWVzdC5UeXBlUgR0eXBlIisKBFR5cGUSCwoHUk'
|
||||
'VRVUVTVBAAEgoKBlJFSkVDVBABEgoKBkFDQ0VQVBACGtkDCg1Db250YWN0VXBkYXRlEjgKBHR5'
|
||||
'cGUYASABKA4yJC5FbmNyeXB0ZWRDb250ZW50LkNvbnRhY3RVcGRhdGUuVHlwZVIEdHlwZRI3Ch'
|
||||
'VhdmF0YXJfc3ZnX2NvbXByZXNzZWQYAiABKAxIAFITYXZhdGFyU3ZnQ29tcHJlc3NlZIgBARIf'
|
||||
'Cgh1c2VybmFtZRgDIAEoCUgBUgh1c2VybmFtZYgBARImCgxkaXNwbGF5X25hbWUYBCABKAlIAl'
|
||||
'ILZGlzcGxheU5hbWWIAQESOwoXY3VzdG9tX2F2YXRhcl9yZXF1ZXN0ZWQYBSABKAhIA1IVY3Vz'
|
||||
'dG9tQXZhdGFyUmVxdWVzdGVkiAEBEkgKDWN1c3RvbV9hdmF0YXIYBiABKAsyHi5FbmNyeXB0ZW'
|
||||
'RDb250ZW50LkN1c3RvbUF2YXRhckgEUgxjdXN0b21BdmF0YXKIAQEiHwoEVHlwZRILCgdSRVFV'
|
||||
'RVNUEAASCgoGVVBEQVRFEAFCGAoWX2F2YXRhcl9zdmdfY29tcHJlc3NlZEILCglfdXNlcm5hbW'
|
||||
'VCDwoNX2Rpc3BsYXlfbmFtZUIaChhfY3VzdG9tX2F2YXRhcl9yZXF1ZXN0ZWRCEAoOX2N1c3Rv'
|
||||
'bV9hdmF0YXIazgIKDEN1c3RvbUF2YXRhchIYCgd2ZXJzaW9uGAEgASgNUgd2ZXJzaW9uEi8KE3'
|
||||
'B1YmxpY2F0aW9uX2NvdW50ZXIYAiABKANSEnB1YmxpY2F0aW9uQ291bnRlchI6CgVzdGF0ZRgD'
|
||||
'IAEoDjIkLkVuY3J5cHRlZENvbnRlbnQuQ3VzdG9tQXZhdGFyLlN0YXRlUgVzdGF0ZRIXCgR3ZW'
|
||||
'JwGAQgASgMSABSBHdlYnCIAQESGwoGc2hhMjU2GAUgASgMSAFSBnNoYTI1NogBARIZCgV3aWR0'
|
||||
'aBgGIAEoDUgCUgV3aWR0aIgBARIbCgZoZWlnaHQYByABKA1IA1IGaGVpZ2h0iAEBIiAKBVN0YX'
|
||||
'RlEgwKCFNWR19PTkxZEAASCQoFUEhPVE8QAUIHCgVfd2VicEIJCgdfc2hhMjU2QggKBl93aWR0'
|
||||
'aEIJCgdfaGVpZ2h0Gq8BCglGbGFtZVN5bmMSIwoNZmxhbWVfY291bnRlchgBIAEoA1IMZmxhbW'
|
||||
'VDb3VudGVyEjkKGWxhc3RfZmxhbWVfY291bnRlcl9jaGFuZ2UYAiABKANSFmxhc3RGbGFtZUNv'
|
||||
'dW50ZXJDaGFuZ2USHwoLYmVzdF9mcmllbmQYAyABKAhSCmJlc3RGcmllbmQSIQoMZm9yY2VfdX'
|
||||
'BkYXRlGAQgASgIUgtmb3JjZVVwZGF0ZRpNCg9UeXBpbmdJbmRpY2F0b3ISGwoJaXNfdHlwaW5n'
|
||||
'GAEgASgIUghpc1R5cGluZxIdCgpjcmVhdGVkX2F0GAIgASgDUgljcmVhdGVkQXQaPwoUVXNlck'
|
||||
'Rpc2NvdmVyeVJlcXVlc3QSJwoPY3VycmVudF92ZXJzaW9uGAEgASgMUg5jdXJyZW50VmVyc2lv'
|
||||
'bhoxChNVc2VyRGlzY292ZXJ5VXBkYXRlEhoKCG1lc3NhZ2VzGAEgAygMUghtZXNzYWdlcxo9Ch'
|
||||
'RLZXlWZXJpZmljYXRpb25Qcm9vZhIlCg5jYWxjdWxhdGVkX21hYxgBIAEoDFINY2FsY3VsYXRl'
|
||||
'ZE1hYxqbAQoUUGFzc3dvcmRMZXNzUmVjb3ZlcnkSNQoTcmVjb3ZlcnlTZWNyZXRTaGFyZRgBIA'
|
||||
'EoDEgAUhNyZWNvdmVyeVNlY3JldFNoYXJliAEBEhYKBmRlbGV0ZRgCIAEoCFIGZGVsZXRlEhwK'
|
||||
'CXRocmVzaG9sZBgDIAEoA1IJdGhyZXNob2xkQhYKFF9yZWNvdmVyeVNlY3JldFNoYXJlGjMKHV'
|
||||
'Bhc3N3b3JkTGVzc1JlY292ZXJ5SGVhcnRiZWF0EhIKBGhhc2gYASABKAxSBGhhc2hCCwoJX2dy'
|
||||
'b3VwX2lkQhEKD19pc19kaXJlY3RfY2hhdEIZChdfc2VuZGVyX3Byb2ZpbGVfY291bnRlckIgCh'
|
||||
'5fc2VuZGVyX3VzZXJfZGlzY292ZXJ5X3ZlcnNpb25CHAoaX2Fza19mb3JfZnJpZW5kX3Byb21v'
|
||||
'dGlvbnNCGQoXX3dpZGdldF9zaGFyaW5nX2FsbG93ZWRCFgoUX3NlbmRlcl90d29ubHlfc2Nvcm'
|
||||
'VCHwodX3NlbmRlcl9jdXN0b21fYXZhdGFyX2NvdW50ZXJCIQofX2N1c3RvbV9hdmF0YXJfcHJv'
|
||||
'dG9jb2xfdmVyc2lvbkIRCg9fbWVzc2FnZV91cGRhdGVCCAoGX21lZGlhQg8KDV9tZWRpYV91cG'
|
||||
'RhdGVCEQoPX2NvbnRhY3RfdXBkYXRlQhIKEF9jb250YWN0X3JlcXVlc3RCDQoLX2ZsYW1lX3N5'
|
||||
'bmNCCwoJX3JlYWN0aW9uQg8KDV90ZXh0X21lc3NhZ2VCDwoNX2dyb3VwX2NyZWF0ZUINCgtfZ3'
|
||||
'JvdXBfam9pbkIPCg1fZ3JvdXBfdXBkYXRlQhoKGF9yZXNlbmRfZ3JvdXBfcHVibGljX2tleUIR'
|
||||
'Cg9fZXJyb3JfbWVzc2FnZXNCGgoYX2FkZGl0aW9uYWxfZGF0YV9tZXNzYWdlQhMKEV90eXBpbm'
|
||||
'dfaW5kaWNhdG9yQhkKF191c2VyX2Rpc2NvdmVyeV9yZXF1ZXN0QhgKFl91c2VyX2Rpc2NvdmVy'
|
||||
'eV91cGRhdGVCGQoXX2tleV92ZXJpZmljYXRpb25fcHJvb2ZCGAoWX3Bhc3N3b3JkbGVzc19yZW'
|
||||
'NvdmVyeUIiCiBfcGFzc3dvcmRsZXNzX3JlY292ZXJ5X2hlYXJ0YmVhdEIICgZfc3RvcnlKBAgc'
|
||||
'EB0=');
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'dart:async';
|
||||
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_svg/svg.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
|
|
@ -80,6 +80,7 @@ String _avatarPngPathFor(Contact contact) {
|
|||
class _AvatarIconState extends State<AvatarIcon> {
|
||||
List<Contact> _avatarContacts = [];
|
||||
Set<int> _contactsWithPngAvatar = {};
|
||||
final Map<int, String> _resolvedAvatarPaths = {};
|
||||
String? _myAvatarPath;
|
||||
|
||||
StreamSubscription<List<Contact>>? groupStream;
|
||||
|
|
@ -105,35 +106,20 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
}
|
||||
|
||||
void _setAvatarContacts(List<Contact> contacts) {
|
||||
_avatarContacts = contacts
|
||||
.where((contact) => contact.avatarSvgCompressed != null)
|
||||
.toList();
|
||||
_avatarContacts = contacts;
|
||||
unawaited(_refreshAvatarFiles());
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _refreshAvatarFiles() async {
|
||||
final available = <int>{};
|
||||
for (final contact in _avatarContacts) {
|
||||
final path = _avatarPngPathFor(contact);
|
||||
var exists = _existingAvatarPngPaths.contains(path);
|
||||
if (!exists) {
|
||||
// Async file access keeps avatar discovery off the UI thread.
|
||||
// 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.
|
||||
if (!_unrenderableAvatars.contains(_avatarCacheKey(contact))) {
|
||||
// Rust resolves a received custom avatar before the SVG fallback. Do
|
||||
// not trust the old profile-counter cache here: a custom-avatar update
|
||||
// deliberately leaves that public counter unchanged.
|
||||
unawaited(_renderAvatarPng(contact));
|
||||
}
|
||||
}
|
||||
if (!mounted) return;
|
||||
if (setEquals(_contactsWithPngAvatar, available)) return;
|
||||
setState(() => _contactsWithPngAvatar = available);
|
||||
}
|
||||
|
||||
Future<void> _renderAvatarPng(Contact contact) async {
|
||||
|
|
@ -167,6 +153,7 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
if (_contactsWithPngAvatar.contains(contactId)) return;
|
||||
if (!_avatarContacts.any((entry) => entry.userId == contactId)) return;
|
||||
setState(() {
|
||||
_resolvedAvatarPaths[contactId] = path!;
|
||||
_contactsWithPngAvatar = {..._contactsWithPngAvatar, contactId};
|
||||
});
|
||||
}
|
||||
|
|
@ -189,7 +176,9 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
Widget getAvatarForContact(Contact contact) {
|
||||
if (_contactsWithPngAvatar.contains(contact.userId)) {
|
||||
return Image.file(
|
||||
File(_avatarPngPathFor(contact)),
|
||||
File(
|
||||
_resolvedAvatarPaths[contact.userId] ?? _avatarPngPathFor(contact),
|
||||
),
|
||||
errorBuilder: errorBuilder,
|
||||
);
|
||||
}
|
||||
|
|
@ -206,16 +195,13 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
.watchGroupContact(widget.group!.groupId)
|
||||
.listen((contacts) {
|
||||
_avatarContacts = [];
|
||||
if (contacts.length == 1) {
|
||||
if (contacts.first.avatarSvgCompressed != null) {
|
||||
_avatarContacts.add(contacts.first);
|
||||
}
|
||||
} else {
|
||||
for (final contact in contacts) {
|
||||
if (contact.avatarSvgCompressed != null) {
|
||||
_avatarContacts.add(contact);
|
||||
}
|
||||
_unrenderableAvatars.remove(_avatarCacheKey(contact));
|
||||
}
|
||||
if (contacts.length == 1) {
|
||||
_avatarContacts.add(contacts.first);
|
||||
} else {
|
||||
_avatarContacts.addAll(contacts);
|
||||
}
|
||||
unawaited(_refreshAvatarFiles());
|
||||
setState(() {});
|
||||
|
|
@ -229,7 +215,8 @@ class _AvatarIconState extends State<AvatarIcon> {
|
|||
contactStream = twonlyDB.contactsDao
|
||||
.watchContact(widget.contactId!)
|
||||
.listen((contact) {
|
||||
if (contact != null && contact.avatarSvgCompressed != null) {
|
||||
if (contact != null) {
|
||||
_unrenderableAvatars.remove(_avatarCacheKey(contact));
|
||||
_avatarContacts = [contact];
|
||||
unawaited(_refreshAvatarFiles());
|
||||
setState(() {});
|
||||
|
|
|
|||
|
|
@ -82,7 +82,10 @@ class _StoryTargetSelectorState extends State<StoryTargetSelector> {
|
|||
);
|
||||
if (before != widget.target.contactGroupIds.length) _changed();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
unawaited(_updateAudience());
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -5,11 +5,13 @@ import 'dart:math' as math;
|
|||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.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/twonly.db.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/user.service.dart';
|
||||
import 'package:twonly/src/utils/keyvalue.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/screenshot.helper.dart';
|
||||
|
|
@ -96,6 +98,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
bool _widgetRecipientAvailable = false;
|
||||
bool _sendToWidget = false;
|
||||
bool _updatingWidgetMode = false;
|
||||
bool _widgetExplainerPreferenceLoaded = false;
|
||||
bool _widgetExplainerDismissed =
|
||||
userService.currentUser.hideWidgetShareExplainer;
|
||||
bool _previousMediaSettingsCaptured = false;
|
||||
|
|
@ -103,7 +106,9 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
bool _requiresAuthBeforeWidget = false;
|
||||
|
||||
bool get _showWidgetExplainer =>
|
||||
_widgetRecipientAvailable && !_widgetExplainerDismissed;
|
||||
_widgetRecipientAvailable &&
|
||||
_widgetExplainerPreferenceLoaded &&
|
||||
!_widgetExplainerDismissed;
|
||||
|
||||
MediaFileService get mediaService => widget.mediaFileService;
|
||||
MediaFile get media => widget.mediaFileService.mediaFile;
|
||||
|
|
@ -138,6 +143,7 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
_widgetGroupsSubscription = twonlyDB.groupsDao
|
||||
.watchGroupsAllowedForWidgetShare()
|
||||
.listen(_updateWidgetRecipientAvailability);
|
||||
unawaited(_loadWidgetExplainerPreference());
|
||||
if (media.type == MediaType.image || media.type == MediaType.gif) {
|
||||
_loadInitialImage();
|
||||
}
|
||||
|
|
@ -162,9 +168,27 @@ class _ShareImageEditorView extends State<ShareImageEditorView> {
|
|||
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 {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/// escape hatch to the device gallery in the top app bar.
|
||||
class StickerSourcePickerView extends StatefulWidget {
|
||||
const StickerSourcePickerView({super.key});
|
||||
const StickerSourcePickerView({this.forAvatar = false, super.key});
|
||||
|
||||
final bool forAvatar;
|
||||
|
||||
@override
|
||||
State<StickerSourcePickerView> createState() =>
|
||||
|
|
@ -63,7 +65,11 @@ class _StickerSourcePickerViewState extends State<StickerSourcePickerView> {
|
|||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.createSticker),
|
||||
title: Text(
|
||||
widget.forAvatar
|
||||
? context.lang.customAvatarChooseSource
|
||||
: context.lang.createSticker,
|
||||
),
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
onPressed: _resolvingMediaId == null ? _pickFromGallery : null,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:twonly/core/services/avatars.dart';
|
||||
import 'package:twonly/core/user_config.dart';
|
||||
import 'package:twonly/locator.dart';
|
||||
import 'package:twonly/src/services/user.service.dart';
|
||||
|
|
@ -13,6 +14,38 @@ class PublicProfilePrivacyView extends StatefulWidget {
|
|||
}
|
||||
|
||||
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(
|
||||
TwonlyScoreVisibility visibility,
|
||||
) 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:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.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/src/services/user.service.dart';
|
||||
import 'package:twonly/src/utils/misc.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 {
|
||||
const ModifyAvatarView({super.key});
|
||||
|
|
@ -16,30 +20,340 @@ class ModifyAvatarView extends StatefulWidget {
|
|||
}
|
||||
|
||||
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;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final svg = userService.currentUser.avatarSvg;
|
||||
if (svg != null && svg.isNotEmpty) {
|
||||
_avatarMakerController = _CustomAvatarMakerController(
|
||||
svg: svg,
|
||||
);
|
||||
} else {
|
||||
_avatarMakerController = _CustomAvatarMakerController.defaultAvatar();
|
||||
}
|
||||
_avatarMakerController = svg != null && svg.isNotEmpty
|
||||
? _CustomAvatarMakerController(svg: svg)
|
||||
: _CustomAvatarMakerController.defaultAvatar();
|
||||
}
|
||||
|
||||
Future<void> updateUserAvatar(String svg) async {
|
||||
@override
|
||||
void dispose() {
|
||||
_avatarMakerController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _updateUserAvatar(String svg) async {
|
||||
await UserService.update(
|
||||
(u) => u
|
||||
(user) => user
|
||||
..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;
|
||||
return AvatarMakerThemeData(
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope<bool?>(
|
||||
|
|
@ -117,31 +395,23 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
if (didPop) return;
|
||||
if (_avatarMakerController.getAvatarSVGSync() !=
|
||||
userService.currentUser.avatarSvg) {
|
||||
// there where changes
|
||||
final shouldPop = await _showBackDialog() ?? false;
|
||||
if (context.mounted && shouldPop) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
if (context.mounted && shouldPop) Navigator.pop(context);
|
||||
} else {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.lang.settingsProfileCustomizeAvatar),
|
||||
),
|
||||
appBar: AppBar(title: Text(context.lang.customAvatarSvgTab)),
|
||||
body: Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.zero,
|
||||
child: AvatarMakerAvatar(
|
||||
AvatarMakerAvatar(
|
||||
radius: 130,
|
||||
backgroundColor: Colors.transparent,
|
||||
controller: _avatarMakerController,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Wrap(
|
||||
|
|
@ -151,11 +421,14 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
children: [
|
||||
MyButton(
|
||||
variant: MyButtonVariant.primaryDense,
|
||||
onPressed: storeAvatarAndExit,
|
||||
onPressed: _storeAvatarAndExit,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.floppyDisk, size: 16),
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.floppyDisk,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(context.lang.avatarSaveChangesStore),
|
||||
],
|
||||
|
|
@ -177,13 +450,14 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
MyButton(
|
||||
variant: MyButtonVariant.secondaryDense,
|
||||
onPressed: _avatarMakerController.restoreState,
|
||||
onLongPress: () {
|
||||
_avatarMakerController.clearCustomizations();
|
||||
},
|
||||
onLongPress: _avatarMakerController.clearCustomizations,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.rotateLeft, size: 16),
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.rotateLeft,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(context.lang.avatarCustomizeReset),
|
||||
],
|
||||
|
|
@ -193,15 +467,13 @@ class _ModifyAvatarViewState extends State<ModifyAvatarView> {
|
|||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 30,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 30),
|
||||
child: AvatarMakerCustomizer(
|
||||
scaffoldWidth: min(
|
||||
600,
|
||||
MediaQuery.of(context).size.width * 0.95,
|
||||
),
|
||||
theme: getAvatarMakerTheme(context),
|
||||
theme: _theme(context),
|
||||
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_custom_avatar_update(t, from_user_id, &content).await?;
|
||||
|
||||
if content.ask_for_friend_promotions == Some(true) {
|
||||
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};
|
||||
|
||||
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(
|
||||
t: &mut Transaction<'_, Sqlite>,
|
||||
|
|
@ -68,6 +134,8 @@ pub(crate) async fn check_for_profile_update(
|
|||
username: None,
|
||||
display_name: None,
|
||||
avatar_svg_compressed: None,
|
||||
custom_avatar_requested: None,
|
||||
custom_avatar: None,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
|
|
@ -216,6 +284,11 @@ pub(crate) async fn handle_contact_update(
|
|||
})
|
||||
.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(
|
||||
tr,
|
||||
from_user_id,
|
||||
|
|
@ -225,8 +298,16 @@ pub(crate) async fn handle_contact_update(
|
|||
username: Some(user.username),
|
||||
display_name: Some(user.display_name),
|
||||
avatar_svg_compressed,
|
||||
custom_avatar_requested: None,
|
||||
custom_avatar: custom_avatar.clone(),
|
||||
}),
|
||||
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()
|
||||
},
|
||||
true,
|
||||
|
|
@ -240,6 +321,21 @@ pub(crate) async fn handle_contact_update(
|
|||
}
|
||||
|
||||
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!(
|
||||
"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(|| {
|
||||
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 =
|
||||
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_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(
|
||||
sqlx::query_scalar!(
|
||||
"SELECT widget_sharing_granted FROM contacts WHERE user_id = ?",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,10 @@ message EncryptedContent {
|
|||
optional bool widget_sharing_allowed = 29;
|
||||
// Sum of all exchanged media, omitted when profile sharing is disabled.
|
||||
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 Media media = 6;
|
||||
|
|
@ -177,6 +181,25 @@ message EncryptedContent {
|
|||
optional bytes avatar_svg_compressed = 2;
|
||||
optional string username = 3;
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -198,6 +198,9 @@ impl RustApi {
|
|||
|
||||
pub async fn current_user_avatar_path() -> Result<Option<String>> {
|
||||
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))
|
||||
.await
|
||||
.map_err(|error| {
|
||||
|
|
@ -206,6 +209,33 @@ impl RustApi {
|
|||
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
|
||||
/// flight and resumes any upload a terminated process left behind.
|
||||
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 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
|
||||
/// tables are deliberately absent because they are reconstructed locally.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
|||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
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
|
||||
|
||||
|
|
@ -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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
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(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
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 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
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),
|
||||
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),
|
||||
52 => wire__crate__bridge__api__rust_api_create_sticker_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),
|
||||
54 => wire__crate__bridge__api__rust_api_current_user_avatar_path_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),
|
||||
57 => wire__crate__bridge__api__rust_api_delete_memory_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),
|
||||
59 => wire__crate__bridge__api__rust_api_disable_memories_backup_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),
|
||||
61 => wire__crate__bridge__api__rust_api_download_media_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),
|
||||
63 => wire__crate__bridge__api__rust_api_ensure_avatar_png_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),
|
||||
65 => wire__crate__bridge__api__rust_api_events_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),
|
||||
67 => wire__crate__bridge__api__rust_api_force_ipa_check_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),
|
||||
69 => wire__crate__bridge__api__rust_api_get_memories_usage_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),
|
||||
71 => wire__crate__bridge__api__rust_api_get_proof_of_work_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),
|
||||
73 => wire__crate__bridge__api__rust_api_get_user_by_id_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),
|
||||
75 => wire__crate__bridge__api__rust_api_get_user_id_from_username_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),
|
||||
77 => wire__crate__bridge__api__rust_api_initialize_media_upload_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),
|
||||
79 => wire__crate__bridge__api__rust_api_insert_and_send_additional_data_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),
|
||||
81 => wire__crate__bridge__api__rust_api_insert_and_send_contact_share_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),
|
||||
83 => wire__crate__bridge__api__rust_api_ipa_purchase_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),
|
||||
85 => wire__crate__bridge__api__rust_api_media_size_limit_report_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),
|
||||
87 => wire__crate__bridge__api__rust_api_notification_badge_count_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),
|
||||
89 => wire__crate__bridge__api__rust_api_perform_passwordless_recovery_heartbeat_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),
|
||||
91 => wire__crate__bridge__api__rust_api_prewarm_memory_location_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),
|
||||
93 => wire__crate__bridge__api__rust_api_purge_widget_media_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),
|
||||
95 => wire__crate__bridge__api__rust_api_refresh_widget_manifest_impl(port, ptr, rust_vec_len, data_len),
|
||||
96 => wire__crate__bridge__api__rust_api_register_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),
|
||||
98 => wire__crate__bridge__api__rust_api_register_passwordless_notification_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),
|
||||
100 => wire__crate__bridge__api__rust_api_reload_configuration_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),
|
||||
102 => wire__crate__bridge__api__rust_api_remove_media_files_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),
|
||||
104 => wire__crate__bridge__api__rust_api_request_binary_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),
|
||||
106 => wire__crate__bridge__api__rust_api_request_media_reupload_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),
|
||||
108 => wire__crate__bridge__api__rust_api_retransmit_all_messages_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),
|
||||
110 => wire__crate__bridge__api__rust_api_reupload_pending_media_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),
|
||||
112 => wire__crate__bridge__api__rust_api_send_binary_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),
|
||||
114 => wire__crate__bridge__api__rust_api_send_encrypted_content_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),
|
||||
116 => wire__crate__bridge__api__rust_api_send_media_to_groups_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),
|
||||
118 => wire__crate__bridge__api__rust_api_send_text_message_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),
|
||||
120 => wire__crate__bridge__api__rust_api_set_background_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),
|
||||
122 => wire__crate__bridge__api__rust_api_set_login_token_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),
|
||||
124 => wire__crate__bridge__api__rust_api_set_media_requires_authentication_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),
|
||||
126 => wire__crate__bridge__api__rust_api_set_network_available_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),
|
||||
128 => wire__crate__bridge__api__rust_api_submit_recovery_share_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),
|
||||
130 => wire__crate__bridge__api__rust_api_toggle_media_remove_audio_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),
|
||||
132 => wire__crate__bridge__api__rust_api_unregister_home_widget_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),
|
||||
134 => wire__crate__bridge__api__rust_api_update_signed_pre_key_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),
|
||||
136 => wire__crate__bridge__wrapper__app_database__rust_app_database_changes_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),
|
||||
138 => wire__crate__bridge__wrapper__app_database__rust_app_database_legacy_import_complete_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),
|
||||
140 => wire__crate__bridge__wrapper__app_database__rust_app_database_select_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),
|
||||
142 => wire__crate__bridge__wrapper__backup__rust_backup_archive_get_backup_download_token_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),
|
||||
144 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_backup_id_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),
|
||||
146 => wire__crate__bridge__wrapper__backup__rust_backup_identity_get_identity_backup_bytes_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),
|
||||
148 => wire__crate__bridge__wrapper__backup__rust_backup_identity_restore_identity_backup_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),
|
||||
150 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_decrypt_cloud_media_key_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),
|
||||
152 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_get_signal_identity_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),
|
||||
154 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_import_serialized_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),
|
||||
156 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_remove_local_credentials_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),
|
||||
158 => wire__crate__bridge__wrapper__key_manager__rust_key_manager_set_user_id_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),
|
||||
160 => wire__crate__bridge__wrapper__signal__rust_signal_encrypt_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),
|
||||
162 => wire__crate__bridge__wrapper__signal__rust_signal_generate_pqc_prekeys_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),
|
||||
164 => wire__crate__bridge__wrapper__signal__rust_signal_get_safety_number_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),
|
||||
166 => wire__crate__bridge__wrapper__signal__rust_signal_process_prekey_bundle_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),
|
||||
168 => wire__crate__bridge__wrapper__rust_utils_generate_shares_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),
|
||||
170 => wire__crate__bridge__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
171 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||
172 => wire__crate__bridge__stories__story_audience_chats_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),
|
||||
174 => wire__crate__bridge__groups__update_group_name_impl(port, ptr, rust_vec_len, data_len),
|
||||
175 => wire__crate__bridge__webxdc__updates_after_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),
|
||||
178 => wire__crate__bridge__user_config__user_config_api_import_json_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),
|
||||
180 => wire__crate__bridge__user_config__user_config_api_save_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),
|
||||
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_create_sticker_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),
|
||||
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_account_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_delete_widget_media_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_done_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_download_pending_media_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_establish_signal_session_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_finish_started_media_uploads_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_custom_avatar_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_memories_usage_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_proof_of_work_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_by_id_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_get_user_id_from_username_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_initialize_media_upload_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_additional_data_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_insert_and_send_contact_share_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_ipa_purchase_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_media_size_limit_report_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_notification_badge_count_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_perform_passwordless_recovery_heartbeat_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_prewarm_memory_location_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_purge_widget_media_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_refresh_widget_manifest_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_home_widget_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_register_passwordless_recovery_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_remove_additional_user_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_remove_media_files_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_binary_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_request_media_reupload_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_retransmit_all_messages_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_reupload_pending_media_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_binary_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_encrypted_content_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_media_to_groups_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_send_text_message_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_background_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_home_widget_groups_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_set_media_display_limit_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_set_media_trim_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_store_media_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_sync_widget_permissions_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_try_request_contact_by_id_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__api__rust_api_update_fcm_token_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__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_changes_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__app_database__rust_app_database_legacy_import_complete_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__app_database__rust_app_database_select_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_archive_get_backup_download_token_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_get_backup_id_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__backup__rust_backup_identity_get_identity_backup_bytes_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__backup__rust_backup_identity_restore_identity_backup_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_decrypt_cloud_media_key_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_get_signal_identity_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_import_serialized_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__key_manager__rust_key_manager_remove_local_credentials_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__key_manager__rust_key_manager_set_user_id_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_encrypt_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_generate_pqc_prekeys_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__signal__rust_signal_get_safety_number_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__wrapper__signal__rust_signal_process_prekey_bundle_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__wrapper__rust_utils_generate_shares_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__webxdc__send_update_impl(port, ptr, rust_vec_len, data_len),
|
||||
175 => wire__crate__bridge__webxdc__serve_impl(port, ptr, rust_vec_len, data_len),
|
||||
176 => wire__crate__bridge__stories__story_audience_chats_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),
|
||||
178 => wire__crate__bridge__groups__update_group_name_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_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!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
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),
|
||||
55 => {
|
||||
56 => {
|
||||
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,
|
||||
rust_vec_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!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
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> {
|
||||
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 {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
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::user_config::UserConfig;
|
||||
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::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/ -->
|
||||
<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">
|
||||
|
|
@ -36,6 +44,367 @@ const DEFAULT_AVATAR_SVG: &str = r##"<!-- Taken from: https://getavataaars.com/
|
|||
|
||||
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>> {
|
||||
if bytes.starts_with(&[0x1f, 0x8b]) {
|
||||
let mut decoded = Vec::new();
|
||||
|
|
@ -85,6 +454,22 @@ pub(crate) async fn ensure_contact_avatar_png(
|
|||
contact_id: i64,
|
||||
) -> Result<Option<PathBuf>> {
|
||||
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!(
|
||||
r#"SELECT avatar_svg_compressed, sender_profile_counter FROM contacts WHERE user_id = ?"#,
|
||||
contact_id,
|
||||
|
|
@ -115,6 +500,35 @@ pub(crate) async fn ensure_contact_avatar_png(
|
|||
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(
|
||||
ctx: &Context,
|
||||
sender_id: i64,
|
||||
|
|
@ -281,4 +695,113 @@ mod tests {
|
|||
let image = image::load_from_memory(&png).unwrap();
|
||||
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),
|
||||
display_name: Some(config.display_name),
|
||||
avatar_svg_compressed,
|
||||
custom_avatar_requested: None,
|
||||
custom_avatar: None,
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -571,7 +571,6 @@ pub async fn pending_batch(ctx: &Arc<Context>, locale: &str) -> Result<Notificat
|
|||
r#"
|
||||
SELECT n.event_id, n.notification_id, n.conversation_id, n.sender_id,
|
||||
COALESCE(c.display_name, c.username) AS "sender_name!: String",
|
||||
c.avatar_svg_compressed, c.sender_profile_counter,
|
||||
g.group_name AS conversation_name,
|
||||
COALESCE(g.is_direct_chat, 0) AS "is_direct_chat!: i64",
|
||||
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 body = localized_body(locale, &row);
|
||||
let is_group = row.conversation_id.is_some() && row.is_direct_chat == 0;
|
||||
let avatar_path = match notification_avatar_path(
|
||||
ctx,
|
||||
row.sender_id,
|
||||
row.sender_profile_counter,
|
||||
row.avatar_svg_compressed.as_deref(),
|
||||
) {
|
||||
let avatar_path =
|
||||
match crate::services::avatars::ensure_contact_avatar_png(ctx, row.sender_id).await {
|
||||
Ok(path) => path.map(|path| path.display().to_string()),
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
|
|
@ -873,8 +868,6 @@ struct PendingRow {
|
|||
conversation_id: Option<String>,
|
||||
sender_id: i64,
|
||||
sender_name: String,
|
||||
avatar_svg_compressed: Option<Vec<u8>>,
|
||||
sender_profile_counter: i64,
|
||||
conversation_name: Option<String>,
|
||||
is_direct_chat: i64,
|
||||
message_id: Option<String>,
|
||||
|
|
@ -1091,8 +1084,6 @@ mod tests {
|
|||
conversation_id: None,
|
||||
sender_id: 7,
|
||||
sender_name: "Alice".into(),
|
||||
avatar_svg_compressed: None,
|
||||
sender_profile_counter: 0,
|
||||
conversation_name: None,
|
||||
is_direct_chat: 1,
|
||||
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