From 0ba39b3575fa41387e28e369dc4b8f6dfde99872 Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Wed, 15 Jul 2026 19:33:25 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B1=D0=B5=D1=80=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=BA.=20=D0=A0=D0=B5=D0=B3=D0=B5=D0=BD=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20profile=20=D0=B5=D1=81=D0=BB=D0=B8=20?= =?UTF-8?q?=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=20=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BD=D0=B5=20=D0=BE=D1=82=D0=B4=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/account.dart | 45 ++++++++--- .../modules/account/account_models.dart | 2 + lib/backend/modules/contacts.dart | 15 ++++ lib/core/config/debug_test.dart | 6 ++ lib/core/storage/app_database.dart | 11 ++- .../screens/profile/settings_tab.dart | 75 ++++++++++++------- lib/l10n/app_en.arb | 4 +- lib/l10n/app_localizations.dart | 12 +++ lib/l10n/app_localizations_en.dart | 8 ++ lib/l10n/app_localizations_ru.dart | 8 ++ lib/l10n/app_ru.arb | 4 +- lib/main.dart | 14 ++++ 12 files changed, 164 insertions(+), 40 deletions(-) diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index a322a93..babf93c 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import '../api.dart'; +import '../../core/config/debug_test.dart'; import '../../core/config/komet_settings.dart'; import '../../core/protocol/chat_cache_fingerprint.dart'; import '../../core/protocol/opcode_map.dart'; @@ -41,6 +42,7 @@ class AccountModule { late final ProfileModule _profile = ProfileModule(_api); late final TwoFactorModule _twoFactor = TwoFactorModule(_api, _profile); final _loginStatusController = StreamController.broadcast(); + final _noticeController = StreamController.broadcast(); bool _loggedIn = false; AccountModule(this._api) { @@ -51,6 +53,8 @@ class AccountModule { Stream get loginStatusStream => _loginStatusController.stream; + Stream get noticeStream => _noticeController.stream; + /// `true`, только когда сервер считает сессию ONLINE — после успешного /// login (opcode 19), а не просто после хэндшейка (opcode 6). bool get isLoggedIn => _loggedIn; @@ -565,22 +569,16 @@ class AccountModule { ProfileData profile; final profileMap = data['profile']; - if (profileMap is Map) { - final contact = profileMap['contact']; - if (contact is! Map) { - throw Exception('login: отсутствует profile.contact в ответе'); - } + if (!DebugTest.berserk && + profileMap is Map && + profileMap['contact'] is Map) { profile = ProfileData.fromServerProfile( profileMap.cast(), ); - await AppDatabase.saveProfile(profile, isActive: true); } else { - final cachedProfile = await AppDatabase.loadProfile(accountId); - if (cachedProfile == null) { - throw Exception('login: отсутствует profile в ответе'); - } - profile = cachedProfile; + profile = await _resurrectProfile(accountId); } + await AppDatabase.saveProfile(profile, isActive: true); await AppDatabase.setActiveAccount(profile.id); await _saveSyncState(data, serverTime, profile.id); @@ -625,6 +623,31 @@ class AccountModule { ); } + Future _resurrectProfile(int accountId) async { + if (DebugTest.berserk) { + await AppDatabase.deleteAccount(accountId); + logger.w('login: [BERSERK] профиль удалён из БД, форсирую регенерацию (id=$accountId)'); + } else { + final cached = await AppDatabase.loadProfile(accountId); + if (cached != null) return cached; + } + + _noticeController.add(AccountNotice.resurrectingProfile); + + try { + final fetched = await ContactsModule.fetchSelfProfile(_api, accountId); + if (fetched != null) { + logger.i('login: профиль восстановлен через CONTACT_INFO (id=$accountId)'); + return fetched; + } + } catch (e) { + logger.w('login: восстановление профиля через CONTACT_INFO не удалось: $e'); + } + + logger.w('login: профиль недоступен, использую заглушку (id=$accountId)'); + return ProfileData.stub(accountId); + } + Future _saveSyncState( Map data, int serverTime, diff --git a/lib/backend/modules/account/account_models.dart b/lib/backend/modules/account/account_models.dart index a5b2d47..a8bb7e3 100644 --- a/lib/backend/modules/account/account_models.dart +++ b/lib/backend/modules/account/account_models.dart @@ -282,6 +282,8 @@ enum AuthRequestType { enum LoginStatus { idle, loading, success, error } +enum AccountNotice { resurrectingProfile } + class WrongDeviceTokenException implements Exception { const WrongDeviceTokenException(); @override diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index 29b3c71..33fc381 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -191,6 +191,21 @@ class ContactsModule { await syncFromLoginPayload(map.cast(), accountId); } + static Future fetchSelfProfile(Api api, int accountId) async { + final map = await api.sendRequestMap(Opcode.contactInfo, { + 'contactIds': [accountId], + }); + final contacts = map?['contacts']; + if (contacts is! List) return null; + for (final raw in contacts.whereType()) { + if (raw['id'] != accountId) continue; + final contact = raw.cast(); + _primeContactCache(contact); + return ProfileData.fromServerMap(contact); + } + return null; + } + static void _primeContactCache(Map contact) { final id = contact['id']; if (id is! int) return; diff --git a/lib/core/config/debug_test.dart b/lib/core/config/debug_test.dart index e0176b9..fd30cd7 100644 --- a/lib/core/config/debug_test.dart +++ b/lib/core/config/debug_test.dart @@ -1,20 +1,24 @@ class DebugTest { static bool enabled = false; static int contactCount = 0; + static bool berserk = false; static const int debugAccountId = -424242; static const bool _envEnabled = bool.fromEnvironment('DEBUG_TEST'); + static const bool _envBerserk = bool.fromEnvironment('BERSERK'); static const int _envContacts = int.fromEnvironment( 'DEBUG_CONTACTS', defaultValue: -1, ); static const String _flag = '--debug-test'; + static const String _berserkFlag = '--berserk'; static const String _contactsFlag = '--contacts'; static void parse(List args) { if (_envEnabled) enabled = true; + if (_envBerserk) berserk = true; if (_envContacts >= 0) { enabled = true; contactCount = _envContacts; @@ -24,6 +28,8 @@ class DebugTest { final arg = args[i]; if (arg == _flag) { enabled = true; + } else if (arg == _berserkFlag) { + berserk = true; } else if (arg.startsWith('$_contactsFlag=')) { enabled = true; contactCount = diff --git a/lib/core/storage/app_database.dart b/lib/core/storage/app_database.dart index 9c03f5c..bb5caa8 100644 --- a/lib/core/storage/app_database.dart +++ b/lib/core/storage/app_database.dart @@ -36,6 +36,15 @@ class ProfileData { this.profileOptions, }); + factory ProfileData.stub(int id) => ProfileData( + id: id, + firstName: '', + phone: 0, + country: '', + accountStatus: 0, + updateTime: 0, + ); + factory ProfileData.fromServerProfile(Map profile) { final contact = profile['contact']; if (contact is! Map) { @@ -70,7 +79,7 @@ class ProfileData { id: contact['id'] as int, firstName: firstName, lastName: lastName, - phone: contact['phone'] as int, + phone: (contact['phone'] as int?) ?? 0, photoId: contact['photoId'] as int?, baseUrl: contact['baseUrl'] as String?, baseRawUrl: contact['baseRawUrl'] as String?, diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 19c962a..0fb5da2 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -340,7 +340,9 @@ class _SettingsTabState extends State { final String fullName = '${_profile!.firstName}${_profile!.lastName != null ? ' ${_profile!.lastName}' : ''}'; - final String phone = '+${_profile!.phone}'; + final String phone = _profile!.phone == 0 + ? l10n.profilePhoneRegenFailed + : '+${_profile!.phone}'; final size = MediaQuery.sizeOf(context); final topPad = MediaQuery.paddingOf(context).top; @@ -672,6 +674,7 @@ class _SettingsTabState extends State { ) { final topPad = MediaQuery.paddingOf(context).top; final hasPhoto = (_profile?.baseUrl ?? '').isNotEmpty; + final phoneMissing = (_profile?.phone ?? 0) == 0; final pt = hasPhoto ? t : 0.0; if (pt > 0) _headerEverExpanded = true; @@ -826,37 +829,57 @@ class _SettingsTabState extends State { const SizedBox(height: 6), _headerAligned( pt, - Row( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () => setState( - () => _isPhoneVisible = !_isPhoneVisible, - ), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: _PhoneSpoiler( - text: phone, - isVisible: _isPhoneVisible, + phoneMissing + ? Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Text( + phone, + textAlign: TextAlign.center, style: TextStyle( color: subColor, - fontSize: 14, + fontSize: 12, fontWeight: FontWeight.w400, - letterSpacing: 0.5, + height: 1.3, ), ), + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () => setState( + () => _isPhoneVisible = !_isPhoneVisible, + ), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: _PhoneSpoiler( + text: phone, + isVisible: _isPhoneVisible, + style: TextStyle( + color: subColor, + fontSize: 14, + fontWeight: FontWeight.w400, + letterSpacing: 0.5, + ), + ), + ), + ), + const SizedBox(width: 4), + Icon( + _isPhoneVisible + ? Symbols.visibility + : Symbols.visibility_off, + size: 14, + color: Color.lerp( + cs.mutedText, + Colors.white70, + pt, + ), + ), + ], ), - ), - const SizedBox(width: 4), - Icon( - _isPhoneVisible - ? Symbols.visibility - : Symbols.visibility_off, - size: 14, - color: Color.lerp(cs.mutedText, Colors.white70, pt), - ), - ], - ), ), ], ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 861cf4f..addc23b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -907,5 +907,7 @@ "updateCheck": "Check for updates", "updateChecking": "Checking for updates…", "updateUpToDate": "You have the latest version", - "updateCheckFailed": "Couldn't check for updates. Try again later" + "updateCheckFailed": "Couldn't check for updates. Try again later", + "profileResurrecting": "Oops! The server didn't send your profile. Trying to regenerate…", + "profilePhoneRegenFailed": "Couldn't regenerate your phone number. Please sign in again and report the issue to the developers" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 3069059..8a6fb3a 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4045,6 +4045,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Couldn\'t check for updates. Try again later'** String get updateCheckFailed; + + /// No description provided for @profileResurrecting. + /// + /// In en, this message translates to: + /// **'Oops! The server didn\'t send your profile. Trying to regenerate…'** + String get profileResurrecting; + + /// No description provided for @profilePhoneRegenFailed. + /// + /// In en, this message translates to: + /// **'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers'** + String get profilePhoneRegenFailed; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 218904c..37a3e3d 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2097,4 +2097,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get updateCheckFailed => 'Couldn\'t check for updates. Try again later'; + + @override + String get profileResurrecting => + 'Oops! The server didn\'t send your profile. Trying to regenerate…'; + + @override + String get profilePhoneRegenFailed => + 'Couldn\'t regenerate your phone number. Please sign in again and report the issue to the developers'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index a81bbf5..ba799ba 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2108,4 +2108,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get updateCheckFailed => 'Не удалось проверить обновления. Повторите позже'; + + @override + String get profileResurrecting => + 'Упс! Сервер не прислал profile. Попробую регенерировать…'; + + @override + String get profilePhoneRegenFailed => + 'Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам'; } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0ace50d..90ea00b 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -689,5 +689,7 @@ "updateCheck": "Проверить обновление", "updateChecking": "Проверяем обновления…", "updateUpToDate": "Установлена актуальная версия", - "updateCheckFailed": "Не удалось проверить обновления. Повторите позже" + "updateCheckFailed": "Не удалось проверить обновления. Повторите позже", + "profileResurrecting": "Упс! Сервер не прислал profile. Попробую регенерировать…", + "profilePhoneRegenFailed": "Не удалось регенерировать данные об номере. Перезайдите и сообщите об проблеме разработчикам" } diff --git a/lib/main.dart b/lib/main.dart index a583f13..f3d0a14 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -339,6 +339,7 @@ class KometAppState extends State StreamSubscription? _vpnBypassSub; StreamSubscription? _callIncomingSub; StreamSubscription? _serverErrorSub; + StreamSubscription? _accountNoticeSub; Timer? _scheduleTimer; String? _lastVpnNotice; DateTime _lastVpnNoticeAt = DateTime.fromMillisecondsSinceEpoch(0); @@ -477,6 +478,18 @@ class KometAppState extends State showCustomNotificationOnOverlay(overlay, msg); } }); + + _accountNoticeSub = accountModule.noticeStream.listen((notice) { + final overlay = KometApp.navigatorKey.currentState?.overlay; + final ctx = KometApp.navigatorKey.currentContext; + if (overlay == null || ctx == null || !ctx.mounted) return; + final l10n = AppLocalizations.of(ctx); + if (l10n == null) return; + final message = switch (notice) { + AccountNotice.resurrectingProfile => l10n.profileResurrecting, + }; + showCustomNotificationOnOverlay(overlay, message); + }); } Future _ensureFullScreenIntentPermission() async { @@ -534,6 +547,7 @@ class KometAppState extends State _vpnBypassSub?.cancel(); _callIncomingSub?.cancel(); _serverErrorSub?.cancel(); + _accountNoticeSub?.cancel(); _scheduleTimer?.cancel(); AppThemeModeConfig.current.removeListener(_onThemeModeChanged); AppAmoled.current.removeListener(_onAmoledChanged);