mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-23 17:21:06 +10:00
Merge branch 'main' into fix/cursor-focus
This commit is contained in:
@@ -82,6 +82,18 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showLanguageDialog(context, settingsService),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.location_searching),
|
||||
title: Text(context.l10n.appSettings_enableMessageTracing),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_enableMessageTracingSubtitle,
|
||||
),
|
||||
value: settingsService.settings.enableMessageTracing,
|
||||
onChanged: (value) {
|
||||
settingsService.setEnableMessageTracing(value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -17,11 +17,13 @@ import '../helpers/utf8_length_limiter.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
import '../widgets/jump_to_bottom_button.dart';
|
||||
import '../widgets/gif_picker.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
|
||||
@@ -263,6 +265,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(ChannelMessage message) {
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final gifId = _parseGifId(message.text);
|
||||
final poi = _parsePoiMessage(message.text);
|
||||
@@ -334,110 +338,189 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (poi != null)
|
||||
_buildPoiMessage(context, poi, isOutgoing)
|
||||
_buildPoiMessage(
|
||||
context,
|
||||
poi,
|
||||
isOutgoing,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
)
|
||||
else if (gifId != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Colors.transparent,
|
||||
fallbackTextColor: isOutgoing
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer
|
||||
.withValues(alpha: 0.7)
|
||||
: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Colors.transparent,
|
||||
fallbackTextColor: isOutgoing
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer
|
||||
.withValues(alpha: 0.7)
|
||||
: Theme.of(context).colorScheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutgoing
|
||||
? Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Linkify(
|
||||
text: message.text,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
linkStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
defaultToHttps: false,
|
||||
),
|
||||
linkifiers: const [UrlLinkifier()],
|
||||
onOpen: (link) =>
|
||||
LinkHandler.handleLinkTap(context, link.url),
|
||||
),
|
||||
if (displayPath.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
'via ${_formatPathPrefixes(displayPath)}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Row(
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
Flexible(
|
||||
child: Linkify(
|
||||
text: message.text,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
linkStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
defaultToHttps: false,
|
||||
),
|
||||
linkifiers: const [UrlLinkifier()],
|
||||
onOpen: (link) => LinkHandler.handleLinkTap(
|
||||
context,
|
||||
link.url,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing) ...[
|
||||
const SizedBox(width: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
ChannelMessageStatus.sent &&
|
||||
displayPath.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (enableTracing) ...[
|
||||
if (displayPath.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
'via ${_formatPathPrefixes(displayPath)}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
if (message.repeatCount > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
Icons.repeat,
|
||||
size: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${message.repeatCount}',
|
||||
_formatTime(message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
if (message.repeatCount > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
Icons.repeat,
|
||||
size: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'${message.repeatCount}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (isOutgoing) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
message.status == ChannelMessageStatus.sent
|
||||
? Icons.check
|
||||
: message.status ==
|
||||
ChannelMessageStatus.pending
|
||||
? Icons.schedule
|
||||
: Icons.error_outline,
|
||||
size: 14,
|
||||
color:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed
|
||||
? Colors.red
|
||||
: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
],
|
||||
if (isOutgoing) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
message.status == ChannelMessageStatus.sent
|
||||
? Icons.check
|
||||
: message.status ==
|
||||
ChannelMessageStatus.pending
|
||||
? Icons.schedule
|
||||
: Icons.error_outline,
|
||||
size: 14,
|
||||
color:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed
|
||||
? Colors.red
|
||||
: Colors.grey[600],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -650,7 +733,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return _PoiInfo(lat: lat, lon: lon, label: label);
|
||||
}
|
||||
|
||||
Widget _buildPoiMessage(BuildContext context, _PoiInfo poi, bool isOutgoing) {
|
||||
Widget _buildPoiMessage(
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
bool isOutgoing, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textColor = isOutgoing
|
||||
? colorScheme.onPrimaryContainer
|
||||
@@ -696,6 +784,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[const SizedBox(width: 4), trailing],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/storage/channel_message_store.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
@@ -105,6 +106,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final channelMessageStore = ChannelMessageStore();
|
||||
|
||||
// Auto-navigate back to scanner if disconnected
|
||||
if (!checkConnectionAndNavigate(connector)) {
|
||||
@@ -304,6 +306,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
return _buildChannelTile(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
showDragHandle: true,
|
||||
dragIndex: index,
|
||||
@@ -323,6 +326,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
return _buildChannelTile(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
);
|
||||
},
|
||||
@@ -352,6 +356,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Widget _buildChannelTile(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
ChannelMessageStore channelMessageStore,
|
||||
Channel channel, {
|
||||
bool showDragHandle = false,
|
||||
int? dragIndex,
|
||||
@@ -468,7 +473,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
);
|
||||
}
|
||||
},
|
||||
onLongPress: () => _showChannelActions(context, connector, channel),
|
||||
onLongPress: () => _showChannelActions(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -476,6 +486,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
void _showChannelActions(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
ChannelMessageStore channelMessageStore,
|
||||
Channel channel,
|
||||
) {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
@@ -528,7 +539,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
Navigator.pop(context);
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (context.mounted) {
|
||||
_confirmDeleteChannel(context, connector, channel);
|
||||
_confirmDeleteChannel(
|
||||
context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1474,6 +1490,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
void _confirmDeleteChannel(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
ChannelMessageStore channelMessageStore,
|
||||
Channel channel,
|
||||
) {
|
||||
showDialog(
|
||||
@@ -1492,6 +1509,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
onPressed: () {
|
||||
Navigator.pop(dialogContext);
|
||||
connector.deleteChannel(channel.index);
|
||||
channelMessageStore.clearChannelMessages(channel.index);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
|
||||
+164
-85
@@ -13,6 +13,7 @@ import 'package:latlong2/latlong.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../helpers/chat_scroll_controller.dart';
|
||||
import '../helpers/link_handler.dart';
|
||||
import '../helpers/utf8_length_limiter.dart';
|
||||
@@ -20,6 +21,7 @@ import '../models/channel_message.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../widgets/elements_ui.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
@@ -1173,6 +1175,8 @@ class _MessageBubble extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final gifId = _parseGifId(message.text);
|
||||
@@ -1250,102 +1254,175 @@ class _MessageBubble extends StatelessWidget {
|
||||
if (gifId == null) const SizedBox(height: 4),
|
||||
],
|
||||
if (poi != null)
|
||||
_buildPoiMessage(context, poi, textColor, metaColor)
|
||||
_buildPoiMessage(
|
||||
context,
|
||||
poi,
|
||||
textColor,
|
||||
metaColor,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
MessageStatus.delivered &&
|
||||
message.pathBytes.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
MessageStatus.failed,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
)
|
||||
else if (gifId != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Colors.transparent,
|
||||
fallbackTextColor: textColor.withValues(
|
||||
alpha: 0.7,
|
||||
Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Colors.transparent,
|
||||
fallbackTextColor: textColor.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!enableTracing && isOutgoing)
|
||||
Positioned(
|
||||
top: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(3),
|
||||
decoration: BoxDecoration(
|
||||
color: bubbleColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10),
|
||||
topRight: Radius.circular(12),
|
||||
),
|
||||
),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
MessageStatus.delivered &&
|
||||
message.pathBytes.isNotEmpty,
|
||||
isFailed:
|
||||
message.status ==
|
||||
MessageStatus.failed,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Linkify(
|
||||
text: messageText,
|
||||
style: TextStyle(color: textColor),
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
defaultToHttps: false,
|
||||
),
|
||||
linkifiers: const [UrlLinkifier()],
|
||||
onOpen: (link) =>
|
||||
LinkHandler.handleLinkTap(context, link.url),
|
||||
),
|
||||
if (isOutgoing && message.retryCount > 0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
context.l10n.chat_retryCount(
|
||||
message.retryCount,
|
||||
4,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: metaColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: metaColor,
|
||||
Flexible(
|
||||
child: Linkify(
|
||||
text: messageText,
|
||||
style: TextStyle(color: textColor),
|
||||
linkStyle: const TextStyle(
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
defaultToHttps: false,
|
||||
),
|
||||
linkifiers: const [UrlLinkifier()],
|
||||
onOpen: (link) => LinkHandler.handleLinkTap(
|
||||
context,
|
||||
link.url,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isOutgoing) ...[
|
||||
if (!enableTracing && isOutgoing) ...[
|
||||
const SizedBox(width: 4),
|
||||
_buildStatusIcon(metaColor),
|
||||
],
|
||||
if (message.tripTimeMs != null &&
|
||||
message.status ==
|
||||
MessageStatus.delivered) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.speed,
|
||||
size: 10,
|
||||
color: isOutgoing
|
||||
? metaColor
|
||||
: Colors.green[700],
|
||||
),
|
||||
Text(
|
||||
'${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: isOutgoing
|
||||
? metaColor
|
||||
: Colors.green[700],
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
child: MessageStatusIcon(
|
||||
isAcked:
|
||||
message.status ==
|
||||
MessageStatus.delivered &&
|
||||
message.pathBytes.isNotEmpty,
|
||||
isFailed:
|
||||
message.status == MessageStatus.failed,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (enableTracing) ...[
|
||||
if (isOutgoing && message.retryCount > 0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
context.l10n.chat_retryCount(
|
||||
message.retryCount,
|
||||
4,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: metaColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 4,
|
||||
)
|
||||
: EdgeInsets.zero,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: metaColor,
|
||||
),
|
||||
),
|
||||
if (isOutgoing) ...[
|
||||
const SizedBox(width: 4),
|
||||
_buildStatusIcon(metaColor),
|
||||
],
|
||||
if (message.tripTimeMs != null &&
|
||||
message.status ==
|
||||
MessageStatus.delivered) ...[
|
||||
const SizedBox(width: 4),
|
||||
Icon(
|
||||
Icons.speed,
|
||||
size: 10,
|
||||
color: isOutgoing
|
||||
? metaColor
|
||||
: Colors.green[700],
|
||||
),
|
||||
Text(
|
||||
'${(message.tripTimeMs! / 1000).toStringAsFixed(1)}s',
|
||||
style: TextStyle(
|
||||
fontSize: 9,
|
||||
color: isOutgoing
|
||||
? metaColor
|
||||
: Colors.green[700],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1388,8 +1465,9 @@ class _MessageBubble extends StatelessWidget {
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
Color textColor,
|
||||
Color metaColor,
|
||||
) {
|
||||
Color metaColor, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -1426,6 +1504,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (trailing != null) ...[const SizedBox(width: 4), trailing],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user