diff --git a/lib/api/api_service.dart b/lib/api/api_service.dart index 17c08e2..6f0f956 100644 --- a/lib/api/api_service.dart +++ b/lib/api/api_service.dart @@ -9,6 +9,7 @@ import 'package:gwid/connection/connection_logger.dart'; import 'package:gwid/connection/connection_state.dart' as conn_state; import 'package:gwid/connection/health_monitor.dart'; import 'package:gwid/image_cache_service.dart'; +import 'package:gwid/models/complaint.dart'; import 'package:gwid/models/contact.dart'; import 'package:gwid/models/message.dart'; import 'package:gwid/models/profile.dart'; @@ -31,6 +32,7 @@ part 'api_service_contacts.dart'; part 'api_service_chats.dart'; part 'api_service_media.dart'; part 'api_service_privacy.dart'; +part 'api_service_complaints.dart'; class ApiService { ApiService._privateConstructor(); @@ -85,9 +87,7 @@ class ApiService { final Map _contactCache = {}; DateTime? _lastContactsUpdate; - static const Duration _contactCacheExpiry = Duration( - minutes: 5, - ); + static const Duration _contactCacheExpiry = Duration(minutes: 5); final CacheService _cacheService = CacheService(); final AvatarCacheService _avatarCacheService = AvatarCacheService(); @@ -295,4 +295,3 @@ class ApiService { _messageController.close(); } } - diff --git a/lib/api/api_service_complaints.dart b/lib/api/api_service_complaints.dart new file mode 100644 index 0000000..601c214 --- /dev/null +++ b/lib/api/api_service_complaints.dart @@ -0,0 +1,18 @@ +part of 'api_service.dart'; + +extension ApiServiceComplaints on ApiService { + void getComplaints() { + final payload = {"complainSync": 0}; + _sendMessage(162, payload); + } + + void sendComplaint(int chatId, String messageId, int typeId, int reasonId) { + final payload = { + "reasonId": reasonId, + "parentId": chatId, + "typeId": 3, + "ids": [int.parse(messageId)], // Конвертируем в число + }; + _sendMessage(161, payload); + } +} diff --git a/lib/api/api_service_connection.dart b/lib/api/api_service_connection.dart index a3b3225..e6f747b 100644 --- a/lib/api/api_service_connection.dart +++ b/lib/api/api_service_connection.dart @@ -90,8 +90,8 @@ extension ApiServiceConnection on ApiService { print( 'Используем ${proxySettings.protocol.name.toUpperCase()} прокси ${proxySettings.host}:${proxySettings.port}', ); - final customHttpClient = - await ProxyService.instance.getHttpClientWithProxy(); + final customHttpClient = await ProxyService.instance + .getHttpClientWithProxy(); _channel = IOWebSocketChannel.connect( uri, headers: headers, @@ -166,7 +166,8 @@ extension ApiServiceConnection on ApiService { final userAgentPayload = await _buildUserAgentPayload(); final prefs = await SharedPreferences.getInstance(); - final deviceId = prefs.getString('spoof_deviceid') ?? generateRandomDeviceId(); + final deviceId = + prefs.getString('spoof_deviceid') ?? generateRandomDeviceId(); if (prefs.getString('spoof_deviceid') == null) { await prefs.setString('spoof_deviceid', deviceId); @@ -335,8 +336,9 @@ extension ApiServiceConnection on ApiService { _log(loggableMessage); try { - final decodedMessage = - message is String ? jsonDecode(message) : message; + final decodedMessage = message is String + ? jsonDecode(message) + : message; if (decodedMessage is Map && decodedMessage['opcode'] == 97 && @@ -588,6 +590,23 @@ extension ApiServiceConnection on ApiService { }); } + if (decodedMessage is Map && + decodedMessage['opcode'] == 162 && + decodedMessage['cmd'] == 1) { + final payload = decodedMessage['payload']; + print('Получены данные жалоб: $payload'); + + try { + final complaintData = ComplaintData.fromJson(payload); + _messageController.add({ + 'type': 'complaints_data', + 'complaintData': complaintData, + }); + } catch (e) { + print('Ошибка парсинга данных жалоб: $e'); + } + } + if (decodedMessage is Map) { _messageController.add(decodedMessage); } @@ -783,4 +802,3 @@ extension ApiServiceConnection on ApiService { _connectionStatusController.add("disconnected"); } } - diff --git a/lib/chat_screen.dart b/lib/chat_screen.dart index 5af935e..c5f9f71 100644 --- a/lib/chat_screen.dart +++ b/lib/chat_screen.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:gwid/models/contact.dart'; import 'package:gwid/models/message.dart'; import 'package:gwid/widgets/chat_message_bubble.dart'; +import 'package:gwid/widgets/complaint_dialog.dart'; import 'package:image_picker/image_picker.dart'; import 'package:gwid/services/chat_cache_service.dart'; import 'package:gwid/services/avatar_cache_service.dart'; @@ -1163,6 +1164,14 @@ class _ChatScreenState extends State { }); } + void _showComplaintDialog(String messageId) { + showDialog( + context: context, + builder: (context) => + ComplaintDialog(messageId: messageId, chatId: widget.chatId), + ); + } + void _showBlockDialog() { showDialog( context: context, @@ -1750,6 +1759,7 @@ class _ChatScreenState extends State { isGrouped: item.isGrouped, avatarVerticalOffset: -8.0, // Смещение аватарки вверх на 8px + onComplain: () => _showComplaintDialog(item.message.id), ); Widget finalMessageWidget = bubble as Widget; diff --git a/lib/models/complaint.dart b/lib/models/complaint.dart new file mode 100644 index 0000000..14b1a79 --- /dev/null +++ b/lib/models/complaint.dart @@ -0,0 +1,45 @@ +class ComplaintType { + final int typeId; + final List reasons; + + ComplaintType({required this.typeId, required this.reasons}); + + factory ComplaintType.fromJson(Map json) { + return ComplaintType( + typeId: json['typeId'], + reasons: (json['reasons'] as List) + .map((reason) => ComplaintReason.fromJson(reason)) + .toList(), + ); + } +} + +class ComplaintReason { + final String reasonTitle; + final int reasonId; + + ComplaintReason({required this.reasonTitle, required this.reasonId}); + + factory ComplaintReason.fromJson(Map json) { + return ComplaintReason( + reasonTitle: json['reasonTitle'], + reasonId: json['reasonId'], + ); + } +} + +class ComplaintData { + final List complainTypes; + final int complainSync; + + ComplaintData({required this.complainTypes, required this.complainSync}); + + factory ComplaintData.fromJson(Map json) { + return ComplaintData( + complainTypes: (json['complains'] as List) + .map((type) => ComplaintType.fromJson(type)) + .toList(), + complainSync: json['complainSync'], + ); + } +} diff --git a/lib/widgets/chat_message_bubble.dart b/lib/widgets/chat_message_bubble.dart index 2aac6cd..861350f 100644 --- a/lib/widgets/chat_message_bubble.dart +++ b/lib/widgets/chat_message_bubble.dart @@ -176,6 +176,7 @@ class ChatMessageBubble extends StatelessWidget { final VoidCallback? onRemoveReaction; final VoidCallback? onReply; final VoidCallback? onForward; + final VoidCallback? onComplain; final int? myUserId; final bool? canEditMessage; final bool isGroupChat; @@ -207,6 +208,7 @@ class ChatMessageBubble extends StatelessWidget { this.onRemoveReaction, this.onReply, this.onForward, + this.onComplain, this.myUserId, this.canEditMessage, this.isGroupChat = false, @@ -890,6 +892,7 @@ class ChatMessageBubble extends StatelessWidget { onReaction: onReaction, onRemoveReaction: onRemoveReaction, onForward: onForward, + onComplain: onComplain, canEditMessage: canEditMessage ?? false, hasUserReaction: hasUserReaction, isChannel: isChannel, @@ -1541,7 +1544,9 @@ class ChatMessageBubble extends StatelessWidget { } Widget _buildPhotoOnlyMessage(BuildContext context) { - final photos = message.attaches.where((a) => a['_type'] == 'PHOTO').toList(); + final photos = message.attaches + .where((a) => a['_type'] == 'PHOTO') + .toList(); final themeProvider = Provider.of(context); final isUltraOptimized = themeProvider.ultraOptimizeChats; final messageOpacity = themeProvider.messageBubbleOpacity; @@ -1586,7 +1591,12 @@ class ChatMessageBubble extends StatelessWidget { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - _buildSmartPhotoGroup(context, photos, textColor, isUltraOptimized), + _buildSmartPhotoGroup( + context, + photos, + textColor, + isUltraOptimized, + ), Padding( padding: const EdgeInsets.only(top: 4, right: 6), child: Row( @@ -1620,7 +1630,9 @@ class ChatMessageBubble extends StatelessWidget { } Widget _buildVideoOnlyMessage(BuildContext context) { - final videos = message.attaches.where((a) => a['_type'] == 'VIDEO').toList(); + final videos = message.attaches + .where((a) => a['_type'] == 'VIDEO') + .toList(); final timeColor = Theme.of(context).brightness == Brightness.dark ? const Color(0xFF9bb5c7) @@ -1714,7 +1726,10 @@ class ChatMessageBubble extends StatelessWidget { children: [ Text( _formatMessageTime(context, message.time), - style: TextStyle(fontSize: 12, color: timeColor), + style: TextStyle( + fontSize: 12, + color: timeColor, + ), ), ], ), @@ -3925,6 +3940,7 @@ class _MessageContextMenu extends StatefulWidget { final Function(String)? onReaction; final VoidCallback? onRemoveReaction; final VoidCallback? onForward; + final VoidCallback? onComplain; final bool canEditMessage; final bool hasUserReaction; final bool isChannel; @@ -3939,6 +3955,7 @@ class _MessageContextMenu extends StatefulWidget { this.onReaction, this.onRemoveReaction, this.onForward, + this.onComplain, required this.canEditMessage, required this.hasUserReaction, this.isChannel = false, @@ -4267,6 +4284,16 @@ class _MessageContextMenuState extends State<_MessageContextMenu> widget.onDeleteForAll!(); }, ), + if (widget.onComplain != null) + _buildActionButton( + icon: Icons.report_rounded, + text: 'Пожаловаться', + color: theme.colorScheme.error, + onTap: () { + Navigator.pop(context); + widget.onComplain!(); + }, + ), ], ); } diff --git a/lib/widgets/complaint_dialog.dart b/lib/widgets/complaint_dialog.dart new file mode 100644 index 0000000..5dfd18e --- /dev/null +++ b/lib/widgets/complaint_dialog.dart @@ -0,0 +1,173 @@ +import 'package:flutter/material.dart'; +import 'package:gwid/models/complaint.dart'; +import 'package:gwid/api/api_service.dart'; +import 'dart:async'; + +class ComplaintDialog extends StatefulWidget { + final String messageId; + final int chatId; + + const ComplaintDialog({ + super.key, + required this.messageId, + required this.chatId, + }); + + @override + State createState() => _ComplaintDialogState(); +} + +class _ComplaintDialogState extends State { + ComplaintData? _complaintData; + bool _isLoading = true; + String? _error; + StreamSubscription? _messageSubscription; + + @override + void initState() { + super.initState(); + _loadComplaints(); + _listenForComplaintsData(); + } + + @override + void dispose() { + _messageSubscription?.cancel(); + super.dispose(); + } + + void _loadComplaints() { + setState(() { + _isLoading = true; + _error = null; + }); + + ApiService.instance.getComplaints(); + } + + void _listenForComplaintsData() { + _messageSubscription = ApiService.instance.messages.listen((message) { + if (message['type'] == 'complaints_data' && mounted) { + setState(() { + _complaintData = message['complaintData'] as ComplaintData?; + _isLoading = false; + _error = null; + }); + } + }); + } + + void _handleComplaintSelected(int typeId, int reasonId) { + ApiService.instance.sendComplaint( + widget.chatId, + widget.messageId, + typeId, + reasonId, + ); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Жалоба отправлена'), + behavior: SnackBarBehavior.floating, + duration: Duration(seconds: 2), + ), + ); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Жалоба на сообщение'), + content: SizedBox( + width: double.maxFinite, + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _error != null + ? Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Ошибка: $_error'), + const SizedBox(height: 16), + ElevatedButton( + onPressed: _loadComplaints, + child: const Text('Повторить'), + ), + ], + ), + ) + : _complaintData == null + ? const Center(child: Text('Нет данных о жалобах')) + : ListView.builder( + shrinkWrap: true, + itemCount: _getAllReasons().length, + itemBuilder: (context, index) { + final reasonData = _getAllReasons()[index]; + return ListTile( + leading: Icon( + reasonData['icon'] as IconData, + color: Theme.of(context).colorScheme.primary, + ), + title: Text(reasonData['title'] as String), + onTap: () => _handleComplaintSelected( + reasonData['typeId'] as int, + reasonData['reasonId'] as int, + ), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Отмена'), + ), + ], + ); + } + + List> _getAllReasons() { + if (_complaintData == null) return []; + + final uniqueReasons = >{}; + for (final complaintType in _complaintData!.complainTypes) { + for (final reason in complaintType.reasons) { + if (!uniqueReasons.containsKey(reason.reasonId)) { + String title = reason.reasonTitle; + // Забавный прикол для оскорблений + if (reason.reasonId == 11) { + // Оскорбления + title = 'Абзывательства матюки'; + } + + uniqueReasons[reason.reasonId] = { + 'title': title, + 'typeId': complaintType.typeId, + 'reasonId': reason.reasonId, + 'icon': _getReasonIcon(reason.reasonId), + }; + } + } + } + return uniqueReasons.values.toList(); + } + + IconData _getReasonIcon(int reasonId) { + switch (reasonId) { + case 7: // Другое + return Icons.more_horiz; + case 8: // Мошенничество + return Icons.warning; + case 9: // Спам + return Icons.campaign; + case 10: // Шантаж + return Icons.gavel; + case 11: // Оскорбления + return Icons.sentiment_very_dissatisfied; + case 12: // Недостоверная информация + return Icons.help_outline; + default: + return Icons.report_problem; + } + } +} diff --git a/pubspec.lock b/pubspec.lock index 3b073f6..f5b7c08 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -809,10 +809,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1318,10 +1318,10 @@ packages: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" timezone: dependency: "direct main" description: