diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
index c5f1e85b..a90d828e 100644
--- a/example/android/app/src/main/AndroidManifest.xml
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -8,7 +8,7 @@
CFBundleInfoDictionaryVersion
6.0
CFBundleName
- example
+ ChatSample
CFBundlePackageType
APPL
CFBundleShortVersionString
diff --git a/example/lib/main.dart b/example/lib/main.dart
index 667b1e0f..ca727cf7 100644
--- a/example/lib/main.dart
+++ b/example/lib/main.dart
@@ -98,7 +98,9 @@ class MyApp extends StatelessWidget {
return MaterialApp(
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
- themeMode: ThemeMode.system,
+ themeMode: ThemeMode.light,
+
+ ///TODO change to system once dark theme is implemented
builder: (context, widget) {
return StreamChat(
child: widget,
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index 842f471c..0cba8d30 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -1,6 +1,7 @@
name: example
description: A new Flutter project.
-version: 1.0.39+41
+
+version: 1.0.43+45
environment:
sdk: ">=2.2.2 <3.0.0"
@@ -18,7 +19,13 @@ dev_dependencies:
sdk: flutter
flutter_driver:
sdk: flutter
+ flutter_launcher_icons: ^0.8.0
test: any
flutter:
uses-material-design: true
+
+flutter_icons:
+ android: true
+ ios: true
+ image_path: "assets/icon.png"
diff --git a/lib/src/giphy_attachment.dart b/lib/src/giphy_attachment.dart
index f913b557..bfa21239 100644
--- a/lib/src/giphy_attachment.dart
+++ b/lib/src/giphy_attachment.dart
@@ -41,195 +41,211 @@ class GiphyAttachment extends StatelessWidget {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
- Card(
- clipBehavior: Clip.antiAlias,
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.only(
- topRight: Radius.circular(16.0),
- bottomRight: Radius.circular(0.0),
- topLeft: Radius.circular(16.0),
- bottomLeft: Radius.circular(16.0),
+ DecoratedBox(
+ decoration: BoxDecoration(),
+ child: Card(
+ elevation: 2,
+ clipBehavior: Clip.antiAlias,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.only(
+ topRight: Radius.circular(16.0),
+ bottomRight: Radius.circular(0.0),
+ topLeft: Radius.circular(16.0),
+ bottomLeft: Radius.circular(16.0),
+ ),
),
- ),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Padding(
- padding: const EdgeInsets.all(8.0),
- child: GestureDetector(
- onTap: () {
- Navigator.push(context, MaterialPageRoute(builder: (_) {
- return FullScreenImage(
- url: attachment.imageUrl ??
- attachment.assetUrl ??
- attachment.thumbUrl,
- );
- }));
- },
- child: CachedNetworkImage(
- height: size?.height,
- width: size?.width,
- placeholder: (_, __) {
- return Container(
- width: size?.width,
- height: size?.height,
- child: Center(
- child: CircularProgressIndicator(),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Stack(
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: GestureDetector(
+ onTap: () {
+ Navigator.push(context,
+ MaterialPageRoute(builder: (_) {
+ return FullScreenImage(
+ url: attachment.imageUrl ??
+ attachment.assetUrl ??
+ attachment.thumbUrl,
+ );
+ }));
+ },
+ child: CachedNetworkImage(
+ height: size?.height,
+ width: size?.width,
+ placeholder: (_, __) {
+ return Container(
+ width: size?.width,
+ height: size?.height,
+ child: Center(
+ child: CircularProgressIndicator(),
+ ),
+ );
+ },
+ imageUrl: attachment.thumbUrl ??
+ attachment.imageUrl ??
+ attachment.assetUrl,
+ errorWidget: (context, url, error) => AttachmentError(
+ attachment: attachment,
+ size: size,
+ ),
+ fit: BoxFit.cover,
),
- );
- },
- imageUrl: attachment.thumbUrl ??
- attachment.imageUrl ??
- attachment.assetUrl,
- errorWidget: (context, url, error) => AttachmentError(
- attachment: attachment,
- size: size,
+ ),
),
- fit: BoxFit.cover,
- ),
- ),
- ),
- Positioned(
- left: 0,
- top: 0,
- child: Container(
- decoration: BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.only(
- bottomRight: Radius.circular(16.0),
- )),
- child: Padding(
- padding: const EdgeInsets.only(
- left: 8.0,
- right: 8.0,
- top: 8.0,
- bottom: 4.0,
- ),
- child: Row(
- children: [
- Icon(
- StreamIcons.lightning,
- color: StreamChatTheme.of(context).accentColor,
- size: 16.0,
- ),
- Text(
- 'GIPHY',
- style: TextStyle(
- color: StreamChatTheme.of(context).accentColor,
- fontWeight: FontWeight.bold,
- fontSize: 11.0,
+ Positioned(
+ left: 0,
+ top: 0,
+ child: Container(
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.only(
+ bottomRight: Radius.circular(16.0),
+ )),
+ child: Padding(
+ padding: const EdgeInsets.only(
+ left: 8.0,
+ right: 8.0,
+ top: 8.0,
+ bottom: 4.0,
+ ),
+ child: Row(
+ children: [
+ Icon(
+ StreamIcons.lightning,
+ color: StreamChatTheme.of(context).accentColor,
+ size: 16.0,
+ ),
+ Text(
+ 'GIPHY',
+ style: TextStyle(
+ color:
+ StreamChatTheme.of(context).accentColor,
+ fontWeight: FontWeight.bold,
+ fontSize: 11.0,
+ ),
+ ),
+ ],
),
),
- ],
+ ),
),
- ),
+ ],
),
- ),
- if (attachment.title != null)
- Container(
- alignment: Alignment.bottomCenter,
- child: Material(
- color: Colors.white,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Card(
- child: IconButton(
- icon: Icon(
- StreamIcons.left,
- size: 24.0,
+ if (attachment.title != null)
+ Container(
+ alignment: Alignment.bottomCenter,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Card(
+ elevation: 2,
+ child: IconButton(
+ padding: const EdgeInsets.all(0),
+ constraints: BoxConstraints.tight(Size(32, 32)),
+ icon: Icon(
+ StreamIcons.left,
+ size: 24.0,
+ ),
+ splashRadius: 16,
+ onPressed: () {
+ streamChannel.channel.sendAction(message, {
+ 'image_action': 'shuffle',
+ });
+ },
),
- splashRadius: 24,
- onPressed: () {
- streamChannel.channel.sendAction(message, {
- 'image_action': 'shuffle',
- });
- },
+ shape: CircleBorder(),
),
- shape: CircleBorder(),
- ),
- Expanded(
- child: Center(
- child: Text(
- '"${attachment.title}"',
- style: TextStyle(
- fontStyle: FontStyle.italic,
+ Expanded(
+ child: Center(
+ child: Text(
+ '"${attachment.title}"',
+ style: TextStyle(
+ fontStyle: FontStyle.italic,
+ ),
),
),
),
- ),
- Card(
- child: IconButton(
- icon: Icon(
- StreamIcons.right,
- size: 24.0,
+ Card(
+ elevation: 2,
+ child: IconButton(
+ padding: const EdgeInsets.all(0),
+ constraints: BoxConstraints.tight(Size(32, 32)),
+ icon: Icon(
+ StreamIcons.right,
+ size: 24.0,
+ ),
+ splashRadius: 16,
+ onPressed: () {
+ streamChannel.channel.sendAction(message, {
+ 'image_action': 'shuffle',
+ });
+ },
),
- splashRadius: 24,
- onPressed: () {
- streamChannel.channel.sendAction(message, {
- 'image_action': 'shuffle',
- });
- },
+ shape: CircleBorder(),
),
- shape: CircleBorder(),
- ),
- ],
+ ],
+ ),
),
),
+ SizedBox(
+ height: 4.0,
),
- SizedBox(
- height: 4.0,
- ),
- Container(
- color: Colors.black.withOpacity(0.2),
- width: double.infinity,
- height: 0.5,
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.start,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Expanded(
- child: FlatButton(
- height: 50,
- onPressed: () {
- streamChannel.channel.sendAction(message, {
- 'image_action': 'cancel',
- });
- },
- child: Text(
- 'Cancel',
- style: TextStyle(
- fontWeight: FontWeight.bold,
- color: Colors.black.withOpacity(0.5)),
+ Container(
+ color: Colors.black.withOpacity(0.2),
+ width: double.infinity,
+ height: 0.5,
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Expanded(
+ child: FlatButton(
+ height: 50,
+ onPressed: () {
+ streamChannel.channel.sendAction(message, {
+ 'image_action': 'cancel',
+ });
+ },
+ child: Text(
+ 'Cancel',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ color: Colors.black.withOpacity(0.5)),
+ ),
),
),
- ),
- Container(
- width: 0.5,
- color: Colors.black.withOpacity(0.2),
- height: 50.0,
- ),
- Expanded(
- child: FlatButton(
- height: 50,
- onPressed: () {
- streamChannel.channel.sendAction(message, {
- 'image_action': 'send',
- });
- },
- child: Text(
- 'Send',
- style: TextStyle(
- color: StreamChatTheme.of(context).accentColor,
- fontWeight: FontWeight.bold),
+ Container(
+ width: 0.5,
+ color: Colors.black.withOpacity(0.2),
+ height: 50.0,
+ ),
+ Expanded(
+ child: FlatButton(
+ height: 50,
+ onPressed: () {
+ streamChannel.channel.sendAction(message, {
+ 'image_action': 'send',
+ });
+ },
+ child: Text(
+ 'Send',
+ style: TextStyle(
+ color: StreamChatTheme.of(context).accentColor,
+ fontWeight: FontWeight.bold),
+ ),
),
),
- ),
- ],
- ),
- ],
+ ],
+ ),
+ ],
+ ),
),
),
SizedBox(
@@ -313,7 +329,10 @@ class GiphyAttachment extends StatelessWidget {
),
),
Padding(
- padding: const EdgeInsets.only(top: 8.0),
+ padding: const EdgeInsets.only(
+ top: 8.0,
+ bottom: 8,
+ ),
child: Row(
children: [
Row(
diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart
index 74ed535a..2ba93347 100644
--- a/lib/src/message_input.dart
+++ b/lib/src/message_input.dart
@@ -1,11 +1,13 @@
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';
import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
+import 'package:flutter_svg/flutter_svg.dart';
import 'package:http_parser/http_parser.dart';
import 'package:image_picker/image_picker.dart';
import 'package:mime/mime.dart';
@@ -14,6 +16,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 +171,8 @@ class MessageInputState extends State {
bool _messageIsPresent = false;
bool _typingStarted = false;
bool _commandEnabled = false;
- OverlayEntry _commandsOverlay, _mentionsOverlay;
+ OverlayEntry _commandsOverlay, _mentionsOverlay, _emojiOverlay;
+ Iterable _emojiNames;
Command _chosenCommand;
bool _actionsShrunk = false;
@@ -227,22 +231,66 @@ class MessageInputState extends State {
}
Widget _buildDmCheckbox() {
- return Row(
- children: [
- Checkbox(
- value: _sendAsDm,
- onChanged: (val) => setState(
- () {
- _sendAsDm = val;
- },
+ return Container(
+ height: 36,
+ padding: const EdgeInsets.only(
+ left: 12,
+ bottom: 12,
+ top: 8,
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Container(
+ height: 16,
+ width: 16,
+ foregroundDecoration: BoxDecoration(
+ border: _sendAsDm
+ ? null
+ : Border.all(
+ color: Colors.black.withOpacity(.5),
+ width: 2,
+ ),
+ borderRadius: BorderRadius.circular(3),
+ ),
+ child: Center(
+ child: Material(
+ borderRadius: BorderRadius.circular(3),
+ color: _sendAsDm
+ ? StreamChatTheme.of(context).accentColor
+ : Colors.white,
+ child: InkWell(
+ onTap: () {
+ setState(() {
+ _sendAsDm = !_sendAsDm;
+ });
+ },
+ child: AnimatedCrossFade(
+ duration: Duration(milliseconds: 300),
+ reverseDuration: Duration(milliseconds: 300),
+ crossFadeState: _sendAsDm
+ ? CrossFadeState.showFirst
+ : CrossFadeState.showSecond,
+ firstChild: Icon(
+ StreamIcons.check,
+ size: 16.0,
+ color: Colors.white,
+ ),
+ secondChild: SizedBox(
+ height: 16,
+ width: 16,
+ ),
+ ),
+ ),
+ ),
+ ),
),
- activeColor: StreamChatTheme.of(context).accentColor,
- ),
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8.0),
- child: Text('Send also as direct message'),
- ),
- ],
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Text('Send also as direct message'),
+ ),
+ ],
+ ),
);
}
@@ -326,42 +374,14 @@ class MessageInputState extends State {
_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(() {
@@ -427,8 +447,73 @@ class MessageInputState extends State {
);
}
+ 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
@@ -456,13 +541,6 @@ class MessageInputState extends State {
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,
borderRadius: BorderRadius.circular(8.0)),
child: ListView(
@@ -480,25 +558,19 @@ class MessageInputState extends State {
child: Icon(StreamIcons.lightning,
color: StreamChatTheme.of(context).accentColor),
),
- Text('Instant Commands')
+ Text(
+ 'Instant Commands',
+ style: TextStyle(
+ color: Colors.black.withOpacity(.5),
+ ),
+ )
],
),
),
...commands
.map(
(c) => ListTile(
- leading: c.name == 'giphy'
- ? CircleAvatar(
- backgroundColor: Colors.black,
- child: Image.asset(
- 'images/giphy_icon.png',
- package: 'stream_chat_flutter',
- width: 16.0,
- height: 16.0,
- ),
- maxRadius: 12.0,
- )
- : null,
+ leading: c.name == 'giphy' ? _buildGiphyIcon() : null,
title: Text.rich(
TextSpan(
text: '${c.name.capitalize()}',
@@ -641,12 +713,12 @@ class MessageInputState extends State {
case 0:
return GridView.builder(
itemCount: _mediaData.item2.length,
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 3),
+ gridDelegate:
+ SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3),
itemBuilder: (context, position) {
return Padding(
- padding: const EdgeInsets.symmetric(
- horizontal: 1.0, vertical: 1.0),
+ padding:
+ const EdgeInsets.symmetric(horizontal: 1.0, vertical: 1.0),
child: InkWell(
child: Stack(
children: [
@@ -655,13 +727,13 @@ class MessageInputState extends State {
child: Image.memory(
_mediaData.item2[position],
fit: _mediaData.item1[position].height >
- _mediaData.item1[position].width
+ _mediaData.item1[position].width
? BoxFit.fitWidth
: BoxFit.fitHeight,
),
),
if (_attachments.any((element) =>
- element.id == _mediaData.item1[position].id))
+ element.id == _mediaData.item1[position].id))
Container(
color: Colors.black.withOpacity(0.5),
child: Center(
@@ -676,11 +748,11 @@ class MessageInputState extends State {
),
onTap: () {
if (!_attachments.any((element) =>
- element.id == _mediaData.item1[position].id)) {
+ element.id == _mediaData.item1[position].id)) {
_addAttachment(_mediaData.item1[position]);
} else {
_attachments.removeWhere((element) =>
- element.id == _mediaData.item1[position].id);
+ element.id == _mediaData.item1[position].id);
setState(() {});
}
},
@@ -775,10 +847,36 @@ class MessageInputState extends State {
}
setState(() {
- _mediaData = Tuple2, List>(resultList, resultThumbnailList);
+ _mediaData =
+ Tuple2, List>(resultList, resultThumbnailList);
});
}
+ CircleAvatar _buildGiphyIcon() {
+ if (kIsWeb) {
+ return CircleAvatar(
+ backgroundColor: Colors.black,
+ child: Image.asset(
+ 'images/giphy_icon.png',
+ package: 'stream_chat_flutter',
+ width: 24.0,
+ height: 24.0,
+ ),
+ radius: 12,
+ );
+ } else {
+ return CircleAvatar(
+ child: SvgPicture.asset(
+ 'svgs/giphy_icon.svg',
+ package: 'stream_chat_flutter',
+ width: 24.0,
+ height: 24.0,
+ ),
+ radius: 12,
+ );
+ }
+ }
+
OverlayEntry _buildMentionsOverlayEntry() {
final splits = textEditingController.text
.substring(0, textEditingController.value.selection.start)
@@ -819,13 +917,6 @@ class MessageInputState extends State {
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: FutureBuilder>(
@@ -838,6 +929,12 @@ class MessageInputState extends State {
children: snapshot.data
.map((m) => ListTile(
leading: UserAvatar(
+ constraints: BoxConstraints.tight(
+ Size(
+ 40,
+ 40,
+ ),
+ ),
user: m.user,
),
title: Text(
@@ -878,6 +975,125 @@ class MessageInputState extends State {
});
}
+ 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 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(() {
@@ -1433,11 +1649,13 @@ class MessageInputState extends State {
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);
@@ -1453,6 +1671,15 @@ class MessageInputState extends State {
});
}
}
+
+ if (_emojiOverlay != null) {
+ if (textEditingController.text.contains(':')) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _emojiOverlay = _buildEmojiOverlay();
+ Overlay.of(context).insert(_emojiOverlay);
+ });
+ }
+ }
} else {
if (_commandsOverlay != null) {
_commandsOverlay.remove();
@@ -1460,6 +1687,9 @@ class MessageInputState extends State {
if (_mentionsOverlay != null) {
_mentionsOverlay.remove();
}
+ if (_emojiOverlay != null) {
+ _emojiOverlay.remove();
+ }
}
});
}
@@ -1494,6 +1724,7 @@ class MessageInputState extends State {
@override
void dispose() {
_commandsOverlay?.remove();
+ _emojiOverlay?.remove();
_mentionsOverlay?.remove();
_keyboardListener?.cancel();
super.dispose();
diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart
index bb0f98c0..946ad66f 100644
--- a/lib/src/message_list_view.dart
+++ b/lib/src/message_list_view.dart
@@ -1,12 +1,13 @@
import 'dart:async';
+import 'dart:math';
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
+import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/src/message_widget.dart';
import 'package:stream_chat_flutter/src/system_message.dart';
import 'package:visibility_detector/visibility_detector.dart';
-import 'package:widgets_visibility_provider/widgets_visibility_provider.dart';
import '../stream_chat_flutter.dart';
import 'date_divider.dart';
@@ -106,7 +107,10 @@ class MessageListView extends StatefulWidget {
this.onThreadTap,
this.dateDividerBuilder,
this.scrollPhysics = const AlwaysScrollableScrollPhysics(),
- this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
+ this.initialScrollIndex = 0,
+ this.initialAlignment = 0,
+ this.scrollController,
+ this.itemPositionListener,
}) : super(key: key);
/// Function used to build a custom message widget
@@ -131,95 +135,101 @@ class MessageListView extends StatefulWidget {
/// Builder used to render date dividers
final Widget Function(DateTime) dateDividerBuilder;
+ /// Index of an item to initially align within the viewport.
+ final int initialScrollIndex;
+
+ /// Determines where the leading edge of the item at [initialScrollIndex]
+ /// should be placed.
+ final double initialAlignment;
+
+ /// Controller for jumping or scrolling to an item.
+ final ItemScrollController scrollController;
+
+ /// Provides a listenable iterable of [itemPositions] of items that are on
+ /// screen and their locations.
+ final ItemPositionsListener itemPositionListener;
+
/// The ScrollPhysics used by the ListView
final ScrollPhysics scrollPhysics;
- /// The [ScrollViewKeyboardDismissBehavior] used by the ListView
- final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
-
@override
_MessageListViewState createState() => _MessageListViewState();
}
class _MessageListViewState extends State {
- static const _newMessageLoadingOffset = 100;
- final ScrollController _scrollController = ScrollController();
- bool _bottomWasVisible = true;
+ ItemScrollController _scrollController;
+ bool _bottomWasVisible = false;
bool _topWasVisible = false;
- List _messages = [];
- List _newMessageList = [];
Function _onThreadTap;
bool _showScrollToBottom = false;
+ ItemPositionsListener _itemPositionListener;
@override
Widget build(BuildContext context) {
final streamChannel = StreamChannel.of(context);
- /// TODO: find a better solution when (https://github.com/flutter/flutter/issues/21023) is fixed
- return WidgetsVisibilityProvider(
- condition: (positionData) =>
- positionData.endPosition >= 20 &&
- positionData.startPosition <= positionData.viewportSize,
- child: Stack(
- alignment: Alignment.center,
- children: [
- NotificationListener(
- onNotification: (_) {
- if (_scrollController.offset < 150 &&
- _newMessageList.isNotEmpty) {
- setState(() {
- _messages.insertAll(0, _newMessageList);
- _newMessageList.clear();
- });
- }
- return true;
- },
- child: ListView.custom(
- key: Key('messageListView'),
- physics: widget.scrollPhysics,
- keyboardDismissBehavior: widget.keyboardDismissBehavior,
- controller: _scrollController,
- reverse: true,
- childrenDelegate: SliverChildBuilderDelegate(
- (context, i) {
- if (i == _messages.length + 1) {
- if (widget.parentMessage != null) {
- if (widget.parentMessageBuilder != null) {
- return widget.parentMessageBuilder(
- context,
- widget.parentMessage,
- );
- } else {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- buildParentMessage(widget.parentMessage),
- Padding(
- padding:
- const EdgeInsets.symmetric(horizontal: 32),
- child: Container(
- padding: const EdgeInsets.all(8),
- child: Text(
- 'Start of thread',
- textAlign: TextAlign.center,
- ),
- color:
- Theme.of(context).accentColor.withAlpha(50),
- ),
- ),
- ],
- );
- }
+ final messagesStream = widget.parentMessage != null
+ ? streamChannel.channel.state.threadsStream
+ .where((threads) => threads.containsKey(widget.parentMessage.id))
+ .map((threads) => threads[widget.parentMessage.id])
+ : streamChannel.channel.state.messagesStream;
+
+ return StreamBuilder>(
+ stream: messagesStream,
+ initialData: widget.parentMessage != null
+ ? streamChannel.channel.state.threads[widget.parentMessage.id]
+ : streamChannel.channel.state.messages,
+ builder: (context, snapshot) {
+ final messages = snapshot.data?.reversed?.toList() ?? [];
+ return Stack(
+ alignment: Alignment.center,
+ children: [
+ ScrollablePositionedList.builder(
+ itemPositionsListener: _itemPositionListener,
+ addAutomaticKeepAlives: true,
+ key: Key('messageListView'),
+ initialScrollIndex: widget.initialScrollIndex,
+ initialAlignment: widget.initialAlignment,
+ physics: widget.scrollPhysics,
+ itemScrollController: _scrollController,
+ reverse: true,
+ itemCount: messages.length +
+ 1 +
+ (widget.parentMessage != null ? 1 : 0),
+ itemBuilder: (context, i) {
+ if (i == messages.length + 1) {
+ if (widget.parentMessageBuilder != null) {
+ return widget.parentMessageBuilder(
+ context,
+ widget.parentMessage,
+ );
} else {
- return SizedBox();
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ buildParentMessage(widget.parentMessage),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: Container(
+ padding: const EdgeInsets.all(8),
+ child: Text(
+ 'Start of thread',
+ textAlign: TextAlign.center,
+ ),
+ color:
+ Theme.of(context).accentColor.withAlpha(50),
+ ),
+ ),
+ ],
+ );
}
}
- if (i == _messages.length) {
+ if (i == messages.length) {
return _buildLoadingIndicator(streamChannel);
}
- final message = _messages[i];
- final nextMessage = i > 0 ? _messages[i - 1] : null;
+ final message = messages[i];
+ final nextMessage = i > 0 ? messages[i - 1] : null;
Widget messageWidget;
@@ -227,14 +237,14 @@ class _MessageListViewState extends State {
messageWidget = _buildBottomMessage(
context,
message,
- _messages,
+ messages,
streamChannel,
);
- } else if (i == _messages.length - 1) {
+ } else if (i == messages.length - 1) {
messageWidget = _buildTopMessage(
context,
message,
- _messages,
+ messages,
streamChannel,
);
} else {
@@ -246,13 +256,13 @@ class _MessageListViewState extends State {
MessageDetails(
context,
message,
- _messages,
+ messages,
i,
),
- _messages),
+ messages),
);
} else {
- messageWidget = buildMessage(message, _messages, i);
+ messageWidget = buildMessage(message, messages, i);
}
}
@@ -265,84 +275,74 @@ class _MessageListViewState extends State {
messageWidget,
Padding(
padding: const EdgeInsets.symmetric(vertical: 12.0),
- child: VisibleNotifierWidget(
- condition: (
- ScrollNotification previousNotification,
- PositionData previousPositionData,
- ScrollNotification currentNotification,
- PositionData currentPositionData,
- ) {
- print(
- 'currentPositionData.endPosition: ${currentPositionData.endPosition}');
- return true;
- },
- data: i,
- child: widget.dateDividerBuilder != null
- ? widget.dateDividerBuilder(
- nextMessage.createdAt.toLocal())
- : DateDivider(
- dateTime: nextMessage.createdAt.toLocal(),
- ),
- ),
+ child: widget.dateDividerBuilder != null
+ ? widget.dateDividerBuilder(
+ nextMessage.createdAt.toLocal())
+ : DateDivider(
+ dateTime: nextMessage.createdAt.toLocal(),
+ ),
),
],
);
}
- return VisibleNotifierWidget(
- child: messageWidget,
- data: i,
- );
- },
- childCount: _messages.length + 2,
- findChildIndexCallback: (key) {
- final ValueKey valueKey = key;
- final index = _messages
- .indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value);
- return index != -1 ? index : null;
+ return messageWidget;
},
),
- ),
- ),
- if (widget.showScrollToBottom)
- StreamBuilder(
- stream: streamChannel.channel.on(
- EventType.messageNew,
- ),
- builder: (context, _) {
- if (!_showScrollToBottom ||
- streamChannel.channel.state.unreadCount == 0) {
- return SizedBox();
- }
- return _buildScrollToBottom(streamChannel);
- }),
- Positioned(
- top: 20.0,
- child: WidgetsVisibilityConsumer(
- listener: (
- context,
- event,
- ) {},
- builder: (context, event) {
- if (event.positionDataList == null ||
- event.positionDataList.isEmpty) {
- return Container();
- }
+ if (widget.showScrollToBottom)
+ StreamBuilder(
+ stream: streamChannel.channel.state.unreadCountStream,
+ builder: (context, snapshot) {
+ if (!_showScrollToBottom ||
+ !snapshot.hasData ||
+ snapshot.data == 0) {
+ return SizedBox();
+ }
+ return _buildScrollToBottom(snapshot.data);
+ }),
+ Positioned(
+ top: 20.0,
+ child: ValueListenableBuilder>(
+ valueListenable: _itemPositionListener.itemPositions,
+ builder: (context, values, _) {
+ final items = _itemPositionListener.itemPositions?.value;
+ if (items.isEmpty || messages.isEmpty) {
+ return SizedBox();
+ }
- return DateDivider(
- dateTime: _messages[event.positionDataList.last.data]
- .createdAt
- .toLocal(),
- );
- },
- ),
- ),
- ],
- ),
- );
+ var index = _getTopElement(values).index;
+
+ if (index > messages.length) {
+ return SizedBox();
+ }
+
+ if (index == messages.length) {
+ index = max(index - 1, 0);
+ }
+
+ return widget.dateDividerBuilder != null
+ ? widget.dateDividerBuilder(
+ messages[index].createdAt.toLocal(),
+ )
+ : DateDivider(
+ dateTime: messages[index].createdAt.toLocal(),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ });
}
- Widget _buildScrollToBottom(StreamChannelState streamChannel) {
+ ItemPosition _getTopElement(Iterable values) {
+ return values
+ .where((ItemPosition position) => position.itemLeadingEdge < 0.9)
+ .reduce((ItemPosition max, ItemPosition position) =>
+ position.itemLeadingEdge > max.itemLeadingEdge ? position : max);
+ }
+
+ Widget _buildScrollToBottom(int unreadCount) {
return Positioned(
bottom: 8,
right: 8,
@@ -358,13 +358,11 @@ class _MessageListViewState extends State {
color: Colors.black,
),
onPressed: () {
- Future.delayed(Duration(milliseconds: 500), () {
- setState(() {
- _showScrollToBottom = false;
- });
+ setState(() {
+ _showScrollToBottom = false;
});
- _scrollController.animateTo(
- 0,
+ _scrollController.scrollTo(
+ index: 0,
duration: Duration(seconds: 1),
curve: Curves.easeInOut,
);
@@ -377,7 +375,7 @@ class _MessageListViewState extends State {
top: -10,
child: CircleAvatar(
radius: 20,
- child: Text(streamChannel.channel.state.unreadCount.toString()),
+ child: Text(unreadCount.toString()),
),
),
],
@@ -387,7 +385,9 @@ class _MessageListViewState extends State {
Container _buildLoadingIndicator(StreamChannelState streamChannel) {
return Container(
+ key: Key('LOADING-INDICATOR'),
height: 50,
+ width: double.infinity,
child: StreamBuilder(
stream: streamChannel.queryMessage,
initialData: false,
@@ -401,7 +401,7 @@ class _MessageListViewState extends State {
);
}
if (!snapshot.data) {
- return Container();
+ return SizedBox();
}
return Center(
child: Padding(
@@ -428,14 +428,14 @@ class _MessageListViewState extends State {
MessageDetails(
context,
message,
- _messages,
- _messages.length - 1,
+ messages,
+ messages.length - 1,
),
- _messages,
+ messages,
),
);
} else {
- messageWidget = buildMessage(message, messages, _messages.length - 1);
+ messageWidget = buildMessage(message, messages, messages.length - 1);
}
return VisibilityDetector(
@@ -470,10 +470,10 @@ class _MessageListViewState extends State {
MessageDetails(
context,
message,
- _messages,
+ messages,
0,
),
- _messages,
+ messages,
),
);
} else {
@@ -492,9 +492,11 @@ class _MessageListViewState extends State {
}
}
_bottomWasVisible = isVisible;
- setState(() {
- _showScrollToBottom = !isVisible;
- });
+ if (mounted) {
+ setState(() {
+ _showScrollToBottom = !isVisible;
+ });
+ }
},
child: messageWidget,
);
@@ -541,6 +543,7 @@ class _MessageListViewState extends State {
) {
if (message.type == 'system' && message.text?.isNotEmpty == true) {
return SystemMessage(
+ key: ValueKey('MESSAGE-${message.id}'),
message: message,
);
}
@@ -563,6 +566,7 @@ class _MessageListViewState extends State {
final allRead = readList.length >= (channel.memberCount ?? 0) - 1;
return MessageWidget(
+ key: ValueKey('MESSAGE-${message.id}'),
message: message,
reverse: isMyMessage,
showReactions: !message.isDeleted,
@@ -600,64 +604,45 @@ class _MessageListViewState extends State {
);
}
- StreamSubscription _streamListener;
+ StreamSubscription _messageNewListener;
@override
void initState() {
- super.initState();
+ _scrollController = widget.scrollController ?? ItemScrollController();
+ _itemPositionListener =
+ widget.itemPositionListener ?? ItemPositionsListener.create();
final streamChannel = StreamChannel.of(context);
- Stream> stream;
-
- if (widget.parentMessage == null) {
- stream = streamChannel.channel.state.messagesStream;
- } else {
- streamChannel.getReplies(widget.parentMessage.id);
- stream = streamChannel.channel.state.threadsStream
- .where((threads) => threads.containsKey(widget.parentMessage.id))
- .map((threads) => threads[widget.parentMessage.id]);
- }
-
- _streamListener = stream
- .map((messages) =>
- messages
- ?.where((m) =>
- !(m.status == MessageSendingStatus.FAILED && m.isDeleted))
- ?.toList() ??
- [])
- .listen((newMessages) {
- newMessages = newMessages.reversed.toList();
- if (_messages.isEmpty ||
- newMessages.isEmpty ||
- newMessages.first.id != _messages.first.id) {
- if (!_scrollController.hasClients ||
- _scrollController.offset < _newMessageLoadingOffset) {
- if (streamChannel.channel.state.unreadCount > 0) {
- streamChannel.channel.markRead();
- }
- setState(() {
- _messages = newMessages;
- });
- } else if (newMessages.first.user.id ==
- streamChannel.channel.client.state.user.id) {
- _scrollController.jumpTo(0);
- WidgetsBinding.instance.addPostFrameCallback((_) {
- setState(() {
- _messages = newMessages;
- });
- });
- } else {
- _newMessageList = newMessages;
- }
- } else {
- setState(() {
- _messages = newMessages;
+ _messageNewListener =
+ streamChannel.channel.on(EventType.messageNew).listen((event) {
+ final firstElementInViewport =
+ _itemPositionListener.itemPositions.value.first;
+ if (event.message.user.id == streamChannel.channel.client.state.user.id) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _scrollController.jumpTo(
+ index: 0,
+ );
});
+ } else {
+ if (firstElementInViewport.index != 0) {
+ _scrollController.jumpTo(
+ index: firstElementInViewport.index + 1,
+ alignment: firstElementInViewport.itemLeadingEdge,
+ );
+ }
+ }
+ if (firstElementInViewport.index == 0) {
+ streamChannel.channel.markRead();
}
});
+ if (widget.parentMessage != null) {
+ streamChannel.getReplies(widget.parentMessage.id);
+ }
+
_getOnThreadTap();
+ super.initState();
}
void _getOnThreadTap() {
@@ -696,7 +681,7 @@ class _MessageListViewState extends State {
@override
void dispose() {
- _streamListener.cancel();
+ _messageNewListener?.cancel();
super.dispose();
}
}
diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart
index ad574fc8..476217f3 100644
--- a/lib/src/message_widget.dart
+++ b/lib/src/message_widget.dart
@@ -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 {
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 {
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,
- );
- }
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 70b8188a..6b2aeff1 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -14,7 +14,9 @@ dependencies:
flutter_app_badger: ^1.1.2
photo_view: ^0.10.1
rxdart: ^0.24.1
+ scrollable_positioned_list: ^0.1.8
jiffy: ^3.0.1
+ flutter_svg: ^0.19.1
flutter_portal: ^0.3.0
cached_network_image: ^2.2.0+1
flutter_markdown: ^0.5.0
@@ -25,18 +27,20 @@ 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
- widgets_visibility_provider: ^2.0.2
photo_gallery: ^0.3.0
flutter:
assets:
- - images/giphy_icon.png
+ - images/
+ - svgs/
fonts:
- family: stream-icons
fonts:
diff --git a/svgs/giphy_icon.svg b/svgs/giphy_icon.svg
new file mode 100644
index 00000000..e1a1d4bb
--- /dev/null
+++ b/svgs/giphy_icon.svg
@@ -0,0 +1,8 @@
+