highlight mentions
This commit is contained in:
+118
-11
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user