Починка показа людей для перессылки(не отдельный экран, потом), возможность написать тому кто переслал сообщение, какая то дохлая система показа и перехода к последнему прочитанному сообщению(не работает), копирование пересланных сообщений

This commit is contained in:
jganenok
2025-12-04 14:49:59 +07:00
parent bcc7e499de
commit 61f0eb349a
4 changed files with 506 additions and 183 deletions

View File

@@ -17,6 +17,7 @@ import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:gwid/screens/chat_screen.dart';
import 'package:gwid/services/avatar_cache_service.dart';
import 'package:gwid/widgets/user_profile_panel.dart';
import 'package:gwid/api/api_service.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
@@ -479,117 +480,152 @@ class ChatMessageBubble extends StatelessWidget {
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: textColor.withOpacity(0.08 * messageTextOpacity),
border: Border(
left: BorderSide(
color: textColor.withOpacity(0.3 * messageTextOpacity),
width: 3, // Делаем рамку жирнее для отличия от ответа
// Пытаемся определить userId оригинального отправителя для открытия панели профиля
int? originalSenderId;
if (forwardedMessage['sender'] is int) {
originalSenderId = forwardedMessage['sender'] as int;
}
void handleTap() {
final myId = myUserId ?? 0;
if (originalSenderId == null || myId == 0) {
return;
}
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (ctx) => UserProfilePanel(
userId: originalSenderId!,
name: forwardedSenderName,
firstName: null,
lastName: null,
avatarUrl: forwardedSenderAvatarUrl,
description: null,
myId: myId,
currentChatId: chatId,
contactData: null,
dialogChatId: null,
),
);
}
return InkWell(
onTap: handleTap,
borderRadius: BorderRadius.circular(8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
decoration: BoxDecoration(
color: textColor.withOpacity(0.08 * messageTextOpacity),
border: Border(
left: BorderSide(
color: textColor.withOpacity(0.3 * messageTextOpacity),
width: 3, // Делаем рамку жирнее для отличия от ответа
),
),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// "Заголовок" с именем автора и аватаркой
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.forward,
size: 14,
color: textColor.withOpacity(0.6 * messageTextOpacity),
),
const SizedBox(width: 6),
if (forwardedSenderAvatarUrl != null)
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: textColor.withOpacity(0.2 * messageTextOpacity),
width: 1,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// "Заголовок" с именем автора и аватаркой
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.forward,
size: 14,
color: textColor.withOpacity(0.6 * messageTextOpacity),
),
const SizedBox(width: 6),
if (forwardedSenderAvatarUrl != null)
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: textColor.withOpacity(0.2 * messageTextOpacity),
width: 1,
),
),
),
child: ClipOval(
child: Image.network(
forwardedSenderAvatarUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: textColor.withOpacity(
0.1 * messageTextOpacity,
),
child: Icon(
Icons.person,
size: 12,
child: ClipOval(
child: Image.network(
forwardedSenderAvatarUrl,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: textColor.withOpacity(
0.5 * messageTextOpacity,
0.1 * messageTextOpacity,
),
),
);
},
child: Icon(
Icons.person,
size: 12,
color: textColor.withOpacity(
0.5 * messageTextOpacity,
),
),
);
},
),
),
)
else
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: textColor.withOpacity(0.1 * messageTextOpacity),
border: Border.all(
color: textColor.withOpacity(0.2 * messageTextOpacity),
width: 1,
),
),
child: Icon(
Icons.person,
size: 12,
color: textColor.withOpacity(0.5 * messageTextOpacity),
),
),
)
else
Container(
width: 20,
height: 20,
margin: const EdgeInsets.only(right: 6),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: textColor.withOpacity(0.1 * messageTextOpacity),
border: Border.all(
color: textColor.withOpacity(0.2 * messageTextOpacity),
width: 1,
Flexible(
child: Text(
forwardedSenderName,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: textColor.withOpacity(0.9 * messageTextOpacity),
),
),
child: Icon(
Icons.person,
size: 12,
color: textColor.withOpacity(0.5 * messageTextOpacity),
overflow: TextOverflow.ellipsis,
),
),
Flexible(
child: Text(
forwardedSenderName,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: textColor.withOpacity(0.9 * messageTextOpacity),
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 6),
// Содержимое пересланного сообщения (фото и/или текст)
if (attaches.isNotEmpty) ...[
..._buildPhotosWithCaption(
context,
attaches, // Передаем вложения из вложенного сообщения
textColor,
isUltraOptimized,
messageTextOpacity,
],
),
const SizedBox(height: 6),
],
if (text.isNotEmpty)
Text(
text,
style: TextStyle(
color: textColor.withOpacity(0.9 * messageTextOpacity),
fontSize: 14,
// Содержимое пересланного сообщения (фото и/или текст)
if (attaches.isNotEmpty) ...[
..._buildPhotosWithCaption(
context,
attaches, // Передаем вложения из вложенного сообщения
textColor,
isUltraOptimized,
messageTextOpacity,
),
),
],
const SizedBox(height: 6),
],
if (text.isNotEmpty)
Text(
text,
style: TextStyle(
color: textColor.withOpacity(0.9 * messageTextOpacity),
fontSize: 14,
),
),
],
),
),
);
}
@@ -1207,7 +1243,12 @@ class ChatMessageBubble extends StatelessWidget {
!message.isReply &&
!message.isForwarded;
final bubbleColor = _getBubbleColor(isMe, themeProvider, messageOpacity, context);
final bubbleColor = _getBubbleColor(
isMe,
themeProvider,
messageOpacity,
context,
);
final textColor = _getTextColor(
isMe,
bubbleColor,
@@ -1774,7 +1815,12 @@ class ChatMessageBubble extends StatelessWidget {
final themeProvider = Provider.of<ThemeProvider>(context);
final isUltraOptimized = themeProvider.ultraOptimizeChats;
final messageOpacity = themeProvider.messageBubbleOpacity;
final bubbleColor = _getBubbleColor(isMe, themeProvider, messageOpacity, context);
final bubbleColor = _getBubbleColor(
isMe,
themeProvider,
messageOpacity,
context,
);
final textColor = _getTextColor(
isMe,
bubbleColor,
@@ -3879,7 +3925,8 @@ class ChatMessageBubble extends StatelessWidget {
final bool isDark = Theme.of(context).brightness == Brightness.dark;
final baseColor = isMe
? (themeProvider.myBubbleColor ?? const Color(0xFF2b5278))
: (themeProvider.theirBubbleColor ?? (isDark ? const Color(0xFF182533) : const Color(0xFF464646)));
: (themeProvider.theirBubbleColor ??
(isDark ? const Color(0xFF182533) : const Color(0xFF464646)));
return baseColor.withOpacity(1.0 - messageOpacity);
}
@@ -5146,7 +5193,31 @@ class _MessageContextMenuState extends State<_MessageContextMenu>
}
void _onCopy() {
Clipboard.setData(ClipboardData(text: widget.message.text));
String textToCopy = widget.message.text;
// Для пересланных сообщений пробуем взять текст оригинального сообщения
if (textToCopy.isEmpty &&
widget.message.isForwarded &&
widget.message.link is Map<String, dynamic>) {
final link = widget.message.link as Map<String, dynamic>;
final forwardedMessage = link['message'] as Map<String, dynamic>?;
final forwardedText = forwardedMessage?['text'] as String? ?? '';
textToCopy = forwardedText;
}
if (textToCopy.isEmpty) {
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Нет текста для копирования'),
behavior: SnackBarBehavior.floating,
duration: Duration(seconds: 2),
),
);
return;
}
Clipboard.setData(ClipboardData(text: textToCopy));
Navigator.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
@@ -5294,7 +5365,9 @@ class _MessageContextMenuState extends State<_MessageContextMenu>
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (widget.message.text.isNotEmpty)
// Для пересланных сообщений разрешаем копирование даже при пустом text,
// т.к. текст может быть внутри link['message'].
if (widget.message.text.isNotEmpty || widget.message.isForwarded)
_buildActionButton(
icon: Icons.copy_rounded,
text: 'Копировать',

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:gwid/services/avatar_cache_service.dart';
import 'package:gwid/widgets/contact_name_widget.dart';
import 'package:gwid/widgets/contact_avatar_widget.dart';
import 'package:gwid/services/contact_local_names_service.dart';
import 'package:gwid/api/api_service.dart';
import 'package:gwid/models/contact.dart';
import 'package:gwid/screens/chat_screen.dart';
class UserProfilePanel extends StatefulWidget {
final int userId;
@@ -39,6 +41,7 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
final ScrollController _nameScrollController = ScrollController();
String? _localDescription;
StreamSubscription? _changesSubscription;
bool _isOpeningChat = false;
String get _displayName {
final displayName = getContactDisplayName(
@@ -243,8 +246,9 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
_buildActionButton(
icon: Icons.message,
label: 'Написать',
onPressed: null,
onPressed: _isOpeningChat ? null : _handleWriteMessage,
colors: colors,
isLoading: _isOpeningChat,
),
],
),
@@ -309,4 +313,77 @@ class _UserProfilePanelState extends State<UserProfilePanel> {
],
);
}
Future<void> _handleWriteMessage() async {
if (_isOpeningChat) return;
setState(() {
_isOpeningChat = true;
});
try {
// Сначала пробуем использовать dialogChatId, если он уже есть
int? chatId = widget.dialogChatId;
if (chatId == null || chatId == 0) {
// Если нет, считаем chatId по формуле chatId = userId1 ^ userId2
chatId = await ApiService.instance.getChatIdByUserId(widget.userId);
}
if (chatId == null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Не удалось открыть чат с пользователем'),
behavior: SnackBarBehavior.floating,
),
);
}
return;
}
if (!mounted) return;
// Закрываем панель профиля
Navigator.of(context).pop();
// Открываем экран чата
Navigator.of(context).push(
MaterialPageRoute(
builder: (ctx) => ChatScreen(
chatId: chatId!,
contact: Contact(
id: widget.userId,
name: widget.name ?? _displayName,
firstName: widget.firstName ?? '',
lastName: widget.lastName ?? '',
description: widget.description,
photoBaseUrl: widget.avatarUrl,
accountStatus: 0,
status: null,
options: const [],
),
myId: widget.myId,
isGroupChat: false,
isChannel: false,
),
),
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка при открытии чата: $e'),
behavior: SnackBarBehavior.floating,
),
);
}
} finally {
if (mounted) {
setState(() {
_isOpeningChat = false;
});
}
}
}
}