Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Flutter applications with [Flexible Backend Integration][chatViewConnect].
- One-on-one and group chat support
- Message reactions with emoji
- Reply to messages functionality
- User mentions/tagging with @ symbol
- Link preview for URLs
- Voice messages support
- Image sharing capabilities
Expand Down
83 changes: 83 additions & 0 deletions doc/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Flutter applications with [Flexible Backend Integration](https://pub.dev/package
- One-on-one and group chat support
- Message reactions with emoji
- Reply to messages functionality
- User mentions/tagging with @ symbol
- Link preview
- Voice messages
- Image sharing
Expand Down Expand Up @@ -1305,6 +1306,88 @@ textFieldConfig: TextFieldConfiguration(
),
```

### User Mentions/Tagging

ChatView now supports user mentions (tagging) with @ symbol, similar to platforms like Slack or WhatsApp. When users type @ in the text field, a searchable list of users appears, and selecting a user inserts their mention into the message. Mentions are visually distinct in sent messages.

#### Setting Up Mentions

Configure mentions through `TextFieldConfiguration`:

```dart
sendMessageConfig: SendMessageConfiguration(
textFieldConfig: TextFieldConfiguration(
onMentionTriggered: (searchText) {
// Filter users based on search text
final users = chatController.otherUsers
.where((user) => user.name
.toLowerCase()
.contains(searchText.toLowerCase()))
.toList();

// Convert users to suggestions
final suggestions = users.map((user) {
return SuggestionItemData(
text: user.name,
config: const SuggestionItemConfig(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
textStyle: TextStyle(color: Colors.white),
),
);
}).toList();

// Update suggestions in chat controller
chatController.newSuggestions.value = suggestions;
},
mentionTriggerCharacter: '@', // Default is '@'
mentionTextStyle: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
```

#### Handling Mention Selection

Configure `ReplySuggestionsConfig` to handle mention insertion:

```dart
replySuggestionsConfig: ReplySuggestionsConfig(
onTap: (item) {
// Get the SendMessageWidget state
final sendMessageWidgetKey = context.findAncestorStateOfType<SendMessageWidgetState>();
if (sendMessageWidgetKey != null) {
// Insert mention at cursor position
sendMessageWidgetKey.insertMention(item.text);
// Clear suggestions after selection
chatController.removeReplySuggestions();
}
},
),
```

#### Visual Styling

Mentions in messages are automatically styled based on `mentionTextStyle`. By default:
- Outgoing messages: mentions are bold and yellow
- Incoming messages: mentions are bold and blue

You can customize this by setting `mentionTextStyle` in `TextFieldConfiguration`.

#### Key Features

- **Auto-detection**: Typing @ automatically triggers mention mode
- **Live filtering**: User list filters as you type
- **Smart insertion**: Mentions are inserted at cursor position
- **Visual distinction**: @mentions are highlighted in messages
- **Customizable**: Configure trigger character and styling
- **Non-intrusive**: Only activates when @ is typed

# Contributors

## Main Contributors
Expand Down
7 changes: 7 additions & 0 deletions example/lib/data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,13 @@ class Data {
];

static getMessageList({bool isExampleOne = true}) => [
Message(
id: '0',
message: "Hey @John, can you help @Sarah with the project?",
createdAt: DateTime.now().subtract(const Duration(minutes: 5)),
sentBy: '1',
status: MessageStatus.read,
),
Message(
id: '1',
message: "How's it going?",
Expand Down
71 changes: 66 additions & 5 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,9 @@ class _ExampleOneChatScreenState extends State<ExampleOneChatScreen> {
otherUsers: Data.otherUsers,
);

// Track if we're in mention mode to handle suggestion taps appropriately
bool _isMentionMode = false;

@override
Widget build(BuildContext context) {
return Scaffold(
Expand Down Expand Up @@ -750,6 +753,50 @@ class _ExampleOneChatScreenState extends State<ExampleOneChatScreen> {
),
),
],
onMentionTriggered: (searchText) {
// Update mention mode flag
_isMentionMode = searchText.isNotEmpty;

if (searchText.isEmpty) {
// Clear suggestions when mention mode ends
_chatController.removeReplySuggestions();
return;
}

// Filter users based on search text
final users = _chatController.otherUsers
.where((user) => user.name
.toLowerCase()
.contains(searchText.toLowerCase()))
.toList();

// Convert users to suggestions
final suggestions = users.map((user) {
return SuggestionItemData(
text: user.name,
config: const SuggestionItemConfig(
padding: EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: AppColors.uiOnePurple,
borderRadius: BorderRadius.all(Radius.circular(8)),
),
textStyle: TextStyle(
color: Colors.white,
fontSize: 14,
),
),
);
}).toList();

// Update suggestions in chat controller
_chatController.newSuggestions.value = suggestions;
},
mentionTriggerCharacter: '@',
mentionTextStyle: const TextStyle(
fontWeight: FontWeight.bold,
color: AppColors.uiOnePurple,
),
),
),
chatBubbleConfig: ChatBubbleConfiguration(
Expand Down Expand Up @@ -883,11 +930,25 @@ class _ExampleOneChatScreenState extends State<ExampleOneChatScreen> {
),
textStyle: TextStyle(color: _theme.textColor),
),
onTap: (item) => _onSendTap(
item.text,
const ReplyMessage(),
MessageType.text,
),
onTap: (item) {
if (_isMentionMode) {
// In mention mode, try to insert the mention
final sendMessageWidgetKey =
context.findAncestorStateOfType<SendMessageWidgetState>();
if (sendMessageWidgetKey != null) {
sendMessageWidgetKey.insertMention(item.text);
_chatController.removeReplySuggestions();
_isMentionMode = false;
}
} else {
// Normal suggestion behavior - send as message
_onSendTap(
item.text,
const ReplyMessage(),
MessageType.text,
);
}
},
),
),
),
Expand Down
14 changes: 14 additions & 0 deletions lib/src/models/config_models/send_message_configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ class TextFieldConfiguration {
this.hintMaxLines,
this.trailingActions,
this.leadingActions,
this.onMentionTriggered,
this.mentionTriggerCharacter = '@',
this.mentionTextStyle,
});

/// Used to give max lines in text field.
Expand Down Expand Up @@ -239,6 +242,17 @@ class TextFieldConfiguration {
///
/// Default is `true`.
final bool hideLeadingActionsOnType;

/// Callback when user types the mention trigger character (default '@').
/// Provides the search text after the trigger character for filtering users.
final MentionCallback? onMentionTriggered;

/// Character that triggers mention suggestions. Default is '@'.
final String mentionTriggerCharacter;

/// Text style for mentions in the text field.
/// If provided, mentions will be styled differently in the message text.
final TextStyle? mentionTextStyle;
}

class ImagePickerConfiguration {
Expand Down
3 changes: 3 additions & 0 deletions lib/src/values/typedefs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,6 @@ typedef EmojiPickerActionCallback = void Function(
String? emoji,
ReplyMessage? replyMessage,
);
typedef MentionCallback = void Function(
String searchText,
);
69 changes: 69 additions & 0 deletions lib/src/widgets/chatui_textfield.dart
Original file line number Diff line number Diff line change
Expand Up @@ -460,5 +460,74 @@ class _ChatUITextFieldState extends State<ChatUITextField> {
composingStatus.value = TypeWriterStatus.typing;
});
_isTextNotEmptyNotifier.value = inputText.trim().isNotEmpty;
_detectMention(inputText);
}

void _detectMention(String text) {
final mentionTrigger =
textFieldConfig?.mentionTriggerCharacter ?? '@';
final onMentionTriggered = textFieldConfig?.onMentionTriggered;

if (onMentionTriggered == null) return;

final selection = widget.textEditingController.selection;
if (!selection.isValid || selection.baseOffset <= 0) return;

// Get text up to cursor position
final textBeforeCursor = text.substring(0, selection.baseOffset);

// Find the last occurrence of mention trigger
final lastTriggerIndex = textBeforeCursor.lastIndexOf(mentionTrigger);

if (lastTriggerIndex == -1) {
// No trigger found, clear suggestions
onMentionTriggered('');
return;
}

// Check if there's a space between trigger and cursor
// This ensures we only suggest mentions while typing a single word
final textAfterTrigger = textBeforeCursor.substring(lastTriggerIndex + 1);
if (textAfterTrigger.contains(' ')) {
// Space found, user has finished typing the mention word
onMentionTriggered('');
return;
}

// Valid mention detected, trigger callback with search text
onMentionTriggered(textAfterTrigger);
}

/// Inserts a mention into the text field at the current cursor position.
/// This method is called when a user selects a mention from the suggestions.
void insertMention(String mention) {
final mentionTrigger =
textFieldConfig?.mentionTriggerCharacter ?? '@';
final controller = widget.textEditingController;
final text = controller.text;
final selection = controller.selection;

if (!selection.isValid) return;

// Get text up to cursor position
final textBeforeCursor = text.substring(0, selection.baseOffset);

// Find the last occurrence of mention trigger
final lastTriggerIndex = textBeforeCursor.lastIndexOf(mentionTrigger);

if (lastTriggerIndex == -1) return;

// Replace from trigger to cursor with the mention
final newText = text.replaceRange(
lastTriggerIndex,
selection.baseOffset,
'$mentionTrigger$mention ',
);

// Update text and cursor position
final newCursorPos = lastTriggerIndex + mentionTrigger.length + mention.length + 1;
controller
..text = newText
..selection = TextSelection.collapsed(offset: newCursorPos);
}
}
1 change: 1 addition & 0 deletions lib/src/widgets/message_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ class _MessageViewState extends State<MessageView>
highlightColor: widget.highlightColor,
highlightMessage: widget.shouldHighlight,
featureActiveConfig: chatViewIW?.featureActiveConfig,
mentionTextStyle: null, // Can be customized via config in future
);
} else if (widget.message.messageType.isVoice) {
return VoiceMessageView(
Expand Down
10 changes: 10 additions & 0 deletions lib/src/widgets/send_message_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ class SendMessageWidgetState extends State<SendMessageWidget> {

final GlobalKey<SelectedImageViewWidgetState> _selectedImageViewWidgetKey =
GlobalKey();

final GlobalKey<_ChatUITextFieldState> _chatUITextFieldKey = GlobalKey();

ReplyMessage _replyMessage = const ReplyMessage();

ReplyMessage get replyMessage => _replyMessage;
Expand Down Expand Up @@ -172,6 +175,7 @@ class SendMessageWidgetState extends State<SendMessageWidget> {
sendMessageConfig: widget.sendMessageConfig,
),
ChatUITextField(
key: _chatUITextFieldKey,
focusNode: _focusNode,
textEditingController: _textEditingController,
onPressed: _onPressed,
Expand Down Expand Up @@ -284,6 +288,12 @@ class SendMessageWidgetState extends State<SendMessageWidget> {
}
}

/// Inserts a mention into the text field.
/// This can be called from outside to insert a user mention.
void insertMention(String mention) {
_chatUITextFieldKey.currentState?.insertMention(mention);
}

double get _bottomPadding => (!kIsWeb && Platform.isIOS)
? (_focusNode.hasFocus
? bottomPadding1
Expand Down
Loading