feat: Added first widgets

This commit is contained in:
Deven Joshi
2021-01-21 17:55:31 +05:30
parent 803b42307b
commit e0474b8ded
7 changed files with 1848 additions and 0 deletions
@@ -0,0 +1,148 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
enum _LoadingStatus { LOADING, STABLE }
/// A widget that wraps a [Widget] and will trigger [onEndOfPage]/[onStartOfPage] when it
/// reaches the bottom/start of the list
class LazyLoadScrollView extends StatefulWidget {
/// The [Widget] that this widget watches for changes on
final Widget child;
/// Called when the [child] reaches the start of the list
final AsyncCallback onStartOfPage;
/// Called when the [child] reaches the end of the list
final AsyncCallback onEndOfPage;
/// Called when the list scrolling starts
final VoidCallback onPageScrollStart;
/// Called when the list scrolling ends
final VoidCallback onPageScrollEnd;
/// Called every time the [child] is in-between the list
final VoidCallback onInBetweenOfPage;
/// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels
final double scrollOffset;
/// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder
final bool isLoading;
/// Initiates a LazyLoadScrollView widget
const LazyLoadScrollView({
Key key,
@required this.child,
this.onStartOfPage,
this.onEndOfPage,
this.onPageScrollStart,
this.onPageScrollEnd,
this.onInBetweenOfPage,
this.isLoading = false,
this.scrollOffset = 100,
}) : assert(child != null),
super(key: key);
@override
State<StatefulWidget> createState() => _LazyLoadScrollViewState();
}
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
double _scrollPosition = 0.0;
@override
Widget build(BuildContext context) {
return NotificationListener(
child: widget.child,
onNotification: _onNotification,
);
}
bool _onNotification(Notification notification) {
if (notification is ScrollStartNotification) {
if (widget.onPageScrollStart != null) {
widget.onPageScrollStart();
return true;
}
}
if (notification is ScrollEndNotification) {
if (widget.onPageScrollEnd != null) {
widget.onPageScrollEnd();
return true;
}
}
if (notification is ScrollUpdateNotification) {
final pixels = notification.metrics.pixels;
final maxScrollExtent = notification.metrics.maxScrollExtent;
final minScrollExtent = notification.metrics.minScrollExtent;
final scrollOffset = widget.scrollOffset;
if (pixels > (minScrollExtent + scrollOffset) &&
pixels < (maxScrollExtent - scrollOffset)) {
if (widget.onInBetweenOfPage != null) {
widget.onInBetweenOfPage();
return true;
}
}
final extentBefore = notification.metrics.extentBefore;
final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels;
if (scrollOffset == null || scrollOffset == 0) {
if (extentAfter == 0) {
_onEndOfPage();
}
if (extentBefore == 0) {
_onStartOfPage();
}
} else {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
} else {
if (extentBefore <= scrollOffset) {
_onStartOfPage();
}
}
}
_scrollPosition = pixels;
return true;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
_onEndOfPage();
}
if (notification.overscroll < 0) {
_onStartOfPage();
}
return true;
}
return false;
}
void _onEndOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (widget.onEndOfPage != null) {
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
}
}
void _onStartOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (widget.onStartOfPage != null) {
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
});
}
}
}
}
@@ -0,0 +1,9 @@
class ReactionIcon {
final String type;
final String assetName;
ReactionIcon({
this.type,
this.assetName,
});
}
@@ -0,0 +1,177 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_theme.dart';
/// Widget used to provide information about the chat to the widget tree
///
/// class MyApp extends StatelessWidget {
/// final Client client;
///
/// MyApp(this.client);
///
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// home: Container(
/// child: StreamChat(
/// client: client,
/// child: ChannelListPage(),
/// ),
/// ),
/// );
/// }
/// }
///
/// Use [StreamChat.of] to get the current [StreamChatState] instance.
class StreamChat extends StatefulWidget {
final Client client;
final Widget child;
final StreamChatThemeData streamChatThemeData;
StreamChat({
Key key,
@required this.client,
@required this.child,
this.streamChatThemeData,
}) : super(
key: key,
);
@override
StreamChatState createState() => StreamChatState();
/// Use this method to get the current [StreamChatState] instance
static StreamChatState of(BuildContext context) {
StreamChatState streamChatState;
streamChatState = context.findAncestorStateOfType<StreamChatState>();
if (streamChatState == null) {
throw Exception(
'You must have a StreamChat widget at the top of your widget tree');
}
return streamChatState;
}
}
/// The current state of the StreamChat widget
class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
Client get client => widget.client;
Timer _disconnectTimer;
@override
Widget build(BuildContext context) {
final theme = _getTheme(context, widget.streamChatThemeData);
return StreamChatTheme(
data: theme,
child: Builder(
builder: (context) {
final materialTheme = Theme.of(context);
final streamTheme = StreamChatTheme.of(context);
return Theme(
data: materialTheme.copyWith(
primaryIconTheme: streamTheme.primaryIconTheme,
accentColor: streamTheme.colorTheme.accentBlue,
scaffoldBackgroundColor: streamTheme.colorTheme.white,
),
child: widget.child,
);
},
),
);
}
StreamChatThemeData _getTheme(
BuildContext context,
StreamChatThemeData themeData,
) {
final defaultTheme = StreamChatThemeData.getDefaultTheme(Theme.of(context));
return defaultTheme.merge(themeData) ?? themeData;
}
/// The current user
User get user => widget.client.state.user;
/// The current user as a stream
Stream<User> get userStream => widget.client.state.userStream;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// client.state?.totalUnreadCountStream?.listen((count) {
// if (count > 0) {
// FlutterAppBadger.updateBadgeCount(count);
// } else {
// FlutterAppBadger.removeBadge();
// }
// });
}
StreamSubscription _newMessageSubscription;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (client.state?.user != null) {
if (state == AppLifecycleState.paused) {
if (client.showLocalNotification != null) {
_newMessageSubscription = client
.on(EventType.messageNew)
.where((e) => e.user?.id != user.id)
.where((e) => e.message.silent != true)
.where((e) => e.message.shadowed != true)
.listen((event) async {
final channel = client.channel(
event.channelType,
id: event.channelId,
);
client.showLocalNotification(
event.message,
ChannelModel(
id: channel.id,
createdAt: channel.createdAt,
extraData: channel.extraData,
type: channel.type,
memberCount: channel.memberCount,
frozen: channel.frozen,
cid: channel.cid,
deletedAt: channel.deletedAt,
config: channel.config,
createdBy: channel.createdBy,
updatedAt: channel.updatedAt,
lastMessageAt: channel.lastMessageAt,
),
);
});
_disconnectTimer = Timer(client.backgroundKeepAlive, () {
client.disconnect();
});
} else {
client.disconnect();
}
} else if (state == AppLifecycleState.resumed) {
_newMessageSubscription?.cancel();
if (_disconnectTimer?.isActive == true) {
_disconnectTimer.cancel();
} else {
if (client.wsConnectionStatus.value ==
ConnectionStatus.disconnected) {
NotificationService.handleIosMessageQueue(client);
client.connect();
}
}
}
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
}
@@ -0,0 +1,898 @@
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/reaction_icon.dart';
/// Inherited widget providing the [StreamChatThemeData] to the widget tree
class StreamChatTheme extends InheritedWidget {
final StreamChatThemeData data;
StreamChatTheme({
Key key,
@required this.data,
Widget child,
}) : super(
key: key,
child: child,
);
@override
bool updateShouldNotify(StreamChatTheme old) {
return data != old.data;
}
/// Use this method to get the current [StreamChatThemeData] instance
static StreamChatThemeData of(BuildContext context) {
final streamChatTheme =
context.dependOnInheritedWidgetOfExactType<StreamChatTheme>();
if (streamChatTheme == null) {
throw Exception(
'You must have a StreamChatTheme widget at the top of your widget tree',
);
}
return streamChatTheme.data;
}
}
/// Theme data
class StreamChatThemeData {
/// The text themes used in the widgets
final TextTheme textTheme;
/// The button themes used in the widgets
final ButtonThemeData buttonTheme;
/// The text themes used in the widgets
final ColorTheme colorTheme;
/// Theme of the [ChannelPreview]
final ChannelPreviewTheme channelPreviewTheme;
/// Theme of the chat widgets dedicated to a channel
final ChannelTheme channelTheme;
/// Theme of the current user messages
final MessageTheme ownMessageTheme;
/// Theme of other users messages
final MessageTheme otherMessageTheme;
/// The widget that will be built when the channel image is unavailable
final Widget Function(BuildContext, Channel) defaultChannelImage;
/// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage;
/// Primary icon theme
final IconThemeData primaryIconTheme;
/// Assets used for rendering reactions
final List<ReactionIcon> reactionIcons;
/// Create a theme from scratch
const StreamChatThemeData({
this.textTheme,
this.buttonTheme,
this.colorTheme,
this.channelPreviewTheme,
this.channelTheme,
this.otherMessageTheme,
this.ownMessageTheme,
this.defaultChannelImage,
this.defaultUserImage,
this.primaryIconTheme,
this.reactionIcons,
});
/// Create a theme from a Material [Theme]
factory StreamChatThemeData.fromTheme(ThemeData theme) {
final defaultTheme = getDefaultTheme(theme);
final customizedTheme = StreamChatThemeData(
primaryIconTheme: theme.primaryIconTheme,
ownMessageTheme: MessageTheme(
replies: TextStyle(color: theme.accentColor),
messageLinks: TextStyle(color: theme.accentColor),
),
otherMessageTheme: MessageTheme(
replies: TextStyle(color: theme.accentColor),
messageLinks: TextStyle(color: theme.accentColor),
),
);
return defaultTheme.merge(customizedTheme) ?? customizedTheme;
}
/// Creates a copy of [StreamChatThemeData] with specified attributes overridden.
StreamChatThemeData copyWith({
TextTheme textTheme,
ButtonThemeData buttonTheme,
ColorTheme colorTheme,
ChannelPreviewTheme channelPreviewTheme,
ChannelTheme channelTheme,
MessageTheme ownMessageTheme,
MessageTheme otherMessageTheme,
Widget Function(BuildContext, Channel) defaultChannelImage,
Widget Function(BuildContext, User) defaultUserImage,
IconThemeData primaryIconTheme,
List<ReactionIcon> reactionIcons,
}) =>
StreamChatThemeData(
textTheme: textTheme ?? this.textTheme,
buttonTheme: buttonTheme ?? this.buttonTheme,
colorTheme: colorTheme ?? this.colorTheme,
primaryIconTheme: primaryIconTheme ?? this.primaryIconTheme,
defaultChannelImage: defaultChannelImage ?? this.defaultChannelImage,
defaultUserImage: defaultUserImage ?? this.defaultUserImage,
channelPreviewTheme: channelPreviewTheme ?? this.channelPreviewTheme,
channelTheme: channelTheme ?? this.channelTheme,
ownMessageTheme: ownMessageTheme ?? this.ownMessageTheme,
otherMessageTheme: otherMessageTheme ?? this.otherMessageTheme,
reactionIcons: reactionIcons ?? this.reactionIcons,
);
StreamChatThemeData merge(StreamChatThemeData other) {
if (other == null) return this;
return copyWith(
textTheme: textTheme?.merge(other.textTheme) ?? other.textTheme,
buttonTheme: other.buttonTheme,
colorTheme: colorTheme?.merge(other.colorTheme) ?? other.colorTheme,
primaryIconTheme: other.primaryIconTheme,
defaultChannelImage: other.defaultChannelImage,
defaultUserImage: other.defaultUserImage,
channelPreviewTheme:
channelPreviewTheme?.merge(other.channelPreviewTheme) ??
other.channelPreviewTheme,
channelTheme:
channelTheme?.merge(other.channelTheme) ?? other.channelTheme,
ownMessageTheme: ownMessageTheme?.merge(other.ownMessageTheme) ??
other.ownMessageTheme,
otherMessageTheme: otherMessageTheme?.merge(other.otherMessageTheme) ??
other.otherMessageTheme,
reactionIcons: other.reactionIcons,
);
}
/// Get the default Stream Chat theme
static StreamChatThemeData getDefaultTheme(ThemeData theme) {
final accentColor = Color(0xff006cff);
final isDark = theme.brightness == Brightness.dark;
final textTheme = isDark ? TextTheme.dark() : TextTheme.light();
final colorTheme = isDark ? ColorTheme.dark() : ColorTheme.light();
return StreamChatThemeData(
textTheme: textTheme,
colorTheme: colorTheme,
buttonTheme: ButtonThemeData(
height: 48.0,
buttonColor: isDark ? Color(0xffffffff) : Color(0xff006aff),
textTheme: ButtonTextTheme.accent,
colorScheme: theme.colorScheme.copyWith(
secondary: isDark ? Color(0xff005eff) : Color(0xffffffff),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
),
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
defaultChannelImage: (context, channel) => SizedBox(),
defaultUserImage: (context, user) => Center(
child: Image.network(
getRandomPicUrl(user),
filterQuality: FilterQuality.high,
fit: BoxFit.cover,
),
),
channelPreviewTheme: ChannelPreviewTheme(
unreadCounterColor: colorTheme.accentRed,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
title: textTheme.bodyBold,
subtitle: textTheme.footnote.copyWith(
color: Color(0xff7A7A7A),
),
lastMessageAt: textTheme.footnote.copyWith(
color: colorTheme.black.withOpacity(.5),
),
indicatorIconSize: 16.0),
channelTheme: ChannelTheme(
messageInputButtonIconTheme: theme.iconTheme.copyWith(
color: accentColor,
),
channelHeaderTheme: ChannelHeaderTheme(
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 40,
width: 40,
),
),
color: colorTheme.white,
title: TextStyle(
fontSize: 14,
color: colorTheme.black,
),
lastMessageAt: TextStyle(
fontSize: 11,
color: colorTheme.black.withOpacity(.5),
),
),
inputBackground: colorTheme.white.withAlpha(12),
),
ownMessageTheme: MessageTheme(
messageText: textTheme.body,
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
replies: textTheme.footnoteBold.copyWith(color: accentColor),
messageBackgroundColor: colorTheme.greyGainsboro,
reactionsBackgroundColor: colorTheme.white,
reactionsBorderColor: colorTheme.greyWhisper,
reactionsMaskColor: colorTheme.whiteSnow,
messageBorderColor: colorTheme.greyGainsboro,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 32,
width: 32,
),
),
messageLinks: TextStyle(
color: accentColor,
),
),
otherMessageTheme: MessageTheme(
reactionsBackgroundColor: colorTheme.greyGainsboro,
reactionsBorderColor: colorTheme.white,
reactionsMaskColor: colorTheme.whiteSnow,
messageText: textTheme.body,
createdAt: textTheme.footnote.copyWith(color: colorTheme.grey),
replies: textTheme.footnoteBold.copyWith(color: accentColor),
messageLinks: TextStyle(
color: accentColor,
),
messageBackgroundColor: colorTheme.white,
messageBorderColor: colorTheme.greyWhisper,
avatarTheme: AvatarTheme(
borderRadius: BorderRadius.circular(20),
constraints: BoxConstraints.tightFor(
height: 32,
width: 32,
),
),
),
reactionIcons: [
ReactionIcon(
type: 'love',
assetName: 'Icon_love_reaction.svg',
),
ReactionIcon(
type: 'like',
assetName: 'Icon_thumbs_up_reaction.svg',
),
ReactionIcon(
type: 'sad',
assetName: 'Icon_thumbs_down_reaction.svg',
),
ReactionIcon(
type: 'haha',
assetName: 'Icon_LOL_reaction.svg',
),
ReactionIcon(
type: 'wow',
assetName: 'Icon_wut_reaction.svg',
),
],
);
}
}
enum TextThemeType {
light,
dark,
}
class TextTheme {
final TextStyle title;
final TextStyle headlineBold;
final TextStyle headline;
final TextStyle bodyBold;
final TextStyle body;
final TextStyle footnoteBold;
final TextStyle footnote;
final TextStyle captionBold;
TextTheme.light({
this.title = const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.headlineBold = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.headline = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.bodyBold = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.black,
),
this.body = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.footnoteBold = const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.black,
),
this.footnote = const TextStyle(
fontSize: 12,
color: Colors.black,
),
this.captionBold = const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.black,
),
});
TextTheme.dark({
this.title = const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.headlineBold = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.headline = const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.bodyBold = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white,
),
this.body = const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.footnoteBold = const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white,
),
this.footnote = const TextStyle(
fontSize: 12,
color: Colors.white,
),
this.captionBold = const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
),
});
TextTheme copyWith({
TextThemeType type = TextThemeType.light,
TextStyle body,
TextStyle title,
TextStyle headlineBold,
TextStyle headline,
TextStyle bodyBold,
TextStyle footnoteBold,
TextStyle footnote,
TextStyle captionBold,
}) {
return type == TextThemeType.light
? TextTheme.light(
body: body ?? this.body,
title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold,
headline: headline ?? this.headline,
bodyBold: bodyBold ?? this.bodyBold,
footnoteBold: footnoteBold ?? this.footnoteBold,
footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold,
)
: TextTheme.dark(
body: body ?? this.body,
title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold,
headline: headline ?? this.headline,
bodyBold: bodyBold ?? this.bodyBold,
footnoteBold: footnoteBold ?? this.footnoteBold,
footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold,
);
}
TextTheme merge(TextTheme other) {
if (other == null) return this;
return copyWith(
body: body?.merge(other.body) ?? other.body,
title: title?.merge(other.title) ?? other.title,
headlineBold:
headlineBold?.merge(other.headlineBold) ?? other.headlineBold,
headline: headline?.merge(other.headline) ?? other.headline,
bodyBold: bodyBold?.merge(other.bodyBold) ?? other.bodyBold,
footnoteBold:
footnoteBold?.merge(other.footnoteBold) ?? other.footnoteBold,
footnote: footnote?.merge(other.footnote) ?? other.footnote,
captionBold: captionBold?.merge(other.captionBold) ?? other.captionBold,
);
}
}
enum ColorThemeType {
light,
dark,
}
class ColorTheme {
final Color black;
final Color grey;
final Color greyGainsboro;
final Color greyWhisper;
final Color whiteSmoke;
final Color whiteSnow;
final Color white;
final Color blueAlice;
final Color accentBlue;
final Color accentRed;
final Color accentGreen;
final Effect borderTop;
final Effect borderBottom;
final Effect shadowIconButton;
final Effect modalShadow;
final Color highlight;
final Color overlay;
final Color overlayDark;
final Gradient bgGradient;
ColorTheme.light({
this.black = const Color(0xff000000),
this.grey = const Color(0xff7a7a7a),
this.greyGainsboro = const Color(0xffdbdbdb),
this.greyWhisper = const Color(0xffecebeb),
this.whiteSmoke = const Color(0xfff2f2f2),
this.whiteSnow = const Color(0xfffcfcfc),
this.white = const Color(0xffffffff),
this.blueAlice = const Color(0xffe9f2ff),
this.accentBlue = const Color(0xff005FFF),
this.accentRed = const Color(0xffFF3842),
this.accentGreen = const Color(0xff20E070),
this.highlight = const Color(0xfffbf4dd),
this.overlay = const Color.fromRGBO(0, 0, 0, 0.2),
this.overlayDark = const Color.fromRGBO(0, 0, 0, 0.6),
this.bgGradient = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [const Color(0xfff7f7f7), const Color(0xfffcfcfc)],
stops: [0, 1],
),
this.borderTop = const Effect(
sigmaX: 0,
sigmaY: -1,
color: Color(0xff000000),
blur: 0.0,
alpha: 0.08),
this.borderBottom = const Effect(
sigmaX: 0, sigmaY: 1, color: Color(0xff000000), blur: 0.0, alpha: 0.08),
this.shadowIconButton = const Effect(
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
this.modalShadow = const Effect(
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
});
ColorTheme.dark({
this.black = const Color(0xffffffff),
this.grey = const Color(0xff7a7a7a),
this.greyGainsboro = const Color(0xff2d2f2f),
this.greyWhisper = const Color(0xff1c1e22),
this.whiteSmoke = const Color(0xff13151b),
this.whiteSnow = const Color(0xff070A0D),
this.white = const Color(0xff101418),
this.blueAlice = const Color(0xff00193D),
this.accentBlue = const Color(0xff005FFF),
this.accentRed = const Color(0xffFF3742),
this.accentGreen = const Color(0xff20E070),
this.borderTop = const Effect(
sigmaX: 0, sigmaY: -1, color: Color(0xff141924), blur: 0.0),
this.borderBottom = const Effect(
sigmaX: 0, sigmaY: 1, color: Color(0xff141924), blur: 0.0, alpha: 1.0),
this.shadowIconButton = const Effect(
sigmaX: 0, sigmaY: 2, color: Color(0xff000000), alpha: 0.5, blur: 4.0),
this.modalShadow = const Effect(
sigmaX: 0, sigmaY: 0, color: Color(0xff000000), alpha: 1, blur: 8.0),
this.highlight = const Color(0xff302d22),
this.overlay = const Color.fromRGBO(0, 0, 0, 0.4),
this.overlayDark = const Color.fromRGBO(255, 255, 255, 0.6),
this.bgGradient = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [const Color(0xff101214), const Color(0xff070a0d)],
stops: [0, 1],
),
});
ColorTheme copyWith({
ColorThemeType type = ColorThemeType.light,
Color black,
Color grey,
Color greyGainsboro,
Color greyWhisper,
Color whiteSmoke,
Color whiteSnow,
Color white,
Color blueAlice,
Color accentBlue,
Color accentRed,
Color accentGreen,
Effect borderTop,
Effect borderBottom,
Effect shadowIconButton,
Effect modalShadow,
Color highlight,
Color overlay,
Color overlayDark,
Gradient bgGradient,
}) {
return type == ColorThemeType.light
? ColorTheme.light(
black: black ?? this.black,
grey: grey ?? this.grey,
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
greyWhisper: greyWhisper ?? this.greyWhisper,
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
whiteSnow: whiteSnow ?? this.whiteSnow,
white: white ?? this.white,
blueAlice: blueAlice ?? this.blueAlice,
accentBlue: accentBlue ?? this.accentBlue,
accentRed: accentRed ?? this.accentRed,
accentGreen: accentGreen ?? this.accentGreen,
borderTop: borderTop ?? this.borderTop,
borderBottom: borderBottom ?? this.borderBottom,
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
modalShadow: modalShadow ?? this.modalShadow,
highlight: highlight ?? this.highlight,
overlay: overlay ?? this.overlay,
overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient,
)
: ColorTheme.dark(
black: black ?? this.black,
grey: grey ?? this.grey,
greyGainsboro: greyGainsboro ?? this.greyGainsboro,
greyWhisper: greyWhisper ?? this.greyWhisper,
whiteSmoke: whiteSmoke ?? this.whiteSmoke,
whiteSnow: whiteSnow ?? this.whiteSnow,
white: white ?? this.white,
blueAlice: blueAlice ?? this.blueAlice,
accentBlue: accentBlue ?? this.accentBlue,
accentRed: accentRed ?? this.accentRed,
accentGreen: accentGreen ?? this.accentGreen,
borderTop: borderTop ?? this.borderTop,
borderBottom: borderBottom ?? this.borderBottom,
shadowIconButton: shadowIconButton ?? this.shadowIconButton,
modalShadow: modalShadow ?? this.modalShadow,
highlight: highlight ?? this.highlight,
overlay: overlay ?? this.overlay,
overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient,
);
}
ColorTheme merge(ColorTheme other) {
if (other == null) return this;
return copyWith(
black: other.black,
grey: other.grey,
greyGainsboro: other.greyGainsboro,
greyWhisper: other.greyWhisper,
whiteSmoke: other.whiteSmoke,
whiteSnow: other.whiteSnow,
white: other.white,
blueAlice: other.blueAlice,
accentBlue: other.accentBlue,
accentRed: other.accentRed,
accentGreen: other.accentGreen,
highlight: other.highlight,
overlay: other.overlay,
overlayDark: other.overlayDark,
bgGradient: other.bgGradient,
borderTop: other.borderTop,
borderBottom: other.borderBottom,
shadowIconButton: other.shadowIconButton,
modalShadow: other.modalShadow,
);
}
}
/// Channel theme data
class ChannelTheme {
/// Theme of the [ChannelHeader] widget
final ChannelHeaderTheme channelHeaderTheme;
/// IconTheme of the send button in [MessageInput]
final IconThemeData messageInputButtonIconTheme;
/// Theme of the send button in [MessageInput]
final ButtonThemeData messageInputButtonTheme;
/// Background color of [MessageInput]
final Color inputBackground;
ChannelTheme({
this.channelHeaderTheme,
this.messageInputButtonIconTheme,
this.messageInputButtonTheme,
this.inputBackground,
});
/// Creates a copy of [ChannelTheme] with specified attributes overridden.
ChannelTheme copyWith({
ChannelHeaderTheme channelHeaderTheme,
IconThemeData messageInputButtonIconTheme,
ButtonThemeData messageInputButtonTheme,
Color inputBackground,
}) =>
ChannelTheme(
channelHeaderTheme: channelHeaderTheme ?? this.channelHeaderTheme,
messageInputButtonIconTheme:
messageInputButtonIconTheme ?? this.messageInputButtonIconTheme,
messageInputButtonTheme:
messageInputButtonTheme ?? this.messageInputButtonTheme,
inputBackground: inputBackground ?? this.inputBackground,
);
ChannelTheme merge(ChannelTheme other) {
if (other == null) return this;
return copyWith(
channelHeaderTheme: channelHeaderTheme?.merge(other.channelHeaderTheme) ??
other.channelHeaderTheme,
messageInputButtonIconTheme: messageInputButtonIconTheme
?.merge(other.messageInputButtonIconTheme) ??
other.messageInputButtonIconTheme,
messageInputButtonTheme: other.messageInputButtonTheme,
inputBackground: other.inputBackground,
);
}
}
class AvatarTheme {
final BoxConstraints constraints;
final BorderRadius borderRadius;
AvatarTheme({
this.constraints,
this.borderRadius,
});
AvatarTheme copyWith({
BoxConstraints constraints,
BorderRadius borderRadius,
}) =>
AvatarTheme(
constraints: constraints ?? this.constraints,
borderRadius: borderRadius ?? this.borderRadius,
);
AvatarTheme merge(AvatarTheme other) {
if (other == null) return this;
return copyWith(
constraints: other.constraints,
borderRadius: other.borderRadius,
);
}
}
class MessageTheme {
final TextStyle messageText;
final TextStyle messageAuthor;
final TextStyle messageLinks;
final TextStyle createdAt;
final TextStyle replies;
final Color messageBackgroundColor;
final Color messageBorderColor;
final Color reactionsBackgroundColor;
final Color reactionsBorderColor;
final Color reactionsMaskColor;
final AvatarTheme avatarTheme;
const MessageTheme({
this.replies,
this.messageText,
this.messageAuthor,
this.messageLinks,
this.messageBackgroundColor,
this.messageBorderColor,
this.reactionsBackgroundColor,
this.reactionsBorderColor,
this.reactionsMaskColor,
this.avatarTheme,
this.createdAt,
});
MessageTheme copyWith({
TextStyle messageText,
TextStyle messageAuthor,
TextStyle messageLinks,
TextStyle createdAt,
TextStyle replies,
Color messageBackgroundColor,
Color messageBorderColor,
AvatarTheme avatarTheme,
Color reactionsBackgroundColor,
Color reactionsBorderColor,
Color reactionsMaskColor,
}) =>
MessageTheme(
messageText: messageText ?? this.messageText,
messageAuthor: messageAuthor ?? this.messageAuthor,
messageLinks: messageLinks ?? this.messageLinks,
createdAt: createdAt ?? this.createdAt,
messageBackgroundColor:
messageBackgroundColor ?? this.messageBackgroundColor,
messageBorderColor: messageBorderColor ?? this.messageBorderColor,
avatarTheme: avatarTheme ?? this.avatarTheme,
replies: replies ?? this.replies,
reactionsBackgroundColor:
reactionsBackgroundColor ?? this.reactionsBackgroundColor,
reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor,
reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor,
);
MessageTheme merge(MessageTheme other) {
if (other == null) return this;
return copyWith(
messageText: messageText?.merge(other.messageText) ?? other.messageText,
messageAuthor:
messageAuthor?.merge(other.messageAuthor) ?? other.messageAuthor,
messageLinks:
messageLinks?.merge(other.messageLinks) ?? other.messageLinks,
createdAt: createdAt?.merge(other.createdAt) ?? other.createdAt,
replies: replies?.merge(other.replies) ?? other.replies,
messageBackgroundColor: other.messageBackgroundColor,
messageBorderColor: other.messageBorderColor,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
reactionsBackgroundColor: other.reactionsBackgroundColor,
reactionsBorderColor: other.reactionsBorderColor,
reactionsMaskColor: other.reactionsMaskColor,
);
}
}
class ChannelPreviewTheme {
final TextStyle title;
final TextStyle subtitle;
final TextStyle lastMessageAt;
final AvatarTheme avatarTheme;
final Color unreadCounterColor;
final double indicatorIconSize;
const ChannelPreviewTheme({
this.title,
this.subtitle,
this.lastMessageAt,
this.avatarTheme,
this.unreadCounterColor,
this.indicatorIconSize,
});
ChannelPreviewTheme copyWith({
TextStyle title,
TextStyle subtitle,
TextStyle lastMessageAt,
AvatarTheme avatarTheme,
Color unreadCounterColor,
double indicatorIconSize,
}) =>
ChannelPreviewTheme(
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
avatarTheme: avatarTheme ?? this.avatarTheme,
unreadCounterColor: unreadCounterColor ?? this.unreadCounterColor,
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
);
ChannelPreviewTheme merge(ChannelPreviewTheme other) {
if (other == null) return this;
return copyWith(
title: title?.merge(other.title) ?? other.title,
subtitle: subtitle?.merge(other.subtitle) ?? other.subtitle,
lastMessageAt:
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
unreadCounterColor: other.unreadCounterColor,
);
}
}
class ChannelHeaderTheme {
final TextStyle title;
final TextStyle lastMessageAt;
final AvatarTheme avatarTheme;
final Color color;
const ChannelHeaderTheme({
this.title,
this.lastMessageAt,
this.avatarTheme,
this.color,
});
ChannelHeaderTheme copyWith({
TextStyle title,
TextStyle lastMessageAt,
AvatarTheme avatarTheme,
Color color,
}) =>
ChannelHeaderTheme(
title: title ?? this.title,
lastMessageAt: lastMessageAt ?? this.lastMessageAt,
avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color,
);
ChannelHeaderTheme merge(ChannelHeaderTheme other) {
if (other == null) return this;
return copyWith(
title: title?.merge(other.title) ?? other.title,
lastMessageAt:
lastMessageAt?.merge(other.lastMessageAt) ?? other.lastMessageAt,
avatarTheme: avatarTheme?.merge(other.avatarTheme) ?? other.avatarTheme,
color: other.color,
);
}
}
class Effect {
final double sigmaX;
final double sigmaY;
final Color color;
final double alpha;
final double blur;
const Effect({
this.sigmaX,
this.sigmaY,
this.color,
this.alpha,
this.blur,
});
Effect copyWith({
double sigmaX,
double sigmaY,
Color color,
double alpha,
double blur,
}) =>
Effect(
sigmaX: sigmaX ?? this.sigmaX,
sigmaY: sigmaY ?? this.sigmaY,
color: color ?? this.color,
alpha: color ?? this.alpha,
blur: blur ?? this.blur,
);
}
/// Get random png with initials
String getRandomPicUrl(User user) =>
'https://getstream.io/random_png/?id=${user.id}&name=${user.name}';
@@ -0,0 +1,507 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/lazy_load_scroll_view.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_theme.dart';
/// Callback called when tapping on a user
typedef UserTapCallback = void Function(User, Widget);
/// Builder used to create a custom [ListUserItem] from a [User]
typedef UserItemBuilder = Widget Function(BuildContext, User, bool);
///
/// It shows the list of current users.
///
/// ```dart
/// class UsersListPage extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: UsersListView(
/// filter: {
/// 'members': {
/// '\$in': [StreamChat.of(context).user.id],
/// }
/// },
/// sort: [SortOption('last_message_at')],
/// pagination: PaginationParams(
/// limit: 20,
/// ),
/// channelWidget: ChannelPage(),
/// ),
/// );
/// }
/// }
/// ```
///
///
/// Make sure to have a [UsersBloc] ancestor in order to provide the information about the users.
/// The widget uses a [ListView.separated], [GridView.builder] to render the list, grid of channels.
///
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class UserListView extends StatefulWidget {
/// Instantiate a new UserListView
const UserListView({
Key key,
this.errorBuilder,
this.emptyBuilder,
this.filter,
this.options,
this.sort,
this.pagination,
this.onUserTap,
this.onUserLongPress,
this.userWidget,
this.userItemBuilder,
this.separatorBuilder,
this.onImageTap,
this.selectedUsers,
this.pullToRefresh = true,
this.groupAlphabetically = false,
this.crossAxisCount = 1,
}) : assert(
crossAxisCount == 1 || groupAlphabetically == false,
'Cannot group alphabetically when crossAxisCount > 1',
),
super(key: key);
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
/// The builder used when the channel list is empty.
final WidgetBuilder emptyBuilder;
/// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields.
final Map<String, dynamic> filter;
/// Query channels options.
///
/// state: if true returns the Channel state
/// watch: if true listen to changes to this Channel in real time.
final Map<String, dynamic> options;
/// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be provided.
/// You can sort based on last_updated, last_message_at, updated_at, created_at or member_count.
/// Direction can be ascending or descending.
final List<SortOption> sort;
/// Pagination parameters
/// limit: the number of users to return (max is 30)
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams pagination;
/// Function called when tapping on a channel
/// By default it calls [Navigator.push] building a [MaterialPageRoute]
/// with the widget [userWidget] as child.
final UserTapCallback onUserTap;
/// Function called when long pressing on a channel
final Function(User) onUserLongPress;
/// Widget used when opening a channel
final Widget userWidget;
/// Builder used to create a custom user preview
final UserItemBuilder userItemBuilder;
/// Builder used to create a custom item separator
final Function(BuildContext, int) separatorBuilder;
/// The function called when the image is tapped
final Function(User) onImageTap;
/// Set it to false to disable the pull-to-refresh widget
final bool pullToRefresh;
/// Sets a blue trailing checkMark in [ListUserItem] for all the [selectedUsers]
final Set<User> selectedUsers;
/// Set it to true to group users by their first character
///
/// defaults to false
final bool groupAlphabetically;
/// The number of children in the cross axis.
final int crossAxisCount;
@override
_UserListViewState createState() => _UserListViewState();
}
class _UserListViewState extends State<UserListView>
with WidgetsBindingObserver {
bool get _isListView => widget.crossAxisCount == 1;
@override
void initState() {
super.initState();
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
}
@override
Widget build(BuildContext context) {
final usersBloc = UsersBloc.of(context);
if (!widget.pullToRefresh) {
return _buildListView(usersBloc);
}
return RefreshIndicator(
onRefresh: () async {
return usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
options: widget.options,
pagination: widget.pagination,
);
},
child: _buildListView(usersBloc),
);
}
bool get isListAlreadySorted =>
widget.sort?.any((e) => e.field == 'name' && e.direction == 1) ?? false;
Stream<List<ListItem>> _buildUserStream(
UsersBlocState usersBlocState,
) {
return usersBlocState.usersStream.map(
(users) {
if (widget.groupAlphabetically) {
var temp = users;
if (!isListAlreadySorted) {
temp = users..sort((curr, next) => curr.name.compareTo(next.name));
}
final groupedUsers = <String, List<User>>{};
for (var e in temp) {
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (var key in groupedUsers.keys) {
items.add(ListHeaderItem(key));
items.addAll(groupedUsers[key].map((e) => ListUserItem(e)));
}
return items;
}
return users.map((e) => ListUserItem(e)).toList();
},
);
}
StreamBuilder<List<ListItem>> _buildListView(
UsersBlocState usersBlocState,
) {
return StreamBuilder(
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
if (widget.errorBuilder != null) {
return widget.errorBuilder(snapshot.error);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
if (dioError.type == DioErrorType.RESPONSE) {
message = dioError.message;
} else {
message = 'Check your connection and retry';
}
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text.rich(
TextSpan(
children: [
WidgetSpan(
child: Padding(
padding: const EdgeInsets.only(
right: 2.0,
),
child: Icon(Icons.error_outline),
),
),
TextSpan(text: 'Error loading channels'),
],
),
style: Theme.of(context).textTheme.headline6,
),
Padding(
padding: const EdgeInsets.only(
top: 16.0,
),
child: Text(message),
),
FlatButton(
onPressed: () {
usersBlocState.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
},
child: Text('Retry'),
),
],
),
);
}
if (!snapshot.hasData) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: CircularProgressIndicator(),
),
),
);
},
);
}
final items = snapshot.data;
if (items.isEmpty && widget.emptyBuilder != null) {
return widget.emptyBuilder(context);
}
if (items.isEmpty && widget.emptyBuilder == null) {
return LayoutBuilder(
builder: (context, viewportConstraints) {
return SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: viewportConstraints.maxHeight,
),
child: Center(
child: Text('There are no users currently'),
),
),
);
},
);
}
final child = _isListView
? ListView.separated(
physics: AlwaysScrollableScrollPhysics(),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) {
return _listItemBuilder(context, index, items);
},
)
: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: widget.crossAxisCount,
),
itemCount: items.isNotEmpty ? items.length + 1 : items.length,
physics: AlwaysScrollableScrollPhysics(),
itemBuilder: (context, index) {
return _gridItemBuilder(context, index, items);
},
);
return LazyLoadScrollView(
onEndOfPage: () async {
return _listenUserPagination(usersBlocState);
},
child: child,
);
},
);
}
Widget _listItemBuilder(BuildContext context, int i, List<ListItem> items) {
final usersProvider = UsersBloc.of(context);
if (i < items.length) {
final item = items[i];
return item.when(
headerItem: (header) {
return Container(
key: ValueKey<String>('HEADER-$header'),
color:
StreamChatTheme.of(context).colorTheme.black.withOpacity(0.05),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6),
child: Text(
header,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14.5,
color: StreamChatTheme.of(context).colorTheme.grey,
),
),
),
);
},
userItem: (user) {
final selected = widget.selectedUsers?.contains(user) ?? false;
return Container(
key: ValueKey<String>('USER-${user.id}'),
child: widget.userItemBuilder(context, user, selected),
);
},
);
} else {
return _buildQueryProgressIndicator(context, usersProvider);
}
}
Widget _gridItemBuilder(BuildContext context, int i, List<ListItem> items) {
final usersProvider = UsersBloc.of(context);
if (i < items.length) {
final item = items[i];
return item.when(
headerItem: (_) => Offstage(),
userItem: (user) {
final selected = widget.selectedUsers?.contains(user) ?? false;
return Container(
key: ValueKey<String>('USER-${user.id}'),
child: widget.userItemBuilder(context, user, selected),
);
},
);
} else {
return _buildQueryProgressIndicator(context, usersProvider);
}
}
Widget _buildQueryProgressIndicator(context, UsersBlocState usersProvider) {
return StreamBuilder<bool>(
stream: usersProvider.queryUsersLoading,
initialData: false,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Container(
color: StreamChatTheme.of(context)
.colorTheme
.accentRed
.withOpacity(.2),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Center(
child: Text('Error loading users'),
),
),
);
}
return Container(
height: 100,
padding: EdgeInsets.all(32),
child: Center(
child: snapshot.data ? CircularProgressIndicator() : Container(),
),
);
});
}
Widget _separatorBuilder(context, i) {
return Container(
height: 1,
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
);
}
void _listenUserPagination(UsersBlocState usersProvider) {
usersProvider.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
offset: usersProvider.users?.length ?? 0,
),
options: widget.options,
);
}
@override
void didUpdateWidget(UserListView oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
}
}
}
abstract class ListItem {
String get key {
if (this is ListHeaderItem) {
final header = (this as ListHeaderItem).heading;
return 'HEADER-$header';
}
if (this is ListUserItem) {
final user = (this as ListUserItem).user;
return 'USER-${user.id}';
}
return null;
}
Widget when({
@required Widget Function(String heading) headerItem,
@required Widget Function(User user) userItem,
}) {
if (this is ListHeaderItem) {
return headerItem((this as ListHeaderItem).heading);
}
if (this is ListUserItem) {
return userItem((this as ListUserItem).user);
}
return SizedBox();
}
}
class ListHeaderItem extends ListItem {
final String heading;
ListHeaderItem(this.heading);
}
class ListUserItem extends ListItem {
final User user;
ListUserItem(this.user);
}
@@ -0,0 +1,108 @@
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat.dart';
/// Widget dedicated to the management of a users list with pagination
class UsersBloc extends StatefulWidget {
/// The widget child
final Widget child;
/// Instantiate a new UsersBloc
const UsersBloc({
Key key,
@required this.child,
}) : super(key: key);
@override
UsersBlocState createState() => UsersBlocState();
/// Use this method to get the current [UsersBlocState] instance
static UsersBlocState of(BuildContext context) {
UsersBlocState state;
state = context.findAncestorStateOfType<UsersBlocState>();
if (state == null) {
throw Exception('You must have a UsersBloc widget as ancestor');
}
return state;
}
}
/// The current state of the [UsersBloc]
class UsersBlocState extends State<UsersBloc>
with AutomaticKeepAliveClientMixin {
/// The current users list
List<User> get users => _usersController.value;
/// The current users list as a stream
Stream<List<User>> get usersStream => _usersController.stream;
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
final BehaviorSubject<bool> _queryUsersLoadingController =
BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
/// Calls [Client.queryUsers] updating [queryUsersLoading] stream
Future<void> queryUsers({
Map<String, dynamic> filter,
List<SortOption> sort,
Map<String, dynamic> options,
PaginationParams pagination,
}) async {
final client = StreamChat.of(context).client;
if (client.state?.user == null ||
_queryUsersLoadingController.value == true) {
return;
}
_queryUsersLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
pagination.offset == 0;
final oldUsers = List<User>.from(users ?? []);
final usersResponse = await client.queryUsers(
filter: filter,
sort: sort,
options: options,
pagination: pagination,
);
if (clear) {
_usersController.add(usersResponse.users);
} else {
final temp = oldUsers + usersResponse.users;
_usersController.add(temp);
}
_queryUsersLoadingController.add(false);
} catch (err, stackTrace) {
_queryUsersLoadingController.addError(err, stackTrace);
}
}
@override
Widget build(BuildContext context) {
super.build(context);
return widget.child;
}
@override
void dispose() {
_usersController.close();
_queryUsersLoadingController.close();
super.dispose();
}
@override
bool get wantKeepAlive => true;
}
@@ -9,6 +9,7 @@ environment:
flutter: ">=1.17.0"
dependencies:
stream_chat: ^0.2.23+3
flutter:
sdk: flutter