diff --git a/.flutter-plugins b/.flutter-plugins
new file mode 100644
index 00000000..25fdba30
--- /dev/null
+++ b/.flutter-plugins
@@ -0,0 +1,9 @@
+# This is a generated file; do not edit or check into version control.
+path_provider=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/path_provider-1.6.1/
+screen=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/screen-0.0.5/
+sqflite=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/sqflite-1.2.1/
+url_launcher=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher-5.4.2/
+url_launcher_macos=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_macos-0.0.1+4/
+url_launcher_web=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/url_launcher_web-0.1.1+1/
+video_player=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/video_player-0.10.8+1/
+video_player_web=/Users/salvatoregiordano/Development/flutter/.pub-cache/hosted/pub.dartlang.org/video_player_web-0.1.2+1/
diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies
new file mode 100644
index 00000000..c8053162
--- /dev/null
+++ b/.flutter-plugins-dependencies
@@ -0,0 +1 @@
+{"_info":"// This is a generated file; do not edit or check into version control.","dependencyGraph":[{"name":"path_provider","dependencies":[]},{"name":"screen","dependencies":[]},{"name":"sqflite","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_web","url_launcher_macos"]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"video_player","dependencies":["video_player_web"]},{"name":"video_player_web","dependencies":[]}]}
\ No newline at end of file
diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..681f41ae
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ xmlns:android
+
+ ^$
+
+
+
+
+
+
+
+
+ xmlns:.*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*:id
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ .*:name
+
+ http://schemas.android.com/apk/res/android
+
+
+
+
+
+
+
+
+ name
+
+ ^$
+
+
+
+
+
+
+
+
+ style
+
+ ^$
+
+
+
+
+
+
+
+
+ .*
+
+ ^$
+
+
+ BY_NAME
+
+
+
+
+
+
+ .*
+
+ http://schemas.android.com/apk/res/android
+
+
+ ANDROID_ATTRIBUTE_ORDER
+
+
+
+
+
+
+ .*
+
+ .*
+
+
+ BY_NAME
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml
new file mode 100644
index 00000000..13cf87ee
--- /dev/null
+++ b/.idea/libraries/Dart_Packages.xml
@@ -0,0 +1,452 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Dart_SDK.xml b/.idea/libraries/Dart_SDK.xml
new file mode 100644
index 00000000..41443ebd
--- /dev/null
+++ b/.idea/libraries/Dart_SDK.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml
new file mode 100644
index 00000000..1b261293
--- /dev/null
+++ b/.idea/libraries/Flutter_Plugins.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..4db1dd48
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/stream-chat-flutter.iml b/.idea/stream-chat-flutter.iml
new file mode 100644
index 00000000..d3275515
--- /dev/null
+++ b/.idea/stream-chat-flutter.iml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 00000000..35eb1ddf
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 00000000..39b44ea0
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,361 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ channelsStream
+ _channelsController
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1582540093655
+
+
+ 1582540093655
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b137891..1de3bc7f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1 +1,3 @@
+## 0.0.1
+- First release
\ No newline at end of file
diff --git a/README.md b/README.md
index 3fb35e2f..77454051 100644
--- a/README.md
+++ b/README.md
@@ -28,7 +28,7 @@ TODO
```yaml
dependencies:
- stream_chat_flutter: ^0.1.0
+ stream_chat_flutter: ^0.0.1
```
You should then run `flutter packages get`
diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart
new file mode 100644
index 00000000..f28ec764
--- /dev/null
+++ b/lib/src/message_list_view.dart
@@ -0,0 +1,283 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_widgets/flutter_widgets.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+import 'message_widget.dart';
+import 'stream_channel.dart';
+
+typedef MessageBuilder = Widget Function(BuildContext, Message, int index);
+typedef ParentMessageBuilder = Widget Function(BuildContext, Message);
+typedef ParentTapCallback = void Function(Message parent);
+
+class MessageListView extends StatefulWidget {
+ MessageListView({
+ Key key,
+ MessageBuilder messageBuilder,
+ this.parentMessageBuilder,
+ this.parentMessage,
+ this.parentTapCallback,
+ }) : _messageBuilder = messageBuilder,
+ super(key: key);
+
+ final MessageBuilder _messageBuilder;
+ final ParentMessageBuilder parentMessageBuilder;
+ final ParentTapCallback parentTapCallback;
+ final Message parentMessage;
+
+ @override
+ _MessageListViewState createState() => _MessageListViewState();
+}
+
+class _MessageListViewState extends State {
+ static const _newMessageLoadingOffset = 100;
+ final ScrollController _scrollController = ScrollController();
+ bool _isBottom = true;
+ bool _topWasVisible = false;
+ List _messages = [];
+ List _newMessageList = [];
+
+ @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 NotificationListener(
+ onNotification: (_) {
+ if (_scrollController.offset < 150 && _newMessageList.isNotEmpty) {
+ setState(() {
+ _messages.insertAll(0, _newMessageList);
+ _newMessageList.clear();
+ });
+ }
+ return true;
+ },
+ child: ListView.custom(
+ physics: AlwaysScrollableScrollPhysics(),
+ controller: _scrollController,
+ reverse: true,
+ childrenDelegate: SliverChildBuilderDelegate(
+ (context, i) {
+ if (i == this._messages.length + 1) {
+ if (widget.parentMessage != null) {
+ if (widget.parentMessageBuilder != null) {
+ return widget.parentMessageBuilder(
+ context, widget.parentMessage);
+ } else {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ MessageWidget(
+ key: ValueKey(
+ 'PARENT-MESSAGE-${widget.parentMessage.id}'),
+ previousMessage: null,
+ message: widget.parentMessage.copyWith(replyCount: 0),
+ nextMessage: null,
+ parentTapCallback: widget.parentTapCallback,
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: Container(
+ padding: const EdgeInsets.all(8),
+ child: Text(
+ 'Start of a new thread',
+ textAlign: TextAlign.center,
+ ),
+ color: Theme.of(context).primaryColorLight,
+ ),
+ ),
+ ],
+ );
+ }
+ } else {
+ return SizedBox.fromSize(
+ size: Size.zero,
+ );
+ }
+ }
+
+ if (i == this._messages.length) {
+ return _buildLoadingIndicator(streamChannel);
+ }
+ final message = this._messages[i];
+
+ if (widget._messageBuilder != null) {
+ return widget._messageBuilder(context, message, i);
+ }
+
+ final previousMessage =
+ i < this._messages.length - 1 ? this._messages[i + 1] : null;
+ final nextMessage = i > 0 ? this._messages[i - 1] : null;
+
+ if (i == 0) {
+ return _buildBottomMessage(
+ streamChannel,
+ previousMessage,
+ message,
+ context,
+ );
+ }
+
+ if (i == this._messages.length - 1) {
+ return _buildTopMessage(
+ message,
+ nextMessage,
+ streamChannel,
+ context,
+ );
+ }
+
+ return MessageWidget(
+ key: ValueKey('MESSAGE-${message.id}'),
+ previousMessage: previousMessage,
+ message: message,
+ nextMessage: nextMessage,
+ parentTapCallback: widget.parentTapCallback,
+ );
+ },
+ childCount: this._messages.length + 2,
+ findChildIndexCallback: (key) {
+ final ValueKey valueKey = key;
+ final index = this
+ ._messages
+ .indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value);
+ return index != -1 ? index : null;
+ },
+ ),
+ ),
+ );
+ }
+
+ Container _buildLoadingIndicator(StreamChannel streamChannel) {
+ return Container(
+ height: 50,
+ child: StreamBuilder(
+ stream: streamChannel.queryMessage,
+ initialData: false,
+ builder: (context, snapshot) {
+ if (snapshot.hasError) {
+ print((snapshot.error as Error).stackTrace.toString());
+ return Center(
+ child: Text(snapshot.error.toString()),
+ );
+ }
+ if (!snapshot.data) {
+ return Container();
+ }
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: CircularProgressIndicator(),
+ ),
+ );
+ }),
+ );
+ }
+
+ Widget _buildTopMessage(
+ Message message,
+ Message nextMessage,
+ StreamChannel channelBloc,
+ BuildContext context,
+ ) {
+ return VisibilityDetector(
+ key: ValueKey('TOP-MESSAGE'),
+ child: MessageWidget(
+ key: ValueKey('MESSAGE-${message.id}'),
+ previousMessage: null,
+ message: message,
+ nextMessage: nextMessage,
+ parentTapCallback: widget.parentTapCallback,
+ ),
+ onVisibilityChanged: (visibility) {
+ final topIsVisible = visibility.visibleBounds != Rect.zero;
+ if (topIsVisible && !_topWasVisible) {
+ channelBloc.queryMessages();
+ }
+ _topWasVisible = topIsVisible;
+ },
+ );
+ }
+
+ Widget _buildBottomMessage(
+ StreamChannel channelBloc,
+ Message previousMessage,
+ Message message,
+ BuildContext context,
+ ) {
+ return VisibilityDetector(
+ key: ValueKey('BOTTOM-MESSAGE'),
+ onVisibilityChanged: (visibility) {
+ this._isBottom = visibility.visibleBounds != Rect.zero;
+ if (this._isBottom) {
+ if (channelBloc.channelClient.state.unreadCount > 0) {
+ channelBloc.channelClient.markRead();
+ }
+ }
+ },
+ child: MessageWidget(
+ key: ValueKey('MESSAGE-${message.id}'),
+ previousMessage: previousMessage,
+ message: message,
+ nextMessage: null,
+ parentTapCallback: widget.parentTapCallback,
+ ),
+ );
+ }
+
+ StreamSubscription _streamListener;
+
+ @override
+ void initState() {
+ super.initState();
+
+ final streamChannel = StreamChannel.of(context);
+ if (streamChannel.channelClient.state.unreadCount > 0) {
+ streamChannel.channelClient.markRead();
+ }
+
+ Stream> stream;
+
+ if (widget.parentMessage == null) {
+ stream = streamChannel.channelStateStream.map((c) => c.messages);
+ } else {
+ streamChannel.getReplies(widget.parentMessage.id);
+ stream = streamChannel.channelClient.state.threadsStream
+ .where((threads) => threads.containsKey(widget.parentMessage.id))
+ .map((threads) => threads[widget.parentMessage.id]);
+ }
+
+ _streamListener = stream.listen((newMessages) {
+ newMessages = newMessages.reversed.toList();
+ if (_messages.isEmpty || newMessages.first.id != _messages.first.id) {
+ if (!_scrollController.hasClients ||
+ _scrollController.offset < _newMessageLoadingOffset) {
+ setState(() {
+ this._messages = newMessages;
+ });
+ } else if (newMessages.first.user.id ==
+ streamChannel.channelClient.client.user.id) {
+ _scrollController.jumpTo(0);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ setState(() {
+ this._messages = newMessages;
+ });
+ });
+ } else {
+ _newMessageList = newMessages;
+ }
+ } else {
+ setState(() {
+ this._messages = newMessages;
+ });
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _streamListener.cancel();
+ super.dispose();
+ }
+}
diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart
new file mode 100644
index 00000000..f1b2723b
--- /dev/null
+++ b/lib/src/message_widget.dart
@@ -0,0 +1,351 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:chewie/chewie.dart';
+import 'package:date_format/date_format.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_markdown/flutter_markdown.dart';
+import 'package:stream_chat/stream_chat.dart';
+import 'package:url_launcher/url_launcher.dart';
+import 'package:video_player/video_player.dart';
+
+import 'message_list_view.dart';
+import 'stream_chat.dart';
+import 'user_avatar.dart';
+
+class MessageWidget extends StatefulWidget {
+ const MessageWidget({
+ Key key,
+ @required this.previousMessage,
+ @required this.message,
+ @required this.nextMessage,
+ this.parentTapCallback,
+ }) : super(key: key);
+
+ final Message previousMessage;
+ final Message message;
+ final Message nextMessage;
+ final ParentTapCallback parentTapCallback;
+
+ @override
+ _MessageWidgetState createState() => _MessageWidgetState();
+}
+
+class _MessageWidgetState extends State
+ with AutomaticKeepAliveClientMixin {
+ final Map _videoControllers = {};
+ final Map _chuwieControllers = {};
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+
+ final streamChat = StreamChat.of(context);
+ final currentUserId = streamChat.user.id;
+ final messageUserId = widget.message.user.id;
+ final previousUserId = widget.previousMessage?.user?.id;
+ final nextUserId = widget.nextMessage?.user?.id;
+ final isMyMessage = messageUserId == currentUserId;
+ final isLastUser = previousUserId == messageUserId;
+ final isNextUser = nextUserId == messageUserId;
+ final alignment =
+ isMyMessage ? Alignment.centerRight : Alignment.centerLeft;
+
+ List row = [
+ Column(
+ crossAxisAlignment:
+ isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
+ children: [
+ _buildBubble(context, isMyMessage, isLastUser),
+ widget.message.replyCount > 0
+ ? GestureDetector(
+ onTap: () {
+ widget?.parentTapCallback(widget.message);
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 2.0),
+ child: Row(
+ children: [
+ Text(
+ 'Replies: ${widget.message.replyCount}',
+ style: Theme.of(context)
+ .textTheme
+ .subtitle
+ .copyWith(color: Colors.blue),
+ ),
+ Icon(
+ Icons.subdirectory_arrow_left,
+ color: Colors.black12,
+ ),
+ ],
+ ),
+ ),
+ )
+ : Container(),
+ isNextUser ? Container() : _buildTimestamp(isMyMessage, alignment),
+ ],
+ ),
+ isNextUser
+ ? Container(
+ width: 40,
+ )
+ : Padding(
+ padding: EdgeInsets.only(
+ left: isMyMessage ? 8.0 : 0,
+ right: isMyMessage ? 0 : 8.0,
+ ),
+ child: UserAvatar(user: widget.message.user),
+ ),
+ ];
+
+ if (!isMyMessage) {
+ row = row.reversed.toList();
+ }
+
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 10.0),
+ margin: EdgeInsets.only(
+ top: isLastUser ? 5 : 24,
+ bottom: widget.nextMessage == null ? 30 : 0,
+ ),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisAlignment:
+ isMyMessage ? MainAxisAlignment.end : MainAxisAlignment.start,
+ mainAxisSize: MainAxisSize.max,
+ children: row,
+ ),
+ );
+ }
+
+ Widget _buildBubble(
+ BuildContext context,
+ bool isMyMessage,
+ bool isLastUser,
+ ) {
+ int nOfAttachmentWidgets = 0;
+
+ final column = Column(
+ crossAxisAlignment:
+ isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
+ children: widget.message.attachments.map((attachment) {
+ nOfAttachmentWidgets++;
+
+ Widget attachmentWidget;
+ if (attachment.type == 'video') {
+ attachmentWidget = _buildVideo(attachment, isMyMessage, isLastUser);
+ } else if (attachment.type == 'image' || attachment.type == 'giphy') {
+ attachmentWidget = _buildImage(isMyMessage, isLastUser, attachment);
+ }
+
+ if (attachmentWidget != null) {
+ final boxDecoration = _buildBoxDecoration(isMyMessage, isLastUser)
+ .copyWith(color: Color(0xffebebeb));
+ return ClipRRect(
+ borderRadius: boxDecoration.borderRadius,
+ child: Container(
+ decoration: boxDecoration,
+ constraints: BoxConstraints.loose(Size.fromWidth(300)),
+ child: Stack(
+ children: [
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ attachmentWidget,
+ attachment.title != null
+ ? Container(
+ constraints:
+ BoxConstraints.loose(Size.fromHeight(70)),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ attachment.title,
+ overflow: TextOverflow.ellipsis,
+ style: Theme.of(context)
+ .textTheme
+ .subtitle
+ .copyWith(color: Colors.blue),
+ ),
+ Text(
+ Uri.parse(attachment.thumbUrl)
+ .authority
+ .split('.')
+ .reversed
+ .take(2)
+ .toList()
+ .reversed
+ .join('.'),
+ overflow: TextOverflow.ellipsis,
+ style:
+ Theme.of(context).textTheme.caption,
+ ),
+ ],
+ ),
+ ),
+ color: Color(0xffebebeb),
+ )
+ : Container(),
+ ],
+ ),
+ attachment.type == 'image' && attachment.titleLink != null
+ ? Positioned.fill(
+ child: Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => _launchURL(attachment.titleLink),
+ ),
+ ),
+ )
+ : SizedBox.fromSize(
+ size: Size.zero,
+ ),
+ ],
+ ),
+ margin: EdgeInsets.only(
+ top: nOfAttachmentWidgets > 1 ? 5 : 0,
+ ),
+ ),
+ );
+ }
+
+ nOfAttachmentWidgets--;
+ return Container();
+ }).toList(),
+ );
+
+ if (widget.message.text.trim().isNotEmpty) {
+ column.children.add(Container(
+ margin: EdgeInsets.only(
+ top: nOfAttachmentWidgets > 0 ? 5 : 0,
+ ),
+ decoration: _buildBoxDecoration(
+ isMyMessage, isLastUser || nOfAttachmentWidgets > 0),
+ padding: EdgeInsets.all(10),
+ constraints: BoxConstraints.loose(Size.fromWidth(300)),
+ child: MarkdownBody(
+ data: '${widget.message.text}',
+ onTapLink: (link) {
+ _launchURL(link);
+ },
+ styleSheet: MarkdownStyleSheet.fromTheme(Theme.of(context)),
+ ),
+ ));
+ }
+
+ return column;
+ }
+
+ Widget _buildImage(
+ bool isMyMessage,
+ bool isLastUser,
+ Attachment attachment,
+ ) {
+ return CachedNetworkImage(
+ imageUrl: attachment.thumbUrl ?? attachment.imageUrl,
+ fit: BoxFit.cover,
+ );
+ }
+
+ Widget _buildVideo(
+ Attachment attachment,
+ bool isMyMessage,
+ bool isLastUser,
+ ) {
+ VideoPlayerController videoController;
+ if (_videoControllers.containsKey(attachment.assetUrl)) {
+ videoController = _videoControllers[attachment.assetUrl];
+ } else {
+ videoController = VideoPlayerController.network(attachment.assetUrl);
+ _videoControllers[attachment.assetUrl] = videoController;
+ }
+
+ ChewieController chewieController;
+ if (_chuwieControllers.containsKey(attachment.assetUrl)) {
+ chewieController = _chuwieControllers[attachment.assetUrl];
+ } else {
+ chewieController = ChewieController(
+ videoPlayerController: videoController,
+ autoInitialize: true,
+ errorBuilder: (_, e) {
+ return Stack(
+ children: [
+ Container(
+ decoration: BoxDecoration(
+ image: DecorationImage(
+ fit: BoxFit.cover,
+ image: CachedNetworkImageProvider(
+ attachment.thumbUrl,
+ ),
+ ),
+ ),
+ ),
+ Material(
+ color: Colors.transparent,
+ child: InkWell(
+ onTap: () => _launchURL(attachment.titleLink),
+ ),
+ ),
+ ],
+ );
+ });
+ _chuwieControllers[attachment.assetUrl] = chewieController;
+ }
+
+ return Chewie(
+ key: ValueKey(
+ 'ATTACHMENT-${attachment.title}-${widget.message.id}'),
+ controller: chewieController,
+ );
+ }
+
+ _launchURL(String url) async {
+ if (await canLaunch(url)) {
+ await launch(url);
+ } else {
+ Scaffold.of(context).showSnackBar(
+ SnackBar(
+ content: Text('Cannot launch the url'),
+ ),
+ );
+ }
+ }
+
+ @override
+ void dispose() {
+ _videoControllers.values.forEach((element) {
+ element.dispose();
+ });
+ super.dispose();
+ }
+
+ Widget _buildTimestamp(bool isMyMessage, Alignment alignment) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 5.0),
+ child: Text(
+ formatDate(widget.message.createdAt.toLocal(), [HH, ':', nn]),
+ ),
+ );
+ }
+
+ BoxDecoration _buildBoxDecoration(bool isMyMessage, bool isLastUser) {
+ return BoxDecoration(
+ border: isMyMessage ? null : Border.all(color: Colors.black.withAlpha(8)),
+ borderRadius: BorderRadius.only(
+ topLeft: Radius.circular((isMyMessage || !isLastUser) ? 16 : 2),
+ bottomLeft: Radius.circular(isMyMessage ? 16 : 2),
+ topRight: Radius.circular((isMyMessage && isLastUser) ? 2 : 16),
+ bottomRight: Radius.circular(isMyMessage ? 2 : 16),
+ ),
+ color: isMyMessage ? Color(0xffebebeb) : Colors.white,
+ );
+ }
+
+ @override
+ bool get wantKeepAlive {
+ return widget.message.attachments.isNotEmpty;
+ }
+}
diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart
new file mode 100644
index 00000000..65f9d8aa
--- /dev/null
+++ b/lib/src/stream_channel.dart
@@ -0,0 +1,99 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:rxdart/rxdart.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+class StreamChannel extends InheritedWidget {
+ final ChannelClient channelClient;
+
+ ChannelState get channelState => channelClient.state.channelState;
+ Stream get channelStateStream =>
+ channelClient.state.channelStateStream;
+
+ StreamChannel({
+ Key key,
+ @required Widget child,
+ @required this.channelClient,
+ }) : super(
+ key: key,
+ child: child,
+ );
+
+ final BehaviorSubject _queryMessageController = BehaviorSubject();
+
+ Stream get queryMessage => _queryMessageController.stream;
+
+ void queryMessages() {
+ _queryMessageController.add(true);
+
+ String firstId;
+ if (channelState.messages.isNotEmpty) {
+ firstId = channelState.messages.first.id;
+ }
+
+ channelClient
+ .query(
+ messagesPagination: PaginationParams(
+ lessThan: firstId,
+ limit: 100,
+ ),
+ )
+ .then((res) {
+ _queryMessageController.add(false);
+ }).catchError((e, stack) {
+ _queryMessageController.addError(e, stack);
+ });
+ }
+
+ Future getReplies(String parentId) async {
+ _queryMessageController.add(true);
+
+ String firstId;
+ if (channelClient.state.threads.containsKey(parentId)) {
+ firstId = channelClient.state.threads[parentId].first.id;
+ }
+
+ return channelClient
+ .getReplies(
+ parentId,
+ PaginationParams(
+ lessThan: firstId,
+ limit: 100,
+ ),
+ )
+ .then((res) {
+ _queryMessageController.add(false);
+ }).catchError((e, stack) {
+ _queryMessageController.addError(e, stack);
+ });
+ }
+
+ void dispose() {
+ _queryMessageController.close();
+ channelClient.dispose();
+ }
+
+ @override
+ bool updateShouldNotify(InheritedWidget oldWidget) {
+ return true;
+ }
+
+ static StreamChannel of(BuildContext context, [bool listen = false]) {
+ StreamChannel streamChannel;
+
+ if (listen) {
+ streamChannel =
+ context.dependOnInheritedWidgetOfExactType();
+ } else {
+ streamChannel = context.findAncestorWidgetOfExactType();
+ }
+
+ if (streamChannel == null) {
+ throw Exception(
+ 'You must have a StreamChannel widget at the top of your widget tree');
+ }
+
+ return streamChannel;
+ }
+}
diff --git a/lib/src/stream_chat.dart b/lib/src/stream_chat.dart
new file mode 100644
index 00000000..a0a32388
--- /dev/null
+++ b/lib/src/stream_chat.dart
@@ -0,0 +1,120 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:rxdart/rxdart.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+class StreamChat extends InheritedWidget {
+ final Client client;
+ final List _subscriptions = [];
+
+ StreamChat({
+ Key key,
+ @required this.client,
+ @required Widget child,
+ }) : super(
+ key: key,
+ child: child,
+ ) {
+ _subscriptions.add(client.on('message.new').listen((Event e) {
+ final index = channels.indexWhere((c) => c.channel.cid == e.cid);
+ if (index > 0) {
+ final channel = channels.removeAt(index);
+ channels.insert(0, channel);
+ _channelsController.add(channels);
+ }
+ }));
+ }
+
+ User get user => _userController.value;
+
+ Stream get userStream => _userController.stream;
+ final BehaviorSubject _userController = BehaviorSubject();
+
+ void setUser(User newUser, [String token]) async {
+ _userController.sink.add(null);
+
+ try {
+ if (token != null) {
+ await client.setUser(newUser, token);
+ } else {
+ await client.setUserWithProvider(newUser);
+ }
+ _userController.sink.add(newUser);
+ } catch (e, stack) {
+ _userController.sink.addError(e, stack);
+ }
+ }
+
+ Stream> get channelsStream => _channelsController.stream;
+ final BehaviorSubject> _channelsController =
+ BehaviorSubject();
+ final List channels = [];
+
+ final BehaviorSubject _queryChannelsLoadingController =
+ BehaviorSubject.seeded(false);
+
+ Stream get queryChannelsLoading =>
+ _queryChannelsLoadingController.stream;
+
+ Future queryChannels({
+ Map filter,
+ List sortOptions,
+ PaginationParams paginationParams,
+ Map options,
+ }) async {
+ if (_queryChannelsLoadingController.value) {
+ return;
+ }
+ _queryChannelsLoadingController.sink.add(true);
+
+ try {
+ final res = await client.queryChannels(
+ filter: filter,
+ sort: sortOptions,
+ options: options,
+ paginationParams: paginationParams,
+ );
+ channels.addAll(res.map((c) => c.state.channelState));
+ _channelsController.sink.add(channels);
+ _queryChannelsLoadingController.sink.add(false);
+ } catch (e) {
+ _channelsController.sink.addError(e);
+ }
+ }
+
+ void clearChannels() {
+ channels.clear();
+ }
+
+ void dispose() {
+ client.dispose();
+ _subscriptions.forEach((s) => s.cancel());
+ _userController.close();
+ _queryChannelsLoadingController.close();
+ _channelsController.close();
+ }
+
+ @override
+ bool updateShouldNotify(InheritedWidget oldWidget) {
+ return true;
+ }
+
+ static StreamChat of(BuildContext context, [bool listen = false]) {
+ StreamChat streamChat;
+
+ if (listen) {
+ streamChat = context.dependOnInheritedWidgetOfExactType();
+ } else {
+ streamChat = context.findAncestorWidgetOfExactType();
+ }
+
+ if (streamChat == null) {
+ throw Exception(
+ 'You must have a StreamChat widget at the top of your widget tree');
+ }
+
+ return streamChat;
+ }
+}
diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart
new file mode 100644
index 00000000..c815f363
--- /dev/null
+++ b/lib/src/user_avatar.dart
@@ -0,0 +1,29 @@
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:stream_chat/stream_chat.dart';
+
+class UserAvatar extends StatelessWidget {
+ const UserAvatar({
+ Key key,
+ @required this.user,
+ this.radius = 16,
+ }) : super(key: key);
+
+ final User user;
+ final double radius;
+
+ @override
+ Widget build(BuildContext context) {
+ return CircleAvatar(
+ radius: this.radius,
+ backgroundImage: user.extraData.containsKey('image')
+ ? CachedNetworkImageProvider(user.extraData['image'] as String)
+ : null,
+ child: user.extraData.containsKey('image')
+ ? null
+ : Text(user?.extraData?.containsKey('name') ?? false
+ ? user.extraData['name'][0]
+ : ''),
+ );
+ }
+}
diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart
new file mode 100644
index 00000000..01c7ce82
--- /dev/null
+++ b/lib/stream_chat_flutter.dart
@@ -0,0 +1,5 @@
+export 'src/message_list_view.dart';
+export 'src/message_widget.dart';
+export 'src/stream_channel.dart';
+export 'src/stream_chat.dart';
+export 'src/user_avatar.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 00000000..95a660df
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,26 @@
+name: stream_chat_flutter.dart
+homepage: https://github.com/GetStream/stream-chat-flutter
+description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
+version: 0.0.1
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ rxdart: ^0.23.1
+ flutter_widgets: ^0.1.10
+ timeago: ^2.0.26
+ date_format: ^1.0.8
+ cached_network_image: ^2.0.0
+ flutter_markdown: ^0.3.3
+ url_launcher: ^5.4.1
+ video_player: ^0.10.7
+ chewie: ^0.9.8+1
+ animations: ^1.0.0+3
+ stream_chat:
+ path: ../stream_chat_dart
+
+dev_dependencies:
+ pedantic: ^1.8.0+1