diff --git a/lib/backend/api.dart b/lib/backend/api.dart index 8b10cf9..4221c4f 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -171,7 +171,7 @@ class Api { String architecture = 'arm64'; String appVersion = SpoofingService.hardcodedAppVersion; int buildNumber = SpoofingService.hardcodedBuildNumber; - String screen = '1920x1080'; + String screen = '420dpi 420dpi 1080x2340'; tz.initializeTimeZones(); final timeZoneName = await FlutterTimezone.getLocalTimezone(); @@ -237,25 +237,25 @@ class Api { _userAgent = { 'deviceType': deviceType, - 'locale': locale, - 'deviceLocale': deviceLocale, - 'osVersion': osVersion, - 'deviceName': deviceName, 'appVersion': appVersion, - 'screen': screen, + 'osVersion': osVersion, 'timezone': timezone, + 'screen': screen, 'pushDeviceType': 'GCM', 'arch': architecture, + 'locale': locale, 'buildNumber': buildNumber, + 'deviceName': deviceName, + 'deviceLocale': deviceLocale, }; _deviceId = deviceId; final payload = { 'mt_instanceid': await DeviceIdentity.instanceId(), + 'userAgent': _userAgent, 'clientSessionId': DeviceIdentity.clientSessionId, 'deviceId': deviceId, - 'userAgent': _userAgent, }; return sendRequest(Opcode.sessionInit, payload); diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index 522901b..9c5a65a 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -674,6 +674,11 @@ class AccountModule { ); } + Future get2faStatus() async { + final trackId = await enter2faPanel(); + return get2faDetails(trackId); + } + Future check2faPassword(String trackId, String password) async { _ensureOnline(); final packet = await _api.sendRequest(Opcode.authLoginCheckPassword, { diff --git a/lib/core/config/app_stories.dart b/lib/core/config/app_stories.dart new file mode 100644 index 0000000..4f75a2d --- /dev/null +++ b/lib/core/config/app_stories.dart @@ -0,0 +1,20 @@ +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppStories { + static const prefKey = 'dev_stories'; + static const bool defaultValue = false; + + static final ValueNotifier current = ValueNotifier(defaultValue); + + static Future load() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(prefKey) ?? defaultValue; + } + + static Future save(bool value) async { + current.value = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(prefKey, value); + } +} diff --git a/lib/core/protocol/packet.dart b/lib/core/protocol/packet.dart index 75af3a2..5b2b16e 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -82,7 +82,15 @@ String messageFromErrorPayload(dynamic payload) { return s.isNotEmpty ? s : 'Неизвестная ошибка'; } -/// Упаковка пакета для отправки на сервер +/// Payload меньше этого размера отправляется без сжатия (как в оригинале). +const int _compressionThreshold = 32; + +/// Упаковка пакета для отправки на сервер. +/// +/// Payload сериализуется в MsgPack и при размере >= [_compressionThreshold] +/// сжимается LZ4-block. Старший байт поля packedLen — флаг сжатия: +/// `0` — без сжатия, иначе `(rawLen ~/ compLen) + 1` (множитель размера, по +/// которому получатель выделяет буфер под распаковку). Uint8List packPacket(int opcode, Map payload, {int seq = 0}) { final header = ByteData(headerSize); header.setUint8(0, 10); @@ -90,11 +98,19 @@ Uint8List packPacket(int opcode, Map payload, {int seq = 0}) { header.setUint16(2, seq, Endian.big); header.setUint16(4, opcode, Endian.big); - final payloadBytes = msgpack.serialize(payload); - final payloadLen = payloadBytes.length & 0xFFFFFF; - header.setUint32(6, payloadLen, Endian.big); + final raw = Uint8List.fromList(msgpack.serialize(payload)); - return Uint8List.fromList(header.buffer.asUint8List() + payloadBytes); + if (raw.length < _compressionThreshold) { + header.setUint32(6, raw.length & 0xFFFFFF, Endian.big); + return Uint8List.fromList(header.buffer.asUint8List() + raw); + } + + final compressed = lz4Compress(raw); + final compLen = compressed.length; + final flag = (raw.length ~/ compLen) + 1; + header.setUint32(6, ((flag & 0xFF) << 24) | (compLen & 0xFFFFFF), Endian.big); + + return Uint8List.fromList(header.buffer.asUint8List() + compressed); } /// Распаковка пакета от сервера diff --git a/lib/core/storage/spoofing_service.dart b/lib/core/storage/spoofing_service.dart index 548f5ad..b5e8c57 100644 --- a/lib/core/storage/spoofing_service.dart +++ b/lib/core/storage/spoofing_service.dart @@ -1,8 +1,8 @@ import 'package:shared_preferences/shared_preferences.dart'; class SpoofingService { - static const String hardcodedAppVersion = '26.14.1'; - static const int hardcodedBuildNumber = 6606; + static const String hardcodedAppVersion = '26.17.1'; + static const int hardcodedBuildNumber = 6712; static Future?> getSpoofedSessionData() async { final prefs = await SharedPreferences.getInstance(); diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 6790abd..4b66e47 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -19,6 +19,7 @@ import '../auth/login_screen.dart'; import '../../widgets/account_switcher_overlay.dart'; import '../../../backend/api.dart'; import '../../../core/utils/haptics.dart'; +import '../../../core/config/app_stories.dart'; import '../../../backend/models/chat_folder.dart'; import '../../../backend/modules/account.dart'; import '../../../backend/modules/chats.dart'; @@ -406,6 +407,7 @@ class _ChatListScreenState extends State } bool _allowStoriesPullOverscrollTop() { + if (!AppStories.current.value) return false; if (_storiesDockedOpen || _storiesRevealController.isAnimating || _pullRatio > 0) { @@ -462,9 +464,22 @@ class _ChatListScreenState extends State } }); ChatsModule.chatsChanged.addListener(_onChatsChanged); + AppStories.current.addListener(_onStoriesEnabledChanged); _reloadChatsAndFolders(); } + void _onStoriesEnabledChanged() { + if (!mounted) return; + if (!AppStories.current.value) { + _storiesRevealController.stop(); + _pullRatio = 0; + _storiesDockedOpen = false; + _storiesAnimClosing = false; + _storiesOverscrollRevealArmed = false; + } + setState(() {}); + } + @override void didChangeDependencies() { super.didChangeDependencies(); @@ -977,6 +992,7 @@ class _ChatListScreenState extends State appRouteObserver.unsubscribe(this); _settleTimer?.cancel(); ChatsModule.chatsChanged.removeListener(_onChatsChanged); + AppStories.current.removeListener(_onStoriesEnabledChanged); _loginSub?.cancel(); _stateSub?.cancel(); _fabController.dispose(); @@ -1077,7 +1093,8 @@ class _ChatListScreenState extends State children: [ Row( children: [ - if (_pullRatio < 0.8) + if (AppStories.current.value && + _pullRatio < 0.8) Opacity( opacity: 1.0 - _pullRatio, child: Container( @@ -1156,30 +1173,31 @@ class _ChatListScreenState extends State ], ), ), - SizedBox( - height: 96 * _pullRatio, - child: Opacity( - opacity: _pullRatio, - child: ListView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric( - horizontal: 20, + if (AppStories.current.value) + SizedBox( + height: 96 * _pullRatio, + child: Opacity( + opacity: _pullRatio, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + children: [ + _buildStoryItem( + 'Даша', + 'https://i.pravatar.cc/150?u=dasha', + true, + ), + _buildStoryItem( + 'Мастика', + 'https://i.pravatar.cc/150?u=mastika', + false, + ), + ], ), - children: [ - _buildStoryItem( - 'Даша', - 'https://i.pravatar.cc/150?u=dasha', - true, - ), - _buildStoryItem( - 'Мастика', - 'https://i.pravatar.cc/150?u=mastika', - false, - ), - ], ), ), - ), Padding( padding: const EdgeInsets.fromLTRB(20, 3, 20, 4), child: Container( diff --git a/lib/frontend/screens/profile/debug_menu_screen.dart b/lib/frontend/screens/profile/debug_menu_screen.dart index 0fcfd9e..6902b56 100644 --- a/lib/frontend/screens/profile/debug_menu_screen.dart +++ b/lib/frontend/screens/profile/debug_menu_screen.dart @@ -5,6 +5,7 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../../backend/modules/chats.dart'; import '../../../core/config/app_swipe_back_desktop.dart'; import '../../../core/config/app_pranks.dart'; +import '../../../core/config/app_stories.dart'; import '../../../core/protocol/opcode_map.dart'; import '../../../core/protocol/packet.dart'; import '../../../core/utils/logger.dart'; @@ -453,6 +454,68 @@ class _DebugMenuScreenState extends State { ), ), ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: ValueListenableBuilder( + valueListenable: AppStories.current, + builder: (context, storiesOn, _) { + return Container( + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, + ), + child: Row( + children: [ + Icon( + Symbols.amp_stories, + color: cs.onSurfaceVariant, + size: 22, + weight: 400, + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Истории', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 2), + Text( + 'Отображение ленты историй в списке чатов', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, + ), + ), + ], + ), + ), + Switch( + value: storiesOn, + onChanged: (v) { + AppStories.save(v); + }, + ), + ], + ), + ), + ); + }, + ), + ), + ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), diff --git a/lib/frontend/screens/profile/password_entry_screen.dart b/lib/frontend/screens/profile/password_entry_screen.dart index f127133..84ee9b9 100644 --- a/lib/frontend/screens/profile/password_entry_screen.dart +++ b/lib/frontend/screens/profile/password_entry_screen.dart @@ -24,10 +24,16 @@ class _PasswordEntryScreenState extends State { Future _check2faStatus() async { try { - final profile = await AppDatabase.loadActiveProfile(); + bool is2faEnabled; + try { + is2faEnabled = (await accountModule.get2faStatus()).enabled; + } catch (_) { + final profile = await AppDatabase.loadActiveProfile(); + is2faEnabled = profile?.profileOptions?.contains(2) ?? false; + } if (mounted) { setState(() { - _is2faEnabled = profile?.profileOptions?.contains(2) ?? false; + _is2faEnabled = is2faEnabled; _isLoading = false; }); } diff --git a/lib/frontend/screens/profile/security_screen.dart b/lib/frontend/screens/profile/security_screen.dart index a33306c..680234b 100644 --- a/lib/frontend/screens/profile/security_screen.dart +++ b/lib/frontend/screens/profile/security_screen.dart @@ -47,12 +47,18 @@ class _SecurityScreenState extends State accountModule.getBlockedContacts(), AppDatabase.loadActiveProfile(), ]); + bool is2faEnabled; + try { + is2faEnabled = (await accountModule.get2faStatus()).enabled; + } catch (_) { + final profile = results[2] as ProfileData?; + is2faEnabled = profile?.profileOptions?.contains(2) ?? false; + } if (mounted) { setState(() { _privacyConfig = results[0] as PrivacyConfig; _blockedContacts = results[1] as List; - final profile = results[2] as ProfileData?; - _is2faEnabled = profile?.profileOptions?.contains(2) ?? false; + _is2faEnabled = is2faEnabled; _isLoading = false; }); } diff --git a/lib/main.dart b/lib/main.dart index 9f611f9..4ddfaca 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,7 @@ import 'core/config/app_fonts.dart'; import 'core/config/app_message_actions_style.dart'; import 'core/config/app_swipe_back_desktop.dart'; import 'core/config/app_pranks.dart'; +import 'core/config/app_stories.dart'; import 'core/config/app_theme_mode.dart'; import 'core/config/app_theme_schedule.dart'; import 'backend/modules/account.dart'; @@ -84,6 +85,7 @@ void main() async { final messageActionsFuture = AppMessageActionsStyle.load(); final swipeBackFuture = AppSwipeBackDesktop.load(); final pranksFuture = AppPranks.load(); + final storiesFuture = AppStories.load(); await api.connect(); @@ -116,6 +118,7 @@ void main() async { AppMessageActionsStyle.current.value = await messageActionsFuture; AppSwipeBackDesktop.current.value = await swipeBackFuture; AppPranks.current.value = await pranksFuture; + AppStories.current.value = await storiesFuture; runApp( KometApp( initialLocale: initialLocale,