Merge pull request #123 from GetStream/feature/emojiPicker

add emoji picker
This commit is contained in:
Deven Joshi
2020-11-06 16:56:50 +05:30
committed by GitHub
4 changed files with 270 additions and 106 deletions
+1
View File
@@ -1,5 +1,6 @@
name: example
description: A new Flutter project.
version: 1.0.41+43
environment:
+208 -68
View File
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:math';
import 'package:emojis/emoji.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
@@ -14,6 +15,7 @@ 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 'package:substring_highlight/substring_highlight.dart';
import '../stream_chat_flutter.dart';
import 'stream_channel.dart';
@@ -168,7 +170,8 @@ class MessageInputState extends State<MessageInput> {
bool _messageIsPresent = false;
bool _typingStarted = false;
bool _commandEnabled = false;
OverlayEntry _commandsOverlay, _mentionsOverlay;
OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay;
Iterable<String> _emojiNames;
Command _chosenCommand;
bool _actionsShrunk = false;
@@ -364,42 +367,14 @@ class MessageInputState extends State<MessageInput> {
_commandsOverlay = null;
_mentionsOverlay?.remove();
_mentionsOverlay = null;
_emojiOverlay?.remove();
_emojiOverlay = null;
if (s.startsWith('/')) {
var matchedCommandsList = StreamChannel.of(context)
.channel
.config
.commands
.where((element) => element.name == s.substring(1))
.toList();
_checkCommands(s.trimLeft(), context);
if (matchedCommandsList.length == 1) {
_chosenCommand = matchedCommandsList[0];
textEditingController.clear();
_messageIsPresent = false;
setState(() {
_commandEnabled = true;
});
_commandsOverlay?.remove();
_commandsOverlay = null;
} else {
_commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
}
}
_checkMentions(s, context);
if (textEditingController.selection.isCollapsed &&
s.isNotEmpty &&
(s[textEditingController.selection.start - 1] == '@' ||
textEditingController.text
.substring(
0, textEditingController.selection.start)
.split(' ')
.last
.contains('@'))) {
_mentionsOverlay = _buildMentionsOverlayEntry();
Overlay.of(context).insert(_mentionsOverlay);
}
_checkEmoji(s, context);
},
onTap: () {
setState(() {
@@ -465,42 +440,73 @@ class MessageInputState extends State<MessageInput> {
);
}
Positioned _buildBorder(BuildContext context) {
return Positioned.fill(
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
gradient: _getGradient(context),
),
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context)
.channelTheme
.inputBackground
.withAlpha(255),
borderRadius: BorderRadius.circular(10.0),
),
child: Container(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).channelTheme.inputBackground,
borderRadius: BorderRadius.circular(10.0),
border: Border.all(
color: _typingStarted
? Colors.transparent
: Theme.of(context).brightness == Brightness.dark
? Colors.white.withOpacity(.2)
: Colors.black.withOpacity(.2)),
),
),
),
),
);
void _checkEmoji(String s, BuildContext context) {
if (textEditingController.selection.isCollapsed &&
(s.isNotEmpty && s[textEditingController.selection.start - 1] == ':' ||
textEditingController.text
.substring(
0,
textEditingController.selection.start,
)
.contains(':'))) {
final textToSelection = textEditingController.text
.substring(0, textEditingController.value.selection.start);
final splits = textToSelection.split(':');
final query = splits[splits.length - 2]?.toLowerCase();
final emoji = Emoji.byName(query);
if (textToSelection.endsWith(':') && emoji != null) {
_chooseEmoji(splits.sublist(0, splits.length - 1), emoji);
} else {
_emojiOverlay = _buildEmojiOverlay();
if (_emojiOverlay != null) {
Overlay.of(context).insert(_emojiOverlay);
}
}
}
}
void _checkMentions(String s, BuildContext context) {
if (textEditingController.selection.isCollapsed &&
(s.isNotEmpty && s[textEditingController.selection.start - 1] == '@' ||
textEditingController.text
.substring(0, textEditingController.selection.start)
.split(' ')
.last
.contains('@'))) {
_mentionsOverlay = _buildMentionsOverlayEntry();
Overlay.of(context).insert(_mentionsOverlay);
}
}
void _checkCommands(String s, BuildContext context) {
if (s.startsWith('/')) {
var matchedCommandsList = StreamChannel.of(context)
.channel
.config
.commands
.where((element) => element.name == s.substring(1))
.toList();
if (matchedCommandsList.length == 1) {
_chosenCommand = matchedCommandsList[0];
textEditingController.clear();
_messageIsPresent = false;
setState(() {
_commandEnabled = true;
});
_commandsOverlay.remove();
_commandsOverlay = null;
} else {
_commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
}
}
}
OverlayEntry _buildCommandsOverlayEntry() {
final text = textEditingController.text;
final text = textEditingController.text.trimLeft();
final commands = StreamChannel.of(context)
.channel
.config
@@ -721,6 +727,125 @@ class MessageInputState extends State<MessageInput> {
});
}
OverlayEntry _buildEmojiOverlay() {
final splits = textEditingController.text
.substring(0, textEditingController.value.selection.start)
.split(':');
final query = splits.last.toLowerCase();
if (query.isEmpty) {
return null;
}
final emojis = _emojiNames
.where((e) => e.contains(query))
.map((e) => Emoji.byName(e))
.where((e) => e != null);
if (emojis.isEmpty) {
return null;
}
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: Card(
margin: EdgeInsets.all(8.0),
elevation: 2.0,
color: StreamChatTheme.of(context).primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
clipBehavior: Clip.antiAlias,
child: Container(
constraints: BoxConstraints.loose(Size.fromHeight(200)),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
spreadRadius: -8,
blurRadius: 5.0,
offset: Offset(0, -4),
),
],
color: StreamChatTheme.of(context).primaryColor,
),
child: ListView.builder(
padding: const EdgeInsets.all(0),
shrinkWrap: true,
itemCount: emojis.length + 1,
itemBuilder: (context, i) {
if (i == 0) {
return Padding(
padding: const EdgeInsets.only(left: 8.0, top: 8.0),
child: Row(
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0),
child: Icon(
StreamIcons.smile,
color: StreamChatTheme.of(context).accentColor,
),
),
Flexible(
child: Text(
'Emoji matching "$query"',
style: TextStyle(
color: Colors.black.withOpacity(.5),
),
),
)
],
),
);
}
final emoji = emojis.elementAt(i - 1);
return ListTile(
title: SubstringHighlight(
text: "${emoji.char} ${emoji.name.replaceAll('_', ' ')}",
term: query,
textStyleHighlight:
Theme.of(context).textTheme.headline6.copyWith(
fontSize: 14.5,
fontWeight: FontWeight.bold,
),
textStyle: Theme.of(context).textTheme.headline6.copyWith(
fontSize: 14.5,
),
),
onTap: () {
_chooseEmoji(splits, emoji);
},
);
}),
),
),
);
});
}
void _chooseEmoji(List<String> splits, Emoji emoji) {
final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char;
textEditingController.value = TextEditingValue(
text: rejoin +
textEditingController.text
.substring(textEditingController.selection.start),
selection: TextSelection.collapsed(
offset: rejoin.length,
),
);
_emojiOverlay?.remove();
_emojiOverlay = null;
}
void _setCommand(Command c) {
textEditingController.clear();
setState(() {
@@ -1275,11 +1400,13 @@ class MessageInputState extends State<MessageInput> {
void initState() {
super.initState();
_emojiNames = Emoji.all().map((e) => e.name);
if (!kIsWeb) {
_keyboardListener = KeyboardVisibility.onChange.listen((visible) {
if (visible) {
if (_commandsOverlay != null) {
if (textEditingController.text.startsWith('/')) {
if (textEditingController.text.trimLeft().startsWith('/')) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_commandsOverlay = _buildCommandsOverlayEntry();
Overlay.of(context).insert(_commandsOverlay);
@@ -1295,6 +1422,15 @@ class MessageInputState extends State<MessageInput> {
});
}
}
if (_emojiOverlay != null) {
if (textEditingController.text.contains(':')) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_emojiOverlay = _buildEmojiOverlay();
Overlay.of(context).insert(_emojiOverlay);
});
}
}
} else {
if (_commandsOverlay != null) {
_commandsOverlay.remove();
@@ -1302,6 +1438,9 @@ class MessageInputState extends State<MessageInput> {
if (_mentionsOverlay != null) {
_mentionsOverlay.remove();
}
if (_emojiOverlay != null) {
_emojiOverlay.remove();
}
}
});
}
@@ -1330,6 +1469,7 @@ class MessageInputState extends State<MessageInput> {
@override
void dispose() {
_commandsOverlay?.remove();
_emojiOverlay?.remove();
_mentionsOverlay?.remove();
_keyboardListener?.cancel();
super.dispose();
+59 -38
View File
@@ -1,6 +1,7 @@
import 'dart:math';
import 'dart:ui';
import 'package:emojis/emoji.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -729,37 +730,68 @@ class _MessageWidgetState extends State<MessageWidget> {
return SizedBox();
}
Widget _wrapTextInBubble({
BuildContext context,
Widget child,
}) {
return Material(
shape: widget.shape ??
RoundedRectangleBorder(
side: widget.borderSide ??
BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withAlpha(24)
: Colors.black.withAlpha(24),
),
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
),
color: _getBackgroundColor(),
child: child,
);
}
Widget _buildTextBubble(BuildContext context) {
final isOnlyEmoji =
widget.message.text.characters.every((c) => Emoji.byChar(c) != null);
Widget child = Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Padding(
padding: widget.textPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
getFailedMessageWidget(context),
widget.textBuilder != null
? widget.textBuilder(context, widget.message)
: MessageText(
onLinkTap: widget.onLinkTap,
message: widget.message,
onMentionTap: widget.onMentionTap,
messageTheme: isOnlyEmoji
? widget.messageTheme.copyWith(
messageText:
widget.messageTheme.messageText.copyWith(
fontSize: 40,
))
: widget.messageTheme,
),
],
),
),
);
if (!isOnlyEmoji) {
child = _wrapTextInBubble(
context: context,
child: child,
);
}
return GestureDetector(
onTap: () => retryMessage(context),
onLongPress: () => onLongPress(context),
child: Material(
shape: widget.shape ??
RoundedRectangleBorder(
side: widget.borderSide ??
BorderSide(
color: Theme.of(context).brightness == Brightness.dark
? Colors.white.withAlpha(24)
: Colors.black.withAlpha(24),
),
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
),
color: _getBackgroundColor(),
child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Padding(
padding: widget.textPadding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
getFailedMessageWidget(context),
_buildText(context),
],
),
),
),
),
child: child,
);
}
@@ -793,15 +825,4 @@ class _MessageWidgetState extends State<MessageWidget> {
return;
}
}
Widget _buildText(BuildContext context) {
return widget.textBuilder != null
? widget.textBuilder(context, widget.message)
: MessageText(
onLinkTap: widget.onLinkTap,
message: widget.message,
onMentionTap: widget.onMentionTap,
messageTheme: widget.messageTheme,
);
}
}
+2
View File
@@ -27,9 +27,11 @@ dependencies:
image_picker: ^0.6.7+2
flutter_keyboard_visibility: ^3.2.1
stream_chat: ^0.2.10+1
emojis: ^0.9.3
mime: ^0.9.6+3
visibility_detector: ^0.1.5
http_parser: ^3.1.4
substring_highlight: ^0.1.2
flutter_slidable: ^0.5.4
carousel_slider: ^2.2.1
clipboard: ^0.1.2+8