feat: Added first widgets
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user