highlight mentions

This commit is contained in:
Salvatore Giordano
2020-03-04 15:41:52 +01:00
parent c37ed34802
commit a563e8f19a
3 changed files with 146 additions and 12 deletions
+118 -11
View File
@@ -8,6 +8,7 @@ import 'package:keyboard_visibility/keyboard_visibility.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/user_avatar.dart';
import '../stream_chat_flutter.dart'; import '../stream_chat_flutter.dart';
import 'stream_channel.dart'; import 'stream_channel.dart';
@@ -78,11 +79,12 @@ class MessageInput extends StatefulWidget {
class _MessageInputState extends State<MessageInput> { class _MessageInputState extends State<MessageInput> {
final List<_SendingAttachment> _attachments = []; final List<_SendingAttachment> _attachments = [];
final _focusNode = FocusNode(); final _focusNode = FocusNode();
final List<User> _mentionedUsers = [];
TextEditingController _textController; TextEditingController _textController;
bool _messageIsPresent = false; bool _messageIsPresent = false;
bool _typingStarted = false; bool _typingStarted = false;
OverlayEntry _commandsOverlay; OverlayEntry _commandsOverlay, _mentionsOverlay;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -149,11 +151,24 @@ class _MessageInputState extends State<MessageInput> {
_commandsOverlay?.remove(); _commandsOverlay?.remove();
_commandsOverlay = null; _commandsOverlay = null;
_mentionsOverlay?.remove();
_mentionsOverlay = null;
if (s.startsWith('/')) { if (s.startsWith('/')) {
_commandsOverlay = _buildOverlayEntry(); _commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay); Overlay.of(context).insert(_commandsOverlay);
} }
if (_textController.selection.isCollapsed &&
(s[_textController.selection.start - 1] == '@' ||
_textController.text
.substring(0, _textController.selection.start)
.split(' ')
.last
.contains('@'))) {
_mentionsOverlay = _buildMentionsOverlayEntry();
Overlay.of(context).insert(_mentionsOverlay);
}
}, },
onTap: () { onTap: () {
setState(() { setState(() {
@@ -200,7 +215,7 @@ class _MessageInputState extends State<MessageInput> {
); );
} }
OverlayEntry _buildOverlayEntry() { OverlayEntry _buildCommandsOverlayEntry() {
final text = _textController.text; final text = _textController.text;
final commands = StreamChannel.of(context) final commands = StreamChannel.of(context)
.channel .channel
@@ -269,17 +284,88 @@ class _MessageInputState extends State<MessageInput> {
}); });
} }
void _setCommand(Command c) { OverlayEntry _buildMentionsOverlayEntry() {
setState(() { final splits = _textController.text
_textController.value = TextEditingValue( .substring(0, _textController.value.selection.start)
text: '/${c.name} ', .split('@');
selection: TextSelection.fromPosition( final query = splits.last.toLowerCase();
TextPosition(
offset: c.name.length + 2, final members = StreamChannel.of(context).channel.state.members?.where((m) {
return m.user.name.toLowerCase().contains(query);
})?.toList() ??
[];
RenderBox renderBox = context.findRenderObject();
final size = renderBox.size;
return OverlayEntry(builder: (context) {
return Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: Material(
color: StreamChatTheme.of(context).primaryColor,
child: Container(
constraints: BoxConstraints.loose(Size.fromHeight(400)),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
spreadRadius: -8,
blurRadius: 5.0,
offset: Offset(0, -4),
),
],
color: StreamChatTheme.of(context).primaryColor,
),
child: Flex(
mainAxisSize: MainAxisSize.min,
direction: Axis.vertical,
children: <Widget>[
ListView(
padding: const EdgeInsets.all(0),
shrinkWrap: true,
children: members
.map((m) => ListTile(
leading: UserAvatar(
user: m.user,
),
title: Text('${m.user.name}'),
onTap: () {
_mentionedUsers.add(m.user);
splits[splits.length - 1] = m.user.name;
final rejoin = splits.join('@');
_textController.value = TextEditingValue(
text: rejoin +
_textController.text.substring(
_textController.selection.start),
selection: TextSelection.collapsed(
offset: rejoin.length,
),
);
_mentionsOverlay?.remove();
_mentionsOverlay = null;
},
))
.toList(),
),
],
),
), ),
), ),
); );
}); });
}
void _setCommand(Command c) {
_textController.value = TextEditingValue(
text: '/${c.name} ',
selection: TextSelection.collapsed(
offset: c.name.length + 2,
),
);
_commandsOverlay?.remove(); _commandsOverlay?.remove();
_commandsOverlay = null; _commandsOverlay = null;
} }
@@ -555,6 +641,8 @@ class _MessageInputState extends State<MessageInput> {
_commandsOverlay?.remove(); _commandsOverlay?.remove();
_commandsOverlay = null; _commandsOverlay = null;
_mentionsOverlay?.remove();
_mentionsOverlay = null;
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
@@ -566,6 +654,8 @@ class _MessageInputState extends State<MessageInput> {
.where((attachment) => attachment.type == 'giphy') .where((attachment) => attachment.type == 'giphy')
.toList() + .toList() +
_getAttachments(attachments).toList(), _getAttachments(attachments).toList(),
mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
); );
StreamChat.of(context).client.updateMessage(message).then((_) { StreamChat.of(context).client.updateMessage(message).then((_) {
if (widget.onMessageSent != null) { if (widget.onMessageSent != null) {
@@ -577,6 +667,8 @@ class _MessageInputState extends State<MessageInput> {
parentId: widget.parentMessage?.id, parentId: widget.parentMessage?.id,
text: text, text: text,
attachments: _getAttachments(attachments).toList(), attachments: _getAttachments(attachments).toList(),
mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
); );
StreamChannel.of(context).channel.sendMessage(message).then((_) { StreamChannel.of(context).channel.sendMessage(message).then((_) {
if (widget.onMessageSent != null) { if (widget.onMessageSent != null) {
@@ -613,21 +705,35 @@ class _MessageInputState extends State<MessageInput> {
void initState() { void initState() {
super.initState(); super.initState();
StreamChannel.of(context).queryMembersAndWatchers();
_keyboardListener = KeyboardVisibilityNotification().addNewListener( _keyboardListener = KeyboardVisibilityNotification().addNewListener(
onHide: () { onHide: () {
if (_commandsOverlay != null) { if (_commandsOverlay != null) {
_commandsOverlay.remove(); _commandsOverlay.remove();
} }
if (_mentionsOverlay != null) {
_mentionsOverlay.remove();
}
}, },
onShow: () { onShow: () {
if (_commandsOverlay != null) { if (_commandsOverlay != null) {
if (_textController.text.startsWith('/')) { if (_textController.text.startsWith('/')) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
_commandsOverlay = _buildOverlayEntry(); _commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay); Overlay.of(context).insert(_commandsOverlay);
}); });
} }
} }
if (_mentionsOverlay != null) {
if (_textController.text.contains('@')) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_mentionsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_mentionsOverlay);
});
}
}
}, },
); );
@@ -666,6 +772,7 @@ class _MessageInputState extends State<MessageInput> {
@override @override
void dispose() { void dispose() {
_commandsOverlay?.remove(); _commandsOverlay?.remove();
_mentionsOverlay?.remove();
KeyboardVisibilityNotification().removeListener(_keyboardListener); KeyboardVisibilityNotification().removeListener(_keyboardListener);
super.dispose(); super.dispose();
} }
+6 -1
View File
@@ -361,6 +361,11 @@ class _MessageWidgetState extends State<MessageWidget>
); );
if (widget.message.text.trim().isNotEmpty) { if (widget.message.text.trim().isNotEmpty) {
String text = widget.message.text;
widget.message.mentionedUsers?.forEach((u) {
text = text.replaceAll(u.name, '**${u.name}**');
});
column.children.addAll( column.children.addAll(
<Widget>[ <Widget>[
Column( Column(
@@ -393,7 +398,7 @@ class _MessageWidgetState extends State<MessageWidget>
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
constraints: BoxConstraints.loose(Size.fromWidth(300)), constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: MarkdownBody( child: MarkdownBody(
data: '${widget.message.text}', data: '${text}',
onTapLink: (link) { onTapLink: (link) {
_launchURL(link); _launchURL(link);
}, },
+22
View File
@@ -102,6 +102,28 @@ class StreamChannelState extends State<StreamChannel> {
}); });
} }
Future<void> queryMembersAndWatchers() async {
String firstMemberId;
if (channel.state.members?.isNotEmpty == true) {
firstMemberId = channel.state.members.first.userId;
}
String firstWatcherId;
if (channel.state.watchers?.isNotEmpty == true) {
firstWatcherId = channel.state.watchers.first.id;
}
await widget.channel.query(
membersPagination: PaginationParams(
lessThan: firstMemberId,
limit: 100,
),
watchersPagination: PaginationParams(
lessThan: firstWatcherId,
limit: 100,
),
);
}
@override @override
void dispose() { void dispose() {
_queryMessageController.close(); _queryMessageController.close();