experiment with streambuilder
This commit is contained in:
@@ -213,7 +213,7 @@ class StreamChatClient {
|
|||||||
_wsConnectionStatusController.add(status);
|
_wsConnectionStatusController.add(status);
|
||||||
|
|
||||||
/// The current status value of the websocket connection
|
/// The current status value of the websocket connection
|
||||||
ConnectionStatus? get wsConnectionStatus =>
|
ConnectionStatus get wsConnectionStatus =>
|
||||||
_wsConnectionStatusController.value;
|
_wsConnectionStatusController.value;
|
||||||
|
|
||||||
/// This notifies the connection status of the websocket connection.
|
/// This notifies the connection status of the websocket connection.
|
||||||
|
|||||||
@@ -83,14 +83,14 @@ class ChannelImage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final streamChat = StreamChat.of(context);
|
final streamChat = StreamChat.of(context);
|
||||||
final channel = this.channel ?? StreamChannel.of(context).channel;
|
final channel = this.channel ?? StreamChannel.of(context).channel;
|
||||||
return StreamBuilder<Map<String, dynamic>>(
|
return BetterStreamBuilder<Map<String, dynamic>>(
|
||||||
stream: channel.extraDataStream,
|
stream: channel.extraDataStream,
|
||||||
initialData: channel.extraData,
|
initialData: channel.extraData,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
String? image;
|
String? image;
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
if (snapshot.data!.containsKey('image') == true) {
|
if (snapshot.containsKey('image') == true) {
|
||||||
image = snapshot.data!['image'];
|
image = snapshot['image'];
|
||||||
} else if (channel.state?.members.length == 2) {
|
} else if (channel.state?.members.length == 2) {
|
||||||
final otherMember = channel.state?.members
|
final otherMember = channel.state?.members
|
||||||
.firstWhere((member) => member.user?.id != streamChat.user?.id);
|
.firstWhere((member) => member.user?.id != streamChat.user?.id);
|
||||||
@@ -153,9 +153,7 @@ class ChannelImage extends StatelessWidget {
|
|||||||
imageUrl: image,
|
imageUrl: image,
|
||||||
errorWidget: (_, __, ___) => Center(
|
errorWidget: (_, __, ___) => Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
snapshot.data?.containsKey('name') ?? false
|
snapshot.containsKey('name') ? snapshot['name'][0] : '',
|
||||||
? snapshot.data!['name'][0]
|
|
||||||
: '',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: chatThemeData.colorTheme.white,
|
color: chatThemeData.colorTheme.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -25,13 +25,14 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
return StreamBuilder<List<Member>>(
|
return BetterStreamBuilder<List<Member>>(
|
||||||
stream: channel.state?.membersStream,
|
stream: channel.state!.membersStream,
|
||||||
|
initialData: channel.state!.members,
|
||||||
builder: (context, snapshot) => ConnectionStatusBuilder(
|
builder: (context, snapshot) => ConnectionStatusBuilder(
|
||||||
statusBuilder: (context, status) {
|
statusBuilder: (context, status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
return _buildConnectedTitleState(context, snapshot.data);
|
return _buildConnectedTitleState(context, snapshot);
|
||||||
case ConnectionStatus.connecting:
|
case ConnectionStatus.connecting:
|
||||||
return _buildConnectingTitleState(context);
|
return _buildConnectingTitleState(context);
|
||||||
case ConnectionStatus.disconnected:
|
case ConnectionStatus.disconnected:
|
||||||
|
|||||||
@@ -26,11 +26,14 @@ class ChannelName extends StatelessWidget {
|
|||||||
final client = StreamChat.of(context);
|
final client = StreamChat.of(context);
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
|
||||||
return StreamBuilder<Map<String, dynamic>>(
|
return BetterStreamBuilder<Map<String, Object?>>(
|
||||||
stream: channel.extraDataStream,
|
stream: channel.extraDataStream,
|
||||||
initialData: channel.extraData,
|
initialData: channel.extraData,
|
||||||
builder: (context, snapshot) =>
|
builder: (context, snapshot) => _buildName(
|
||||||
_buildName(snapshot.data!, channel.state?.members, client),
|
snapshot,
|
||||||
|
channel.state?.members,
|
||||||
|
client,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ class ConnectionStatusBuilder extends StatelessWidget {
|
|||||||
const ConnectionStatusBuilder({
|
const ConnectionStatusBuilder({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.statusBuilder,
|
required this.statusBuilder,
|
||||||
this.initialStatus = ConnectionStatus.disconnected,
|
this.initialStatus,
|
||||||
this.connectionStatusStream,
|
this.connectionStatusStream,
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
this.loadingBuilder,
|
this.loadingBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The connection status that will be used to create the initial snapshot.
|
/// The connection status that will be used to create the initial snapshot.
|
||||||
final ConnectionStatus initialStatus;
|
final ConnectionStatus? initialStatus;
|
||||||
|
|
||||||
/// The asynchronous computation to which this builder is currently connected.
|
/// The asynchronous computation to which this builder is currently connected.
|
||||||
final Stream<ConnectionStatus>? connectionStatusStream;
|
final Stream<ConnectionStatus>? connectionStatusStream;
|
||||||
@@ -37,23 +37,19 @@ class ConnectionStatusBuilder extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final stream = connectionStatusStream ??
|
final client = StreamChat.of(context).client;
|
||||||
StreamChat.of(context).client.wsConnectionStatusStream;
|
final stream = connectionStatusStream ?? client.wsConnectionStatusStream;
|
||||||
return StreamBuilder<ConnectionStatus>(
|
return BetterStreamBuilder<ConnectionStatus>(
|
||||||
|
initialData: initialStatus ?? client.wsConnectionStatus,
|
||||||
stream: stream,
|
stream: stream,
|
||||||
builder: (context, snapshot) {
|
loadingBuilder: loadingBuilder,
|
||||||
if (snapshot.hasError) {
|
errorBuilder: (context, error) {
|
||||||
if (errorBuilder != null) {
|
if (errorBuilder != null) {
|
||||||
return errorBuilder!(context, snapshot.error);
|
return errorBuilder!(context, error);
|
||||||
}
|
|
||||||
return const Offstage();
|
|
||||||
}
|
}
|
||||||
if (!snapshot.hasData) {
|
return const Offstage();
|
||||||
if (loadingBuilder != null) return loadingBuilder!(context);
|
|
||||||
return const Offstage();
|
|
||||||
}
|
|
||||||
return statusBuilder(context, snapshot.data!);
|
|
||||||
},
|
},
|
||||||
|
builder: statusBuilder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -606,8 +606,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
||||||
stream: Rx.combineLatest2(
|
stream: Rx.combineLatest2(
|
||||||
streamChannel!.channel.state!.isUpToDateStream,
|
streamChannel!.channel.state!.isUpToDateStream.distinct(),
|
||||||
streamChannel!.channel.state!.unreadCountStream,
|
streamChannel!.channel.state!.unreadCountStream.distinct(),
|
||||||
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
||||||
),
|
),
|
||||||
builder: (_, snapshot) {
|
builder: (_, snapshot) {
|
||||||
@@ -689,23 +689,18 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final stream = direction == QueryDirection.top
|
final stream = direction == QueryDirection.top
|
||||||
? streamChannel.queryTopMessages
|
? streamChannel.queryTopMessages
|
||||||
: streamChannel.queryBottomMessages;
|
: streamChannel.queryBottomMessages;
|
||||||
return StreamBuilder<bool>(
|
return BetterStreamBuilder<bool>(
|
||||||
key: Key('LOADING-INDICATOR $direction'),
|
key: Key('LOADING-INDICATOR $direction'),
|
||||||
stream: stream,
|
stream: stream,
|
||||||
initialData: false,
|
initialData: false,
|
||||||
|
errorBuilder: (context, error) => Container(
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.2),
|
||||||
|
child: const Center(
|
||||||
|
child: Text('Error loading messages'),
|
||||||
|
),
|
||||||
|
),
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (!snapshot) {
|
||||||
return Container(
|
|
||||||
color: StreamChatTheme.of(context)
|
|
||||||
.colorTheme
|
|
||||||
.accentRed
|
|
||||||
.withOpacity(.2),
|
|
||||||
child: const Center(
|
|
||||||
child: Text('Error loading messages'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!snapshot.data!) {
|
|
||||||
if (!_isThreadConversation && direction == QueryDirection.top) {
|
if (!_isThreadConversation && direction == QueryDirection.top) {
|
||||||
return const SizedBox(
|
return const SizedBox(
|
||||||
height: 52,
|
height: 52,
|
||||||
@@ -1163,14 +1158,14 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => StreamBuilder<Message>(
|
builder: (_) => BetterStreamBuilder<Message>(
|
||||||
stream: streamChannel!.channel.state!.messagesStream.map(
|
stream: streamChannel!.channel.state!.messagesStream.map(
|
||||||
(messages) =>
|
(messages) =>
|
||||||
messages!.firstWhere((m) => m.id == message.id)),
|
messages!.firstWhere((m) => m.id == message.id)),
|
||||||
initialData: message,
|
initialData: message,
|
||||||
builder: (_, snapshot) => StreamChannel(
|
builder: (_, snapshot) => StreamChannel(
|
||||||
channel: streamChannel!.channel,
|
channel: streamChannel!.channel,
|
||||||
child: widget.threadBuilder!(context, snapshot.data),
|
child: widget.threadBuilder!(context, snapshot),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -449,182 +449,189 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
final leftPadding =
|
final leftPadding =
|
||||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||||
|
|
||||||
return Portal(
|
return Material(
|
||||||
child: GestureDetector(
|
type: MaterialType.transparency,
|
||||||
onTap: () {
|
child: Portal(
|
||||||
widget.onMessageTap!(widget.message);
|
child: InkWell(
|
||||||
},
|
onTap: () {
|
||||||
onLongPress: widget.message.isDeleted && !isFailedState
|
widget.onMessageTap!(widget.message);
|
||||||
? null
|
},
|
||||||
: () => onLongPress(context),
|
onLongPress: widget.message.isDeleted && !isFailedState
|
||||||
child: Padding(
|
? null
|
||||||
padding: widget.padding ?? const EdgeInsets.all(8),
|
: () => onLongPress(context),
|
||||||
child: FractionallySizedBox(
|
child: Padding(
|
||||||
alignment:
|
padding: widget.padding ?? const EdgeInsets.all(8),
|
||||||
widget.reverse ? Alignment.centerRight : Alignment.centerLeft,
|
child: FractionallySizedBox(
|
||||||
widthFactor: 0.78,
|
alignment:
|
||||||
child: Column(
|
widget.reverse ? Alignment.centerRight : Alignment.centerLeft,
|
||||||
crossAxisAlignment: widget.reverse
|
widthFactor: 0.78,
|
||||||
? CrossAxisAlignment.end
|
child: Column(
|
||||||
: CrossAxisAlignment.start,
|
crossAxisAlignment: widget.reverse
|
||||||
mainAxisSize: MainAxisSize.min,
|
? CrossAxisAlignment.end
|
||||||
children: <Widget>[
|
: CrossAxisAlignment.start,
|
||||||
Stack(
|
mainAxisSize: MainAxisSize.min,
|
||||||
clipBehavior: Clip.none,
|
children: <Widget>[
|
||||||
alignment: widget.reverse
|
Stack(
|
||||||
? AlignmentDirectional.bottomEnd
|
clipBehavior: Clip.none,
|
||||||
: AlignmentDirectional.bottomStart,
|
alignment: widget.reverse
|
||||||
children: [
|
? AlignmentDirectional.bottomEnd
|
||||||
Column(
|
: AlignmentDirectional.bottomStart,
|
||||||
crossAxisAlignment: widget.reverse
|
children: [
|
||||||
? CrossAxisAlignment.end
|
Column(
|
||||||
: CrossAxisAlignment.start,
|
crossAxisAlignment: widget.reverse
|
||||||
mainAxisSize: MainAxisSize.min,
|
? CrossAxisAlignment.end
|
||||||
children: [
|
: CrossAxisAlignment.start,
|
||||||
Row(
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
Row(
|
||||||
children: <Widget>[
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
if (widget.showUserAvatar == DisplayWidget.show &&
|
mainAxisSize: MainAxisSize.min,
|
||||||
widget.message.user != null) ...[
|
children: <Widget>[
|
||||||
_buildUserAvatar(),
|
if (widget.showUserAvatar == DisplayWidget.show &&
|
||||||
const SizedBox(width: 4),
|
widget.message.user != null) ...[
|
||||||
],
|
_buildUserAvatar(),
|
||||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
const SizedBox(width: 4),
|
||||||
SizedBox(width: avatarWidth + 4),
|
],
|
||||||
Flexible(
|
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||||
child: PortalEntry(
|
SizedBox(width: avatarWidth + 4),
|
||||||
portal: Container(
|
Flexible(
|
||||||
transform: Matrix4.translationValues(
|
child: PortalEntry(
|
||||||
widget.reverse ? 12 : -12, 0, 0),
|
portal: Container(
|
||||||
constraints:
|
transform: Matrix4.translationValues(
|
||||||
const BoxConstraints(maxWidth: 22 * 6.0),
|
widget.reverse ? 12 : -12, 0, 0),
|
||||||
child: _buildReactionIndicator(context),
|
constraints: const BoxConstraints(
|
||||||
),
|
maxWidth: 22 * 6.0),
|
||||||
portalAnchor:
|
child: _buildReactionIndicator(context),
|
||||||
Alignment(widget.reverse ? 1 : -1, -1),
|
),
|
||||||
childAnchor:
|
portalAnchor:
|
||||||
Alignment(widget.reverse ? -1 : 1, -1),
|
Alignment(widget.reverse ? 1 : -1, -1),
|
||||||
child: Stack(
|
childAnchor:
|
||||||
clipBehavior: Clip.none,
|
Alignment(widget.reverse ? -1 : 1, -1),
|
||||||
children: [
|
child: Stack(
|
||||||
Padding(
|
clipBehavior: Clip.none,
|
||||||
padding: widget.showReactions
|
children: [
|
||||||
? EdgeInsets.only(
|
Padding(
|
||||||
top: widget.message.reactionCounts
|
padding: widget.showReactions
|
||||||
?.isNotEmpty ==
|
? EdgeInsets.only(
|
||||||
true
|
top: widget
|
||||||
? 18
|
.message
|
||||||
: 0,
|
.reactionCounts
|
||||||
)
|
?.isNotEmpty ==
|
||||||
: EdgeInsets.zero,
|
true
|
||||||
child: (widget.message.isDeleted &&
|
? 18
|
||||||
!isFailedState)
|
: 0,
|
||||||
? Container(
|
)
|
||||||
// ignore: lines_longer_than_80_chars
|
: EdgeInsets.zero,
|
||||||
margin: EdgeInsets.symmetric(
|
child: (widget.message.isDeleted &&
|
||||||
horizontal:
|
!isFailedState)
|
||||||
|
? Container(
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal:
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
widget.showUserAvatar ==
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
DisplayWidget
|
||||||
|
.gone
|
||||||
|
? 0
|
||||||
|
: 4.0),
|
||||||
|
child: DeletedMessage(
|
||||||
|
borderRadiusGeometry: widget
|
||||||
|
.borderRadiusGeometry,
|
||||||
|
borderSide: widget.borderSide,
|
||||||
|
shape: widget.shape,
|
||||||
|
messageTheme:
|
||||||
|
widget.messageTheme,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
elevation: 0,
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal: (isFailedState
|
||||||
|
? 15.0
|
||||||
|
: 0.0) +
|
||||||
// ignore: lines_longer_than_80_chars
|
// ignore: lines_longer_than_80_chars
|
||||||
widget.showUserAvatar ==
|
(widget.showUserAvatar ==
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
DisplayWidget.gone
|
DisplayWidget.gone
|
||||||
? 0
|
? 0
|
||||||
: 4.0),
|
: 4.0),
|
||||||
child: DeletedMessage(
|
),
|
||||||
borderRadiusGeometry:
|
shape: widget.shape ??
|
||||||
widget.borderRadiusGeometry,
|
RoundedRectangleBorder(
|
||||||
borderSide: widget.borderSide,
|
side: widget.borderSide ??
|
||||||
shape: widget.shape,
|
BorderSide(
|
||||||
messageTheme:
|
color: widget
|
||||||
widget.messageTheme,
|
// ignore: lines_longer_than_80_chars
|
||||||
),
|
.messageTheme
|
||||||
)
|
// ignore: lines_longer_than_80_chars
|
||||||
: Card(
|
.messageBorderColor ??
|
||||||
clipBehavior: Clip.antiAlias,
|
Colors.grey,
|
||||||
elevation: 0,
|
),
|
||||||
margin: EdgeInsets.symmetric(
|
borderRadius: widget
|
||||||
horizontal: (isFailedState
|
// ignore: lines_longer_than_80_chars
|
||||||
? 15.0
|
.borderRadiusGeometry ??
|
||||||
: 0.0) +
|
BorderRadius.zero,
|
||||||
// ignore: lines_longer_than_80_chars
|
),
|
||||||
(widget.showUserAvatar ==
|
color: _getBackgroundColor(),
|
||||||
DisplayWidget.gone
|
child: Column(
|
||||||
? 0
|
crossAxisAlignment:
|
||||||
: 4.0),
|
CrossAxisAlignment.end,
|
||||||
),
|
mainAxisSize:
|
||||||
shape: widget.shape ??
|
MainAxisSize.min,
|
||||||
RoundedRectangleBorder(
|
children: <Widget>[
|
||||||
side: widget.borderSide ??
|
if (hasQuotedMessage)
|
||||||
BorderSide(
|
_buildQuotedMessage(),
|
||||||
color: widget
|
if (hasNonUrlAttachments)
|
||||||
// ignore: lines_longer_than_80_chars
|
_parseAttachments(),
|
||||||
.messageTheme
|
if (!isGiphy)
|
||||||
// ignore: lines_longer_than_80_chars
|
_buildTextBubble(),
|
||||||
.messageBorderColor ??
|
],
|
||||||
Colors.grey,
|
),
|
||||||
),
|
|
||||||
borderRadius: widget
|
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
.borderRadiusGeometry ??
|
|
||||||
BorderRadius.zero,
|
|
||||||
),
|
|
||||||
color: _getBackgroundColor(),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.end,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
if (hasQuotedMessage)
|
|
||||||
_buildQuotedMessage(),
|
|
||||||
if (hasNonUrlAttachments)
|
|
||||||
_parseAttachments(),
|
|
||||||
if (!isGiphy)
|
|
||||||
_buildTextBubble(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (widget.showReactionPickerIndicator)
|
||||||
|
Positioned(
|
||||||
|
right: widget.reverse ? null : 4,
|
||||||
|
left: widget.reverse ? 4 : null,
|
||||||
|
top: -8,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: ReactionBubblePainter(
|
||||||
|
StreamChatTheme.of(context)
|
||||||
|
.colorTheme
|
||||||
|
.white,
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.transparent,
|
||||||
|
tailCirclesSpace: 1,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (widget.showReactionPickerIndicator)
|
|
||||||
Positioned(
|
|
||||||
right: widget.reverse ? null : 4,
|
|
||||||
left: widget.reverse ? 4 : null,
|
|
||||||
top: -8,
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: ReactionBubblePainter(
|
|
||||||
StreamChatTheme.of(context)
|
|
||||||
.colorTheme
|
|
||||||
.white,
|
|
||||||
Colors.transparent,
|
|
||||||
Colors.transparent,
|
|
||||||
tailCirclesSpace: 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
|
if (showBottomRow)
|
||||||
|
SizedBox(height: context.textScaleFactor * 18.0),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (showBottomRow)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: leftPadding),
|
||||||
|
child: _bottomRow,
|
||||||
),
|
),
|
||||||
if (showBottomRow)
|
if (isFailedState)
|
||||||
SizedBox(height: context.textScaleFactor * 18.0),
|
Positioned(
|
||||||
],
|
left: widget.reverse ? 0 : null,
|
||||||
),
|
right: widget.reverse ? null : 0,
|
||||||
if (showBottomRow)
|
bottom: showBottomRow ? 18 : -2,
|
||||||
Padding(
|
child: StreamSvgIcon.error(size: 20),
|
||||||
padding: EdgeInsets.only(left: leftPadding),
|
),
|
||||||
child: _bottomRow,
|
],
|
||||||
),
|
),
|
||||||
if (isFailedState)
|
],
|
||||||
Positioned(
|
),
|
||||||
left: widget.reverse ? 0 : null,
|
|
||||||
right: widget.reverse ? null : 0,
|
|
||||||
bottom: showBottomRow ? 18 : -2,
|
|
||||||
child: StreamSvgIcon.error(size: 20),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/utils.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
/// Widget to show the current list of typing users
|
/// Widget to show the current list of typing users
|
||||||
@@ -41,11 +42,12 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
child: alternativeWidget ?? const Offstage(),
|
child: alternativeWidget ?? const Offstage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return StreamBuilder<List<User>>(
|
return BetterStreamBuilder<List<User>>(
|
||||||
|
initialData: channelState.typingEvents,
|
||||||
stream: channelState.typingEventsStream,
|
stream: channelState.typingEventsStream,
|
||||||
builder: (context, snapshot) => AnimatedSwitcher(
|
builder: (context, snapshot) => AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: snapshot.data?.isNotEmpty == true
|
child: snapshot.isNotEmpty == true
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Align(
|
child: Align(
|
||||||
@@ -61,7 +63,7 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
// ignore: lines_longer_than_80_chars
|
// ignore: lines_longer_than_80_chars
|
||||||
' ${snapshot.data![0].name}${snapshot.data!.length == 1 ? '' : ' and ${snapshot.data!.length - 1} more'} ${snapshot.data!.length == 1 ? 'is' : 'are'} typing',
|
' ${snapshot[0].name}${snapshot.length == 1 ? '' : ' and ${snapshot.length - 1} more'} ${snapshot.length == 1 ? 'is' : 'are'} typing',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class UnreadIndicator extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: StreamBuilder<int?>(
|
child: BetterStreamBuilder<int?>(
|
||||||
stream: cid != null
|
stream: cid != null
|
||||||
? client.state.channels[cid]?.state?.unreadCountStream
|
? client.state.channels[cid]?.state?.unreadCountStream
|
||||||
: client.state.totalUnreadCountStream,
|
: client.state.totalUnreadCountStream,
|
||||||
@@ -25,8 +25,8 @@ class UnreadIndicator extends StatelessWidget {
|
|||||||
? client.state.channels[cid]?.state?.unreadCount
|
? client.state.channels[cid]?.state?.unreadCount
|
||||||
: client.state.totalUnreadCount,
|
: client.state.totalUnreadCount,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (!snapshot.hasData || snapshot.data == 0) {
|
if (snapshot == null || snapshot == 0) {
|
||||||
return const SizedBox();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
return Material(
|
return Material(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -42,7 +42,7 @@ class UnreadIndicator extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${snapshot.data! > 99 ? '99+' : snapshot.data}',
|
'${snapshot > 99 ? '99+' : snapshot}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class BetterStreamBuilder<T> extends StatefulWidget {
|
||||||
|
const BetterStreamBuilder({
|
||||||
|
required this.stream,
|
||||||
|
required this.initialData,
|
||||||
|
required this.builder,
|
||||||
|
this.loadingBuilder,
|
||||||
|
this.errorBuilder,
|
||||||
|
this.comparator,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Stream<T>? stream;
|
||||||
|
final T initialData;
|
||||||
|
final bool Function(T?, T)? comparator;
|
||||||
|
final Widget Function(BuildContext context, T data) builder;
|
||||||
|
final Widget Function(BuildContext context)? loadingBuilder;
|
||||||
|
final Widget Function(BuildContext context, Object error)? errorBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_BetterStreamBuilderState createState() => _BetterStreamBuilderState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BetterStreamBuilderState<T> extends State<BetterStreamBuilder<T>> {
|
||||||
|
Widget? _child;
|
||||||
|
T? _lastEvent;
|
||||||
|
StreamSubscription? _subscription;
|
||||||
|
Object? _lastError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => _child ?? const Offstage();
|
||||||
|
|
||||||
|
bool _firstTime = true;
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
if (_firstTime) {
|
||||||
|
if (widget.initialData == null && widget.loadingBuilder != null) {
|
||||||
|
_child = widget.loadingBuilder!(context);
|
||||||
|
} else {
|
||||||
|
_onEvent(widget.initialData);
|
||||||
|
}
|
||||||
|
_lastEvent = widget.initialData;
|
||||||
|
_subscription = widget.stream?.listen(
|
||||||
|
_onEvent,
|
||||||
|
onError: _onError,
|
||||||
|
);
|
||||||
|
|
||||||
|
_firstTime = false;
|
||||||
|
}
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant BetterStreamBuilder<T> oldWidget) {
|
||||||
|
if (_lastError != null && oldWidget.errorBuilder != widget.errorBuilder) {
|
||||||
|
_onError(_lastError);
|
||||||
|
} else if (oldWidget.builder != widget.builder) {
|
||||||
|
_onEvent(_lastEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldWidget.stream != widget.stream) {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_subscription = widget.stream?.listen(
|
||||||
|
_onEvent,
|
||||||
|
onError: _onError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onError(error) {
|
||||||
|
if (widget.errorBuilder != null && error != _lastError) {
|
||||||
|
setState(() {
|
||||||
|
_child = widget.errorBuilder!(context, error);
|
||||||
|
});
|
||||||
|
_lastError = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEvent(event) {
|
||||||
|
_lastError = null;
|
||||||
|
if (widget.comparator != null
|
||||||
|
? widget.comparator!(_lastEvent, event)
|
||||||
|
: event != _lastEvent) {
|
||||||
|
setState(() {
|
||||||
|
_child = widget.builder(context, event);
|
||||||
|
});
|
||||||
|
_lastEvent = event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
|
||||||
import 'package:stream_chat_flutter_core/src/stream_channel.dart';
|
import 'package:stream_chat_flutter_core/src/stream_channel.dart';
|
||||||
import 'package:stream_chat_flutter_core/src/typedef.dart';
|
import 'package:stream_chat_flutter_core/src/typedef.dart';
|
||||||
|
|
||||||
@@ -127,6 +129,10 @@ class MessageListCoreState extends State<MessageListCore> {
|
|||||||
.map((threads) => threads[widget.parentMessage!.id])
|
.map((threads) => threads[widget.parentMessage!.id])
|
||||||
: _streamChannel!.channel.state?.messagesStream;
|
: _streamChannel!.channel.state?.messagesStream;
|
||||||
|
|
||||||
|
final initialData = _isThreadConversation
|
||||||
|
? _streamChannel!.channel.state?.threads[widget.parentMessage!.id]
|
||||||
|
: _streamChannel!.channel.state?.messages;
|
||||||
|
|
||||||
bool defaultFilter(Message m) {
|
bool defaultFilter(Message m) {
|
||||||
final isMyMessage = m.user?.id == _currentUser?.id;
|
final isMyMessage = m.user?.id == _currentUser?.id;
|
||||||
final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true;
|
final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true;
|
||||||
@@ -134,30 +140,27 @@ class MessageListCoreState extends State<MessageListCore> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return StreamBuilder<List<Message>?>(
|
return BetterStreamBuilder<List<Message>?>(
|
||||||
stream: messagesStream?.map(
|
initialData: initialData,
|
||||||
|
comparator: const ListEquality().equals,
|
||||||
|
stream: messagesStream!.map(
|
||||||
(messages) =>
|
(messages) =>
|
||||||
messages?.where(widget.messageFilter ?? defaultFilter).toList(
|
messages?.where(widget.messageFilter ?? defaultFilter).toList(
|
||||||
growable: false,
|
growable: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
errorBuilder: widget.errorWidgetBuilder,
|
||||||
|
loadingBuilder: widget.loadingBuilder,
|
||||||
builder: (context, snapshot) {
|
builder: (context, snapshot) {
|
||||||
if (snapshot.hasError) {
|
final messageList = snapshot?.reversed.toList(growable: false) ?? [];
|
||||||
return widget.errorWidgetBuilder(context, snapshot.error!);
|
if (messageList.isEmpty && !_isThreadConversation) {
|
||||||
} else if (!snapshot.hasData) {
|
if (_upToDate) {
|
||||||
return widget.loadingBuilder(context);
|
return widget.emptyBuilder(context);
|
||||||
} else {
|
|
||||||
final messageList =
|
|
||||||
snapshot.data?.reversed.toList(growable: false) ?? [];
|
|
||||||
if (messageList.isEmpty && !_isThreadConversation) {
|
|
||||||
if (_upToDate) {
|
|
||||||
return widget.emptyBuilder(context);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_messages = messageList;
|
|
||||||
}
|
}
|
||||||
return widget.messageListBuilder(context, _messages);
|
} else {
|
||||||
|
_messages = messageList;
|
||||||
}
|
}
|
||||||
|
return widget.messageListBuilder(context, _messages);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ library stream_chat_flutter_core;
|
|||||||
|
|
||||||
export 'package:stream_chat/stream_chat.dart';
|
export 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
export 'src/better_stream_builder.dart';
|
||||||
export 'src/channel_list_core.dart' hide ChannelListCoreState;
|
export 'src/channel_list_core.dart' hide ChannelListCoreState;
|
||||||
export 'src/channels_bloc.dart';
|
export 'src/channels_bloc.dart';
|
||||||
export 'src/lazy_load_scroll_view.dart';
|
export 'src/lazy_load_scroll_view.dart';
|
||||||
|
|||||||
Reference in New Issue
Block a user