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_flutter/src/message_list_view.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_channel.dart';
@@ -78,11 +79,12 @@ class MessageInput extends StatefulWidget {
class _MessageInputState extends State<MessageInput> {
final List<_SendingAttachment> _attachments = [];
final _focusNode = FocusNode();
final List<User> _mentionedUsers = [];
TextEditingController _textController;
bool _messageIsPresent = false;
bool _typingStarted = false;
OverlayEntry _commandsOverlay;
OverlayEntry _commandsOverlay, _mentionsOverlay;
@override
Widget build(BuildContext context) {
@@ -149,11 +151,24 @@ class _MessageInputState extends State<MessageInput> {
_commandsOverlay?.remove();
_commandsOverlay = null;
_mentionsOverlay?.remove();
_mentionsOverlay = null;
if (s.startsWith('/')) {
_commandsOverlay = _buildOverlayEntry();
_commandsOverlay = _buildCommandsOverlayEntry();
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: () {
setState(() {
@@ -200,7 +215,7 @@ class _MessageInputState extends State<MessageInput> {
);
}
OverlayEntry _buildOverlayEntry() {
OverlayEntry _buildCommandsOverlayEntry() {
final text = _textController.text;
final commands = StreamChannel.of(context)
.channel
@@ -269,17 +284,88 @@ class _MessageInputState extends State<MessageInput> {
});
}
void _setCommand(Command c) {
setState(() {
_textController.value = TextEditingValue(
text: '/${c.name} ',
selection: TextSelection.fromPosition(
TextPosition(
offset: c.name.length + 2,
OverlayEntry _buildMentionsOverlayEntry() {
final splits = _textController.text
.substring(0, _textController.value.selection.start)
.split('@');
final query = splits.last.toLowerCase();
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 = null;
}
@@ -555,6 +641,8 @@ class _MessageInputState extends State<MessageInput> {
_commandsOverlay?.remove();
_commandsOverlay = null;
_mentionsOverlay?.remove();
_mentionsOverlay = null;
FocusScope.of(context).unfocus();
@@ -566,6 +654,8 @@ class _MessageInputState extends State<MessageInput> {
.where((attachment) => attachment.type == 'giphy')
.toList() +
_getAttachments(attachments).toList(),
mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
);
StreamChat.of(context).client.updateMessage(message).then((_) {
if (widget.onMessageSent != null) {
@@ -577,6 +667,8 @@ class _MessageInputState extends State<MessageInput> {
parentId: widget.parentMessage?.id,
text: text,
attachments: _getAttachments(attachments).toList(),
mentionedUsers:
_mentionedUsers.where((u) => text.contains('@${u.name}')).toList(),
);
StreamChannel.of(context).channel.sendMessage(message).then((_) {
if (widget.onMessageSent != null) {
@@ -613,21 +705,35 @@ class _MessageInputState extends State<MessageInput> {
void initState() {
super.initState();
StreamChannel.of(context).queryMembersAndWatchers();
_keyboardListener = KeyboardVisibilityNotification().addNewListener(
onHide: () {
if (_commandsOverlay != null) {
_commandsOverlay.remove();
}
if (_mentionsOverlay != null) {
_mentionsOverlay.remove();
}
},
onShow: () {
if (_commandsOverlay != null) {
if (_textController.text.startsWith('/')) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_commandsOverlay = _buildOverlayEntry();
_commandsOverlay = _buildCommandsOverlayEntry();
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
void dispose() {
_commandsOverlay?.remove();
_mentionsOverlay?.remove();
KeyboardVisibilityNotification().removeListener(_keyboardListener);
super.dispose();
}
+6 -1
View File
@@ -361,6 +361,11 @@ class _MessageWidgetState extends State<MessageWidget>
);
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(
<Widget>[
Column(
@@ -393,7 +398,7 @@ class _MessageWidgetState extends State<MessageWidget>
padding: EdgeInsets.all(10),
constraints: BoxConstraints.loose(Size.fromWidth(300)),
child: MarkdownBody(
data: '${widget.message.text}',
data: '${text}',
onTapLink: (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
void dispose() {
_queryMessageController.close();