mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-30 15:23:02 +10:00
Merge branch 'dev' into update-pacox-multibyte
This commit is contained in:
@@ -26,7 +26,6 @@ import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../helpers/contact_ui.dart';
|
||||
import '../widgets/byte_count_input.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
@@ -40,6 +39,8 @@ import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/translated_message_content.dart';
|
||||
import '../widgets/unread_divider.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
|
||||
@@ -109,7 +110,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
totalMessages: messages.length,
|
||||
onJumped: () {
|
||||
if (!mounted) return;
|
||||
_scrollToMessage(anchor!.messageId);
|
||||
_scrollToMessage(anchor!.messageId, quiet: true);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -194,9 +195,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _scrollToMessage(String messageId) async {
|
||||
Future<void> _scrollToMessage(String messageId, {bool quiet = false}) async {
|
||||
final key = _messageKeys[messageId];
|
||||
if (key == null) {
|
||||
// The auto unread-jump can resolve a frame after navigating away;
|
||||
// a deactivated context can't host a snackbar.
|
||||
if (quiet || !mounted || !context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.chat_originalMessageNotFound),
|
||||
@@ -492,6 +496,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final gifId = GifHelper.parseGif(message.text);
|
||||
final poi = parseMarkerText(message.text);
|
||||
final translatedDisplayText =
|
||||
@@ -511,9 +516,34 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
message.pathHashWidth ??
|
||||
context.read<MeshCoreConnector>().pathHashByteWidth;
|
||||
|
||||
// Bubble colors — outgoing uses MeshPalette.me / meBorder / meInk.
|
||||
final bubbleColor = isOutgoing
|
||||
? MeshPalette.me
|
||||
: scheme.surfaceContainerLow;
|
||||
final bubbleBorder = isOutgoing
|
||||
? MeshPalette.meBorder
|
||||
: scheme.outlineVariant;
|
||||
final textColor = isOutgoing ? MeshPalette.meInk : scheme.onSurface;
|
||||
final metaColor = textColor.withValues(alpha: 0.65);
|
||||
const bodyFontSize = 14.0;
|
||||
|
||||
// Asymmetric radius matching chat_screen bubbles.
|
||||
final borderRadius = isOutgoing
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(MeshRadii.lg),
|
||||
topRight: Radius.circular(MeshRadii.lg),
|
||||
bottomLeft: Radius.circular(MeshRadii.lg),
|
||||
bottomRight: Radius.circular(MeshRadii.xs),
|
||||
)
|
||||
: const BorderRadius.only(
|
||||
topLeft: Radius.circular(MeshRadii.xs),
|
||||
topRight: Radius.circular(MeshRadii.lg),
|
||||
bottomLeft: Radius.circular(MeshRadii.lg),
|
||||
bottomRight: Radius.circular(MeshRadii.lg),
|
||||
);
|
||||
|
||||
const maxSwipeOffset = 64.0;
|
||||
const replySwipeThreshold = 64.0;
|
||||
const bodyFontSize = 14.0;
|
||||
final messageBody = LayoutBuilder(
|
||||
builder: (context, constraints) => Column(
|
||||
crossAxisAlignment: isOutgoing
|
||||
@@ -524,11 +554,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
mainAxisAlignment: isOutgoing
|
||||
? MainAxisAlignment.end
|
||||
: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (!isOutgoing) ...[
|
||||
_buildAvatar(message.senderName),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
Flexible(
|
||||
child: GestureDetector(
|
||||
@@ -544,15 +574,12 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
vertical: 8,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: constraints.maxWidth * 0.65,
|
||||
maxWidth: constraints.maxWidth * 0.72,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isOutgoing
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: bubbleColor,
|
||||
borderRadius: borderRadius,
|
||||
border: Border.all(color: bubbleBorder, width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -568,14 +595,14 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
message.senderName,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: _colorForName(message.senderName),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (gifId == null) const SizedBox(height: 4),
|
||||
if (gifId == null) const SizedBox(height: 2),
|
||||
],
|
||||
if (message.replyToMessageId != null) ...[
|
||||
_buildReplyPreview(message, textScale),
|
||||
@@ -598,13 +625,9 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
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),
|
||||
fallbackTextColor: textColor.withValues(
|
||||
alpha: 0.7,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -619,43 +642,51 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
displayText: translatedDisplayText,
|
||||
originalText: originalDisplayText,
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
originalStyle: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.72),
|
||||
color: textColor.withValues(alpha: 0.72),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (enableTracing && displayPath.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 3),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: Text(
|
||||
context.l10n.channels_via(
|
||||
_formatPathPrefixes(
|
||||
displayPath,
|
||||
displayPathHashWidth,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
RouteChip(
|
||||
isDirect: (message.pathLength ?? -1) >= 0,
|
||||
hops: (message.pathLength ?? -1) >= 0
|
||||
? message.pathLength
|
||||
: null,
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
context.l10n.channels_via(
|
||||
_formatPathPrefixes(
|
||||
displayPath,
|
||||
displayPathHashWidth,
|
||||
),
|
||||
),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 9.5 * textScale,
|
||||
color: metaColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 3),
|
||||
Padding(
|
||||
padding: gifId != null
|
||||
? const EdgeInsets.only(
|
||||
@@ -669,30 +700,24 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
children: [
|
||||
Text(
|
||||
_formatTime(context, message.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10 * textScale,
|
||||
color: metaColor,
|
||||
),
|
||||
),
|
||||
if (enableTracing && message.repeatCount > 0) ...[
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
Icons.repeat,
|
||||
size: 12 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
size: 11 * textScale,
|
||||
color: metaColor,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'${message.repeatCount}',
|
||||
style: TextStyle(
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurfaceVariant,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10 * textScale,
|
||||
color: metaColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -712,6 +737,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
isFailed:
|
||||
message.status ==
|
||||
ChannelMessageStatus.failed,
|
||||
onColor: metaColor,
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -727,7 +753,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
if (message.reactions.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: isOutgoing ? 0 : 48),
|
||||
padding: EdgeInsets.only(left: isOutgoing ? 0 : 42),
|
||||
child: _buildReactionsDisplay(message),
|
||||
),
|
||||
],
|
||||
@@ -746,7 +772,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: messageBody,
|
||||
);
|
||||
}
|
||||
@@ -843,7 +869,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderRadius: BorderRadius.circular(MeshRadii.sm),
|
||||
border: Border(
|
||||
left: BorderSide(color: colorScheme.primary, width: 3),
|
||||
),
|
||||
@@ -856,9 +882,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
style: TextStyle(
|
||||
fontSize: 11 * textScale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOwnNode
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
color: isOwnNode ? colorScheme.primary : colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
@@ -870,6 +894,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
|
||||
Widget _buildReactionsDisplay(ChannelMessage message) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
@@ -880,27 +905,29 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
color: scheme.surfaceContainerHigh,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
border: Border.all(color: scheme.outlineVariant, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(emoji, style: const TextStyle(fontSize: 16)),
|
||||
Text(
|
||||
emoji,
|
||||
style: MeshTheme.emoji(fontSize: 16),
|
||||
textHeightBehavior: const TextHeightBehavior(
|
||||
applyHeightToFirstAscent: false,
|
||||
applyHeightToLastDescent: false,
|
||||
),
|
||||
),
|
||||
if (count > 1) ...[
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$count',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: scheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -919,20 +946,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
String senderName, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textColor = isOutgoing
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurface;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textColor = isOutgoing ? MeshPalette.meInk : scheme.onSurface;
|
||||
final metaColor = textColor.withValues(alpha: 0.7);
|
||||
final channelColor = widget.channel.isPublicChannel
|
||||
? Colors.orange
|
||||
: Colors.blue;
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.location_on_outlined, color: channelColor),
|
||||
icon: Icon(Icons.location_on_outlined, color: scheme.primary),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(minWidth: 40, minHeight: 40),
|
||||
onPressed: () {
|
||||
@@ -998,41 +1020,24 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
}
|
||||
|
||||
Widget _buildAvatar(String senderName) {
|
||||
final initial = firstCharacterOrEmoji(senderName);
|
||||
final color = colorForName(senderName);
|
||||
|
||||
return CircleAvatar(
|
||||
radius: 18,
|
||||
backgroundColor: color.withValues(alpha: 0.2),
|
||||
child: Text(
|
||||
initial,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
);
|
||||
return AvatarCircle(name: senderName, size: 32);
|
||||
}
|
||||
|
||||
Widget _buildReplyBanner(double textScale) {
|
||||
final message = _replyingToMessage!;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
color: scheme.surfaceContainerHigh,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1),
|
||||
bottom: BorderSide(color: scheme.outlineVariant, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.reply,
|
||||
size: 18,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
Icon(Icons.reply, size: 18, color: scheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
@@ -1040,10 +1045,10 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.chat_replyingTo(message.senderName),
|
||||
style: TextStyle(
|
||||
fontSize: 12 * textScale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11 * textScale,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: scheme.primary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
@@ -1052,9 +1057,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1063,7 +1066,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: _cancelReply,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
color: scheme.onSurfaceVariant,
|
||||
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
||||
),
|
||||
],
|
||||
@@ -1075,6 +1078,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final maxBytes = maxChannelMessageBytes(connector.selfName);
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -1088,123 +1092,166 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
},
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
color: scheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(color: scheme.outlineVariant, width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.gif_box),
|
||||
onPressed: () => _showGifPicker(context),
|
||||
tooltip: context.l10n.chat_sendGif,
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
builder: (context, value, child) {
|
||||
final gifId = GifHelper.parseGif(value.text);
|
||||
if (gifId != null) {
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
(event.logicalKey == LogicalKeyboardKey.enter ||
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.numpadEnter)) {
|
||||
_sendMessage();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
fallbackTextColor: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
maxSize: 160,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.gif_box),
|
||||
onPressed: () => _showGifPicker(context),
|
||||
tooltip: context.l10n.chat_sendGif,
|
||||
),
|
||||
if (settings.translationEnabled)
|
||||
MessageTranslationButton(
|
||||
enabled: settings.composerTranslationEnabled,
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
builder: (context, value, child) {
|
||||
final gifId = GifHelper.parseGif(value.text);
|
||||
if (gifId != null) {
|
||||
return Focus(
|
||||
autofocus: true,
|
||||
onKeyEvent: (node, event) {
|
||||
if (event is KeyDownEvent &&
|
||||
(event.logicalKey ==
|
||||
LogicalKeyboardKey.enter ||
|
||||
event.logicalKey ==
|
||||
LogicalKeyboardKey.numpadEnter)) {
|
||||
_sendMessage();
|
||||
return KeyEventResult.handled;
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: GifMessage(
|
||||
url:
|
||||
'https://media.giphy.com/media/$gifId/giphy.gif',
|
||||
backgroundColor:
|
||||
scheme.surfaceContainerHighest,
|
||||
fallbackTextColor: scheme.onSurface
|
||||
.withValues(alpha: 0.6),
|
||||
maxSize: 160,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ByteCountedTextField(
|
||||
maxBytes: maxBytes,
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
encoder:
|
||||
(connector.isChannelSmazEnabled(
|
||||
widget.channel.index,
|
||||
) ||
|
||||
connector.isChannelCyr2LatEnabled(
|
||||
widget.channel.index,
|
||||
))
|
||||
? (text) => connector.prepareChannelOutboundText(
|
||||
widget.channel.index,
|
||||
text,
|
||||
)
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
MeshRadii.pill,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: scheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
_textController.clear();
|
||||
_textFieldFocusNode.requestFocus();
|
||||
},
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
MeshRadii.pill,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: scheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
MeshRadii.pill,
|
||||
),
|
||||
borderSide: BorderSide(
|
||||
color: scheme.primary,
|
||||
width: 1.5,
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: scheme.surfaceContainerLow,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 18,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
builder: (context, value, _) {
|
||||
final hasText = value.text.trim().isNotEmpty;
|
||||
return AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
curve: Curves.easeInOut,
|
||||
child: IconButton.filled(
|
||||
icon: const Icon(Icons.send, size: 20),
|
||||
tooltip: context.l10n.chat_sendMessage,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: hasText
|
||||
? scheme.primary
|
||||
: scheme.surfaceContainerHighest,
|
||||
foregroundColor: hasText
|
||||
? scheme.onPrimary
|
||||
: scheme.onSurfaceVariant,
|
||||
minimumSize: const Size(40, 40),
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
onPressed: hasText
|
||||
? () {
|
||||
HapticFeedback.lightImpact();
|
||||
_sendMessage();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ByteCountedTextField(
|
||||
maxBytes: maxBytes,
|
||||
controller: _textController,
|
||||
focusNode: _textFieldFocusNode,
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
onSubmitted: (_) => _sendMessage(),
|
||||
encoder:
|
||||
(connector.isChannelSmazEnabled(
|
||||
widget.channel.index,
|
||||
) ||
|
||||
connector.isChannelCyr2LatEnabled(
|
||||
widget.channel.index,
|
||||
))
|
||||
? (text) => connector.prepareChannelOutboundText(
|
||||
widget.channel.index,
|
||||
text,
|
||||
)
|
||||
: null,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.send),
|
||||
tooltip: context.l10n.chat_sendMessage,
|
||||
onPressed: _sendMessage,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1345,12 +1392,20 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
) &&
|
||||
(message.translatedText?.trim().isEmpty ?? true);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
showMeshSheet(
|
||||
context,
|
||||
builder: (sheetContext) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
BottomSheetHeader(
|
||||
title: message.text.length > 40
|
||||
? '${message.text.substring(0, 40)}…'
|
||||
: message.text,
|
||||
subtitle: message.senderName.isNotEmpty
|
||||
? message.senderName
|
||||
: null,
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.reply),
|
||||
title: Text(context.l10n.chat_reply),
|
||||
@@ -1409,7 +1464,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
_markAsUnread(message);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
Icons.delete_outline,
|
||||
@@ -1424,6 +1479,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
await _deleteMessage(message);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1507,6 +1563,23 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
pathHashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(',');
|
||||
}
|
||||
|
||||
/// Deterministic name-to-hue mapping consistent with [AvatarCircle].
|
||||
Color _colorForName(String name) {
|
||||
const hues = [
|
||||
MeshPalette.blue,
|
||||
MeshPalette.magenta,
|
||||
MeshPalette.signal,
|
||||
MeshPalette.warn,
|
||||
Color(0xFF8FA8F0),
|
||||
Color(0xFF6FD9CE),
|
||||
];
|
||||
var h = 0;
|
||||
for (final c in name.codeUnits) {
|
||||
h = (h * 31 + c) & 0x7fffffff;
|
||||
}
|
||||
return hues[h % hues.length];
|
||||
}
|
||||
}
|
||||
|
||||
class _SwipeReplyBubble extends StatefulWidget {
|
||||
@@ -1648,7 +1721,7 @@ class _SwipeReplyBubbleState extends State<_SwipeReplyBubble> {
|
||||
onPointerUp: (event) => _handleSwipePointerUp(event.position),
|
||||
onPointerCancel: (_) => _resetSwipe(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 8),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user