исправлено отображение системных действий в предпросмотре, добавлено прокси на экран входа, вроде как добавлена поддержка socks5 прокси
This commit is contained in:
@@ -168,7 +168,14 @@ class ApiService {
|
||||
Future<void> _connectToUrl(String url) async {
|
||||
_isSessionOnline = false;
|
||||
_onlineCompleter = Completer<void>();
|
||||
_chatsFetchedInThisSession = false;
|
||||
final bool hadChatsFetched = _chatsFetchedInThisSession;
|
||||
final bool hasValidToken = authToken != null;
|
||||
|
||||
if (!hasValidToken) {
|
||||
_chatsFetchedInThisSession = false;
|
||||
} else {
|
||||
_chatsFetchedInThisSession = hadChatsFetched;
|
||||
}
|
||||
|
||||
_connectionStatusController.add('connecting');
|
||||
|
||||
@@ -192,7 +199,7 @@ class ApiService {
|
||||
|
||||
if (proxySettings.isEnabled && proxySettings.host.isNotEmpty) {
|
||||
print(
|
||||
'Используем HTTP/HTTPS прокси ${proxySettings.host}:${proxySettings.port}',
|
||||
'Используем ${proxySettings.protocol.name.toUpperCase()} прокси ${proxySettings.host}:${proxySettings.port}',
|
||||
);
|
||||
final customHttpClient = await ProxyService.instance
|
||||
.getHttpClientWithProxy();
|
||||
@@ -1250,7 +1257,6 @@ class ApiService {
|
||||
void clearChatsCache() {
|
||||
_lastChatsPayload = null;
|
||||
_lastChatsAt = null;
|
||||
_chatsFetchedInThisSession = false;
|
||||
print("Кэш чатов очищен.");
|
||||
}
|
||||
|
||||
@@ -1903,9 +1909,9 @@ class ApiService {
|
||||
authToken = token;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString('authToken', token);
|
||||
if (_channel != null) {
|
||||
disconnect();
|
||||
}
|
||||
|
||||
disconnect();
|
||||
|
||||
await connect();
|
||||
await getChatsAndContacts(force: true);
|
||||
if (userId != null) {
|
||||
|
||||
@@ -351,7 +351,7 @@ class _ChatsScreenState extends State<ChatsScreen>
|
||||
final oldChat = _allChats[chatIndex];
|
||||
|
||||
if (deletedMessageIds.contains(oldChat.lastMessage.id)) {
|
||||
ApiService.instance.getChatsAndContacts(force: true).then((data) {
|
||||
ApiService.instance.getChatsOnly(force: true).then((data) {
|
||||
if (mounted) {
|
||||
final chats = data['chats'] as List<dynamic>;
|
||||
final filtered = chats
|
||||
@@ -3547,7 +3547,6 @@ class _SferumWebViewPanelState extends State<SferumWebViewPanel> {
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -39,7 +39,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
|
||||
_apiSubscription = ApiService.instance.messages.listen((message) {
|
||||
if (!mounted) return;
|
||||
|
||||
|
||||
if (message['type'] == 'password_set_success') {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
@@ -60,7 +59,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (message['cmd'] == 3 && message['opcode'] == 116) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
@@ -147,7 +145,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!password.contains(RegExp(r'[A-Z]')) ||
|
||||
!password.contains(RegExp(r'[a-z]'))) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -166,7 +163,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!password.contains(RegExp(r'[0-9]'))) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -182,7 +178,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!password.contains(RegExp(r'[!@#$%^&*(),.?":{}|<>]'))) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -252,7 +247,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
@@ -288,7 +282,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
|
||||
Text(
|
||||
'Установить пароль',
|
||||
style: Theme.of(
|
||||
@@ -338,7 +331,6 @@ class _PasswordManagementScreenState extends State<PasswordManagementScreen> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:flutter/scheduler.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:gwid/api_service.dart';
|
||||
import 'package:gwid/otp_screen.dart';
|
||||
import 'package:gwid/proxy_service.dart';
|
||||
import 'package:gwid/screens/settings/proxy_settings_screen.dart';
|
||||
import 'package:gwid/screens/settings/session_spoofing_screen.dart';
|
||||
import 'package:gwid/token_auth_screen.dart';
|
||||
import 'package:gwid/tos_screen.dart'; // Импорт экрана ToS
|
||||
@@ -61,6 +63,7 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
bool _isButtonEnabled = false;
|
||||
bool _isLoading = false;
|
||||
bool _hasCustomAnonymity = false;
|
||||
bool _hasProxyConfigured = false;
|
||||
StreamSubscription? _apiSubscription;
|
||||
bool _showContent = false;
|
||||
bool _isTosAccepted = false; // Состояние для отслеживания принятия соглашения
|
||||
@@ -103,6 +106,7 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
|
||||
_initializeMaskFormatter();
|
||||
_checkAnonymitySettings();
|
||||
_checkProxySettings();
|
||||
_phoneController.addListener(_onPhoneChanged);
|
||||
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
@@ -206,6 +210,19 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
if (mounted) setState(() => _hasCustomAnonymity = anonymityEnabled);
|
||||
}
|
||||
|
||||
Future<void> _checkProxySettings() async {
|
||||
final settings = await ProxyService.instance.loadProxySettings();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_hasProxyConfigured = settings.isEnabled && settings.host.isNotEmpty;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void refreshProxySettings() {
|
||||
_checkProxySettings();
|
||||
}
|
||||
|
||||
void _requestOtp() async {
|
||||
if (!_isButtonEnabled || _isLoading || !_isTosAccepted) return;
|
||||
setState(() => _isLoading = true);
|
||||
@@ -413,6 +430,8 @@ class _PhoneEntryScreenState extends State<PhoneEntryScreen>
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
_AnonymityCard(isConfigured: _hasCustomAnonymity),
|
||||
const SizedBox(height: 16),
|
||||
_ProxyCard(isConfigured: _hasProxyConfigured),
|
||||
const SizedBox(height: 24),
|
||||
Text.rich(
|
||||
textAlign: TextAlign.center,
|
||||
@@ -664,3 +683,89 @@ class _AnonymityCard extends StatelessWidget {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _ProxyCard extends StatelessWidget {
|
||||
final bool isConfigured;
|
||||
const _ProxyCard({required this.isConfigured});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
final Color cardColor = isConfigured
|
||||
? colors.secondaryContainer
|
||||
: colors.surfaceContainerHighest.withOpacity(0.5);
|
||||
final Color onCardColor = isConfigured
|
||||
? colors.onSecondaryContainer
|
||||
: colors.onSurfaceVariant;
|
||||
final IconData icon = isConfigured ? Icons.vpn_key : Icons.vpn_key_outlined;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: cardColor,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: BorderSide(color: colors.outline.withOpacity(0.5)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: onCardColor, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isConfigured
|
||||
? 'Прокси-сервер настроен и активен'
|
||||
: 'Настройте прокси-сервер для подключения',
|
||||
style: GoogleFonts.manrope(
|
||||
textStyle: textTheme.bodyMedium,
|
||||
color: onCardColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: isConfigured
|
||||
? FilledButton.tonalIcon(
|
||||
onPressed: _navigateToProxyScreen(context),
|
||||
icon: const Icon(Icons.settings, size: 18),
|
||||
label: Text(
|
||||
'Изменить настройки',
|
||||
style: GoogleFonts.manrope(fontWeight: FontWeight.bold),
|
||||
),
|
||||
)
|
||||
: FilledButton.icon(
|
||||
onPressed: _navigateToProxyScreen(context),
|
||||
icon: const Icon(Icons.vpn_key, size: 18),
|
||||
label: Text(
|
||||
'Настроить прокси',
|
||||
style: GoogleFonts.manrope(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
VoidCallback _navigateToProxyScreen(BuildContext context) {
|
||||
return () async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const ProxySettingsScreen()),
|
||||
);
|
||||
if (context.mounted) {
|
||||
final state = context.findAncestorStateOfType<_PhoneEntryScreenState>();
|
||||
state?.refreshProxySettings();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
@@ -31,9 +29,14 @@ class ProxyService {
|
||||
return ProxySettings();
|
||||
}
|
||||
|
||||
|
||||
Future<void> checkProxy(ProxySettings settings) async {
|
||||
print("Проверка прокси: ${settings.host}:${settings.port}");
|
||||
|
||||
if (settings.protocol == ProxyProtocol.socks5) {
|
||||
await _checkSocks5Proxy(settings);
|
||||
return;
|
||||
}
|
||||
|
||||
HttpClient client = _createClientWithOptions(settings);
|
||||
|
||||
client.connectionTimeout = const Duration(seconds: 10);
|
||||
@@ -47,7 +50,6 @@ class ProxyService {
|
||||
print("Ответ от прокси получен, статус: ${response.statusCode}");
|
||||
|
||||
if (response.statusCode >= 400) {
|
||||
|
||||
throw Exception('Прокси вернул ошибку: ${response.statusCode}');
|
||||
}
|
||||
} on HandshakeException catch (e) {
|
||||
@@ -71,39 +73,78 @@ class ProxyService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _checkSocks5Proxy(ProxySettings settings) async {
|
||||
Socket? proxySocket;
|
||||
try {
|
||||
print("Проверка SOCKS5 прокси: ${settings.host}:${settings.port}");
|
||||
|
||||
proxySocket = await Socket.connect(
|
||||
settings.host,
|
||||
settings.port,
|
||||
timeout: const Duration(seconds: 10),
|
||||
);
|
||||
|
||||
print("SOCKS5 прокси доступен: ${settings.host}:${settings.port}");
|
||||
print(
|
||||
"Внимание: Полная проверка SOCKS5 требует дополнительной реализации",
|
||||
);
|
||||
|
||||
// Закрываем соединение
|
||||
await proxySocket.close();
|
||||
print("SOCKS5 прокси работает корректно");
|
||||
} on SocketException catch (e) {
|
||||
print("Ошибка сокета при проверке SOCKS5 прокси: $e");
|
||||
throw Exception('Неверный хост или порт');
|
||||
} on TimeoutException catch (_) {
|
||||
print("Таймаут при проверке SOCKS5 прокси");
|
||||
throw Exception('Сервер не отвечает (таймаут)');
|
||||
} catch (e) {
|
||||
print("Ошибка при проверке SOCKS5 прокси: $e");
|
||||
throw Exception('Ошибка подключения: ${e.toString()}');
|
||||
} finally {
|
||||
await proxySocket?.close();
|
||||
}
|
||||
}
|
||||
|
||||
Future<HttpClient> getHttpClientWithProxy() async {
|
||||
final settings = await loadProxySettings();
|
||||
return _createClientWithOptions(settings);
|
||||
}
|
||||
|
||||
|
||||
HttpClient _createClientWithOptions(ProxySettings settings) {
|
||||
final client = HttpClient();
|
||||
|
||||
if (settings.isEnabled && settings.host.isNotEmpty) {
|
||||
print("Используется прокси: ${settings.toFindProxyString()}");
|
||||
|
||||
client.findProxy = (uri) {
|
||||
return settings.toFindProxyString();
|
||||
};
|
||||
|
||||
if (settings.username != null && settings.username!.isNotEmpty) {
|
||||
print(
|
||||
"Настраивается аутентификация на прокси для пользователя: ${settings.username}",
|
||||
);
|
||||
client.authenticateProxy = (host, port, scheme, realm) async {
|
||||
client.addProxyCredentials(
|
||||
host,
|
||||
port,
|
||||
realm ?? '',
|
||||
HttpClientBasicCredentials(
|
||||
settings.username!,
|
||||
settings.password ?? '',
|
||||
),
|
||||
);
|
||||
return true;
|
||||
if (settings.protocol == ProxyProtocol.socks5) {
|
||||
print("Используется SOCKS5 прокси: ${settings.host}:${settings.port}");
|
||||
print("Внимание: SOCKS5 для HTTP клиента может работать ограниченно");
|
||||
client.findProxy = (uri) {
|
||||
return settings.toFindProxyString();
|
||||
};
|
||||
} else {
|
||||
print("Используется прокси: ${settings.toFindProxyString()}");
|
||||
|
||||
client.findProxy = (uri) {
|
||||
return settings.toFindProxyString();
|
||||
};
|
||||
|
||||
if (settings.username != null && settings.username!.isNotEmpty) {
|
||||
print(
|
||||
"Настраивается аутентификация на прокси для пользователя: ${settings.username}",
|
||||
);
|
||||
client.authenticateProxy = (host, port, scheme, realm) async {
|
||||
client.addProxyCredentials(
|
||||
host,
|
||||
port,
|
||||
realm ?? '',
|
||||
HttpClientBasicCredentials(
|
||||
settings.username!,
|
||||
settings.password ?? '',
|
||||
),
|
||||
);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
client.badCertificateCallback =
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:gwid/proxy_service.dart';
|
||||
import 'package:gwid/proxy_settings.dart';
|
||||
@@ -40,7 +38,6 @@ class _ProxySettingsScreenState extends State<ProxySettingsScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Future<void> _testProxyConnection() async {
|
||||
if (_formKey.currentState?.validate() != true) {
|
||||
return;
|
||||
@@ -49,7 +46,6 @@ class _ProxySettingsScreenState extends State<ProxySettingsScreen> {
|
||||
_isTesting = true;
|
||||
});
|
||||
|
||||
|
||||
final settingsToTest = ProxySettings(
|
||||
isEnabled: true, // Для теста прокси всегда должен быть включен
|
||||
protocol: _settings.protocol,
|
||||
@@ -164,12 +160,7 @@ class _ProxySettingsScreenState extends State<ProxySettingsScreen> {
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: ProxyProtocol.values
|
||||
|
||||
.where(
|
||||
(p) =>
|
||||
p != ProxyProtocol.socks4 &&
|
||||
p != ProxyProtocol.socks5,
|
||||
)
|
||||
.where((p) => p != ProxyProtocol.socks4)
|
||||
.map(
|
||||
(protocol) => DropdownMenuItem(
|
||||
value: protocol,
|
||||
|
||||
@@ -8,6 +8,202 @@ import 'package:gwid/api_service.dart';
|
||||
import 'package:gwid/widgets/chat_message_bubble.dart';
|
||||
import 'package:gwid/chat_screen.dart';
|
||||
|
||||
class ControlMessageChip extends StatelessWidget {
|
||||
final Message message;
|
||||
final Map<int, Contact> contacts;
|
||||
final int myId;
|
||||
|
||||
const ControlMessageChip({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.contacts,
|
||||
required this.myId,
|
||||
});
|
||||
|
||||
String _formatControlMessage() {
|
||||
final controlAttach = message.attaches.firstWhere(
|
||||
(a) => a['_type'] == 'CONTROL',
|
||||
);
|
||||
|
||||
final eventType = controlAttach['event'];
|
||||
final senderName = contacts[message.senderId]?.name ?? 'Неизвестный';
|
||||
final isMe = message.senderId == myId;
|
||||
final senderDisplayName = isMe ? 'Вы' : senderName;
|
||||
|
||||
String _formatUserList(List<int> userIds) {
|
||||
if (userIds.isEmpty) {
|
||||
return '';
|
||||
}
|
||||
final userNames = userIds
|
||||
.map((id) {
|
||||
if (id == myId) {
|
||||
return 'Вы';
|
||||
}
|
||||
return contacts[id]?.name ?? 'участник с ID $id';
|
||||
})
|
||||
.where((name) => name.isNotEmpty)
|
||||
.join(', ');
|
||||
return userNames;
|
||||
}
|
||||
|
||||
switch (eventType) {
|
||||
case 'new':
|
||||
final title = controlAttach['title'] ?? 'Новая группа';
|
||||
return '$senderDisplayName создал(а) группу "$title"';
|
||||
|
||||
case 'add':
|
||||
final userIds = List<int>.from(
|
||||
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
|
||||
);
|
||||
if (userIds.isEmpty) {
|
||||
return 'К чату присоединились новые участники';
|
||||
}
|
||||
final userNames = _formatUserList(userIds);
|
||||
if (userNames.isEmpty) {
|
||||
return 'К чату присоединились новые участники';
|
||||
}
|
||||
return '$senderDisplayName добавил(а) в чат: $userNames';
|
||||
|
||||
case 'remove':
|
||||
case 'kick':
|
||||
final userIds = List<int>.from(
|
||||
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
|
||||
);
|
||||
if (userIds.isEmpty) {
|
||||
return '$senderDisplayName удалил(а) участников из чата';
|
||||
}
|
||||
final userNames = _formatUserList(userIds);
|
||||
if (userNames.isEmpty) {
|
||||
return '$senderDisplayName удалил(а) участников из чата';
|
||||
}
|
||||
|
||||
if (userIds.contains(myId)) {
|
||||
return 'Вы были удалены из чата';
|
||||
}
|
||||
return '$senderDisplayName удалил(а) из чата: $userNames';
|
||||
|
||||
case 'leave':
|
||||
if (isMe) {
|
||||
return 'Вы покинули группу';
|
||||
}
|
||||
return '$senderName покинул(а) группу';
|
||||
|
||||
case 'title':
|
||||
final newTitle = controlAttach['title'] ?? '';
|
||||
if (newTitle.isEmpty) {
|
||||
return '$senderDisplayName изменил(а) название группы';
|
||||
}
|
||||
return '$senderDisplayName изменил(а) название группы на "$newTitle"';
|
||||
|
||||
case 'avatar':
|
||||
case 'photo':
|
||||
return '$senderDisplayName изменил(а) фото группы';
|
||||
|
||||
case 'description':
|
||||
return '$senderDisplayName изменил(а) описание группы';
|
||||
|
||||
case 'admin':
|
||||
case 'promote':
|
||||
final userIds = List<int>.from(
|
||||
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
|
||||
);
|
||||
if (userIds.isEmpty) {
|
||||
return '$senderDisplayName назначил(а) администраторов';
|
||||
}
|
||||
final userNames = _formatUserList(userIds);
|
||||
if (userNames.isEmpty) {
|
||||
return '$senderDisplayName назначил(а) администраторов';
|
||||
}
|
||||
|
||||
if (userIds.contains(myId) && userIds.length == 1) {
|
||||
return 'Вас назначили администратором';
|
||||
}
|
||||
return '$senderDisplayName назначил(а) администраторов: $userNames';
|
||||
|
||||
case 'demote':
|
||||
final userIds = List<int>.from(
|
||||
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
|
||||
);
|
||||
if (userIds.isEmpty) {
|
||||
return '$senderDisplayName снял(а) администраторов';
|
||||
}
|
||||
final userNames = _formatUserList(userIds);
|
||||
if (userNames.isEmpty) {
|
||||
return '$senderDisplayName снял(а) администраторов';
|
||||
}
|
||||
|
||||
if (userIds.contains(myId) && userIds.length == 1) {
|
||||
return 'С вас сняли права администратора';
|
||||
}
|
||||
return '$senderDisplayName снял(а) права администратора с: $userNames';
|
||||
|
||||
case 'ban':
|
||||
final userIds = List<int>.from(
|
||||
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
|
||||
);
|
||||
if (userIds.isEmpty) {
|
||||
return '$senderDisplayName заблокировал(а) участников';
|
||||
}
|
||||
final userNames = _formatUserList(userIds);
|
||||
if (userNames.isEmpty) {
|
||||
return '$senderDisplayName заблокировал(а) участников';
|
||||
}
|
||||
|
||||
if (userIds.contains(myId)) {
|
||||
return 'Вы были заблокированы в чате';
|
||||
}
|
||||
return '$senderDisplayName заблокировал(а): $userNames';
|
||||
|
||||
case 'unban':
|
||||
final userIds = List<int>.from(
|
||||
(controlAttach['userIds'] as List?)?.map((id) => id as int) ?? [],
|
||||
);
|
||||
if (userIds.isEmpty) {
|
||||
return '$senderDisplayName разблокировал(а) участников';
|
||||
}
|
||||
final userNames = _formatUserList(userIds);
|
||||
if (userNames.isEmpty) {
|
||||
return '$senderDisplayName разблокировал(а) участников';
|
||||
}
|
||||
return '$senderDisplayName разблокировал(а): $userNames';
|
||||
|
||||
case 'join':
|
||||
if (isMe) {
|
||||
return 'Вы присоединились к группе';
|
||||
}
|
||||
return '$senderName присоединился(ась) к группе';
|
||||
|
||||
default:
|
||||
final eventTypeStr = eventType?.toString() ?? 'неизвестное';
|
||||
return 'Событие: $eventTypeStr';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: colors.primaryContainer.withOpacity(0.5),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
_formatControlMessage(),
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colors.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MessagePreviewDialog {
|
||||
static String _formatTimestamp(int timestamp) {
|
||||
final dt = DateTime.fromMillisecondsSinceEpoch(timestamp);
|
||||
@@ -323,6 +519,16 @@ class MessagePreviewDialog {
|
||||
|
||||
if (item is MessageItem) {
|
||||
final message = item.message;
|
||||
final isControlMessage = message.attaches.any(
|
||||
(a) => a['_type'] == 'CONTROL',
|
||||
);
|
||||
if (isControlMessage) {
|
||||
return ControlMessageChip(
|
||||
message: message,
|
||||
contacts: contacts,
|
||||
myId: myId,
|
||||
);
|
||||
}
|
||||
final isMe = message.senderId == myId;
|
||||
final senderContact =
|
||||
contacts[message.senderId];
|
||||
|
||||
Reference in New Issue
Block a user