Теперь можно жаловаться на сообщения!!!

This commit is contained in:
jganenok
2025-11-22 11:09:58 +07:00
parent b8b29b547f
commit 4e81b607fa
8 changed files with 308 additions and 18 deletions

View File

@@ -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<int, Contact> _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();
}
}

View File

@@ -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);
}
}

View File

@@ -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<String, dynamic>) {
_messageController.add(decodedMessage);
}
@@ -783,4 +802,3 @@ extension ApiServiceConnection on ApiService {
_connectionStatusController.add("disconnected");
}
}

View File

@@ -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<ChatScreen> {
});
}
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<ChatScreen> {
isGrouped: item.isGrouped,
avatarVerticalOffset:
-8.0, // Смещение аватарки вверх на 8px
onComplain: () => _showComplaintDialog(item.message.id),
);
Widget finalMessageWidget = bubble as Widget;

45
lib/models/complaint.dart Normal file
View File

@@ -0,0 +1,45 @@
class ComplaintType {
final int typeId;
final List<ComplaintReason> reasons;
ComplaintType({required this.typeId, required this.reasons});
factory ComplaintType.fromJson(Map<String, dynamic> 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<String, dynamic> json) {
return ComplaintReason(
reasonTitle: json['reasonTitle'],
reasonId: json['reasonId'],
);
}
}
class ComplaintData {
final List<ComplaintType> complainTypes;
final int complainSync;
ComplaintData({required this.complainTypes, required this.complainSync});
factory ComplaintData.fromJson(Map<String, dynamic> json) {
return ComplaintData(
complainTypes: (json['complains'] as List)
.map((type) => ComplaintType.fromJson(type))
.toList(),
complainSync: json['complainSync'],
);
}
}

View File

@@ -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<ThemeProvider>(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!();
},
),
],
);
}

View File

@@ -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<ComplaintDialog> createState() => _ComplaintDialogState();
}
class _ComplaintDialogState extends State<ComplaintDialog> {
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<Map<String, dynamic>> _getAllReasons() {
if (_complaintData == null) return [];
final uniqueReasons = <int, Map<String, dynamic>>{};
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;
}
}
}