Получение и отправка текста, статусы сообщения

This commit is contained in:
Халимов Рустам
2026-05-15 14:59:54 +03:00
parent 4d4bc8edd1
commit a426b63b7d
22 changed files with 3777 additions and 358 deletions
@@ -41,8 +41,8 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
title: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
final currentChat = state.maybeWhen(
chatSelected: (chat, _, __) => chat,
messagesLoaded: (chat, _, __) => chat,
chatSelected: (chat, messages, typingUsers, lastUpdate) => chat,
messagesLoaded: (chat, messages, typingUsers, lastUpdate) => chat,
orElse: () => widget.chat,
);
return Column(
@@ -65,10 +65,10 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
child: BlocBuilder<ChatBloc, ChatState>(
builder: (context, state) {
return state.maybeWhen(
chatSelected: (_, messages, __) => _buildMessageList(messages),
messagesLoaded: (_, messages, __) => _buildMessageList(messages),
loading: (_) => const Center(child: CircularProgressIndicator()),
error: (msg, _) => Center(child: Text('Ошибка: $msg')),
chatSelected: (chat, messages, typingUsers, lastUpdate) => _buildMessageList(messages),
messagesLoaded: (chat, messages, typingUsers, lastUpdate) => _buildMessageList(messages),
loading: (typingUsers, lastUpdate) => const Center(child: CircularProgressIndicator()),
error: (msg, typingUsers, lastUpdate) => Center(child: Text('Ошибка: $msg')),
orElse: () => const Center(child: Text('Начните общение')),
);
},
@@ -124,17 +124,46 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
],
);
} else if (otherMember.lastSeen != null) {
final lastSeen = otherMember.lastSeen!.toLocal();
final timeStr = DateFormat.Hm(locale).format(lastSeen);
final dateStr = _formatDate(lastSeen, locale);
return Text(
l10n.lastSeen(dateStr, timeStr),
_formatLastSeen(otherMember.lastSeen!, l10n, locale),
style: const TextStyle(fontSize: 12, color: Colors.white70, fontWeight: FontWeight.normal),
);
}
return const SizedBox.shrink();
}
String _formatLastSeen(DateTime utcDate, AppLocalizations l10n, String locale) {
final lastSeen = utcDate.toLocal();
final now = DateTime.now();
final difference = now.difference(lastSeen);
final timeStr = DateFormat.Hm(locale).format(lastSeen);
// Case 1: Same day (within 24h of NOW, but specifically checking calendar day is better for "yesterday")
// Let's use difference for "relative" and calendar days for "yesterday/day before"
final today = DateTime(now.year, now.month, now.day);
final seenDay = DateTime(lastSeen.year, lastSeen.month, lastSeen.day);
final dayDiff = today.difference(seenDay).inDays;
if (dayDiff == 0) {
// Same calendar day
if (difference.inHours > 0) {
return l10n.lastSeenRelative(difference.inHours, difference.inMinutes % 60);
} else if (difference.inMinutes > 0) {
return l10n.lastSeenRelativeMinutes(difference.inMinutes);
} else {
return l10n.lastSeenJustNow;
}
} else if (dayDiff == 1) {
return l10n.lastSeenYesterday(timeStr);
} else if (dayDiff == 2) {
return l10n.lastSeenDayBeforeYesterday(timeStr);
} else {
final dateStr = DateFormat.MMMMd(locale).format(lastSeen);
return l10n.lastSeen(dateStr, timeStr);
}
}
String _formatDate(DateTime date, String locale) {
final now = DateTime.now();
final localDate = date.toLocal();
@@ -152,6 +181,20 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
return Center(child: Text(l10n.noMessages));
}
// Mark as read logic
WidgetsBinding.instance.addPostFrameCallback((_) {
final unreadFromOthers = messages.where((m) =>
m.senderId != widget.currentUserId &&
m.status != MessageStatus.read
).toList();
if (unreadFromOthers.isNotEmpty) {
// Find the message with the highest sequenceId to mark everything up to it as read
final latest = unreadFromOthers.reduce((a, b) => a.sequenceId > b.sequenceId ? a : b);
context.read<ChatBloc>().add(ChatEvent.markAsRead(latest.chatId, latest.id, latest.sequenceId));
}
});
final List<dynamic> items = [];
for (int i = 0; i < messages.length; i++) {
final message = messages[i];
@@ -233,14 +276,23 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
style: const TextStyle(color: Colors.black87, fontSize: 16),
),
const SizedBox(height: 2),
if (message.createdAt != null)
Text(
DateFormat.Hm(locale).format(message.createdAt!.toLocal()),
style: const TextStyle(
fontSize: 10,
color: Colors.black45,
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (message.createdAt != null)
Text(
DateFormat.Hm(locale).format(message.createdAt!.toLocal()),
style: const TextStyle(
fontSize: 10,
color: Colors.black45,
),
),
if (isMe) ...[
const SizedBox(width: 4),
_buildStatusIcon(message),
],
],
),
],
),
),
@@ -249,6 +301,19 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
);
}
Widget _buildStatusIcon(Message message) {
switch (message.status) {
case MessageStatus.sending:
return const Icon(Icons.access_time, size: 12, color: Colors.black45);
case MessageStatus.delivered:
return const Icon(Icons.check, size: 12, color: Colors.black45);
case MessageStatus.read:
return const Icon(Icons.done_all, size: 12, color: Colors.blue);
case MessageStatus.error:
return const Icon(Icons.error_outline, size: 12, color: Colors.red);
}
}
Widget _buildMessageInput(Chat chat) {
return Container(
padding: const EdgeInsets.all(8),
@@ -49,20 +49,13 @@ class _ChatsPageState extends State<ChatsPage> {
body: BlocConsumer<ChatBloc, ChatState>(
listener: (context, state) {
state.whenOrNull(
chatSelected: (chat, _, __) {
chatSelected: (chat, messages, typingUsers, lastUpdate) {
if (_isNavigating) return;
// Extra safety: check if we are already on chat_detail
bool isAlreadyOnDetail = false;
Navigator.popUntil(context, (route) {
if (route.settings.name == 'chat_detail') {
isAlreadyOnDetail = true;
}
return true;
});
if (isAlreadyOnDetail) return;
// Only navigate if we are currently on the ChatsPage
final isCurrent = ModalRoute.of(context)?.isCurrent ?? false;
if (!isCurrent) return;
_isNavigating = true;
final authState = context.read<AuthBloc>().state;
@@ -91,9 +84,9 @@ class _ChatsPageState extends State<ChatsPage> {
},
builder: (context, state) {
return state.maybeWhen(
loading: (_) => const Center(child: CircularProgressIndicator()),
chatsLoaded: (chats, _) => _buildChatList(chats, l10n),
error: (message, _) => Center(child: Text('${l10n.error}: $message')),
loading: (typingUsers, lastUpdate) => const Center(child: CircularProgressIndicator()),
chatsLoaded: (chats, typingUsers, lastUpdate) => _buildChatList(chats, l10n),
error: (message, typingUsers, lastUpdate) => Center(child: Text('${l10n.error}: $message')),
orElse: () => const Center(child: CircularProgressIndicator()),
);
},