From fcf29440e9b637a6b1573073b55311793b2965f6 Mon Sep 17 00:00:00 2001 From: Jganenok Date: Sun, 19 Apr 2026 12:30:44 +0700 Subject: [PATCH 1/7] =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=B3?= =?UTF-8?q?=D0=BE=D0=B2=D0=BD=D0=BE=20=D0=B7=D0=B0=20=D0=B8=D0=BD=D0=B2?= =?UTF-8?q?=D0=B8=D1=81=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../screens/chats/chat_list_screen.dart | 22 ++++--- lib/frontend/screens/chats/chat_screen.dart | 39 ++++++++++++ lib/frontend/widgets/message_bubble.dart | 59 +++++++++---------- 3 files changed, 81 insertions(+), 39 deletions(-) diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 986cbf2..d00b667 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -1042,6 +1042,7 @@ class _ChatListScreenState extends State isOnline: chat.isOnline, unreadCount: chat.unreadCount, isMuted: chat.dontDisturbUntil > 0, + chatType: "DIALOG", ); } else { if (chat.lastMsgSenderId != null ) { @@ -1077,6 +1078,7 @@ class _ChatListScreenState extends State isOnline: chat.isOnline, unreadCount: chat.unreadCount, isMuted: chat.dontDisturbUntil > 0, + chatType: chat.type, ); } }, childCount: _isInitialLoading ? 10 : chats.length), @@ -1679,6 +1681,7 @@ class _ChatListScreenState extends State bool isRead = false, int unreadCount = 0, bool isMuted = false, + String chatType = "CHAT", }) { final cs = Theme.of(context).colorScheme; final isSelected = _selectedChats.contains(id); @@ -1688,16 +1691,17 @@ class _ChatListScreenState extends State if (_isSelectionMode) { _toggleSelection(id); } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ChatScreen( - chatId: int.parse(id), - name: name, - imageUrl: imageUrl, +Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ChatScreen( + chatId: int.parse(id), + name: name, + imageUrl: imageUrl, + chatType: chatType, + ), ), - ), - ); + ); } }, onLongPress: () => _toggleSelection(id), diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 28fb06d..f127ce2 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -14,12 +14,14 @@ class ChatScreen extends StatefulWidget { final int chatId; final String name; final String imageUrl; + final String chatType; const ChatScreen({ super.key, required this.chatId, required this.name, required this.imageUrl, + required this.chatType, }); @override @@ -469,6 +471,43 @@ class _ChatScreenState extends State Widget _buildInputArea(BuildContext context) { final cs = Theme.of(context).colorScheme; final mutedIcon = cs.onSurfaceVariant.withValues(alpha: 0.85); + + if (widget.chatType == "CHANNEL") { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: GestureDetector( + onTap: () {}, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: Color.alphaBlend( + cs.surfaceContainerHighest.withValues(alpha: 0.92), + cs.surface, + ), + borderRadius: BorderRadius.circular(28), + border: Border.all( + color: cs.outlineVariant.withValues(alpha: 0.5), + width: 0.5, + ), + ), + child: Center( + child: Text( + 'Отключить уведомления', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ); + } + return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index 74c4ed9..819b7cc 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -296,41 +296,41 @@ class MessageBubble extends StatelessWidget { return Padding( padding: EdgeInsets.only( - left: isMe ? 60 : 12, - right: isMe ? 12 : 60, + left: isMe ? 12 : 12, + right: isMe ? 12 : 12, top: topMargin, bottom: bottomMargin, ), child: Align( child: Row( mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start, - spacing: 8.0, + crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (senderAvatar != null && senderAvatar.isNotEmpty && !isMe && chatType != "DIALOG" - && nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId) - CircleAvatar( - radius: 15, - backgroundImage: NetworkImage(senderAvatar), - backgroundColor: cs.primaryContainer, - ) - else if (displaySender != null && !isMe && chatType != "DIALOG" - && nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId) - CircleAvatar( - radius: 15, - backgroundColor: cs.primaryContainer, - child: Text( - displaySender!.isNotEmpty - ? displaySender[0].toUpperCase() - : '?', - style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer), - ), - ) - // Заглушка для паддинга - else - CircleAvatar( - radius: 15, - backgroundColor: Color(0x00000000) - ), + if (!isMe && chatType == "CHAT" && nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId) + ...(senderAvatar != null && senderAvatar.isNotEmpty) + ? [ + CircleAvatar( + radius: 15, + backgroundImage: NetworkImage(senderAvatar), + backgroundColor: cs.primaryContainer, + ) + ] + : [ + CircleAvatar( + radius: 15, + backgroundColor: cs.primaryContainer, + child: Text( + displaySender != null && displaySender.isNotEmpty + ? displaySender[0].toUpperCase() + : '?', + style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer), + ), + ) + ] + else if (!isMe && chatType != "CHAT") + SizedBox(width: 0) + else if (!isMe) + CircleAvatar(radius: 15, backgroundColor: Color(0x00000000)), Container( constraints: BoxConstraints( maxWidth: MediaQuery.of(context).size.width * 0.75, @@ -389,11 +389,10 @@ class MessageBubble extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (message.senderId != message.accountId && prevMessage?.senderId != message.senderId) + if (message.senderId != message.accountId && prevMessage?.senderId != message.senderId && chatType == "CHAT") Text( displaySender ?? "", textAlign: TextAlign.left, - // TODO: Получение цветов по хешу ника style: TextStyle(color: cs.onPrimaryContainer) ), Row( From 4ebfb3353ffd95cbb6ee2053287e12dfa0c6288d Mon Sep 17 00:00:00 2001 From: Jganenok Date: Tue, 21 Apr 2026 17:31:36 +0700 Subject: [PATCH 2/7] =?UTF-8?q?=D1=85=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/messages.dart | 31 ++++++++++++------- .../screens/chats/chat_list_screen.dart | 6 ---- lib/frontend/widgets/message_bubble.dart | 4 --- lib/models/attachment.dart | 28 ++++++++--------- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index 00dd1c0..1526cac 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -75,13 +75,13 @@ class CachedMessage { } return CachedMessage( - id: row['id'] as String, - accountId: row['account_id'] as int, - chatId: row['chat_id'] as int, - senderId: row['sender_id'] as int, - text: row['text'] as String?, - time: row['time'] as int, - status: row['status'] as String?, + id: row['id']?.toString() ?? '', + accountId: row['account_id'] is int ? row['account_id'] as int : int.tryParse(row['account_id']?.toString() ?? '') ?? 0, + chatId: row['chat_id'] is int ? row['chat_id'] as int : int.tryParse(row['chat_id']?.toString() ?? '') ?? 0, + senderId: row['sender_id'] is int ? row['sender_id'] as int : int.tryParse(row['sender_id']?.toString() ?? '') ?? 0, + text: row['text']?.toString(), + time: row['time'] is int ? row['time'] as int : int.tryParse(row['time']?.toString() ?? '') ?? 0, + status: row['status']?.toString(), payload: payload, attachments: attachments, ); @@ -209,15 +209,22 @@ class MessagesModule { id: id, accountId: accountId, chatId: chatId, - senderId: (m['sender'] as int?) ?? 0, - text: m['text'] as String?, - time: (m['time'] as int?) ?? 0, - status: m['status'] as String?, + senderId: _parseIntField(m['sender']), + text: m['text']?.toString(), + time: _parseIntField(m['time']), + status: m['status']?.toString(), payload: Map.from(m.cast()), attachments: attachments, ); } + int _parseIntField(dynamic value) { + if (value == null) return 0; + if (value is int) return value; + if (value is String) return int.tryParse(value) ?? 0; + return int.tryParse(value.toString()) ?? 0; + } + Future sendMessage( int accountId, int chatId, @@ -356,6 +363,8 @@ class MessagesModule { final cached = ContactCache.get(contactId); if (cached != null) return cached; + if (_api.state != SessionState.online) return null; + try { final response = await _api.sendRequest(Opcode.contactInfo, { 'contactIds': [contactId], diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index d00b667..9a9878b 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -1028,8 +1028,6 @@ class _ChatListScreenState extends State if (chat.type.isNotEmpty && chat.type == "DIALOG" && chat.id != 0) { final secondId = chat.participants.entries.where((entry) => entry.key != _profile?.id).first.key; - // TODO: Нормальное кеширование контактов - final ss = messagesModule.searchContactById(secondId); final name = ContactCache.get(secondId); final avatar = ContactCache.getAvatar(secondId); @@ -1045,10 +1043,6 @@ class _ChatListScreenState extends State chatType: "DIALOG", ); } else { - if (chat.lastMsgSenderId != null ) { - final ss = messagesModule.searchContactById(chat.lastMsgSenderId!); - } - final name = chat.lastMsgSenderId != null ? ContactCache.get(chat.lastMsgSenderId!) : null; diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index 819b7cc..1fc12c2 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -289,8 +289,6 @@ class MessageBubble extends StatelessWidget { final cs = Theme.of(context).colorScheme; final isDark = cs.brightness == Brightness.dark; - // TODO: Нормальное кеширование контактов - final ss = messagesModule.searchContactById(message.senderId); String? senderAvatar = ContactCache.getAvatar(message.senderId); String? displaySender = ContactCache.get(message.senderId); @@ -382,8 +380,6 @@ class MessageBubble extends StatelessWidget { final forwarded = _getForwardedAttachment(); final isForwarded = forwarded != null && !isForwardedContact; - // TODO: Нормальное кеширование контактов - final ss = messagesModule.searchContactById(message.senderId); String? displaySender = ContactCache.get(message.senderId); return Column( diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 2d3b6b1..25ce0a6 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -344,15 +344,15 @@ class ContactAttachment extends MessageAttachment { factory ContactAttachment.fromMap(Map map) { return ContactAttachment( - previewData: map['previewData'] as String?, - baseUrl: map['baseUrl'] as String?, - userId: map['userId'] as String?, - firstName: map['firstName'] as String?, - lastName: map['lastName'] as String?, - phoneNumber: map['phoneNumber'] as String?, - photoUrl: map['photoUrl'] as String?, - contactId: map['contactId'] as int?, - name: map['name'] as String?, + previewData: map['previewData']?.toString(), + baseUrl: map['baseUrl']?.toString(), + userId: map['userId']?.toString(), + firstName: map['firstName']?.toString(), + lastName: map['lastName']?.toString(), + phoneNumber: map['phoneNumber']?.toString(), + photoUrl: map['photoUrl']?.toString(), + contactId: map['contactId'] is int ? map['contactId'] as int : int.tryParse(map['contactId']?.toString() ?? ''), + name: map['name']?.toString(), ); } @@ -426,11 +426,11 @@ class ControlAttachment extends MessageAttachment { factory ControlAttachment.fromMap(Map map) { return ControlAttachment( - previewData: map['previewData'] as String?, - baseUrl: map['baseUrl'] as String?, - event: map['event'] as String?, - title: map['title'] as String?, - userIds: (map['userIds'] as List?)?.cast(), + previewData: map['previewData']?.toString(), + baseUrl: map['baseUrl']?.toString(), + event: map['event']?.toString(), + title: map['title']?.toString(), + userIds: (map['userIds'] as List?)?.map((e) => e is int ? e : int.tryParse(e?.toString() ?? '') ?? 0).toList(), ); } From 602c9245038528aa13ecce8a3701a1afa2a36a0e Mon Sep 17 00:00:00 2001 From: klockky Date: Tue, 21 Apr 2026 18:59:35 +0300 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=D0=B0=D0=B2=D1=82=D0=BE=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D1=80=D1=8B=D1=82=D0=B8=D0=B5=20=D0=BA=D0=BB=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=B0=D1=82=D1=83=D1=80=D1=8B=20=D0=BD=D0=B0=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=D0=B5=20=D0=BA=D0=BE=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/code_confirmation_screen.dart | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart index 3840751..512a938 100644 --- a/lib/frontend/screens/auth/code_confirmation_screen.dart +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -34,13 +34,14 @@ class _CodeConfirmationScreenState extends State late AnimationController _shakeController; late Animation _shakeAnimation; + bool _keyboardScheduled = false; + Animation? _routeAnimation; + AnimationStatusListener? _routeAnimationListener; + @override void initState() { super.initState(); _startTimer(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _focusNode.requestFocus(); - }); _shakeController = AnimationController( vsync: this, @@ -56,8 +57,17 @@ class _CodeConfirmationScreenState extends State ]).animate(CurvedAnimation(parent: _shakeController, curve: Curves.linear)); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _scheduleKeyboardOpen(); + } + @override void dispose() { + if (_routeAnimationListener != null) { + _routeAnimation?.removeStatusListener(_routeAnimationListener!); + } _timer?.cancel(); _errorTimer?.cancel(); _shakeController.dispose(); @@ -66,6 +76,36 @@ class _CodeConfirmationScreenState extends State super.dispose(); } + void _scheduleKeyboardOpen() { + if (_keyboardScheduled) return; + _keyboardScheduled = true; + + final animation = ModalRoute.of(context)?.animation; + if (animation == null || animation.status == AnimationStatus.completed) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _openKeyboard(); + }); + return; + } + + _routeAnimation = animation; + _routeAnimationListener = (status) { + if (status == AnimationStatus.completed) { + animation.removeStatusListener(_routeAnimationListener!); + _routeAnimationListener = null; + if (mounted) _openKeyboard(); + } + }; + animation.addStatusListener(_routeAnimationListener!); + } + + void _openKeyboard() { + if (!_focusNode.hasFocus) { + _focusNode.requestFocus(); + } + SystemChannels.textInput.invokeMethod('TextInput.show'); + } + void _startTimer() { _timer?.cancel(); _timerSeconds = 30; @@ -215,7 +255,7 @@ class _CodeConfirmationScreenState extends State ), ), GestureDetector( - onTap: () => _focusNode.requestFocus(), + onTap: _openKeyboard, child: FittedBox( child: Row( children: List.generate(6, (index) { From 5db5dfabf5b454eb7945da8423bb1e3b2bf5ec32 Mon Sep 17 00:00:00 2001 From: InviseDivine Date: Tue, 28 Apr 2026 19:11:43 +0200 Subject: [PATCH 4/7] chat info not finished --- .../screens/chats/chat_info_screen.dart | 61 ++++++++ lib/frontend/screens/chats/chat_screen.dart | 140 ++++++++++-------- lib/frontend/widgets/message_bubble.dart | 114 +++++++------- 3 files changed, 198 insertions(+), 117 deletions(-) create mode 100644 lib/frontend/screens/chats/chat_info_screen.dart diff --git a/lib/frontend/screens/chats/chat_info_screen.dart b/lib/frontend/screens/chats/chat_info_screen.dart new file mode 100644 index 0000000..c50ae93 --- /dev/null +++ b/lib/frontend/screens/chats/chat_info_screen.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +class ChatInfoScreen extends StatefulWidget { + final int chatId; + final String name; + final String imageUrl; + final String chatType; + + const ChatInfoScreen({ + super.key, + required this.chatId, + required this.name, + required this.imageUrl, + required this.chatType, + }); + @override + State createState() => _ChatInfoScreenState(); +} +class _ChatInfoScreenState extends State{ + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Scaffold( + backgroundColor: cs.surface, + appBar: AppBar( + + ), + body: + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + if (widget.imageUrl.isNotEmpty) + CircleAvatar( + radius: 36, + backgroundImage: NetworkImage(widget.imageUrl), + ) + else + CircleAvatar( + radius: 36, + backgroundColor: cs.primaryContainer, + child: Text( + widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?', + style: TextStyle(color: cs.onPrimaryContainer, fontSize: 12), + ), + ), + Text( + widget.name, + style: TextStyle(color: Colors.white, fontSize: 24, height: 1.3), + ), + + ], + ), + ), + ); + } +} + diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index f127ce2..41069ac 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:komet/backend/modules/chats.dart'; +import 'package:komet/frontend/screens/chats/chat_info_screen.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../main.dart'; import '../../../backend/api.dart'; @@ -256,71 +257,84 @@ class _ChatScreenState extends State String? status = chat?.type == "CHAT" ? "${chat?.participants.length.toString()} участников" : "last seen recently"; return Scaffold( backgroundColor: cs.surface, - appBar: AppBar( - backgroundColor: cs.surfaceContainerHigh, - foregroundColor: cs.onSurface, - elevation: 0, - surfaceTintColor: Colors.transparent, - iconTheme: IconThemeData(color: cs.onSurface), - leading: IconButton( - icon: const Icon(Symbols.arrow_back, weight: 400), - onPressed: () => Navigator.pop(context), - ), - titleSpacing: 0, - title: Row( - children: [ - if (widget.imageUrl.isNotEmpty) - CircleAvatar( - radius: 18, - backgroundImage: NetworkImage(widget.imageUrl), - ) - else - CircleAvatar( - radius: 18, - backgroundColor: cs.primaryContainer, - child: Text( - widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?', - style: TextStyle(color: cs.onPrimaryContainer, fontSize: 12), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.name, - style: TextStyle( - color: cs.onSurface, - fontSize: 16, - fontWeight: FontWeight.w600, - fontFamily: 'Outfit', - ), - ), - Text( - status ?? "", - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 12, - fontWeight: FontWeight.w400, - ), - ), - ], - ), + appBar: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: InkWell( + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (context) => ChatInfoScreen( + chatId: widget.chatId, + name: widget.name, + imageUrl: widget.imageUrl, + chatType: widget.chatType) + ) + ), + child: AppBar( + backgroundColor: cs.surfaceContainerHigh, + foregroundColor: cs.onSurface, + elevation: 0, + surfaceTintColor: Colors.transparent, + iconTheme: IconThemeData(color: cs.onSurface), + leading: IconButton( + icon: const Icon(Symbols.arrow_back, weight: 400), + onPressed: () => Navigator.pop(context), ), - ], - ), - actions: [ - IconButton( - icon: const Icon(Symbols.call, weight: 400), - onPressed: () {}, + titleSpacing: 0, + title: Row( + children: [ + if (widget.imageUrl.isNotEmpty) + CircleAvatar( + radius: 18, + backgroundImage: NetworkImage(widget.imageUrl), + ) + else + CircleAvatar( + radius: 18, + backgroundColor: cs.primaryContainer, + child: Text( + widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?', + style: TextStyle(color: cs.onPrimaryContainer, fontSize: 12), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.name, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + fontFamily: 'Outfit', + ), + ), + Text( + status ?? "", + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 12, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ], + ), + actions: [ + IconButton( + icon: const Icon(Symbols.call, weight: 400), + onPressed: () {}, + ), + IconButton( + icon: const Icon(Symbols.more_vert, weight: 400), + onPressed: () {}, + ), + ], ), - IconButton( - icon: const Icon(Symbols.more_vert, weight: 400), - onPressed: () {}, - ), - ], - ), + )), body: Column( children: [ Expanded( diff --git a/lib/frontend/widgets/message_bubble.dart b/lib/frontend/widgets/message_bubble.dart index 1fc12c2..1217554 100644 --- a/lib/frontend/widgets/message_bubble.dart +++ b/lib/frontend/widgets/message_bubble.dart @@ -292,61 +292,67 @@ class MessageBubble extends StatelessWidget { String? senderAvatar = ContactCache.getAvatar(message.senderId); String? displaySender = ContactCache.get(message.senderId); - return Padding( - padding: EdgeInsets.only( - left: isMe ? 12 : 12, - right: isMe ? 12 : 12, - top: topMargin, - bottom: bottomMargin, - ), - child: Align( - child: Row( - mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (!isMe && chatType == "CHAT" && nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId) - ...(senderAvatar != null && senderAvatar.isNotEmpty) - ? [ - CircleAvatar( - radius: 15, - backgroundImage: NetworkImage(senderAvatar), - backgroundColor: cs.primaryContainer, - ) - ] - : [ - CircleAvatar( - radius: 15, - backgroundColor: cs.primaryContainer, - child: Text( - displaySender != null && displaySender.isNotEmpty - ? displaySender[0].toUpperCase() - : '?', - style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer), - ), - ) - ] - else if (!isMe && chatType != "CHAT") - SizedBox(width: 0) - else if (!isMe) - CircleAvatar(radius: 15, backgroundColor: Color(0x00000000)), - Container( - constraints: BoxConstraints( - maxWidth: MediaQuery.of(context).size.width * 0.75, + return GestureDetector( + // TODO: действия с сообщением + onTap: () => print("test"), + child: Padding( + padding: EdgeInsets.only( + left: isMe ? 12 : 12, + right: isMe ? 12 : 12, + top: topMargin, + bottom: bottomMargin, + ), + child: Align( + child: Row( + mainAxisAlignment: isMe ? MainAxisAlignment.end : MainAxisAlignment.start, + spacing: 8, + + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (!isMe && chatType == "CHAT" && nextMessage?.senderId != message.senderId && prevMessage?.senderId == message.senderId) + ...(senderAvatar != null && senderAvatar.isNotEmpty) + ? [ + CircleAvatar( + radius: 15, + backgroundImage: NetworkImage(senderAvatar), + backgroundColor: cs.primaryContainer, + ) + ] + : [ + CircleAvatar( + radius: 15, + backgroundColor: cs.primaryContainer, + child: Text( + displaySender != null && displaySender.isNotEmpty + ? displaySender[0].toUpperCase() + : '?', + style: TextStyle(fontSize: 9, color: cs.onPrimaryContainer), + ), + ) + ] + else if (!isMe && chatType != "CHAT") + SizedBox(width: 0) + else if (!isMe) + CircleAvatar(radius: 15, backgroundColor: Color(0x00000000)), + Container( + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * 0.75, + ), + decoration: BoxDecoration( + color: isMe + ? (isDark ? const Color(0xFF2C5F8D) : const Color(0xFF007AFF)) + : (isDark + ? cs.surfaceContainerHighest + : const Color(0xFFE9E9EB)), + borderRadius: _borderRadius, + ), + padding: padding, + child: _buildContent(context), ), - decoration: BoxDecoration( - color: isMe - ? (isDark ? const Color(0xFF2C5F8D) : const Color(0xFF007AFF)) - : (isDark - ? cs.surfaceContainerHighest - : const Color(0xFFE9E9EB)), - borderRadius: _borderRadius, - ), - padding: padding, - child: _buildContent(context), - ), - ], - ) - ), + ], + ) + ), + ) ); } From d9cbadbbb7f52cae65bea8e4f0bb435bf760005f Mon Sep 17 00:00:00 2001 From: prime Date: Wed, 29 Apr 2026 19:02:44 +1000 Subject: [PATCH 5/7] =?UTF-8?q?=D1=80=D1=83=D0=BA=D0=B8=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BB=D0=BE=D0=BC=D0=B0=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 +++++- lib/backend/api.dart | 6 ++++ lib/backend/models/chat_folder.dart | 2 +- lib/backend/modules/account.dart | 28 +++++++++------- lib/backend/modules/calls.dart | 4 +-- lib/backend/modules/chats.dart | 32 +++++++++++++------ lib/backend/modules/contacts.dart | 12 +++---- lib/backend/modules/messages.dart | 19 ++++++----- lib/core/protocol/packet.dart | 1 + lib/core/storage/app_database.dart | 28 +++++++++++++--- lib/core/storage/token_storage.dart | 2 +- lib/core/transport/connection.dart | 4 ++- lib/core/transport/proxy_connector.dart | 24 ++++++++++---- lib/core/transport/sender.dart | 2 +- .../screens/auth/server_settings_sheet.dart | 4 ++- .../screens/chats/chat_list_screen.dart | 1 + lib/frontend/screens/chats/chat_screen.dart | 2 +- .../screens/profile/devices_screen.dart | 5 ++- lib/main.dart | 13 +++++--- lib/models/attachment.dart | 12 ++++--- 20 files changed, 145 insertions(+), 66 deletions(-) diff --git a/.gitignore b/.gitignore index 14601b5..f395ff9 100644 --- a/.gitignore +++ b/.gitignore @@ -125,4 +125,12 @@ app.*.symbols !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock \ No newline at end of file +!/dev/ci/**/Gemfile.lock + +# AI / Agents +agents.md +.claude/ + +# Environment variables +.env +.env.* \ No newline at end of file diff --git a/lib/backend/api.dart b/lib/backend/api.dart index 2c50950..495b56a 100644 --- a/lib/backend/api.dart +++ b/lib/backend/api.dart @@ -237,6 +237,11 @@ class Api { _dispatcher.registerHandler(opcode, handler); } + /// Снимает обработчик пушей с указанного опкода. + void unregisterPushHandler(int opcode) { + _dispatcher.unregisterHandler(opcode); + } + /// Стрим всех входящих пушей от сервера. Stream get pushStream => _dispatcher.pushStream; @@ -248,6 +253,7 @@ class Api { _connection.dispose(); _stateController.close(); _sessionExpiredController.close(); + _handshakeSuccessController.close(); } // Внутрянка diff --git a/lib/backend/models/chat_folder.dart b/lib/backend/models/chat_folder.dart index 0d6044a..8392a6c 100644 --- a/lib/backend/models/chat_folder.dart +++ b/lib/backend/models/chat_folder.dart @@ -25,7 +25,7 @@ class ChatFolder { factory ChatFolder.fromJson(Map json) { return ChatFolder( - id: json['id'].toString(), + id: json['id']?.toString() ?? '', title: json['title']?.toString() ?? '', emoji: json['emoji']?.toString(), include: (json['include'] as List?) diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index 1f57233..4c5c32d 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -289,7 +289,7 @@ class LoginSyncParams { draftsSync: int.tryParse(values[SyncKey.draftsSync] ?? '') ?? 0, bannersSync: int.tryParse(values[SyncKey.bannersSync] ?? '') ?? 0, presenceSync: int.tryParse(values[SyncKey.presenceSync] ?? '') ?? -1, - lastLogin: int.parse(lastLogin), + lastLogin: int.tryParse(lastLogin) ?? 0, configHash: values[SyncKey.configHash], chatCacheFingerprint: values[SyncKey.chatCacheFingerprint], ); @@ -646,19 +646,25 @@ class AccountModule { Future _processProfileUpdate(Packet packet) async { _api.registerPushHandler(Opcode.notifProfile, (p) {}); - await for (final push in _api.pushStream.where( - (p) => p.opcode == Opcode.notifProfile, - )) { - final payload = push.payload; - if (payload is Map) { - final profile = payload['profile']; - if (profile is Map) { - final contact = profile['contact']; - if (contact is Map) { - return ProfileData.fromServerMap(contact.cast()); + try { + await for (final push in _api.pushStream + .where((p) => p.opcode == Opcode.notifProfile) + .timeout(const Duration(seconds: 15))) { + final payload = push.payload; + if (payload is Map) { + final profile = payload['profile']; + if (profile is Map) { + final contact = profile['contact']; + if (contact is Map) { + return ProfileData.fromServerMap(contact.cast()); + } } } } + } on TimeoutException { + throw Exception('Таймаут ожидания обновления профиля'); + } finally { + _api.unregisterPushHandler(Opcode.notifProfile); } throw Exception('Не удалось получить обновлённый профиль'); } diff --git a/lib/backend/modules/calls.dart b/lib/backend/modules/calls.dart index 4593e48..cc2db6a 100644 --- a/lib/backend/modules/calls.dart +++ b/lib/backend/modules/calls.dart @@ -92,8 +92,8 @@ class CallsModule { msg['id']?.toString() ?? DateTime.now().millisecondsSinceEpoch.toString(); - final name = contact?.firstName != null - ? '${contact!.firstName} ${contact.lastName ?? ''}'.trim() + final name = (contact != null && contact.firstName.isNotEmpty) + ? '${contact.firstName} ${contact.lastName ?? ''}'.trim() : 'Неизвестный'; extractedCalls.add( diff --git a/lib/backend/modules/chats.dart b/lib/backend/modules/chats.dart index 3b0fb27..7ee2864 100644 --- a/lib/backend/modules/chats.dart +++ b/lib/backend/modules/chats.dart @@ -3,6 +3,21 @@ import 'dart:convert'; import '../../core/storage/app_database.dart'; import '../../core/utils/logger.dart'; +Map _parseParticipants(dynamic raw) { + try { + final decoded = raw is String ? jsonDecode(raw) : raw; + if (decoded is Map) { + return decoded.map((k, v) => MapEntry( + k is int ? k : int.parse(k.toString()), + v is int ? v : int.tryParse(v.toString()) ?? 0, + )); + } + } catch (e) { + logger.e('Failed to parse participants: $e'); + } + return {}; +} + class CachedChat { final int id; final int accountId; @@ -59,8 +74,7 @@ class CachedChat { dontDisturbUntil: row['dont_disturb_until'] as int, isOnline: (row['is_online'] as int) == 1, seenTime: row['seen_time'] as int, - // watafuc - participants: Map.from(jsonDecode(row['participants'])).map((k, v) => MapEntry(int.parse(k), v)) + participants: _parseParticipants(row['participants']) ); Map toDbRow() => { @@ -242,7 +256,7 @@ class ChatsModule { isOnline = (presence['status'] as int?) == 1; } } - Map participants = Map.from(chat['participants']); + Map participants = _parseParticipants(chat['participants']); return CachedChat( id: id, @@ -282,12 +296,12 @@ class ChatsModule { static String? _nameFromContact(Map contact) { final names = contact['names']; if (names is! List || names.isEmpty) return null; - final name = - names.firstWhere( - (n) => n is Map && n['type'] == 'ONEME', - orElse: () => names.first, - ) - as Map; + final nameRaw = names.firstWhere( + (n) => n is Map && n['type'] == 'ONEME', + orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), + ); + if (nameRaw is! Map) return null; + final name = nameRaw; return name['name'] as String?; } } diff --git a/lib/backend/modules/contacts.dart b/lib/backend/modules/contacts.dart index 1943e52..71e4926 100644 --- a/lib/backend/modules/contacts.dart +++ b/lib/backend/modules/contacts.dart @@ -72,12 +72,12 @@ class ContactsModule { final names = contact['names']; if (names is List && names.isNotEmpty) { - final name = - names.firstWhere( - (n) => n is Map && n['type'] == 'ONEME', - orElse: () => names.first, - ) - as Map; + final nameRaw = names.firstWhere( + (n) => n is Map && n['type'] == 'ONEME', + orElse: () => names.firstWhere((n) => n is Map, orElse: () => null), + ); + if (nameRaw is! Map) return null; + final name = nameRaw; firstName = (name['firstName'] as String?) ?? ''; lastName = name['lastName'] as String?; } diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index 1526cac..255ff2e 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -155,7 +155,9 @@ class MessagesModule { } if (rows.isNotEmpty) { - AppDatabase.saveMessages(rows).ignore(); + AppDatabase.saveMessages(rows).catchError((e) { + debugPrint('saveMessages error: $e'); + }); } return results; @@ -257,9 +259,8 @@ class MessagesModule { if (data is! Map) return null; final content = data['content']; - if (content is String) { - return Uri.parse(content).host.isNotEmpty ? null : null; - } + if (content is Uint8List) return content; + if (content is List) return Uint8List.fromList(content); return null; } catch (e) { return null; @@ -295,9 +296,8 @@ class MessagesModule { if (data is! Map) return null; final content = data['content']; - if (content is String) { - return Uri.parse(content).host.isNotEmpty ? null : null; - } + if (content is Uint8List) return content; + if (content is List) return Uint8List.fromList(content); return null; } catch (e) { return null; @@ -333,9 +333,8 @@ class MessagesModule { if (data is! Map) return null; final content = data['content']; - if (content is String) { - return Uri.parse(content).host.isNotEmpty ? null : null; - } + if (content is Uint8List) return content; + if (content is List) return Uint8List.fromList(content); return null; } catch (e) { return null; diff --git a/lib/core/protocol/packet.dart b/lib/core/protocol/packet.dart index 7b6fb38..5637328 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -190,6 +190,7 @@ Uint8List _lz4BlockDecompress(Uint8List src, int maxSize) { if (pos >= src.length) break; + if (pos + 1 >= src.length) throw StateError('LZ4: unexpected end of input'); final offset = src[pos] | (src[pos + 1] << 8); pos += 2; if (offset == 0) throw StateError('LZ4: offset = 0'); diff --git a/lib/core/storage/app_database.dart b/lib/core/storage/app_database.dart index 5890f21..53bbb91 100644 --- a/lib/core/storage/app_database.dart +++ b/lib/core/storage/app_database.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:komet/core/utils/logger.dart'; @@ -72,10 +73,15 @@ class ProfileData { final profileOptionsStr = row['profile_options'] as String?; List? profileOptions; if (profileOptionsStr != null && profileOptionsStr.isNotEmpty) { - profileOptions = profileOptionsStr - .split(',') - .map((e) => int.parse(e.trim())) - .toList(); + try { + profileOptions = profileOptionsStr + .split(',') + .where((e) => e.trim().isNotEmpty) + .map((e) => int.parse(e.trim())) + .toList(); + } catch (_) { + profileOptions = null; + } } return ProfileData( id: row['id'] as int, @@ -130,8 +136,20 @@ class AppDatabase { } } + static Completer? _initCompleter; + static Future get _instance async { - _db ??= await _open(); + if (_db != null) return _db!; + if (_initCompleter != null) return _initCompleter!.future; + _initCompleter = Completer(); + try { + _db = await _open(); + _initCompleter!.complete(_db!); + } catch (e) { + _initCompleter!.completeError(e); + _initCompleter = null; + rethrow; + } return _db!; } diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart index 75ab4bb..b04ed77 100644 --- a/lib/core/storage/token_storage.dart +++ b/lib/core/storage/token_storage.dart @@ -33,7 +33,7 @@ class TokenStorage { static Future readActiveToken() async { final id = await getActiveAccountId(); if (id == null) return null; - return readToken(id); + return await readToken(id); } static Future deleteAccount(int accountId) async { diff --git a/lib/core/transport/connection.dart b/lib/core/transport/connection.dart index 15c3a53..c0b0532 100644 --- a/lib/core/transport/connection.dart +++ b/lib/core/transport/connection.dart @@ -99,7 +99,9 @@ class Connection { if (socket != null) { try { socket.close(); - } catch (_) {} + } catch (e) { + logger.w('Ошибка при закрытии сокета: $e'); + } } _setState(SocketState.disconnected); diff --git a/lib/core/transport/proxy_connector.dart b/lib/core/transport/proxy_connector.dart index aaac29b..90d4df6 100644 --- a/lib/core/transport/proxy_connector.dart +++ b/lib/core/transport/proxy_connector.dart @@ -60,8 +60,8 @@ class ProxyConnector { if (!useAuth) { throw SocketException('SOCKS5: прокси требует аутентификацию'); } - final usernameBytes = utf8.encode(settings.username!); - final passwordBytes = utf8.encode(settings.password!); + final usernameBytes = utf8.encode(settings.username ?? ''); + final passwordBytes = utf8.encode(settings.password ?? ''); final authPacket = BytesBuilder() ..addByte(0x01) ..addByte(usernameBytes.length) @@ -175,7 +175,10 @@ class ProxyConnector { final responseStr = utf8.decode(headerBytes, allowMalformed: true); final statusLine = responseStr.split('\r\n').first; final parts = statusLine.split(' '); - final statusCode = parts.length >= 2 ? int.tryParse(parts[1]) ?? 0 : 0; + if (parts.length < 2) { + throw SocketException('HTTP CONNECT: некорректный ответ: $statusLine'); + } + final statusCode = int.tryParse(parts[1]) ?? 0; if (statusCode != 200) { throw SocketException( 'HTTP CONNECT: прокси вернул статус $statusCode', @@ -201,10 +204,17 @@ class ProxyConnector { RawSocket proxySocket, _RawSocketIO io, ) async { - final server = await RawServerSocket.bind( - InternetAddress.loopbackIPv4, - 0, - ); + RawServerSocket? server; + try { + server = await RawServerSocket.bind( + InternetAddress.loopbackIPv4, + 0, + ); + } catch (e) { + io.dispose(); + proxySocket.close(); + rethrow; + } final clientSide = await RawSocket.connect( InternetAddress.loopbackIPv4, server.port, diff --git a/lib/core/transport/sender.dart b/lib/core/transport/sender.dart index 69e8c5d..aaa3187 100644 --- a/lib/core/transport/sender.dart +++ b/lib/core/transport/sender.dart @@ -8,7 +8,7 @@ class PacketSender { int get currentSeq => _seq; int _nextSeq() { - _seq = (_seq + 1) % 256; + _seq = (_seq + 1) % 65536; return _seq; } diff --git a/lib/frontend/screens/auth/server_settings_sheet.dart b/lib/frontend/screens/auth/server_settings_sheet.dart index 534338a..e458642 100644 --- a/lib/frontend/screens/auth/server_settings_sheet.dart +++ b/lib/frontend/screens/auth/server_settings_sheet.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -53,7 +55,7 @@ class _ServerSettingsSheetState extends State { await prefs.setString(ServerConfig.prefHostKey, host); await prefs.setInt(ServerConfig.prefPortKey, port); await api.disconnect(); - api.connect(); + unawaited(api.connect()); final online = await api.stateStream .firstWhere((s) => s == SessionState.online || s == SessionState.disconnected) diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index 9a9878b..be2dacf 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -638,6 +638,7 @@ class _ChatListScreenState extends State ..removeListener(_onStoriesRevealTick) ..removeStatusListener(_onStoriesRevealStatus) ..dispose(); + _shimmerController.dispose(); _folderPageController.dispose(); while (_folderChatScrollControllers.isNotEmpty) { final c = _folderChatScrollControllers.removeLast(); diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 41069ac..22c8d0c 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -381,7 +381,7 @@ class _ChatScreenState extends State myId: _myId, prevMessage: prevMessage, nextMessage: nextMessage, - chatType: chat!.type, + chatType: chat?.type ?? 'CHAT', ); }, ); diff --git a/lib/frontend/screens/profile/devices_screen.dart b/lib/frontend/screens/profile/devices_screen.dart index 8030921..ab9b715 100644 --- a/lib/frontend/screens/profile/devices_screen.dart +++ b/lib/frontend/screens/profile/devices_screen.dart @@ -271,8 +271,9 @@ class _DevicesScreenState extends State setState(() => _loadingIps.add(id)); } + HttpClient? client; try { - final client = HttpClient(); + client = HttpClient(); client.connectionTimeout = const Duration(seconds: 5); final request = await client.getUrl( Uri.parse( @@ -296,6 +297,8 @@ class _DevicesScreenState extends State setState(() => _loadingIps.remove(id)); showCustomNotification(context, 'Ошибка IP: $e'); } + } finally { + client?.close(); } } diff --git a/lib/main.dart b/lib/main.dart index 0ce4998..5c62627 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; @@ -71,6 +73,7 @@ class KometAppState extends State { late Locale _locale; bool _isLoggingOut = false; + StreamSubscription? _sessionExpiredSub; late final ValueNotifier fpsOverlayEnabled = ValueNotifier( widget.initialFpsOverlay, ); @@ -83,15 +86,16 @@ class KometAppState extends State { api.setReconnectCallback(() async { try { final accountId = await TokenStorage.getActiveAccountId(); - if (accountId != null && - await TokenStorage.readToken(accountId) != null) { + if (accountId != null) { final token = await TokenStorage.readToken(accountId); - await accountModule.login(accountId: accountId, token: token); + if (token != null) { + await accountModule.login(accountId: accountId, token: token); + } } } catch (_) {} }); - api.sessionExpiredStream.listen((SessionExpiredException e) async { + _sessionExpiredSub = api.sessionExpiredStream.listen((SessionExpiredException e) async { if (_isLoggingOut) return; _isLoggingOut = true; @@ -118,6 +122,7 @@ class KometAppState extends State { @override void dispose() { + _sessionExpiredSub?.cancel(); fpsOverlayEnabled.dispose(); super.dispose(); } diff --git a/lib/models/attachment.dart b/lib/models/attachment.dart index 25ce0a6..825e348 100644 --- a/lib/models/attachment.dart +++ b/lib/models/attachment.dart @@ -136,7 +136,8 @@ class VideoAttachment extends MessageAttachment { } else if (previewRaw is List) { try { final bytes = List.from(previewRaw); - previewStr = String.fromCharCodes(bytes); + final base64 = String.fromCharCodes(bytes); + previewStr = 'data:image/webp;base64,$base64'; } catch (_) {} } @@ -192,7 +193,8 @@ class AudioAttachment extends MessageAttachment { } else if (previewRaw is List) { try { final bytes = List.from(previewRaw); - previewStr = String.fromCharCodes(bytes); + final base64 = String.fromCharCodes(bytes); + previewStr = 'data:image/webp;base64,$base64'; } catch (_) {} } @@ -244,7 +246,8 @@ class FileAttachment extends MessageAttachment { } else if (previewRaw is List) { try { final bytes = List.from(previewRaw); - previewStr = String.fromCharCodes(bytes); + final base64 = String.fromCharCodes(bytes); + previewStr = 'data:image/webp;base64,$base64'; } catch (_) {} } @@ -294,7 +297,8 @@ class StickerAttachment extends MessageAttachment { } else if (previewRaw is List) { try { final bytes = List.from(previewRaw); - previewStr = String.fromCharCodes(bytes); + final base64 = String.fromCharCodes(bytes); + previewStr = 'data:image/webp;base64,$base64'; } catch (_) {} } From c6ea5d6dcfe75cf807ec9aceb00983b854d8e91b Mon Sep 17 00:00:00 2001 From: prime Date: Wed, 29 Apr 2026 19:21:53 +1000 Subject: [PATCH 6/7] =?UTF-8?q?=D0=BD=D1=83=20=D0=B7=D0=B0=D1=82=D0=BE=20?= =?UTF-8?q?=D1=85=D1=83=D0=B9=D0=BD=D1=8E=20=D0=BD=D0=B0=D1=88=D0=B5=D0=BB?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/frontend/screens/calls/calls_tab.dart | 8 +++++++- lib/frontend/screens/chats/chat_list_screen.dart | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/frontend/screens/calls/calls_tab.dart b/lib/frontend/screens/calls/calls_tab.dart index 09edce7..7e08f23 100644 --- a/lib/frontend/screens/calls/calls_tab.dart +++ b/lib/frontend/screens/calls/calls_tab.dart @@ -30,7 +30,13 @@ class _CallsTabState extends State { } final callsModule = CallsModule(api); - final calls = await callsModule.fetchHistory(p.id, p.id); + List calls; + try { + calls = await callsModule.fetchHistory(p.id, p.id); + } catch (e) { + if (mounted) setState(() => _isLoading = false); + return; + } final List grouped = []; for (final call in calls) { diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index be2dacf..b52a632 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -1255,6 +1255,7 @@ class _ChatListScreenState extends State }); }, child: Stack( + clipBehavior: Clip.hardEdge, children: [ AnimatedPositioned( duration: _navDragging From 07d39ffa24e044f807bce0b2af5947c17ec2dfdc Mon Sep 17 00:00:00 2001 From: Jganenok Date: Wed, 13 May 2026 21:26:46 +0700 Subject: [PATCH 7/7] =?UTF-8?q?=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=84=D0=BE=D1=80=D0=BC=D0=B0=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B2=20=D0=BF=D0=BE=D0=B4=D1=80=D0=BE=D0=B1=D0=BD=D0=B5=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/backend/modules/account.dart | 120 ++++++ lib/backend/modules/chats.dart | 22 + lib/core/storage/app_database.dart | 14 + lib/core/storage/spoofing_service.dart | 2 +- .../screens/chats/chat_info_screen.dart | 379 ++++++++++++++++-- .../screens/profile/debug_menu_screen.dart | 132 +++++- lib/frontend/screens/profile/info_screen.dart | 286 +++++++++++++ .../screens/profile/security_screen.dart | 25 +- .../screens/profile/settings_tab.dart | 21 +- lib/l10n/app_en.arb | 57 ++- lib/l10n/app_localizations.dart | 324 +++++++++++++++ lib/l10n/app_localizations_en.dart | 162 ++++++++ lib/l10n/app_localizations_ru.dart | 162 ++++++++ lib/l10n/app_ru.arb | 57 ++- 14 files changed, 1724 insertions(+), 39 deletions(-) create mode 100644 lib/frontend/screens/profile/info_screen.dart diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index 4c5c32d..8d6f6b8 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -27,6 +27,7 @@ class PrivacyConfig { final String chatsInvite; final bool pushNewContacts; final bool unsafeFiles; + final String phoneNumberPrivacy; final String inactiveTtl; final bool showReadMark; final bool altKeyboard; @@ -48,6 +49,7 @@ class PrivacyConfig { required this.chatsInvite, required this.pushNewContacts, required this.unsafeFiles, + required this.phoneNumberPrivacy, required this.inactiveTtl, required this.showReadMark, required this.altKeyboard, @@ -71,6 +73,7 @@ class PrivacyConfig { chatsInvite: map['CHATS_INVITE']?.toString() ?? 'CONTACTS', pushNewContacts: map['PUSH_NEW_CONTACTS'] ?? false, unsafeFiles: map['UNSAFE_FILES'] ?? true, + phoneNumberPrivacy: map['PHONE_NUMBER_PRIVACY']?.toString() ?? 'ALL', inactiveTtl: map['INACTIVE_TTL']?.toString() ?? '6M', showReadMark: map['SHOW_READ_MARK'] ?? true, altKeyboard: map['ALT_KEYBOARD'] ?? false, @@ -94,6 +97,7 @@ class PrivacyConfig { 'CHATS_INVITE': chatsInvite, 'PUSH_NEW_CONTACTS': pushNewContacts, 'UNSAFE_FILES': unsafeFiles, + 'PHONE_NUMBER_PRIVACY': phoneNumberPrivacy, 'INACTIVE_TTL': inactiveTtl, 'SHOW_READ_MARK': showReadMark, 'ALT_KEYBOARD': altKeyboard, @@ -125,6 +129,7 @@ class PrivacyConfig { chatsInvite: 'CONTACTS', pushNewContacts: false, unsafeFiles: true, + phoneNumberPrivacy: 'ALL', inactiveTtl: '6M', showReadMark: true, altKeyboard: false, @@ -946,6 +951,12 @@ class AccountModule { logger.w('Папки чатов: $e'); } + try { + await _saveLoginInfo(data, profile.id); + } catch (e) { + logger.w('Info: $e'); + } + return LoginResult( profile: profile, updatedToken: updatedToken, @@ -980,6 +991,115 @@ class AccountModule { } } + Future _saveLoginInfo( + Map data, + int accountId, + ) async { + final contact = data['profile']?['contact'] as Map?; + final videoChatHistory = data['videoChatHistory']; + final chats = data['chats'] as List?; + final config = data['config'] as Map?; + final serverConfig = config?['server'] as Map?; + final userConfig = config?['user'] as Map?; + final yMap = serverConfig?['y-map'] as Map?; + final whiteListLinks = serverConfig?['white-list-links'] as List?; + final fileUploadUnsupported = serverConfig?['file-upload-unsupported-types'] as List?; + final time = data['time'] as int?; + + final info = { + 'registrationTime': contact?['registrationTime'], + 'country': contact?['country'], + 'videoChatHistory': videoChatHistory, + 'updateTime': contact?['updateTime'], + 'id': contact?['id'], + 'chatMarker': chats != null && chats.isNotEmpty + ? _extractChatMarker(chats.cast()) + : null, + 'time': time, + 'server': serverConfig != null + ? _extractServerInfo(serverConfig, yMap, whiteListLinks, fileUploadUnsupported) + : null, + 'user': userConfig != null ? _extractUserConfig(userConfig) : null, + }; + + await AppDatabase.saveLoginInfo(accountId, jsonEncode(info)); + } + + Map _extractChatMarker(List chats) { + int? latestTime; + for (final chat in chats) { + final lastEventTime = chat['lastEventTime'] as int?; + if (lastEventTime != null && (latestTime == null || lastEventTime > latestTime)) { + latestTime = lastEventTime; + } + } + return {'chatMarker': latestTime}; + } + + Map _extractServerInfo( + Map serverConfig, + Map? yMap, + List? whiteListLinks, + List? fileUploadUnsupported, + ) { + return { + 'account-removal-enabled': serverConfig['account-removal-enabled'], + 'image-size': serverConfig['image-size'], + 'gce': serverConfig['gce'], + 'gcce': serverConfig['gcce'], + 'max-msg-length': serverConfig['max-msg-length'], + 'quotes-enabled': serverConfig['quotes-enabled'], + 'calls-endpoint': serverConfig['calls-endpoint'], + 'send-location-enabled': serverConfig['send-location-enabled'], + 'lgce': serverConfig['lgce'], + 'wud': serverConfig['wud'], + 'video-msg-enabled': serverConfig['video-msg-enabled'], + 'grse': serverConfig['grse'], + 'edit-timeout': serverConfig['edit-timeout'], + 'image-quality': serverConfig['image-quality'], + 'unsafe-files-alert': serverConfig['unsafe-files-alert'], + 'account-nickname-enabled': serverConfig['account-nickname-enabled'], + 'mentions_entity_names_limit': serverConfig['mentions_entity_names_limit'], + 'reactions-enabled': serverConfig['reactions-enabled'], + 'y-map': yMap != null ? { + 'tile': yMap['tile'], + 'geocoder': yMap['geocoder'], + 'static': yMap['static'], + } : null, + 'white-list-links': whiteListLinks, + 'file-upload-unsupported-types': fileUploadUnsupported, + }; + } + + Map _extractUserConfig(Map userConfig) { + return { + 'CHATS_PUSH_NOTIFICATION': userConfig['CHATS_PUSH_NOTIFICATION'], + 'PUSH_DETAILS': userConfig['PUSH_DETAILS'], + 'PUSH_SOUND': userConfig['PUSH_SOUND'], + 'PHONE_NUMBER_PRIVACY': userConfig['PHONE_NUMBER_PRIVACY'], + 'INACTIVE_TTL': userConfig['INACTIVE_TTL'], + 'SHOW_READ_MARK': userConfig['SHOW_READ_MARK'], + 'AUDIO_TRANSCRIPTION_ENABLED': userConfig['AUDIO_TRANSCRIPTION_ENABLED'], + 'SEARCH_BY_PHONE': userConfig['SEARCH_BY_PHONE'], + 'INCOMING_CALL': userConfig['INCOMING_CALL'], + 'DOUBLE_TAP_REACTION_DISABLED': userConfig['DOUBLE_TAP_REACTION_DISABLED'], + 'SAFE_MODE_NO_PIN': userConfig['SAFE_MODE_NO_PIN'], + 'CHATS_PUSH_SOUND': userConfig['CHATS_PUSH_SOUND'], + 'DOUBLE_TAP_REACTION_VALUE': userConfig['DOUBLE_TAP_REACTION_VALUE'], + 'FAMILY_PROTECTION': userConfig['FAMILY_PROTECTION'], + 'HIDDEN': userConfig['HIDDEN'], + 'CHATS_INVITE': userConfig['CHATS_INVITE'], + 'PUSH_NEW_CONTACTS': userConfig['PUSH_NEW_CONTACTS'], + 'UNSAFE_FILES': userConfig['UNSAFE_FILES'], + 'DONT_DISTURB_UNTIL': userConfig['DONT_DISTURB_UNTIL'], + 'ALT_KEYBOARD': userConfig['ALT_KEYBOARD'], + 'CONTENT_LEVEL_ACCESS': userConfig['CONTENT_LEVEL_ACCESS'], + 'STICKERS_SUGGEST': userConfig['STICKERS_SUGGEST'], + 'SAFE_MODE': userConfig['SAFE_MODE'], + 'M_CALL_PUSH_NOTIFICATION': userConfig['M_CALL_PUSH_NOTIFICATION'], + }; + } + Future _requestCodeInternal( String phone, AuthRequestType type, diff --git a/lib/backend/modules/chats.dart b/lib/backend/modules/chats.dart index 7ee2864..f47aa70 100644 --- a/lib/backend/modules/chats.dart +++ b/lib/backend/modules/chats.dart @@ -1,7 +1,9 @@ import 'dart:convert'; +import '../../core/protocol/opcode_map.dart'; import '../../core/storage/app_database.dart'; import '../../core/utils/logger.dart'; +import '../api.dart'; Map _parseParticipants(dynamic raw) { try { @@ -304,4 +306,24 @@ class ChatsModule { final name = nameRaw; return name['name'] as String?; } + + static Future?> getChatInfo(Api api, int chatId) async { + final packet = await api.sendRequest(Opcode.chatInfo, { + 'chatIds': [chatId], + }); + if (packet.isError) return null; + final payload = packet.payload as Map?; + final chats = payload?['chats'] as List?; + if (chats == null || chats.isEmpty) return null; + return Map.from(chats.first as Map); + } + + static Future searchById(Api api, int userId) async { + final packet = await api.sendRequest(Opcode.publicSearch, { + 'query': userId.toString(), + 'from': 0, + 'count': 10, + }); + return packet.payload; + } } diff --git a/lib/core/storage/app_database.dart b/lib/core/storage/app_database.dart index 53bbb91..7283d9b 100644 --- a/lib/core/storage/app_database.dart +++ b/lib/core/storage/app_database.dart @@ -124,6 +124,7 @@ abstract class SyncKey { static const configHash = 'config_hash'; static const chatCacheFingerprint = 'chat_cache_fingerprint'; static const serverTime = 'server_time'; + static const loginInfo = 'login_info'; } class AppDatabase { @@ -392,6 +393,19 @@ class AppDatabase { return rows.first['value'] as String; } + static Future saveLoginInfo(int accountId, String jsonInfo) async { + final db = await _instance; + await db.insert('sync_state', { + 'account_id': accountId, + 'key': SyncKey.loginInfo, + 'value': jsonInfo, + }, conflictAlgorithm: ConflictAlgorithm.replace); + } + + static Future getLoginInfo(int accountId) async { + return getSyncValue(accountId, SyncKey.loginInfo); + } + static Future close() async { await _db?.close(); _db = null; diff --git a/lib/core/storage/spoofing_service.dart b/lib/core/storage/spoofing_service.dart index b58cce2..c27240b 100644 --- a/lib/core/storage/spoofing_service.dart +++ b/lib/core/storage/spoofing_service.dart @@ -1,7 +1,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class SpoofingService { - static const String hardcodedAppVersion = '26.8.1'; + static const String hardcodedAppVersion = '26.15.3'; static const int hardcodedBuildNumber = 6606; static Future?> getSpoofedSessionData() async { diff --git a/lib/frontend/screens/chats/chat_info_screen.dart b/lib/frontend/screens/chats/chat_info_screen.dart index c50ae93..70a4ae7 100644 --- a/lib/frontend/screens/chats/chat_info_screen.dart +++ b/lib/frontend/screens/chats/chat_info_screen.dart @@ -1,4 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import '../../../core/protocol/opcode_map.dart'; +import '../../../l10n/app_localizations.dart'; +import '../../../main.dart' as main; +import '../../widgets/custom_notification.dart'; class ChatInfoScreen extends StatefulWidget { final int chatId; @@ -13,49 +18,367 @@ class ChatInfoScreen extends StatefulWidget { required this.imageUrl, required this.chatType, }); + @override State createState() => _ChatInfoScreenState(); } -class _ChatInfoScreenState extends State{ + +class _ChatInfoScreenState extends State + with TickerProviderStateMixin { + bool _isLoading = true; + Map? _chatData; + late AnimationController _shimmerController; + + @override + void initState() { + super.initState(); + _shimmerController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1500), + )..repeat(); + _loadChatData(); + } + + @override + void dispose() { + _shimmerController.dispose(); + super.dispose(); + } + + Future _loadChatData() async { + try { + final packet = await main.api.sendRequest(Opcode.chatInfo, { + 'chatIds': [widget.chatId], + }); + + final payload = packet.payload as Map?; + if (payload == null) { + if (mounted) setState(() => _isLoading = false); + return; + } + + final errorField = payload['error']; + if (errorField != null) { + String errorMsg = 'Error'; + if (errorField is Map) { + errorMsg = errorField['localizedMessage'] ?? errorField['message'] ?? errorField.toString(); + } else if (errorField is String) { + errorMsg = errorField; + } + if (mounted) showCustomNotification(context, errorMsg); + setState(() => _isLoading = false); + return; + } + + final chats = payload['chats'] as List?; + if (chats != null && chats.isNotEmpty) { + _chatData = Map.from(chats.first as Map); + } else if (chats != null && chats.isEmpty) { + if (mounted) showCustomNotification(context, 'No data found'); + } + + if (mounted) setState(() => _isLoading = false); + } catch (e) { + if (mounted) { + showCustomNotification(context, 'Error: $e'); + setState(() => _isLoading = false); + } + } + } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); return Scaffold( backgroundColor: cs.surface, appBar: AppBar( - + backgroundColor: cs.surface, + elevation: 0, + leading: IconButton( + icon: Icon(Symbols.arrow_back, color: cs.onSurface), + onPressed: () => Navigator.pop(context), + ), + title: Text( + l10n?.chatInfoTitle ?? 'Info', + style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600), + ), ), - body: - SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (widget.imageUrl.isNotEmpty) - CircleAvatar( - radius: 36, - backgroundImage: NetworkImage(widget.imageUrl), - ) - else - CircleAvatar( - radius: 36, - backgroundColor: cs.primaryContainer, - child: Text( - widget.name.isNotEmpty ? widget.name[0].toUpperCase() : '?', - style: TextStyle(color: cs.onPrimaryContainer, fontSize: 12), - ), - ), - Text( - widget.name, - style: TextStyle(color: Colors.white, fontSize: 24, height: 1.3), + body: _isLoading + ? _buildShimmer(cs) + : _chatData == null + ? Center( + child: Text( + 'No data', + style: TextStyle(color: cs.onSurfaceVariant), + ), + ) + : _buildContent(cs, l10n), + ); + } + + Widget _buildShimmer(ColorScheme cs) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + Center( + child: Container( + width: 72, + height: 72, + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + shape: BoxShape.circle, ), - - ], + ), + ), + const SizedBox(height: 12), + Center( + child: Container( + width: 120, + height: 20, + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(6), + ), + ), + ), + const SizedBox(height: 24), + ...List.generate( + 10, + (_) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Container( + height: 48, + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + ], + ); + } + + Widget _buildContent(ColorScheme cs, AppLocalizations? l10n) { + final chat = _chatData!; + final type = chat['type'] as String? ?? ''; + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + Center( + child: Container( + width: 72, + height: 72, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: cs.primaryContainer, + ), + child: widget.imageUrl.isNotEmpty + ? ClipOval( + child: Image.network(widget.imageUrl, fit: BoxFit.cover), + ) + : Center( + child: Text( + widget.name.isNotEmpty + ? widget.name[0].toUpperCase() + : '?', + style: TextStyle( + color: cs.onPrimaryContainer, + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + const SizedBox(height: 12), + Center( + child: Text( + widget.name, + style: TextStyle( + color: cs.onSurface, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + const SizedBox(height: 24), + + if (type == 'CHANNEL') ...[ + _buildSectionTitle('Channel', cs), + _buildRow( + l10n?.chatInfoSubscribers ?? 'subscribers:', + (chat['participantsCount'] as int?)?.toString() ?? '-', + cs, + ), + if ((chat['link'] as String?)?.isNotEmpty ?? false) + _buildRow( + l10n?.chatInfoLink ?? 'link:', + chat['link'] as String, + cs, + ), + _buildRow( + l10n?.chatInfoOfficial ?? 'official:', + (chat['options']?['OFFICIAL'] as bool?)?.toString() ?? '-', + cs, + ), + _buildRow( + l10n?.chatInfoComments ?? 'comments:', + (chat['options']?['COMMENTS'] as bool?)?.toString() ?? '-', + cs, + ), + _buildRow( + l10n?.chatInfoAplus ?? 'approved by Roskomnadzor:', + (chat['options']?['A_PLUS_CHANNEL'] as bool?)?.toString() ?? '-', + cs, + ), + _buildRow( + l10n?.chatInfoSignAdmin ?? 'admin signature:', + (chat['options']?['SIGN_ADMIN'] as bool?)?.toString() ?? '-', + cs, + ), + if ((chat['modified'] as int?) != null) + _buildRow( + l10n?.chatInfoLastChanged ?? 'last changed:', + _formatTs(chat['modified'] as int), + cs, + ), + if ((chat['created'] as int?) != null) + _buildRow( + l10n?.chatInfoCreated ?? 'created:', + _formatTs(chat['created'] as int), + cs, + ), + ], + + if (type == 'CHAT') ...[ + _buildSectionTitle('Chat', cs), + _buildRow( + l10n?.chatInfoMembers ?? 'members:', + (chat['participantsCount'] as int?)?.toString() ?? '-', + cs, + ), + if ((chat['hasBots'] as bool?) ?? false) + _buildRow( + l10n?.chatInfoHasBots ?? 'has bots:', + 'true', + cs, + ), + if ((chat['blockedParticipantsCount'] as int?) != null && + chat['blockedParticipantsCount'] > 0) + _buildRow( + l10n?.chatInfoBlockedCount ?? 'blocked in group:', + (chat['blockedParticipantsCount'] as int).toString(), + cs, + ), + _buildRow( + l10n?.chatInfoOfficialStatus ?? 'official status:', + (chat['options']?['OFFICIAL'] as bool?)?.toString() ?? '-', + cs, + ), + if ((chat['modified'] as int?) != null) + _buildRow( + l10n?.chatInfoLastChanged ?? 'last changed:', + _formatTs(chat['modified'] as int), + cs, + ), + if ((chat['joinTime'] as int?) != null && chat['joinTime'] != 1) + _buildRow( + l10n?.chatInfoJoined ?? 'joined:', + _formatTs(chat['joinTime'] as int), + cs, + ), + if ((chat['created'] as int?) != null) + _buildRow( + l10n?.chatInfoGroupCreated ?? 'group created:', + _formatTs(chat['created'] as int), + cs, + ), + if ((chat['owner'] as int?) != null) + _buildRow( + l10n?.chatInfoGroupOwner ?? 'group owner:', + (chat['owner'] as int).toString(), + cs, + ), + ], + + if (type == 'DIALOG') ...[ + if ((chat['created'] as int?) != null && + chat['created'] != 0 && + chat['created'] != 1) + _buildRow( + l10n?.chatInfoDialogStarted ?? 'dialog started:', + _formatTs(chat['created'] as int), + cs, + ), + ], + + const SizedBox(height: 120), + ], + ); + } + + Widget _buildSectionTitle(String title, ColorScheme cs) { + return Padding( + padding: const EdgeInsets.only(top: 16, bottom: 8, left: 4, right: 4), + child: Text( + title, + style: TextStyle( + color: cs.primary, + fontSize: 13, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, ), ), ); } -} + Widget _buildRow(String label, String value, ColorScheme cs) { + return Container( + margin: const EdgeInsets.only(bottom: 1), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + flex: 2, + child: Text( + label, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 3, + child: Text( + value, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.end, + ), + ), + ], + ), + ); + } + + String _formatTs(int ts) { + if (ts < 1000000000000) return ts.toString(); + final dt = DateTime.fromMillisecondsSinceEpoch(ts); + return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ' + '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; + } +} \ No newline at end of file diff --git a/lib/frontend/screens/profile/debug_menu_screen.dart b/lib/frontend/screens/profile/debug_menu_screen.dart index 5f7de26..fc43f31 100644 --- a/lib/frontend/screens/profile/debug_menu_screen.dart +++ b/lib/frontend/screens/profile/debug_menu_screen.dart @@ -1,11 +1,55 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; - +import '../../../backend/modules/chats.dart'; +import '../../../core/utils/logger.dart'; import '../../../main.dart'; -class DebugMenuScreen extends StatelessWidget { +class DebugMenuScreen extends StatefulWidget { const DebugMenuScreen({super.key}); + @override + State createState() => _DebugMenuScreenState(); +} + +class _DebugMenuScreenState extends State { + final _idController = TextEditingController(); + String? _searchResult; + bool _isSearching = false; + + @override + void dispose() { + _idController.dispose(); + super.dispose(); + } + + Future _search() async { + final id = int.tryParse(_idController.text); + if (id == null) return; + setState(() { + _isSearching = true; + _searchResult = null; + }); + try { + final result = await ChatsModule.searchById(api, id); + logger.i('searchById result: $result'); + if (!mounted) return; + if (result is Map && result.containsKey('error')) { + final errorMsg = result['localizedMessage'] ?? result['message'] ?? result['error'] ?? 'Error'; + setState(() => _searchResult = 'Error: $errorMsg'); + } else if (result is Map) { + setState(() => _searchResult = result.toString()); + } else { + setState(() => _searchResult = result?.toString() ?? 'null'); + } + } catch (e) { + if (mounted) { + setState(() => _searchResult = 'Exception: $e'); + } + } finally { + if (mounted) setState(() => _isSearching = false); + } + } + @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; @@ -115,10 +159,92 @@ class DebugMenuScreen extends StatelessWidget { ), ), ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: Container( + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Поиск по ID (opcode 60)', + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextField( + controller: _idController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + hintText: 'Введите user ID', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onSubmitted: (_) => _search(), + ), + ), + const SizedBox(width: 12), + FilledButton( + onPressed: _isSearching ? null : _search, + child: _isSearching + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Symbols.search, size: 20), + ), + ], + ), + if (_searchResult != null) ...[ + const SizedBox(height: 12), + Container( + width: double.infinity, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + ), + constraints: const BoxConstraints(maxHeight: 400), + child: SingleChildScrollView( + child: Text( + _searchResult!, + style: TextStyle( + color: cs.onSurface, + fontSize: 12, + fontFamily: 'monospace', + ), + ), + ), + ), + ], + ], + ), + ), + ), + ), const SliverToBoxAdapter(child: SizedBox(height: 120)), ], ), ), ); } -} +} \ No newline at end of file diff --git a/lib/frontend/screens/profile/info_screen.dart b/lib/frontend/screens/profile/info_screen.dart new file mode 100644 index 0000000..715ccd1 --- /dev/null +++ b/lib/frontend/screens/profile/info_screen.dart @@ -0,0 +1,286 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import '../../../core/storage/app_database.dart'; +import '../../../core/storage/token_storage.dart'; +import '../../../l10n/app_localizations.dart'; +import '../../widgets/custom_notification.dart'; + +class InfoScreen extends StatefulWidget { + const InfoScreen({super.key}); + + @override + State createState() => _InfoScreenState(); +} + +class _InfoScreenState extends State { + bool _isLoading = true; + Map? _info; + + @override + void initState() { + super.initState(); + _loadData(); + } + + Future _loadData() async { + try { + final accountId = await TokenStorage.getActiveAccountId(); + if (accountId == null) { + if (mounted) setState(() => _isLoading = false); + return; + } + final jsonStr = await AppDatabase.getLoginInfo(accountId); + if (jsonStr != null) { + setState(() => _info = jsonDecode(jsonStr) as Map); + } + if (mounted) setState(() => _isLoading = false); + } catch (e) { + if (mounted) { + showCustomNotification(context, 'Error: $e'); + setState(() => _isLoading = false); + } + } + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context); + + return Scaffold( + backgroundColor: cs.surface, + appBar: AppBar( + backgroundColor: cs.surface, + elevation: 0, + leading: IconButton( + icon: Icon(Symbols.arrow_back, color: cs.onSurface), + onPressed: () => Navigator.pop(context), + ), + title: Text( + l10n?.infoTitle ?? 'Info', + style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600), + ), + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _info == null + ? Center( + child: Text( + 'No data', + style: TextStyle(color: cs.onSurfaceVariant), + ), + ) + : _buildContent(cs, l10n!), + ); + } + + Widget _buildContent(ColorScheme cs, AppLocalizations l10n) { + final info = _info!; + final server = info['server'] as Map?; + final user = info['user'] as Map?; + final yMap = server?['y-map'] as Map?; + + final accountKeys = { + 'registrationTime': l10n.infoRegistrationTime, + 'country': l10n.infoCountry, + 'videoChatHistory': l10n.infoVideoChatHistory, + 'updateTime': l10n.infoUpdateTime, + 'id': l10n.infoId, + 'chatMarker': l10n.infoChatMarker, + }; + + final serverKeys = { + 'account-removal-enabled': l10n.infoAccountRemovalEnabled, + 'image-size': l10n.infoImageSize, + 'gce': l10n.infoGce, + 'gcce': l10n.infoGcce, + 'max-msg-length': l10n.infoMaxMsgLength, + 'quotes-enabled': l10n.infoQuotesEnabled, + 'calls-endpoint': l10n.infoCallsEndpoint, + 'send-location-enabled': l10n.infoSendLocationEnabled, + 'lgce': l10n.infoLgce, + 'wud': l10n.infoWud, + 'video-msg-enabled': l10n.infoVideoMsgEnabled, + 'grse': l10n.infoGrse, + 'edit-timeout': l10n.infoEditTimeout, + 'image-quality': l10n.infoImageQuality, + 'unsafe-files-alert': l10n.infoUnsafeFilesAlert, + 'account-nickname-enabled': l10n.infoAccountNicknameEnabled, + 'mentions_entity_names_limit': l10n.infoMentionsEntityNamesLimit, + 'reactions-enabled': l10n.infoReactionsEnabled, + }; + + return ListView( + padding: const EdgeInsets.all(16), + children: [ + _buildSectionTitle(l10n.infoAccountSection, cs), + ...accountKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(info[e.key]), cs)), + + const SizedBox(height: 16), + _buildSectionTitle(l10n.infoServerSection, cs), + ...serverKeys.entries.map((e) => _buildRow(e.key, e.value, _formatValue(server?[e.key]), cs)), + + const SizedBox(height: 8), + _buildSectionTitle(l10n.infoYMapSection, cs), + _buildRow('tile', l10n.infoTile, yMap?['tile']?.toString() ?? '-', cs), + _buildRow('geocoder', l10n.infoGeocoder, yMap?['geocoder']?.toString() ?? '-', cs), + _buildRow('static', l10n.infoStatic, yMap?['static']?.toString() ?? '-', cs), + + const SizedBox(height: 8), + _buildSectionTitle(l10n.infoFileUploadTypes, cs), + _buildListRow(server?['file-upload-unsupported-types'] as List?, cs), + + const SizedBox(height: 8), + _buildSectionTitle(l10n.infoWhiteListLinks, cs), + _buildListRow(server?['white-list-links'] as List?, cs), + + const SizedBox(height: 8), + _buildSectionTitle(l10n.infoUserSection, cs), + if (user != null) + ...user.entries + .where((e) => e.value != null) + .map((e) => _buildRow(e.key, e.key, e.value.toString(), cs)), + + const SizedBox(height: 120), + ], + ); + } + + Widget _buildSectionTitle(String title, ColorScheme cs) { + return Padding( + padding: const EdgeInsets.only(top: 16, bottom: 8, left: 4, right: 4), + child: Text( + title, + style: TextStyle( + color: cs.primary, + fontSize: 13, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + ); + } + + Widget _buildRow(String key, String label, String value, ColorScheme cs) { + return Container( + margin: const EdgeInsets.only(bottom: 1), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Expanded( + flex: 2, + child: Text( + label, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 3, + child: Text( + value, + style: TextStyle( + color: cs.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.end, + ), + ), + ], + ), + ); + } + + Widget _buildListRow(List? items, ColorScheme cs) { + if (items == null || items.isEmpty) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + ), + child: Text('-', style: TextStyle(color: cs.onSurfaceVariant)), + ); + } + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(12), + ), + child: Wrap( + spacing: 8, + runSpacing: 4, + children: items + .map( + (item) => Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + item.toString(), + style: TextStyle(fontSize: 13, color: cs.onSurface), + ), + ), + ) + .toList(), + ), + ); + } + + String _formatValue(dynamic value) { + if (value == null) return '-'; + if (value is Map && value.containsKey('chatMarker')) { + final ts = value['chatMarker'] as int?; + if (ts != null) { + final dt = DateTime.fromMillisecondsSinceEpoch(ts); + return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ' + '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; + } + return '-'; + } + if (value is int && value > 1000000000000) { + final dt = DateTime.fromMillisecondsSinceEpoch(value); + return '${dt.year}-${dt.month.toString().padLeft(2, '0')}-${dt.day.toString().padLeft(2, '0')} ' + '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}:${dt.second.toString().padLeft(2, '0')}'; + } + if (value is int && value > 86400) { + final weeks = value ~/ 604800; + final days = (value % 604800) ~/ 86400; + if (weeks > 0) { + return '$weeks ${_w(weeks)} ${days > 0 ? '$days ${_d(days)}' : ''}'.trim(); + } + final h = value ~/ 3600; + final m = (value % 3600) ~/ 60; + if (h > 0) return '${h}h ${m}m'; + return '${m}m'; + } + return value.toString(); + } + + String _w(int n) { + final m = n % 10; + if (m == 1 && n != 11) return 'нед'; + if ((m == 2 || m == 3 || m == 4) && (n < 10 || n > 20)) return 'нед'; + return 'нед'; + } + + String _d(int n) { + final m = n % 10; + if (m == 1 && n != 11) return 'дн'; + if ((m == 2 || m == 3 || m == 4) && (n < 10 || n > 20)) return 'дн'; + return 'дн'; + } +} \ No newline at end of file diff --git a/lib/frontend/screens/profile/security_screen.dart b/lib/frontend/screens/profile/security_screen.dart index 676396e..e65ab62 100644 --- a/lib/frontend/screens/profile/security_screen.dart +++ b/lib/frontend/screens/profile/security_screen.dart @@ -222,6 +222,7 @@ class _SecurityScreenState extends State case 'CONTACTS': return 'Мои контакты'; case 'NONE': + case 'NOBODY': return 'Никто'; default: return value; @@ -481,9 +482,31 @@ class _SecurityScreenState extends State icon: Icons.visibility_off_outlined, label: 'Видеть статус «в сети»', value: _privacyConfig?.hidden == true ? 'Никто' : 'Мои контакты', - isLast: true, + isLast: false, onTap: () => _showHiddenStatusSheet(context, cs), ), + _buildOptionRow( + cs, + icon: Symbols.contact_page, + label: 'Видеть мой номер', + value: _getPrivacyLabel( + _privacyConfig?.phoneNumberPrivacy ?? 'ALL', + ), + isLast: true, + onTap: () => _showOptionSheet( + context, + cs, + title: 'Видеть мой номер', + currentValue: _privacyConfig?.phoneNumberPrivacy ?? 'ALL', + options: const [ + ('ALL', 'Все'), + ('CONTACTS', 'Мои контакты'), + ('NOBODY', 'Никто'), + ], + onSelect: (value) => + _updateSetting('PHONE_NUMBER_PRIVACY', value), + ), + ), ], ], ), diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 2389838..3804ac8 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -8,6 +8,7 @@ import '../../../l10n/app_localizations.dart'; import '../auth/proxy_settings_sheet.dart'; import 'debug_menu_screen.dart'; import 'devices_screen.dart'; +import 'info_screen.dart'; import 'security_screen.dart'; import 'spoof_screen.dart'; @@ -96,15 +97,27 @@ class _SettingsTabState extends State { SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), - child: _buildSection( +child: _buildSection( context, cs, - items: const [ - _SettingsItem(icon: Symbols.badge, label: 'Цифровой ID'), - _SettingsItem( + items: [ + const _SettingsItem(icon: Symbols.badge, label: 'Цифровой ID'), + const _SettingsItem( icon: Symbols.language, label: 'Войти в Сферум', ), + _SettingsItem( + icon: Symbols.info, + label: 'Info', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const InfoScreen(), + ), + ); + }, + ), ], ), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 806d0c5..174459b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -104,5 +104,60 @@ } } }, - "profileMenuSpoof": "Spoofing" + "profileMenuSpoof": "Spoofing", + "infoTitle": "Info", + "infoAccountSection": "Account", + "infoServerSection": "Server", + "infoUserSection": "User", + "infoYMapSection": "Y-Map", + "infoFileUploadTypes": "file-upload-unsupported-types", + "infoWhiteListLinks": "white-list-links", + "infoRegistrationTime": "registrationTime", + "infoCountry": "country", + "infoVideoChatHistory": "videoChatHistory", + "infoUpdateTime": "updateTime", + "infoId": "id", + "infoChatMarker": "chatMarker", + "infoAccountRemovalEnabled": "account-removal-enabled", + "infoImageSize": "image-size", + "infoGce": "gce", + "infoGcce": "gcce", + "infoMaxMsgLength": "max-msg-length", + "infoQuotesEnabled": "quotes-enabled", + "infoCallsEndpoint": "calls-endpoint", + "infoSendLocationEnabled": "send-location-enabled", + "infoLgce": "lgce", + "infoWud": "wud", + "infoVideoMsgEnabled": "video-msg-enabled", + "infoGrse": "grse", + "infoEditTimeout": "edit-timeout", + "infoImageQuality": "image-quality", + "infoUnsafeFilesAlert": "unsafe-files-alert", + "infoAccountNicknameEnabled": "account-nickname-enabled", + "infoMentionsEntityNamesLimit": "mentions_entity_names_limit", + "infoReactionsEnabled": "reactions-enabled", + "infoTile": "tile", + "infoGeocoder": "geocoder", + "infoStatic": "static", + "chatInfoSubscribers": "subscribers:", + "chatInfoInvitedBy": "invited by:", + "chatInfoLink": "link:", + "chatInfoOfficial": "official:", + "chatInfoComments": "comments:", + "chatInfoAplus": "approved by Roskomnadzor:", + "chatInfoSignAdmin": "admin signature:", + "chatInfoLastChanged": "last changed:", + "chatInfoJoinTime": "joined:", + "chatInfoCreated": "created:", + "chatInfoTitle": "Info", + "chatInfoMembers": "members:", + "chatInfoLastSeen": "last seen recently", + "chatInfoHasBots": "has bots:", + "chatInfoBlockedCount": "blocked in group:", + "chatInfoOfficialStatus": "official status:", + "chatInfoLastChanged": "last changed:", + "chatInfoJoined": "joined:", + "chatInfoGroupCreated": "group created:", + "chatInfoGroupOwner": "group owner:", + "chatInfoDialogStarted": "dialog started:" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 5bd5013..15fbdfb 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -631,6 +631,330 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Spoofing'** String get profileMenuSpoof; + + /// No description provided for @infoTitle. + /// + /// In en, this message translates to: + /// **'Info'** + String get infoTitle; + + /// No description provided for @infoAccountSection. + /// + /// In en, this message translates to: + /// **'Account'** + String get infoAccountSection; + + /// No description provided for @infoServerSection. + /// + /// In en, this message translates to: + /// **'Server'** + String get infoServerSection; + + /// No description provided for @infoUserSection. + /// + /// In en, this message translates to: + /// **'User'** + String get infoUserSection; + + /// No description provided for @infoYMapSection. + /// + /// In en, this message translates to: + /// **'Y-Map'** + String get infoYMapSection; + + /// No description provided for @infoFileUploadTypes. + /// + /// In en, this message translates to: + /// **'file-upload-unsupported-types'** + String get infoFileUploadTypes; + + /// No description provided for @infoWhiteListLinks. + /// + /// In en, this message translates to: + /// **'white-list-links'** + String get infoWhiteListLinks; + + /// No description provided for @infoRegistrationTime. + /// + /// In en, this message translates to: + /// **'registrationTime'** + String get infoRegistrationTime; + + /// No description provided for @infoCountry. + /// + /// In en, this message translates to: + /// **'country'** + String get infoCountry; + + /// No description provided for @infoVideoChatHistory. + /// + /// In en, this message translates to: + /// **'videoChatHistory'** + String get infoVideoChatHistory; + + /// No description provided for @infoUpdateTime. + /// + /// In en, this message translates to: + /// **'updateTime'** + String get infoUpdateTime; + + /// No description provided for @infoId. + /// + /// In en, this message translates to: + /// **'id'** + String get infoId; + + /// No description provided for @infoChatMarker. + /// + /// In en, this message translates to: + /// **'chatMarker'** + String get infoChatMarker; + + /// No description provided for @infoAccountRemovalEnabled. + /// + /// In en, this message translates to: + /// **'account-removal-enabled'** + String get infoAccountRemovalEnabled; + + /// No description provided for @infoImageSize. + /// + /// In en, this message translates to: + /// **'image-size'** + String get infoImageSize; + + /// No description provided for @infoGce. + /// + /// In en, this message translates to: + /// **'gce'** + String get infoGce; + + /// No description provided for @infoGcce. + /// + /// In en, this message translates to: + /// **'gcce'** + String get infoGcce; + + /// No description provided for @infoMaxMsgLength. + /// + /// In en, this message translates to: + /// **'max-msg-length'** + String get infoMaxMsgLength; + + /// No description provided for @infoQuotesEnabled. + /// + /// In en, this message translates to: + /// **'quotes-enabled'** + String get infoQuotesEnabled; + + /// No description provided for @infoCallsEndpoint. + /// + /// In en, this message translates to: + /// **'calls-endpoint'** + String get infoCallsEndpoint; + + /// No description provided for @infoSendLocationEnabled. + /// + /// In en, this message translates to: + /// **'send-location-enabled'** + String get infoSendLocationEnabled; + + /// No description provided for @infoLgce. + /// + /// In en, this message translates to: + /// **'lgce'** + String get infoLgce; + + /// No description provided for @infoWud. + /// + /// In en, this message translates to: + /// **'wud'** + String get infoWud; + + /// No description provided for @infoVideoMsgEnabled. + /// + /// In en, this message translates to: + /// **'video-msg-enabled'** + String get infoVideoMsgEnabled; + + /// No description provided for @infoGrse. + /// + /// In en, this message translates to: + /// **'grse'** + String get infoGrse; + + /// No description provided for @infoEditTimeout. + /// + /// In en, this message translates to: + /// **'edit-timeout'** + String get infoEditTimeout; + + /// No description provided for @infoImageQuality. + /// + /// In en, this message translates to: + /// **'image-quality'** + String get infoImageQuality; + + /// No description provided for @infoUnsafeFilesAlert. + /// + /// In en, this message translates to: + /// **'unsafe-files-alert'** + String get infoUnsafeFilesAlert; + + /// No description provided for @infoAccountNicknameEnabled. + /// + /// In en, this message translates to: + /// **'account-nickname-enabled'** + String get infoAccountNicknameEnabled; + + /// No description provided for @infoMentionsEntityNamesLimit. + /// + /// In en, this message translates to: + /// **'mentions_entity_names_limit'** + String get infoMentionsEntityNamesLimit; + + /// No description provided for @infoReactionsEnabled. + /// + /// In en, this message translates to: + /// **'reactions-enabled'** + String get infoReactionsEnabled; + + /// No description provided for @infoTile. + /// + /// In en, this message translates to: + /// **'tile'** + String get infoTile; + + /// No description provided for @infoGeocoder. + /// + /// In en, this message translates to: + /// **'geocoder'** + String get infoGeocoder; + + /// No description provided for @infoStatic. + /// + /// In en, this message translates to: + /// **'static'** + String get infoStatic; + + /// No description provided for @chatInfoSubscribers. + /// + /// In en, this message translates to: + /// **'subscribers:'** + String get chatInfoSubscribers; + + /// No description provided for @chatInfoInvitedBy. + /// + /// In en, this message translates to: + /// **'invited by:'** + String get chatInfoInvitedBy; + + /// No description provided for @chatInfoLink. + /// + /// In en, this message translates to: + /// **'link:'** + String get chatInfoLink; + + /// No description provided for @chatInfoOfficial. + /// + /// In en, this message translates to: + /// **'official:'** + String get chatInfoOfficial; + + /// No description provided for @chatInfoComments. + /// + /// In en, this message translates to: + /// **'comments:'** + String get chatInfoComments; + + /// No description provided for @chatInfoAplus. + /// + /// In en, this message translates to: + /// **'approved by Roskomnadzor:'** + String get chatInfoAplus; + + /// No description provided for @chatInfoSignAdmin. + /// + /// In en, this message translates to: + /// **'admin signature:'** + String get chatInfoSignAdmin; + + /// No description provided for @chatInfoLastChanged. + /// + /// In en, this message translates to: + /// **'last changed:'** + String get chatInfoLastChanged; + + /// No description provided for @chatInfoJoinTime. + /// + /// In en, this message translates to: + /// **'joined:'** + String get chatInfoJoinTime; + + /// No description provided for @chatInfoCreated. + /// + /// In en, this message translates to: + /// **'created:'** + String get chatInfoCreated; + + /// No description provided for @chatInfoTitle. + /// + /// In en, this message translates to: + /// **'Info'** + String get chatInfoTitle; + + /// No description provided for @chatInfoMembers. + /// + /// In en, this message translates to: + /// **'members:'** + String get chatInfoMembers; + + /// No description provided for @chatInfoLastSeen. + /// + /// In en, this message translates to: + /// **'last seen recently'** + String get chatInfoLastSeen; + + /// No description provided for @chatInfoHasBots. + /// + /// In en, this message translates to: + /// **'has bots:'** + String get chatInfoHasBots; + + /// No description provided for @chatInfoBlockedCount. + /// + /// In en, this message translates to: + /// **'blocked in group:'** + String get chatInfoBlockedCount; + + /// No description provided for @chatInfoOfficialStatus. + /// + /// In en, this message translates to: + /// **'official status:'** + String get chatInfoOfficialStatus; + + /// No description provided for @chatInfoJoined. + /// + /// In en, this message translates to: + /// **'joined:'** + String get chatInfoJoined; + + /// No description provided for @chatInfoGroupCreated. + /// + /// In en, this message translates to: + /// **'group created:'** + String get chatInfoGroupCreated; + + /// No description provided for @chatInfoGroupOwner. + /// + /// In en, this message translates to: + /// **'group owner:'** + String get chatInfoGroupOwner; + + /// No description provided for @chatInfoDialogStarted. + /// + /// In en, this message translates to: + /// **'dialog started:'** + String get chatInfoDialogStarted; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bfce511..0320c05 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -289,4 +289,166 @@ class AppLocalizationsEn extends AppLocalizations { @override String get profileMenuSpoof => 'Spoofing'; + + @override + String get infoTitle => 'Info'; + + @override + String get infoAccountSection => 'Account'; + + @override + String get infoServerSection => 'Server'; + + @override + String get infoUserSection => 'User'; + + @override + String get infoYMapSection => 'Y-Map'; + + @override + String get infoFileUploadTypes => 'file-upload-unsupported-types'; + + @override + String get infoWhiteListLinks => 'white-list-links'; + + @override + String get infoRegistrationTime => 'registrationTime'; + + @override + String get infoCountry => 'country'; + + @override + String get infoVideoChatHistory => 'videoChatHistory'; + + @override + String get infoUpdateTime => 'updateTime'; + + @override + String get infoId => 'id'; + + @override + String get infoChatMarker => 'chatMarker'; + + @override + String get infoAccountRemovalEnabled => 'account-removal-enabled'; + + @override + String get infoImageSize => 'image-size'; + + @override + String get infoGce => 'gce'; + + @override + String get infoGcce => 'gcce'; + + @override + String get infoMaxMsgLength => 'max-msg-length'; + + @override + String get infoQuotesEnabled => 'quotes-enabled'; + + @override + String get infoCallsEndpoint => 'calls-endpoint'; + + @override + String get infoSendLocationEnabled => 'send-location-enabled'; + + @override + String get infoLgce => 'lgce'; + + @override + String get infoWud => 'wud'; + + @override + String get infoVideoMsgEnabled => 'video-msg-enabled'; + + @override + String get infoGrse => 'grse'; + + @override + String get infoEditTimeout => 'edit-timeout'; + + @override + String get infoImageQuality => 'image-quality'; + + @override + String get infoUnsafeFilesAlert => 'unsafe-files-alert'; + + @override + String get infoAccountNicknameEnabled => 'account-nickname-enabled'; + + @override + String get infoMentionsEntityNamesLimit => 'mentions_entity_names_limit'; + + @override + String get infoReactionsEnabled => 'reactions-enabled'; + + @override + String get infoTile => 'tile'; + + @override + String get infoGeocoder => 'geocoder'; + + @override + String get infoStatic => 'static'; + + @override + String get chatInfoSubscribers => 'subscribers:'; + + @override + String get chatInfoInvitedBy => 'invited by:'; + + @override + String get chatInfoLink => 'link:'; + + @override + String get chatInfoOfficial => 'official:'; + + @override + String get chatInfoComments => 'comments:'; + + @override + String get chatInfoAplus => 'approved by Roskomnadzor:'; + + @override + String get chatInfoSignAdmin => 'admin signature:'; + + @override + String get chatInfoLastChanged => 'last changed:'; + + @override + String get chatInfoJoinTime => 'joined:'; + + @override + String get chatInfoCreated => 'created:'; + + @override + String get chatInfoTitle => 'Info'; + + @override + String get chatInfoMembers => 'members:'; + + @override + String get chatInfoLastSeen => 'last seen recently'; + + @override + String get chatInfoHasBots => 'has bots:'; + + @override + String get chatInfoBlockedCount => 'blocked in group:'; + + @override + String get chatInfoOfficialStatus => 'official status:'; + + @override + String get chatInfoJoined => 'joined:'; + + @override + String get chatInfoGroupCreated => 'group created:'; + + @override + String get chatInfoGroupOwner => 'group owner:'; + + @override + String get chatInfoDialogStarted => 'dialog started:'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 7a1c8b4..d7780cf 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -291,4 +291,166 @@ class AppLocalizationsRu extends AppLocalizations { @override String get profileMenuSpoof => 'Подмена данных'; + + @override + String get infoTitle => 'Info'; + + @override + String get infoAccountSection => 'Аккаунт'; + + @override + String get infoServerSection => 'Сервер'; + + @override + String get infoUserSection => 'Пользователь'; + + @override + String get infoYMapSection => 'Y-Map'; + + @override + String get infoFileUploadTypes => 'запрещённые типы файлов'; + + @override + String get infoWhiteListLinks => 'безопасные ссылки'; + + @override + String get infoRegistrationTime => 'Дата регистрации:'; + + @override + String get infoCountry => 'Регион аккаунта:'; + + @override + String get infoVideoChatHistory => 'videoChatHistory'; + + @override + String get infoUpdateTime => 'Последнее обновление аватарки:'; + + @override + String get infoId => 'id аккаунта:'; + + @override + String get infoChatMarker => 'chatMarker'; + + @override + String get infoAccountRemovalEnabled => 'Мгновенное удаление аккаунта:'; + + @override + String get infoImageSize => 'image-size'; + + @override + String get infoGce => 'gce'; + + @override + String get infoGcce => 'gcce'; + + @override + String get infoMaxMsgLength => 'макс. длина сообщения:'; + + @override + String get infoQuotesEnabled => 'quotes-enabled'; + + @override + String get infoCallsEndpoint => 'calls-endpoint'; + + @override + String get infoSendLocationEnabled => 'отправка гео.:'; + + @override + String get infoLgce => 'lgce'; + + @override + String get infoWud => 'wud'; + + @override + String get infoVideoMsgEnabled => 'Кружки:'; + + @override + String get infoGrse => 'grse'; + + @override + String get infoEditTimeout => 'Можно редактировать сообщение в течении:'; + + @override + String get infoImageQuality => 'image-quality'; + + @override + String get infoUnsafeFilesAlert => 'unsafe-files-alert'; + + @override + String get infoAccountNicknameEnabled => 'account-nickname-enabled'; + + @override + String get infoMentionsEntityNamesLimit => 'макс. кол-во упоминаний:'; + + @override + String get infoReactionsEnabled => 'reactions-enabled'; + + @override + String get infoTile => 'tile'; + + @override + String get infoGeocoder => 'geocoder'; + + @override + String get infoStatic => 'static'; + + @override + String get chatInfoSubscribers => 'подписчиков:'; + + @override + String get chatInfoInvitedBy => 'Приглашён от:'; + + @override + String get chatInfoLink => 'ссылка:'; + + @override + String get chatInfoOfficial => 'оффициальный:'; + + @override + String get chatInfoComments => 'комментарии:'; + + @override + String get chatInfoAplus => 'подтверждён Роскомнадзором:'; + + @override + String get chatInfoSignAdmin => 'Подпись админов:'; + + @override + String get chatInfoLastChanged => 'последнее изменение:'; + + @override + String get chatInfoJoinTime => 'заход в канал:'; + + @override + String get chatInfoCreated => 'канал создан:'; + + @override + String get chatInfoTitle => 'Информация'; + + @override + String get chatInfoMembers => 'участников:'; + + @override + String get chatInfoLastSeen => 'был(а) недавно'; + + @override + String get chatInfoHasBots => 'Есть боты:'; + + @override + String get chatInfoBlockedCount => 'в ЧС группы:'; + + @override + String get chatInfoOfficialStatus => 'Официальный статус:'; + + @override + String get chatInfoJoined => 'Зашли в:'; + + @override + String get chatInfoGroupCreated => 'Группа создана в:'; + + @override + String get chatInfoGroupOwner => 'Создатель группы:'; + + @override + String get chatInfoDialogStarted => 'ЛС начат в:'; } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 0f2378c..c103607 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -104,5 +104,60 @@ } } }, - "profileMenuSpoof": "Подмена данных" + "profileMenuSpoof": "Подмена данных", + "infoTitle": "Info", + "infoAccountSection": "Аккаунт", + "infoServerSection": "Сервер", + "infoUserSection": "Пользователь", + "infoYMapSection": "Y-Map", + "infoFileUploadTypes": "запрещённые типы файлов", + "infoWhiteListLinks": "безопасные ссылки", + "infoRegistrationTime": "Дата регистрации:", + "infoCountry": "Регион аккаунта:", + "infoVideoChatHistory": "videoChatHistory", + "infoUpdateTime": "Последнее обновление аватарки:", + "infoId": "id аккаунта:", + "infoChatMarker": "chatMarker", + "infoAccountRemovalEnabled": "Мгновенное удаление аккаунта:", + "infoImageSize": "image-size", + "infoGce": "gce", + "infoGcce": "gcce", + "infoMaxMsgLength": "макс. длина сообщения:", + "infoQuotesEnabled": "quotes-enabled", + "infoCallsEndpoint": "calls-endpoint", + "infoSendLocationEnabled": "отправка гео.:", + "infoLgce": "lgce", + "infoWud": "wud", + "infoVideoMsgEnabled": "Кружки:", + "infoGrse": "grse", + "infoEditTimeout": "Можно редактировать сообщение в течении:", + "infoImageQuality": "image-quality", + "infoUnsafeFilesAlert": "unsafe-files-alert", + "infoAccountNicknameEnabled": "account-nickname-enabled", + "infoMentionsEntityNamesLimit": "макс. кол-во упоминаний:", + "infoReactionsEnabled": "reactions-enabled", + "infoTile": "tile", + "infoGeocoder": "geocoder", + "infoStatic": "static", + "chatInfoSubscribers": "подписчиков:", + "chatInfoInvitedBy": "Приглашён от:", + "chatInfoLink": "ссылка:", + "chatInfoOfficial": "оффициальный:", + "chatInfoComments": "комментарии:", + "chatInfoAplus": "подтверждён Роскомнадзором:", + "chatInfoSignAdmin": "Подпись админов:", + "chatInfoLastChanged": "последнее изменение:", + "chatInfoJoinTime": "заход в канал:", + "chatInfoCreated": "канал создан:", + "chatInfoTitle": "Информация", + "chatInfoMembers": "участников:", + "chatInfoLastSeen": "был(а) недавно", + "chatInfoHasBots": "Есть боты:", + "chatInfoBlockedCount": "в ЧС группы:", + "chatInfoOfficialStatus": "Официальный статус:", + "chatInfoLastChanged": "последнее изменение:", + "chatInfoJoined": "Зашли в:", + "chatInfoGroupCreated": "Группа создана в:", + "chatInfoGroupOwner": "Создатель группы:", + "chatInfoDialogStarted": "ЛС начат в:" }