add single_conversation example

This commit is contained in:
Salvatore Giordano
2020-02-24 14:39:18 +01:00
parent dd323bcd8a
commit 9f22241f89
70 changed files with 1947 additions and 158 deletions
+137
View File
@@ -0,0 +1,137 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_channel.dart';
class MessageInput extends StatefulWidget {
MessageInput({
Key key,
this.onMessageSent,
this.parent,
}) : super(key: key);
final void Function(Message) onMessageSent;
final Message parent;
@override
_MessageInputState createState() => _MessageInputState();
}
class _MessageInputState extends State<MessageInput> {
final _textController = TextEditingController();
bool _messageIsPresent = false;
bool _typingStarted = false;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Container(
width: MediaQuery.of(context).size.width,
padding: EdgeInsets.all(2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10.0),
gradient: _typingStarted
? LinearGradient(colors: [Color(0xFF00AEFF), Color(0xFF0076FF)])
: null,
),
child: Container(
decoration: BoxDecoration(
color: Theme.of(context).canvasColor,
borderRadius: BorderRadius.circular(10.0),
),
child: Container(
decoration: BoxDecoration(
color: Colors.black.withAlpha(5),
borderRadius: BorderRadius.circular(10.0),
border: Border.all(color: Colors.black.withOpacity(.2)),
),
child: Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
child: TextField(
minLines: null,
maxLines: null,
onSubmitted: (_) {
_sendMessage(context);
},
controller: _textController,
onChanged: (s) {
StreamChannel.of(context).channelClient.keyStroke();
setState(() {
_messageIsPresent = s.trim().isNotEmpty;
});
},
onTap: () {
setState(() {
_typingStarted = true;
});
},
style: Theme.of(context).textTheme.body1,
autofocus: false,
decoration: InputDecoration(
hintText: 'Write a message',
prefixText: ' ',
border: InputBorder.none,
),
),
),
AnimatedCrossFade(
crossFadeState: _messageIsPresent
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
firstChild: _buildSendButton(context),
secondChild: Container(),
duration: Duration(milliseconds: 300),
alignment: Alignment.center,
),
],
),
),
),
),
);
}
IconButton _buildSendButton(BuildContext context) {
return IconButton(
onPressed: () {
_sendMessage(context);
},
icon: Icon(
Icons.send,
),
);
}
void _sendMessage(BuildContext context) {
final text = _textController.text.trim();
if (text.isEmpty) {
return;
}
_textController.clear();
setState(() {
_messageIsPresent = false;
_typingStarted = false;
});
FocusScope.of(context).unfocus();
StreamChannel.of(context)
.channelClient
.sendMessage(
Message(
parentId: widget.parent?.id,
text: text,
),
)
.then((_) {
if (widget.onMessageSent != null) {
widget.onMessageSent(Message(text: text));
}
});
}
}
+24 -25
View File
@@ -9,7 +9,7 @@ import 'stream_channel.dart';
typedef MessageBuilder = Widget Function(BuildContext, Message, int index);
typedef ParentMessageBuilder = Widget Function(BuildContext, Message);
typedef ParentTapCallback = void Function(Message parent);
typedef OnThreadSelectCallback = void Function(Message parent);
class MessageListView extends StatefulWidget {
MessageListView({
@@ -17,13 +17,13 @@ class MessageListView extends StatefulWidget {
MessageBuilder messageBuilder,
this.parentMessageBuilder,
this.parentMessage,
this.parentTapCallback,
this.onThreadSelect,
}) : _messageBuilder = messageBuilder,
super(key: key);
final MessageBuilder _messageBuilder;
final ParentMessageBuilder parentMessageBuilder;
final ParentTapCallback parentTapCallback;
final OnThreadSelectCallback onThreadSelect;
final Message parentMessage;
@override
@@ -59,7 +59,7 @@ class _MessageListViewState extends State<MessageListView> {
reverse: true,
childrenDelegate: SliverChildBuilderDelegate(
(context, i) {
if (i == this._messages.length + 1) {
if (i == _messages.length + 1) {
if (widget.parentMessage != null) {
if (widget.parentMessageBuilder != null) {
return widget.parentMessageBuilder(
@@ -74,7 +74,7 @@ class _MessageListViewState extends State<MessageListView> {
previousMessage: null,
message: widget.parentMessage.copyWith(replyCount: 0),
nextMessage: null,
parentTapCallback: widget.parentTapCallback,
onThreadSelect: widget.onThreadSelect,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
@@ -97,18 +97,18 @@ class _MessageListViewState extends State<MessageListView> {
}
}
if (i == this._messages.length) {
if (i == _messages.length) {
return _buildLoadingIndicator(streamChannel);
}
final message = this._messages[i];
final message = _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;
i < _messages.length - 1 ? _messages[i + 1] : null;
final nextMessage = i > 0 ? _messages[i - 1] : null;
if (i == 0) {
return _buildBottomMessage(
@@ -119,7 +119,7 @@ class _MessageListViewState extends State<MessageListView> {
);
}
if (i == this._messages.length - 1) {
if (i == _messages.length - 1) {
return _buildTopMessage(
message,
nextMessage,
@@ -133,14 +133,13 @@ class _MessageListViewState extends State<MessageListView> {
previousMessage: previousMessage,
message: message,
nextMessage: nextMessage,
parentTapCallback: widget.parentTapCallback,
onThreadSelect: widget.onThreadSelect,
);
},
childCount: this._messages.length + 2,
childCount: _messages.length + 2,
findChildIndexCallback: (key) {
final ValueKey<String> valueKey = key;
final index = this
._messages
final index = _messages
.indexWhere((m) => 'MESSAGE-${m.id}' == valueKey.value);
return index != -1 ? index : null;
},
@@ -149,7 +148,7 @@ class _MessageListViewState extends State<MessageListView> {
);
}
Container _buildLoadingIndicator(StreamChannel streamChannel) {
Container _buildLoadingIndicator(StreamChannelState streamChannel) {
return Container(
height: 50,
child: StreamBuilder<bool>(
@@ -178,7 +177,7 @@ class _MessageListViewState extends State<MessageListView> {
Widget _buildTopMessage(
Message message,
Message nextMessage,
StreamChannel channelBloc,
StreamChannelState streamChannelState,
BuildContext context,
) {
return VisibilityDetector(
@@ -188,12 +187,12 @@ class _MessageListViewState extends State<MessageListView> {
previousMessage: null,
message: message,
nextMessage: nextMessage,
parentTapCallback: widget.parentTapCallback,
onThreadSelect: widget.onThreadSelect,
),
onVisibilityChanged: (visibility) {
final topIsVisible = visibility.visibleBounds != Rect.zero;
if (topIsVisible && !_topWasVisible) {
channelBloc.queryMessages();
streamChannelState.queryMessages();
}
_topWasVisible = topIsVisible;
},
@@ -201,7 +200,7 @@ class _MessageListViewState extends State<MessageListView> {
}
Widget _buildBottomMessage(
StreamChannel channelBloc,
StreamChannelState channelBloc,
Message previousMessage,
Message message,
BuildContext context,
@@ -209,8 +208,8 @@ class _MessageListViewState extends State<MessageListView> {
return VisibilityDetector(
key: ValueKey<String>('BOTTOM-MESSAGE'),
onVisibilityChanged: (visibility) {
this._isBottom = visibility.visibleBounds != Rect.zero;
if (this._isBottom) {
_isBottom = visibility.visibleBounds != Rect.zero;
if (_isBottom) {
if (channelBloc.channelClient.state.unreadCount > 0) {
channelBloc.channelClient.markRead();
}
@@ -221,7 +220,7 @@ class _MessageListViewState extends State<MessageListView> {
previousMessage: previousMessage,
message: message,
nextMessage: null,
parentTapCallback: widget.parentTapCallback,
onThreadSelect: widget.onThreadSelect,
),
);
}
@@ -254,14 +253,14 @@ class _MessageListViewState extends State<MessageListView> {
if (!_scrollController.hasClients ||
_scrollController.offset < _newMessageLoadingOffset) {
setState(() {
this._messages = newMessages;
_messages = newMessages;
});
} else if (newMessages.first.user.id ==
streamChannel.channelClient.client.user.id) {
_scrollController.jumpTo(0);
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
this._messages = newMessages;
_messages = newMessages;
});
});
} else {
@@ -269,7 +268,7 @@ class _MessageListViewState extends State<MessageListView> {
}
} else {
setState(() {
this._messages = newMessages;
_messages = newMessages;
});
}
});
+8 -6
View File
@@ -18,13 +18,13 @@ class MessageWidget extends StatefulWidget {
@required this.previousMessage,
@required this.message,
@required this.nextMessage,
this.parentTapCallback,
this.onThreadSelect,
}) : super(key: key);
final Message previousMessage;
final Message message;
final Message nextMessage;
final ParentTapCallback parentTapCallback;
final OnThreadSelectCallback onThreadSelect;
@override
_MessageWidgetState createState() => _MessageWidgetState();
@@ -50,7 +50,7 @@ class _MessageWidgetState extends State<MessageWidget>
final alignment =
isMyMessage ? Alignment.centerRight : Alignment.centerLeft;
List<Widget> row = <Widget>[
var row = <Widget>[
Column(
crossAxisAlignment:
isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start,
@@ -59,7 +59,9 @@ class _MessageWidgetState extends State<MessageWidget>
widget.message.replyCount > 0
? GestureDetector(
onTap: () {
widget?.parentTapCallback(widget.message);
if (widget.onThreadSelect != null) {
widget.onThreadSelect(widget.message);
}
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
@@ -122,7 +124,7 @@ class _MessageWidgetState extends State<MessageWidget>
bool isMyMessage,
bool isLastUser,
) {
int nOfAttachmentWidgets = 0;
var nOfAttachmentWidgets = 0;
final column = Column(
crossAxisAlignment:
@@ -302,7 +304,7 @@ class _MessageWidgetState extends State<MessageWidget>
);
}
_launchURL(String url) async {
Future<void> _launchURL(String url) async {
if (await canLaunch(url)) {
await launch(url);
} else {
+60 -27
View File
@@ -4,22 +4,45 @@ 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<ChannelState> get channelStateStream =>
channelClient.state.channelStateStream;
class StreamChannel extends StatefulWidget {
StreamChannel({
Key key,
@required Widget child,
@required this.channelClient,
@required this.child,
this.channelClient,
}) : super(
key: key,
child: child,
);
final Widget child;
final ChannelClient channelClient;
static StreamChannelState of(BuildContext context) {
StreamChannelState streamChannelState;
streamChannelState = context.findAncestorStateOfType<StreamChannelState>();
if (streamChannelState == null) {
throw Exception(
'You must have a StreamChannel widget at the top of your widget tree');
}
return streamChannelState;
}
@override
StreamChannelState createState() => StreamChannelState(channelClient);
}
class StreamChannelState extends State<StreamChannel> {
final ChannelClient channelClient;
StreamChannelState(this.channelClient);
ChannelState get channelState => channelClient.state.channelState;
Stream<ChannelState> get channelStateStream =>
channelClient.state.channelStateStream;
final BehaviorSubject<bool> _queryMessageController = BehaviorSubject();
Stream<bool> get queryMessage => _queryMessageController.stream;
@@ -69,31 +92,41 @@ class StreamChannel extends InheritedWidget {
});
}
@override
void dispose() {
_queryMessageController.close();
channelClient.dispose();
super.dispose();
}
@override
bool updateShouldNotify(InheritedWidget oldWidget) {
return true;
void initState() {
super.initState();
}
static StreamChannel of(BuildContext context, [bool listen = false]) {
StreamChannel streamChannel;
if (listen) {
streamChannel =
context.dependOnInheritedWidgetOfExactType<StreamChannel>();
} else {
streamChannel = context.findAncestorWidgetOfExactType<StreamChannel>();
@override
Widget build(BuildContext context) {
if (channelClient == null) {
return Center(
child: CircularProgressIndicator(),
);
}
if (streamChannel == null) {
throw Exception(
'You must have a StreamChannel widget at the top of your widget tree');
}
return streamChannel;
return FutureBuilder<bool>(
future: channelClient.initialized,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(),
);
} else if (snapshot.hasError) {
return Center(
child: Text(snapshot.error),
);
} else {
return widget.child;
}
},
);
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ class StreamChat extends InheritedWidget {
Stream<User> get userStream => _userController.stream;
final BehaviorSubject<User> _userController = BehaviorSubject();
void setUser(User newUser, [String token]) async {
Future<void> setUser(User newUser, [String token]) async {
_userController.sink.add(null);
try {
+1 -1
View File
@@ -15,7 +15,7 @@ class UserAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CircleAvatar(
radius: this.radius,
radius: radius,
backgroundImage: user.extraData.containsKey('image')
? CachedNetworkImageProvider(user.extraData['image'] as String)
: null,