mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-07-06 08:46:40 +10:00
feat(chat): add global pinch-to-zoom text scaling via ChatTextScaleService
This commit is contained in:
@@ -18,7 +18,9 @@ import '../l10n/l10n.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/emoji_picker.dart';
|
||||
import '../widgets/gif_message.dart';
|
||||
import '../widgets/jump_to_bottom_button.dart';
|
||||
@@ -219,37 +221,50 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
ListView.builder(
|
||||
reverse: true, // List grows from bottom up
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// Loading indicator now appears at end (bottom) of reversed list
|
||||
if (_isLoadingOlder && index == itemCount - 1) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
ChatZoomWrapper(
|
||||
child: ListView.builder(
|
||||
reverse: true, // List grows from bottom up
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(8),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// Loading indicator now appears at end (bottom) of reversed list
|
||||
if (_isLoadingOlder && index == itemCount - 1) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final messageIndex = index;
|
||||
final message = reversedMessages[messageIndex];
|
||||
if (!_messageKeys.containsKey(message.messageId)) {
|
||||
_messageKeys[message.messageId] = GlobalKey();
|
||||
}
|
||||
return Container(
|
||||
key: _messageKeys[message.messageId]!,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
final textScale = context
|
||||
.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
return _buildMessageBubble(
|
||||
message,
|
||||
textScale,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
final messageIndex = index;
|
||||
final message = reversedMessages[messageIndex];
|
||||
if (!_messageKeys.containsKey(message.messageId)) {
|
||||
_messageKeys[message.messageId] = GlobalKey();
|
||||
}
|
||||
return Container(
|
||||
key: _messageKeys[message.messageId]!,
|
||||
child: _buildMessageBubble(message),
|
||||
);
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
JumpToBottomButton(scrollController: _scrollController),
|
||||
],
|
||||
@@ -264,7 +279,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(ChannelMessage message) {
|
||||
Widget _buildMessageBubble(ChannelMessage message, double textScale) {
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final enableTracing = settingsService.settings.enableMessageTracing;
|
||||
final isOutgoing = message.isOutgoing;
|
||||
@@ -278,6 +293,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
|
||||
const maxSwipeOffset = 64.0;
|
||||
const replySwipeThreshold = 64.0;
|
||||
const bodyFontSize = 14.0;
|
||||
final messageBody = Column(
|
||||
crossAxisAlignment: isOutgoing
|
||||
? CrossAxisAlignment.end
|
||||
@@ -334,7 +350,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
if (gifId == null) const SizedBox(height: 4),
|
||||
],
|
||||
if (message.replyToMessageId != null) ...[
|
||||
_buildReplyPreview(message),
|
||||
_buildReplyPreview(message, textScale),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
if (poi != null)
|
||||
@@ -342,6 +358,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
context,
|
||||
poi,
|
||||
isOutgoing,
|
||||
textScale,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
@@ -415,9 +432,11 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Flexible(
|
||||
child: Linkify(
|
||||
text: message.text,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
linkStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
style: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
fontSize: bodyFontSize * textScale,
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
@@ -595,7 +614,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReplyPreview(ChannelMessage message) {
|
||||
Widget _buildReplyPreview(ChannelMessage message, double textScale) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final isOwnNode = message.replyToSenderName == connector.selfName;
|
||||
final replyText = message.replyToText ?? '';
|
||||
@@ -623,7 +642,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
context.l10n.chat_location,
|
||||
style: TextStyle(fontSize: 12, color: previewTextColor),
|
||||
style: TextStyle(fontSize: 12 * textScale, color: previewTextColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -633,7 +652,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: 12 * textScale,
|
||||
color: previewTextColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
@@ -657,7 +676,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Text(
|
||||
context.l10n.chat_replyTo(message.replyToSenderName ?? ''),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontSize: 11 * textScale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOwnNode
|
||||
? Theme.of(context).colorScheme.primary
|
||||
@@ -736,7 +755,8 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Widget _buildPoiMessage(
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
bool isOutgoing, {
|
||||
bool isOutgoing,
|
||||
double textScale, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@@ -774,12 +794,16 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.chat_poiShared,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14 * textScale,
|
||||
),
|
||||
),
|
||||
if (poi.label.isNotEmpty)
|
||||
Text(
|
||||
poi.label,
|
||||
style: TextStyle(color: metaColor, fontSize: 12),
|
||||
style: TextStyle(color: metaColor, fontSize: 12 * textScale),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -849,7 +873,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return colors[hash.abs() % colors.length];
|
||||
}
|
||||
|
||||
Widget _buildReplyBanner() {
|
||||
Widget _buildReplyBanner(double textScale) {
|
||||
final message = _replyingToMessage!;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@@ -875,7 +899,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
Text(
|
||||
context.l10n.chat_replyingTo(message.senderName),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: 12 * textScale,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
@@ -885,7 +909,7 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontSize: 11 * textScale,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSecondaryContainer.withValues(alpha: 0.7),
|
||||
@@ -912,7 +936,15 @@ class _ChannelChatScreenState extends State<ChannelChatScreen> {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_replyingToMessage != null) _buildReplyBanner(),
|
||||
if (_replyingToMessage != null)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final textScale = context.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
return _buildReplyBanner(textScale);
|
||||
},
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -22,7 +22,9 @@ import '../models/contact.dart';
|
||||
import '../models/message.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/chat_text_scale_service.dart';
|
||||
import '../services/path_history_service.dart';
|
||||
import '../widgets/chat_zoom_wrapper.dart';
|
||||
import '../widgets/elements_ui.dart';
|
||||
import 'channel_message_path_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
@@ -270,52 +272,62 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
_scrollController.scrollToBottomIfAtBottom();
|
||||
});
|
||||
|
||||
return ListView.builder(
|
||||
reverse: true, // List grows from bottom up
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// Loading indicator now appears at end (bottom) of reversed list
|
||||
if (_isLoadingOlder && index == itemCount - 1) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
return ChatZoomWrapper(
|
||||
child: ListView.builder(
|
||||
reverse: true, // List grows from bottom up
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
|
||||
itemCount: itemCount,
|
||||
itemBuilder: (context, index) {
|
||||
// Loading indicator now appears at end (bottom) of reversed list
|
||||
if (_isLoadingOlder && index == itemCount - 1) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
final messageIndex = index;
|
||||
Contact contact = widget.contact;
|
||||
final message = reversedMessages[messageIndex];
|
||||
String fourByteHex = '';
|
||||
if (widget.contact.type == advTypeRoom) {
|
||||
contact = _resolveContactFrom4Bytes(
|
||||
connector,
|
||||
message.fourByteRoomContactKey.isEmpty
|
||||
? Uint8List.fromList([0, 0, 0, 0])
|
||||
: message.fourByteRoomContactKey,
|
||||
);
|
||||
fourByteHex = message.fourByteRoomContactKey
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
.toUpperCase();
|
||||
}
|
||||
);
|
||||
}
|
||||
final messageIndex = index;
|
||||
Contact contact = widget.contact;
|
||||
final message = reversedMessages[messageIndex];
|
||||
String fourByteHex = '';
|
||||
if (widget.contact.type == advTypeRoom) {
|
||||
contact = _resolveContactFrom4Bytes(
|
||||
connector,
|
||||
message.fourByteRoomContactKey.isEmpty
|
||||
? Uint8List.fromList([0, 0, 0, 0])
|
||||
: message.fourByteRoomContactKey,
|
||||
);
|
||||
fourByteHex = message.fourByteRoomContactKey
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join()
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
return _MessageBubble(
|
||||
message: message,
|
||||
senderName: widget.contact.type == advTypeRoom
|
||||
? "${contact.name} [$fourByteHex]"
|
||||
: contact.name,
|
||||
isRoomServer: widget.contact.type == advTypeRoom,
|
||||
onTap: () => _openMessagePath(message, contact),
|
||||
onLongPress: () => _showMessageActions(message, contact),
|
||||
);
|
||||
},
|
||||
return Builder(
|
||||
builder: (context) {
|
||||
final textScale = context.select<ChatTextScaleService, double>(
|
||||
(service) => service.scale,
|
||||
);
|
||||
return _MessageBubble(
|
||||
message: message,
|
||||
senderName: widget.contact.type == advTypeRoom
|
||||
? "${contact.name} [$fourByteHex]"
|
||||
: contact.name,
|
||||
isRoomServer: widget.contact.type == advTypeRoom,
|
||||
textScale: textScale,
|
||||
onTap: () => _openMessagePath(message, contact),
|
||||
onLongPress: () => _showMessageActions(message, contact),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1163,11 +1175,13 @@ class _MessageBubble extends StatelessWidget {
|
||||
final bool isRoomServer;
|
||||
final VoidCallback? onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final double textScale;
|
||||
|
||||
const _MessageBubble({
|
||||
required this.message,
|
||||
required this.senderName,
|
||||
required this.isRoomServer,
|
||||
required this.textScale,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
});
|
||||
@@ -1190,6 +1204,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
? colorScheme.onErrorContainer
|
||||
: (isOutgoing ? colorScheme.onPrimary : colorScheme.onSurface);
|
||||
final metaColor = textColor.withValues(alpha: 0.7);
|
||||
const bodyFontSize = 14.0;
|
||||
String messageText = message.text;
|
||||
if (isRoomServer && !isOutgoing) {
|
||||
messageText = message.text.substring(4.clamp(0, message.text.length));
|
||||
@@ -1258,6 +1273,7 @@ class _MessageBubble extends StatelessWidget {
|
||||
poi,
|
||||
textColor,
|
||||
metaColor,
|
||||
textScale,
|
||||
trailing: (!enableTracing && isOutgoing)
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 2),
|
||||
@@ -1321,10 +1337,14 @@ class _MessageBubble extends StatelessWidget {
|
||||
Flexible(
|
||||
child: Linkify(
|
||||
text: messageText,
|
||||
style: TextStyle(color: textColor),
|
||||
linkStyle: const TextStyle(
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
linkStyle: TextStyle(
|
||||
color: Colors.green,
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
options: const LinkifyOptions(
|
||||
humanize: false,
|
||||
@@ -1464,7 +1484,8 @@ class _MessageBubble extends StatelessWidget {
|
||||
BuildContext context,
|
||||
_PoiInfo poi,
|
||||
Color textColor,
|
||||
Color metaColor, {
|
||||
Color metaColor,
|
||||
double textScale, {
|
||||
Widget? trailing,
|
||||
}) {
|
||||
return Row(
|
||||
@@ -1493,12 +1514,16 @@ class _MessageBubble extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.chat_poiShared,
|
||||
style: TextStyle(color: textColor, fontWeight: FontWeight.w600),
|
||||
style: TextStyle(
|
||||
color: textColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14 * textScale,
|
||||
),
|
||||
),
|
||||
if (poi.label.isNotEmpty)
|
||||
Text(
|
||||
poi.label,
|
||||
style: TextStyle(color: metaColor, fontSize: 12),
|
||||
style: TextStyle(color: metaColor, fontSize: 12 * textScale),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user