diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
index 88c3c89..a754fbc 100644
--- a/ios/Runner.xcodeproj/project.pbxproj
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -489,7 +489,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 109;
+ CURRENT_PROJECT_VERSION = 111;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -511,7 +511,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 109;
+ CURRENT_PROJECT_VERSION = 111;
DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
@@ -530,7 +530,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 109;
+ CURRENT_PROJECT_VERSION = 111;
DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
@@ -547,7 +547,7 @@
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 109;
+ CURRENT_PROJECT_VERSION = 111;
DEVELOPMENT_TEAM = JND55328G8;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
@@ -679,7 +679,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 109;
+ CURRENT_PROJECT_VERSION = 111;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -702,7 +702,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 109;
+ CURRENT_PROJECT_VERSION = 111;
DEVELOPMENT_TEAM = JND55328G8;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index eac98b6..2e16162 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -43,7 +43,7 @@
CFBundleSignature
????
CFBundleVersion
- 109
+ 111
LSRequiresIPhoneOS
ITSAppUsesNonExemptEncryption
diff --git a/ios/fastlane/report.xml b/ios/fastlane/report.xml
index b4611ce..f33b0e2 100644
--- a/ios/fastlane/report.xml
+++ b/ios/fastlane/report.xml
@@ -5,22 +5,22 @@
-
+
-
+
-
+
-
+
diff --git a/lib/screens/messages_tab.dart b/lib/screens/messages_tab.dart
index 68c5184..4f31a1d 100644
--- a/lib/screens/messages_tab.dart
+++ b/lib/screens/messages_tab.dart
@@ -338,9 +338,35 @@ class _MessagesTabState extends State {
_focusNode.requestFocus();
}
+ void _insertReplyMention(String displayName) {
+ final trimmedName = displayName.trim();
+ if (trimmedName.isEmpty) return;
+
+ final mention = '@[$trimmedName] ';
+ final value = _textController.value;
+ final selection = value.selection;
+ final hasSelection =
+ selection.isValid &&
+ selection.start >= 0 &&
+ selection.end >= selection.start;
+
+ final start = hasSelection ? selection.start : value.text.length;
+ final end = hasSelection ? selection.end : value.text.length;
+ final nextText = value.text.replaceRange(start, end, mention);
+ final nextOffset = start + mention.length;
+
+ _textController.value = value.copyWith(
+ text: nextText,
+ selection: TextSelection.collapsed(offset: nextOffset),
+ composing: TextRange.empty,
+ );
+ _enforceMessageByteLimit();
+ }
+
Future _replyToMessage(Message message) async {
final l10n = AppLocalizations.of(context)!;
final contactsProvider = context.read();
+ Contact? senderContact;
String destinationType;
Contact? recipient;
@@ -360,6 +386,11 @@ class _MessagesTabState extends State {
return;
}
}
+
+ final senderPrefix = message.senderPublicKeyPrefix;
+ if (senderPrefix != null && senderPrefix.length >= 6) {
+ senderContact = contactsProvider.findContactByPrefix(senderPrefix);
+ }
} else {
final senderPrefix = message.senderPublicKeyPrefix;
if (senderPrefix == null || senderPrefix.length < 6) {
@@ -380,6 +411,9 @@ class _MessagesTabState extends State {
await _onRecipientSelected(destinationType, recipient);
if (!mounted) return;
+ if (message.isChannelMessage && senderContact != null) {
+ _insertReplyMention(senderContact.displayName);
+ }
_focusNode.requestFocus();
}
diff --git a/lib/widgets/messages/message_bubble.dart b/lib/widgets/messages/message_bubble.dart
index 5c4ea58..134a168 100644
--- a/lib/widgets/messages/message_bubble.dart
+++ b/lib/widgets/messages/message_bubble.dart
@@ -68,6 +68,7 @@ class MessageBubble extends StatefulWidget {
}
class _MessageBubbleState extends State {
+ static final RegExp _mentionPattern = RegExp(r'@\[(.+?)\]');
bool _isExpanded = false;
bool _showReceivedStats = false;
@@ -95,6 +96,72 @@ class _MessageBubbleState extends State {
});
}
+ Widget _buildMessageTextContent(String text, TextStyle? baseBodyStyle) {
+ final matches = _mentionPattern.allMatches(text).toList();
+ if (matches.isEmpty) {
+ return Text(text, style: baseBodyStyle);
+ }
+
+ final textColor =
+ baseBodyStyle?.color ?? Theme.of(context).colorScheme.onSurface;
+ final backgroundColor = Theme.of(
+ context,
+ ).colorScheme.primary.withValues(alpha: 0.12);
+ final borderColor = Theme.of(
+ context,
+ ).colorScheme.primary.withValues(alpha: 0.25);
+
+ final spans = [];
+ var cursor = 0;
+
+ for (final match in matches) {
+ if (match.start > cursor) {
+ spans.add(
+ TextSpan(
+ text: text.substring(cursor, match.start),
+ style: baseBodyStyle,
+ ),
+ );
+ }
+
+ final mentionName = match.group(1)?.trim() ?? '';
+ if (mentionName.isNotEmpty) {
+ spans.add(
+ WidgetSpan(
+ alignment: PlaceholderAlignment.middle,
+ child: Container(
+ margin: const EdgeInsets.symmetric(horizontal: 1, vertical: 1),
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
+ decoration: BoxDecoration(
+ color: backgroundColor,
+ borderRadius: BorderRadius.circular(999),
+ border: Border.all(color: borderColor),
+ ),
+ child: Text(
+ '@$mentionName',
+ style: baseBodyStyle?.copyWith(
+ color: textColor,
+ fontWeight: FontWeight.w700,
+ height: 1.1,
+ ),
+ ),
+ ),
+ ),
+ );
+ } else {
+ spans.add(TextSpan(text: match.group(0), style: baseBodyStyle));
+ }
+
+ cursor = match.end;
+ }
+
+ if (cursor < text.length) {
+ spans.add(TextSpan(text: text.substring(cursor), style: baseBodyStyle));
+ }
+
+ return Text.rich(TextSpan(children: spans, style: baseBodyStyle));
+ }
+
void _handleBubbleTap({required bool isSarMarker, required bool isDrawing}) {
if (!widget.message.isRead &&
!widget.message.isSentMessage &&
@@ -2229,7 +2296,10 @@ class _MessageBubbleState extends State {
);
if (drawing == null) {
- return Text(message.text, style: baseBodyStyle);
+ return _buildMessageTextContent(
+ message.text,
+ baseBodyStyle,
+ );
}
final String drawingTypeLabel;
@@ -2315,7 +2385,7 @@ class _MessageBubbleState extends State {
)
// Regular message content
else if (!message.isDrawing || widget.isCompact)
- Text(message.text, style: baseBodyStyle),
+ _buildMessageTextContent(message.text, baseBodyStyle),
if (!widget.isCompact &&
!isSarMarker &&