add single_conversation example
This commit is contained in:
@@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user