Initial implementation

Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
Sahil Kumar
2020-12-22 21:45:14 +05:30
parent aa5433a881
commit 209c5a3c24
10 changed files with 359 additions and 313 deletions
+1
View File
@@ -470,6 +470,7 @@ class ThreadPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Color.fromRGBO(252, 252, 252, 1),
appBar: ThreadHeader(
parent: parent,
),
+1 -1
View File
@@ -113,7 +113,7 @@ class MessageActionsModal extends StatelessWidget {
messageTheme: messageTheme,
showReactions: false,
showUsername: false,
showReplyIndicator: false,
showThreadReplyIndicator: false,
showUserAvatar: showUserAvatar,
showTimestamp: false,
translateUserAvatar: false,
+22 -12
View File
@@ -241,7 +241,7 @@ class _MessageListViewState extends State<MessageListView> {
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
child: const CircularProgressIndicator(),
);
}
@@ -336,18 +336,25 @@ class _MessageListViewState extends State<MessageListView> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
buildParentMessage(widget.parentMessage),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 32),
child: Container(
padding: const EdgeInsets.all(8),
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0XFFF7F7F7),
Color(0XFFFCFCFC),
],
),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Start of thread',
'${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}',
textAlign: TextAlign.center,
style: StreamChatTheme.of(context)
.channelTheme
.channelHeaderTheme
.lastMessageAt,
),
color: Theme.of(context)
.accentColor
.withAlpha(50),
),
),
],
@@ -585,7 +592,7 @@ class _MessageListViewState extends State<MessageListView> {
if (!snapshot.data) {
if (direction == QueryDirection.top) {
return Container(
height: 50,
height: 52,
width: double.infinity,
);
}
@@ -679,7 +686,8 @@ class _MessageListViewState extends State<MessageListView> {
final isMyMessage = message.user.id == StreamChat.of(context).user.id;
return MessageWidget(
showReplyIndicator: false,
showThreadReplyIndicator: false,
showInChannelIndicator: false,
message: message,
reverse: isMyMessage,
showUsername: !isMyMessage,
@@ -748,6 +756,8 @@ class _MessageListViewState extends State<MessageListView> {
bottom: index == 0 ? 30 : (isNextUser ? 2 : 7),
top: 3,
),
showInChannelIndicator: widget.parentMessage == null,
showThreadReplyIndicator: widget.parentMessage == null,
showUsername: !isMyMessage && !isNextUser,
showSendingIndicator: isMyMessage &&
(index == 0 || message.status != MessageSendingStatus.SENT)
+1 -1
View File
@@ -105,7 +105,7 @@ class MessageReactionsModal extends StatelessWidget {
showReactions: false,
showUsername: false,
showUserAvatar: showUserAvatar,
showReplyIndicator: false,
showThreadReplyIndicator: false,
showTimestamp: false,
translateUserAvatar: false,
showSendingIndicator: DisplayWidget.gone,
+297 -238
View File
@@ -99,8 +99,11 @@ class MessageWidget extends StatefulWidget {
final bool allRead;
/// If true the widget will show the reply indicator
final bool showReplyIndicator;
/// If true the widget will show the thread reply indicator
final bool showThreadReplyIndicator;
/// If true the widget will show the show in channel indicator
final bool showInChannelIndicator;
/// The function called when tapping on UserAvatar
final void Function(User) onUserAvatarTap;
@@ -123,6 +126,7 @@ class MessageWidget extends StatefulWidget {
/// Center user avatar with bottom of the message
final bool translateUserAvatar;
///
MessageWidget({
Key key,
@required this.message,
@@ -139,7 +143,8 @@ class MessageWidget extends StatefulWidget {
this.showReactionPickerIndicator = false,
this.showUserAvatar = DisplayWidget.show,
this.showSendingIndicator = DisplayWidget.show,
this.showReplyIndicator = true,
this.showThreadReplyIndicator = true,
this.showInChannelIndicator = true,
this.onThreadTap,
this.showUsername = true,
this.showTimestamp = true,
@@ -211,10 +216,23 @@ class MessageWidget extends StatefulWidget {
}
class _MessageWidgetState extends State<MessageWidget> {
bool get showThreadReplyIndicator =>
widget.showThreadReplyIndicator && widget.message.replyCount > 0;
bool get showUsername => widget.showUsername;
bool get showTimeStamp =>
widget.message.createdAt != null && widget.showTimestamp;
bool get showReadList => widget.readList?.isNotEmpty == true;
bool get showInChannel =>
widget.showInChannelIndicator && widget.message?.showInChannel == true;
@override
Widget build(BuildContext context) {
var leftPadding = widget.showUserAvatar != DisplayWidget.gone
? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0
? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5
: 6.0;
final hasFiles =
@@ -234,140 +252,151 @@ class _MessageWidgetState extends State<MessageWidget> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
Stack(
alignment: AlignmentDirectional.bottomStart,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.show)
_buildUserAvatar(),
SizedBox(
width: 6,
),
if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(
width: widget.messageTheme.avatarTheme.constraints
.maxWidth +
8,
),
Flexible(
child: PortalEntry(
portal: Container(
transform: Matrix4.translationValues(-16, 2, 0),
child: _buildReactionIndicator(context),
constraints: BoxConstraints(maxWidth: 22 * 6.0),
),
portalAnchor: Alignment(-1.0, -1.0),
childAnchor: Alignment(1, -1.0),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: widget.message.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero,
child: (widget.message.isDeleted &&
widget.message.status !=
MessageSendingStatus
.FAILED_DELETE)
? Transform(
alignment: Alignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
if (widget.showUserAvatar == DisplayWidget.show)
_buildUserAvatar(),
SizedBox(width: 6),
if (widget.showUserAvatar == DisplayWidget.hide)
SizedBox(
width: widget.messageTheme.avatarTheme
.constraints.maxWidth +
8,
),
Flexible(
child: PortalEntry(
portal: Container(
transform:
Matrix4.translationValues(-16, 2, 0),
child: _buildReactionIndicator(context),
constraints:
BoxConstraints(maxWidth: 22 * 6.0),
),
portalAnchor: Alignment(-1.0, -1.0),
childAnchor: Alignment(1, -1.0),
child: Stack(
clipBehavior: Clip.none,
children: [
Padding(
padding: widget.showReactions
? EdgeInsets.only(
top: widget.message.reactionCounts
?.isNotEmpty ==
true
? 18
: 0,
)
: EdgeInsets.zero,
child: (widget.message.isDeleted &&
widget.message.status !=
MessageSendingStatus
.FAILED_DELETE)
? Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: DeletedMessage(
reverse: widget.reverse,
borderRadiusGeometry:
widget.borderRadiusGeometry,
borderSide: widget.borderSide,
shape: widget.shape,
messageTheme:
widget.messageTheme,
),
)
: Material(
clipBehavior: Clip.antiAlias,
shape: widget.shape ??
RoundedRectangleBorder(
side: isOnlyEmoji
? BorderSide.none
: 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: Padding(
padding: EdgeInsets.all(
hasFiles ? 2.0 : 0.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
..._parseAttachments(
context),
if (widget.message.text
.trim()
.isNotEmpty &&
!isGiphy)
_buildTextBubble(context),
],
),
),
),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: 0,
top: -6,
child: Transform(
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: DeletedMessage(
reverse: widget.reverse,
borderRadiusGeometry:
widget.borderRadiusGeometry,
borderSide: widget.borderSide,
shape: widget.shape,
messageTheme: widget.messageTheme,
),
)
: Material(
clipBehavior: Clip.antiAlias,
shape: widget.shape ??
RoundedRectangleBorder(
side: isOnlyEmoji
? BorderSide.none
: 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: Padding(
padding: EdgeInsets.all(
hasFiles ? 2.0 : 0.0),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
..._parseAttachments(context),
if (widget.message.text
.trim()
.isNotEmpty &&
!isGiphy)
_buildTextBubble(context),
],
child: CustomPaint(
painter: ReactionBubblePainter(
widget.messageTheme
.reactionsBackgroundColor,
widget.messageTheme
.reactionsBorderColor,
),
),
),
),
if (widget.showReactionPickerIndicator)
Positioned(
right: 0,
top: -6,
child: Transform(
transform: Matrix4.rotationY(
widget.reverse ? pi : 0),
child: CustomPaint(
painter: ReactionBubblePainter(
widget.messageTheme
.reactionsBackgroundColor,
widget.messageTheme
.reactionsBorderColor,
),
),
),
),
],
],
),
),
),
),
],
),
if (showThreadReplyIndicator ||
showUsername ||
showTimeStamp ||
showInChannel)
SizedBox(height: 20.0),
],
),
if (widget.showReplyIndicator &&
widget.message.replyCount > 0)
_buildReplyIndicator(leftPadding),
if (showThreadReplyIndicator ||
showUsername ||
showTimeStamp ||
showInChannel)
_buildBottomRows(leftPadding)
],
),
if ((widget.message.createdAt != null &&
widget.showTimestamp) ||
widget.showUsername ||
widget.readList?.isNotEmpty == true)
_buildBottomRow(leftPadding),
],
),
),
@@ -376,6 +405,113 @@ class _MessageWidgetState extends State<MessageWidget> {
);
}
Widget _buildBottomRows(double leftPadding) {
final deleted = widget.message.isDeleted;
var children = <Widget>[];
if (deleted) {
children.add(
Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: Colors.black.withOpacity(0.5),
size: 16.0,
),
SizedBox(width: 8.0),
Text(
'Only visible to you',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 12.0,
),
),
],
),
);
} else {
final showSendingIndicator =
widget.showSendingIndicator == DisplayWidget.show;
final replyCount = widget.message.replyCount;
final msg = showInChannel
? 'Thread Reply'
: replyCount != 0
? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}'
: 'Thread Reply';
final onThreadTap = () async {
try {
var message = widget.message;
if (showInChannel && message.parentId != null) {
final channel = StreamChannel.of(context);
message = await channel.getMessage(widget.message.parentId);
}
return widget.onThreadTap(message);
} catch (e, stk) {
print(e);
print(stk);
return null;
}
};
children.addAll([
if (showSendingIndicator) _buildSendingIndicator(),
if (showReadList)
SizedBox.fromSize(
size: Size((widget.readList.length * 10.0) + 10, 17),
child: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: _buildReadIndicator(),
),
),
if (showThreadReplyIndicator || showInChannel)
InkWell(
onTap: widget.onThreadTap != null ? onThreadTap : null,
child: Text(msg, style: widget.messageTheme?.replies),
),
if (showUsername)
Text(
widget.message.user.name,
style: widget.messageTheme.replies.copyWith(
color: widget.messageTheme.createdAt.color,
),
),
if (showTimeStamp)
Text(
Jiffy(widget.message.createdAt.toLocal()).jm,
style: widget.messageTheme.createdAt,
),
]);
}
if (widget.reverse) children = children.reversed.toList();
return Padding(
padding: EdgeInsets.only(left: leftPadding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (!deleted && (showThreadReplyIndicator || showInChannel))
Container(
margin: EdgeInsets.only(
bottom: widget.messageTheme.replies.fontSize / 2),
child: CustomPaint(
size: const Size(16, 32),
painter: _ThreadReplyPainter(
color: widget.messageTheme.replyThreadColor,
),
),
),
...children.map(
(child) => Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: child,
),
),
].insertBetween(const SizedBox(width: 8.0)),
),
);
}
Widget _buildUrlAttachment() {
var urlAttachment = widget.message.attachments
.firstWhere((element) => element.ogScrapeUrl != null);
@@ -394,87 +530,6 @@ class _MessageWidgetState extends State<MessageWidget> {
);
}
Padding _buildBottomRow(double leftPadding) {
return Padding(
padding: EdgeInsets.only(
left: leftPadding,
top: 2,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
child: RichText(
text: TextSpan(
style: widget.messageTheme.createdAt,
children: <TextSpan>[
if (widget.showUsername)
TextSpan(
text: widget.message.user.name,
style: TextStyle(
fontWeight: FontWeight.bold,
color: widget.messageTheme.createdAt.color
.withOpacity(1)),
),
if (widget.message.createdAt != null && widget.showTimestamp)
TextSpan(
text: Jiffy(widget.message.createdAt.toLocal())
.format(' HH:mm'),
),
],
),
),
),
if (widget.showSendingIndicator == DisplayWidget.show)
_buildSendingIndicator(),
if (widget.readList?.isNotEmpty == true)
SizedBox.fromSize(
size: Size((widget.readList.length * 10.0) + 10, 17),
child: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
child: Padding(
padding: const EdgeInsets.only(left: 4.0),
child: _buildReadIndicator(),
),
),
),
if (widget.message.isDeleted)
Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
StreamSvgIcon.eye(
color: Colors.black.withOpacity(0.5),
size: 16.0,
),
SizedBox(
width: 8.0,
),
Text(
'Only visible to you',
style: TextStyle(
color: Colors.black.withOpacity(0.5),
fontSize: 12.0,
),
),
],
),
),
),
],
),
);
}
bool get isGiphy =>
widget.message.attachments?.any((element) => element.type == 'giphy') ==
true;
@@ -565,7 +620,7 @@ class _MessageWidgetState extends State<MessageWidget> {
true,
showReactions: widget.showReactions,
showReply:
widget.showReplyIndicator && widget.onThreadTap != null,
widget.showThreadReplyIndicator && widget.onThreadTap != null,
),
);
});
@@ -715,38 +770,13 @@ class _MessageWidgetState extends State<MessageWidget> {
return;
}
Widget _buildReplyIndicator(double leftPadding) {
return Padding(
padding: EdgeInsets.only(
left: leftPadding,
),
child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: ReplyIndicator(
message: widget.message,
reversed: widget.reverse,
messageTheme: widget.messageTheme,
onTap: widget.onThreadTap != null
? () {
widget.onThreadTap(widget.message);
}
: null,
),
),
);
}
Widget _buildSendingIndicator() {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: Transform(
transform: Matrix4.rotationY(widget.reverse ? pi : 0),
alignment: Alignment.center,
child: SendingIndicator(
message: widget.message,
allRead: widget.allRead,
),
return Container(
height: widget.messageTheme.createdAt.fontSize + 2,
width: widget.messageTheme.createdAt.fontSize + 2,
child: SendingIndicator(
message: widget.message,
allRead: widget.allRead,
),
);
}
@@ -958,3 +988,32 @@ class _MessageWidgetState extends State<MessageWidget> {
}
}
}
class _ThreadReplyPainter extends CustomPainter {
final Color color;
const _ThreadReplyPainter({@required this.color});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = color ?? Color(0XFFDBDBDB)
..style = PaintingStyle.stroke
..strokeWidth = 1
..strokeCap = StrokeCap.round;
final path = Path()
..moveTo(0, 0)
..quadraticBezierTo(0, size.height * 0.38, 0, size.height * 0.50)
..quadraticBezierTo(
0,
size.height,
size.width,
size.height,
);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
-56
View File
@@ -1,56 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A reply button indicator
class ReplyIndicator extends StatelessWidget {
final Message message;
final VoidCallback onTap;
final bool reversed;
final MessageTheme messageTheme;
const ReplyIndicator({
Key key,
this.message,
this.onTap,
this.reversed = false,
this.messageTheme,
}) : super(key: key);
@override
Widget build(BuildContext context) {
var row = [
Text(
'Replies: ${message.replyCount}',
style: messageTheme?.replies,
),
Transform(
transform: Matrix4.rotationY(reversed ? 0 : pi),
alignment: Alignment.center,
child: Icon(
Icons.subdirectory_arrow_left,
color: Theme.of(context).brightness == Brightness.dark
? Colors.white12
: Colors.black12,
),
),
];
if (!reversed) {
row = row.reversed.toList();
}
return GestureDetector(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: row,
),
),
);
}
}
+13
View File
@@ -267,6 +267,19 @@ class StreamChannelState extends State<StreamChannel> {
return state;
}
///
Future<Message> getMessage(String messageId) async {
var message = channel.state.messages.firstWhere(
(it) => it.id == messageId,
orElse: () => null,
);
if (message == null) {
final response = await channel.getMessagesById([messageId]);
message = response.messages.first;
}
return message;
}
/// Reloads the channel with latest message
Future<void> reloadChannel() => queryAtMessage(before: 30);
+15 -4
View File
@@ -191,6 +191,8 @@ class StreamChatThemeData {
this.ownMessageTheme.messageBackgroundColor,
avatarTheme: ownMessageTheme?.avatarTheme ??
this.ownMessageTheme.avatarTheme,
replyThreadColor: ownMessageTheme?.replyThreadColor ??
this.ownMessageTheme.replyThreadColor,
) ??
this.ownMessageTheme,
otherMessageTheme: otherMessageTheme?.copyWith(
@@ -209,6 +211,8 @@ class StreamChatThemeData {
this.otherMessageTheme.messageBackgroundColor,
avatarTheme: otherMessageTheme?.avatarTheme ??
this.otherMessageTheme.avatarTheme,
replyThreadColor: ownMessageTheme?.replyThreadColor ??
this.ownMessageTheme.replyThreadColor,
) ??
this.otherMessageTheme,
reactionIcons: reactionIcons ?? this.reactionIcons,
@@ -297,16 +301,17 @@ class StreamChatThemeData {
color: isDark
? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5),
fontSize: 11,
fontSize: 12,
),
replies: TextStyle(
color: accentColor,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w600,
fontSize: 12,
),
messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
reactionsBackgroundColor: isDark ? Colors.black : Colors.white,
reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA),
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
@@ -330,17 +335,19 @@ class StreamChatThemeData {
color: isDark
? Colors.white.withOpacity(.5)
: Colors.black.withOpacity(.5),
fontSize: 11,
fontSize: 12,
),
replies: TextStyle(
color: accentColor,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w600,
fontSize: 12,
),
messageLinks: TextStyle(
color: accentColor,
),
messageBackgroundColor: isDark ? Colors.black : Colors.white,
replyThreadColor:
isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24),
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
@@ -455,6 +462,7 @@ class MessageTheme {
final Color messageBackgroundColor;
final Color reactionsBackgroundColor;
final Color reactionsBorderColor;
final Color replyThreadColor;
final AvatarTheme avatarTheme;
const MessageTheme({
@@ -465,6 +473,7 @@ class MessageTheme {
this.messageBackgroundColor,
this.reactionsBackgroundColor,
this.reactionsBorderColor,
this.replyThreadColor,
this.avatarTheme,
this.createdAt,
});
@@ -479,6 +488,7 @@ class MessageTheme {
AvatarTheme avatarTheme,
Color reactionsBackgroundColor,
Color reactionsBorderColor,
Color replyThreadColor,
}) =>
MessageTheme(
messageText: messageText ?? this.messageText,
@@ -492,6 +502,7 @@ class MessageTheme {
reactionsBackgroundColor:
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
replyThreadColor: replyThreadColor ?? this.replyThreadColor,
);
}
+9
View File
@@ -92,3 +92,12 @@ Future<bool> showConfirmationDialog(
/// Get random png with initials
String getRandomPicUrl(User user) =>
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
/// List extension
extension ListX<T> on List<T> {
/// Insert any item<T> inBetween the list items
List<T> insertBetween(T item) => expand((e) sync* {
yield item;
yield e;
}).skip(1).toList(growable: false);
}
-1
View File
@@ -21,7 +21,6 @@ export 'src/message_list_view.dart';
export 'src/message_text.dart';
export 'src/message_widget.dart';
export 'src/reaction_picker.dart';
export 'src/reply_indicator.dart';
export 'src/sending_indicator.dart';
export 'src/stream_channel.dart';
export 'src/stream_chat.dart';