feat: Removed theme and added new widgets
This commit is contained in:
@@ -8,14 +8,6 @@ import 'package:stream_chat_flutter_core/src/channels_bloc.dart';
|
|||||||
|
|
||||||
import 'stream_chat.dart';
|
import 'stream_chat.dart';
|
||||||
|
|
||||||
/// Callback called when tapping on a channel
|
|
||||||
typedef ChannelTapCallback = void Function(Channel, Widget);
|
|
||||||
|
|
||||||
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
|
||||||
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
|
||||||
|
|
||||||
typedef ViewInfoCallback = void Function(Channel);
|
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
/// 
|
/// 
|
||||||
///
|
///
|
||||||
@@ -57,15 +49,16 @@ class ChannelListView extends StatefulWidget {
|
|||||||
this.options,
|
this.options,
|
||||||
this.sort,
|
this.sort,
|
||||||
this.pagination,
|
this.pagination,
|
||||||
this.separatorBuilder,
|
|
||||||
this.errorBuilder,
|
|
||||||
this.swipeToAction = false,
|
|
||||||
this.pullToRefresh = true,
|
this.pullToRefresh = true,
|
||||||
|
@required this.errorBuilder,
|
||||||
@required this.emptyBuilder,
|
@required this.emptyBuilder,
|
||||||
@required this.loadingBuilder,
|
@required this.loadingBuilder,
|
||||||
@required this.listBuilder,
|
@required this.listBuilder,
|
||||||
|
this.channelListController,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final ChannelListController channelListController;
|
||||||
|
|
||||||
/// The builder that will be used in case of error
|
/// The builder that will be used in case of error
|
||||||
final Widget Function(Error error) errorBuilder;
|
final Widget Function(Error error) errorBuilder;
|
||||||
|
|
||||||
@@ -73,9 +66,6 @@ class ChannelListView extends StatefulWidget {
|
|||||||
|
|
||||||
final Function(BuildContext, List<Channel>) listBuilder;
|
final Function(BuildContext, List<Channel>) listBuilder;
|
||||||
|
|
||||||
/// If true a default swipe to action behaviour will be added to this widget
|
|
||||||
final bool swipeToAction;
|
|
||||||
|
|
||||||
/// The builder used when the channel list is empty.
|
/// The builder used when the channel list is empty.
|
||||||
final WidgetBuilder emptyBuilder;
|
final WidgetBuilder emptyBuilder;
|
||||||
|
|
||||||
@@ -102,9 +92,6 @@ class ChannelListView extends StatefulWidget {
|
|||||||
/// message_limit: how many messages should be included to each channel
|
/// message_limit: how many messages should be included to each channel
|
||||||
final PaginationParams pagination;
|
final PaginationParams pagination;
|
||||||
|
|
||||||
/// Builder used to create a custom item separator
|
|
||||||
final Function(BuildContext, int) separatorBuilder;
|
|
||||||
|
|
||||||
/// Set it to false to disable the pull-to-refresh widget
|
/// Set it to false to disable the pull-to-refresh widget
|
||||||
final bool pullToRefresh;
|
final bool pullToRefresh;
|
||||||
|
|
||||||
@@ -114,8 +101,6 @@ class ChannelListView extends StatefulWidget {
|
|||||||
|
|
||||||
class _ChannelListViewState extends State<ChannelListView>
|
class _ChannelListViewState extends State<ChannelListView>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channelsBloc = ChannelsBloc.of(context);
|
final channelsBloc = ChannelsBloc.of(context);
|
||||||
@@ -162,10 +147,7 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return child;
|
||||||
child: child,
|
|
||||||
duration: Duration(milliseconds: 500),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -186,19 +168,17 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
return widget.errorBuilder(snapshot.error);
|
return widget.errorBuilder(snapshot.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenChannelPagination(ChannelsBlocState channelsProvider) {
|
void paginateData() {
|
||||||
if (_scrollController.position.maxScrollExtent ==
|
final channelsBloc = ChannelsBloc.of(context);
|
||||||
_scrollController.offset &&
|
|
||||||
_scrollController.offset != 0) {
|
channelsBloc.queryChannels(
|
||||||
channelsProvider.queryChannels(
|
filter: widget.filter,
|
||||||
filter: widget.filter,
|
sortOptions: widget.sort,
|
||||||
sortOptions: widget.sort,
|
paginationParams: widget.pagination.copyWith(
|
||||||
paginationParams: widget.pagination.copyWith(
|
offset: channelsBloc.channels?.length ?? 0,
|
||||||
offset: channelsProvider.channels?.length ?? 0,
|
),
|
||||||
),
|
options: widget.options,
|
||||||
options: widget.options,
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription _subscription;
|
StreamSubscription _subscription;
|
||||||
@@ -217,14 +197,6 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
options: widget.options,
|
options: widget.options,
|
||||||
);
|
);
|
||||||
|
|
||||||
_scrollController.addListener(() {
|
|
||||||
channelsBloc.queryChannelsLoading.first.then((loading) {
|
|
||||||
if (!loading) {
|
|
||||||
_listenChannelPagination(channelsBloc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
|
|
||||||
_subscription = client
|
_subscription = client
|
||||||
@@ -242,6 +214,10 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
options: widget.options,
|
options: widget.options,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (widget.channelListController != null) {
|
||||||
|
widget.channelListController.paginateData = paginateData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -270,3 +246,8 @@ class _ChannelListViewState extends State<ChannelListView>
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controller used for paginating data in [ChannelListView]
|
||||||
|
class ChannelListController {
|
||||||
|
VoidCallback paginateData;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'stream_channel.dart';
|
||||||
|
|
||||||
|
/// 
|
||||||
|
/// 
|
||||||
|
///
|
||||||
|
/// It shows the list of messages of the current channel.
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// class ChannelPage extends StatelessWidget {
|
||||||
|
/// const ChannelPage({
|
||||||
|
/// Key key,
|
||||||
|
/// }) : super(key: key);
|
||||||
|
///
|
||||||
|
/// @override
|
||||||
|
/// Widget build(BuildContext context) {
|
||||||
|
/// return Scaffold(
|
||||||
|
/// appBar: ChannelHeader(),
|
||||||
|
/// body: Column(
|
||||||
|
/// children: <Widget>[
|
||||||
|
/// Expanded(
|
||||||
|
/// child: MessageListView(
|
||||||
|
/// threadBuilder: (_, parentMessage) {
|
||||||
|
/// return ThreadPage(
|
||||||
|
/// parent: parentMessage,
|
||||||
|
/// );
|
||||||
|
/// },
|
||||||
|
/// ),
|
||||||
|
/// ),
|
||||||
|
/// MessageInput(),
|
||||||
|
/// ],
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels.
|
||||||
|
/// The widget uses a [ListView.custom] to render the list of channels.
|
||||||
|
///
|
||||||
|
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||||
|
/// Modify it to change the widget appearance.
|
||||||
|
class MessageListView extends StatefulWidget {
|
||||||
|
/// Instantiate a new MessageListView
|
||||||
|
MessageListView({
|
||||||
|
Key key,
|
||||||
|
this.showScrollToBottom = true,
|
||||||
|
this.parentMessage,
|
||||||
|
this.dateDividerBuilder,
|
||||||
|
@required this.loadingBuilder,
|
||||||
|
@required this.emptyBuilder,
|
||||||
|
@required this.messageListBuilder,
|
||||||
|
this.messageListController,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
MessageListController messageListController;
|
||||||
|
|
||||||
|
final Widget Function(BuildContext, List<Message>) messageListBuilder;
|
||||||
|
|
||||||
|
/// Function used to build a loading widget
|
||||||
|
final WidgetBuilder loadingBuilder;
|
||||||
|
|
||||||
|
/// Function used to build an empty widget
|
||||||
|
final WidgetBuilder emptyBuilder;
|
||||||
|
|
||||||
|
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero
|
||||||
|
final bool showScrollToBottom;
|
||||||
|
|
||||||
|
/// Parent message in case of a thread
|
||||||
|
final Message parentMessage;
|
||||||
|
|
||||||
|
/// Builder used to render date dividers
|
||||||
|
final Widget Function(DateTime) dateDividerBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageListViewState extends State<MessageListView> {
|
||||||
|
StreamChannelState streamChannel;
|
||||||
|
|
||||||
|
bool get _upToDate => streamChannel.channel.state.isUpToDate;
|
||||||
|
|
||||||
|
bool get _isThreadConversation => widget.parentMessage != null;
|
||||||
|
|
||||||
|
int initialIndex;
|
||||||
|
double initialAlignment;
|
||||||
|
|
||||||
|
List<Message> messages = <Message>[];
|
||||||
|
|
||||||
|
bool initialMessageHighlightComplete = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final messagesStream = _isThreadConversation
|
||||||
|
? streamChannel.channel.state.threadsStream
|
||||||
|
.where((threads) => threads.containsKey(widget.parentMessage.id))
|
||||||
|
.map((threads) => threads[widget.parentMessage.id])
|
||||||
|
: streamChannel.channel.state?.messagesStream;
|
||||||
|
|
||||||
|
return StreamBuilder<List<Message>>(
|
||||||
|
stream: messagesStream?.map((messages) => messages
|
||||||
|
?.where((e) =>
|
||||||
|
(!e.isDeleted && e.shadowed != true) ||
|
||||||
|
(e.isDeleted &&
|
||||||
|
e.user.id == streamChannel.channel.client.state.user.id))
|
||||||
|
?.toList()),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return widget.loadingBuilder(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
final messageList = snapshot.data?.reversed?.toList() ?? [];
|
||||||
|
if (messageList.isEmpty) {
|
||||||
|
if (_upToDate) {
|
||||||
|
return widget.emptyBuilder(context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
messages = messageList;
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.messageListBuilder(context, messages);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> paginateData(QueryDirection direction) {
|
||||||
|
if (!_isThreadConversation) {
|
||||||
|
return streamChannel.queryMessages(direction: direction);
|
||||||
|
} else {
|
||||||
|
return streamChannel.getReplies(widget.parentMessage.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
streamChannel = StreamChannel.of(context);
|
||||||
|
|
||||||
|
if (_isThreadConversation) {
|
||||||
|
streamChannel.getReplies(widget.parentMessage.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.messageListController != null) {
|
||||||
|
widget.messageListController.paginateData = paginateData;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (!_upToDate) {
|
||||||
|
streamChannel.reloadChannel();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controller used for paginating data in [ChannelListView]
|
||||||
|
class MessageListController {
|
||||||
|
Function(QueryDirection direction) paginateData;
|
||||||
|
}
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'lazy_load_scroll_view.dart';
|
||||||
|
import 'message_search_bloc.dart';
|
||||||
|
|
||||||
|
/// Callback called when tapping on a user
|
||||||
|
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
|
||||||
|
|
||||||
|
/// Builder used to create a custom [ListUserItem] from a [User]
|
||||||
|
typedef MessageSearchItemBuilder = Widget Function(
|
||||||
|
BuildContext, GetMessageResponse);
|
||||||
|
|
||||||
|
/// Builder used when [MessageSearchListView] is empty
|
||||||
|
typedef EmptyMessageSearchBuilder = Widget Function(
|
||||||
|
BuildContext context, String searchQuery);
|
||||||
|
|
||||||
|
///
|
||||||
|
/// It shows the list of searched messages.
|
||||||
|
///
|
||||||
|
/// ```dart
|
||||||
|
/// class MessageSearchPage extends StatelessWidget {
|
||||||
|
/// @override
|
||||||
|
/// Widget build(BuildContext context) {
|
||||||
|
/// return Scaffold(
|
||||||
|
/// body: MessageSearchListView(
|
||||||
|
/// messageQuery: _channelQuery,
|
||||||
|
/// filters: {
|
||||||
|
/// 'members': {
|
||||||
|
/// r'$in': [user.id]
|
||||||
|
/// }
|
||||||
|
/// },
|
||||||
|
/// paginationParams: PaginationParams(limit: 20),
|
||||||
|
/// ),
|
||||||
|
/// );
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages.
|
||||||
|
/// The widget uses a [ListView.separated] to render the list of messages.
|
||||||
|
///
|
||||||
|
/// The widget components render the ui based on the first ancestor of type [StreamChatTheme].
|
||||||
|
/// Modify it to change the widget appearance.
|
||||||
|
class MessageSearchListView extends StatefulWidget {
|
||||||
|
/// Instantiate a new MessageSearchListView
|
||||||
|
const MessageSearchListView({
|
||||||
|
Key key,
|
||||||
|
this.messageQuery,
|
||||||
|
this.filters,
|
||||||
|
this.sortOptions,
|
||||||
|
this.paginationParams,
|
||||||
|
this.messageFilters,
|
||||||
|
@required this.emptyBuilder,
|
||||||
|
@required this.errorBuilder,
|
||||||
|
@required this.loadingBuilder,
|
||||||
|
@required this.childBuilder,
|
||||||
|
this.messageSearchListController,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final MessageSearchListController messageSearchListController;
|
||||||
|
|
||||||
|
/// Message String to search on
|
||||||
|
final String messageQuery;
|
||||||
|
|
||||||
|
/// 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> filters;
|
||||||
|
|
||||||
|
/// 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> sortOptions;
|
||||||
|
|
||||||
|
/// 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 paginationParams;
|
||||||
|
|
||||||
|
/// The message 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> messageFilters;
|
||||||
|
|
||||||
|
final Widget Function(List<GetMessageResponse>) childBuilder;
|
||||||
|
|
||||||
|
/// The builder used when the channel list is empty.
|
||||||
|
final EmptyMessageSearchBuilder emptyBuilder;
|
||||||
|
|
||||||
|
/// The builder that will be used in case of error
|
||||||
|
final Widget Function(Error error) errorBuilder;
|
||||||
|
|
||||||
|
final WidgetBuilder loadingBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_MessageSearchListViewState createState() => _MessageSearchListViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||||
|
messageSearchBloc.search(
|
||||||
|
filter: widget.filters,
|
||||||
|
sort: widget.sortOptions,
|
||||||
|
query: widget.messageQuery,
|
||||||
|
pagination: widget.paginationParams,
|
||||||
|
messageFilter: widget.messageFilters,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (widget.messageSearchListController != null) {
|
||||||
|
widget.messageSearchListController.paginateData = paginateData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||||
|
return _buildListView(messageSearchBloc);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildListView(MessageSearchBlocState messageSearchBloc) {
|
||||||
|
return StreamBuilder<List<GetMessageResponse>>(
|
||||||
|
stream: messageSearchBloc.messagesStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
if (snapshot.error is Error) {
|
||||||
|
print((snapshot.error as Error).stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.errorBuilder(snapshot.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!snapshot.hasData) {
|
||||||
|
return widget.loadingBuilder(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
final items = snapshot.data;
|
||||||
|
|
||||||
|
if (items.isEmpty) {
|
||||||
|
return widget.emptyBuilder(context, widget.messageQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.childBuilder(snapshot.data);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void paginateData() {
|
||||||
|
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||||
|
|
||||||
|
messageSearchBloc.loadMore(
|
||||||
|
filter: widget.filters,
|
||||||
|
sort: widget.sortOptions,
|
||||||
|
pagination: widget.paginationParams.copyWith(
|
||||||
|
offset: messageSearchBloc.messageResponses?.length ?? 0,
|
||||||
|
),
|
||||||
|
query: widget.messageQuery,
|
||||||
|
messageFilter: widget.messageFilters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(MessageSearchListView oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.filters?.toString() != oldWidget.filters?.toString() ||
|
||||||
|
jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) ||
|
||||||
|
widget.paginationParams?.toJson()?.toString() !=
|
||||||
|
oldWidget.paginationParams?.toJson()?.toString() ||
|
||||||
|
widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() ||
|
||||||
|
widget.messageFilters?.toString() !=
|
||||||
|
oldWidget.messageFilters?.toString()) {
|
||||||
|
final messageSearchBloc = MessageSearchBloc.of(context);
|
||||||
|
messageSearchBloc.search(
|
||||||
|
filter: widget.filters,
|
||||||
|
sort: widget.sortOptions,
|
||||||
|
query: widget.messageQuery,
|
||||||
|
pagination: widget.paginationParams,
|
||||||
|
messageFilter: widget.messageFilters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Controller used for paginating data in [ChannelListView]
|
||||||
|
class MessageSearchListController {
|
||||||
|
VoidCallback paginateData;
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ import 'dart:async';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:stream_chat_flutter_core/src/stream_chat_theme.dart';
|
|
||||||
|
|
||||||
/// Widget used to provide information about the chat to the widget tree
|
/// Widget used to provide information about the chat to the widget tree
|
||||||
///
|
///
|
||||||
@@ -29,13 +28,11 @@ import 'package:stream_chat_flutter_core/src/stream_chat_theme.dart';
|
|||||||
class StreamChat extends StatefulWidget {
|
class StreamChat extends StatefulWidget {
|
||||||
final Client client;
|
final Client client;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
final StreamChatThemeData streamChatThemeData;
|
|
||||||
|
|
||||||
StreamChat({
|
StreamChat({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.client,
|
@required this.client,
|
||||||
@required this.child,
|
@required this.child,
|
||||||
this.streamChatThemeData,
|
|
||||||
}) : super(
|
}) : super(
|
||||||
key: key,
|
key: key,
|
||||||
);
|
);
|
||||||
@@ -65,32 +62,7 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = _getTheme(context, widget.streamChatThemeData);
|
return widget.child;
|
||||||
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
|
/// The current user
|
||||||
@@ -103,13 +75,6 @@ class StreamChatState extends State<StreamChat> with WidgetsBindingObserver {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
// client.state?.totalUnreadCountStream?.listen((count) {
|
|
||||||
// if (count > 0) {
|
|
||||||
// FlutterAppBadger.updateBadgeCount(count);
|
|
||||||
// } else {
|
|
||||||
// FlutterAppBadger.removeBadge();
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription _newMessageSubscription;
|
StreamSubscription _newMessageSubscription;
|
||||||
|
|||||||
@@ -1,898 +0,0 @@
|
|||||||
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}';
|
|
||||||
@@ -2,15 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:stream_chat_flutter_core/src/lazy_load_scroll_view.dart';
|
|
||||||
import 'package:stream_chat_flutter_core/src/users_bloc.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.
|
/// It shows the list of current users.
|
||||||
@@ -47,31 +39,30 @@ class UserListView extends StatefulWidget {
|
|||||||
/// Instantiate a new UserListView
|
/// Instantiate a new UserListView
|
||||||
const UserListView({
|
const UserListView({
|
||||||
Key key,
|
Key key,
|
||||||
this.errorBuilder,
|
|
||||||
this.emptyBuilder,
|
|
||||||
this.filter,
|
this.filter,
|
||||||
this.options,
|
this.options,
|
||||||
this.sort,
|
this.sort,
|
||||||
this.pagination,
|
this.pagination,
|
||||||
this.onUserTap,
|
|
||||||
this.onUserLongPress,
|
|
||||||
this.userWidget,
|
|
||||||
this.userItemBuilder,
|
|
||||||
this.separatorBuilder,
|
|
||||||
this.onImageTap,
|
|
||||||
this.selectedUsers,
|
|
||||||
this.pullToRefresh = true,
|
this.pullToRefresh = true,
|
||||||
this.groupAlphabetically = false,
|
this.groupAlphabetically = false,
|
||||||
this.crossAxisCount = 1,
|
@required this.errorBuilder,
|
||||||
}) : assert(
|
@required this.emptyBuilder,
|
||||||
crossAxisCount == 1 || groupAlphabetically == false,
|
@required this.loadingBuilder,
|
||||||
'Cannot group alphabetically when crossAxisCount > 1',
|
@required this.listBuilder,
|
||||||
),
|
this.userListController,
|
||||||
super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final UserListController userListController;
|
||||||
|
|
||||||
/// The builder that will be used in case of error
|
/// The builder that will be used in case of error
|
||||||
final Widget Function(Error error) errorBuilder;
|
final Widget Function(Error error) errorBuilder;
|
||||||
|
|
||||||
|
/// The builder that will be used to build the list
|
||||||
|
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
|
||||||
|
|
||||||
|
/// The builder that will be used for loading
|
||||||
|
final WidgetBuilder loadingBuilder;
|
||||||
|
|
||||||
/// The builder used when the channel list is empty.
|
/// The builder used when the channel list is empty.
|
||||||
final WidgetBuilder emptyBuilder;
|
final WidgetBuilder emptyBuilder;
|
||||||
|
|
||||||
@@ -98,48 +89,20 @@ class UserListView extends StatefulWidget {
|
|||||||
/// message_limit: how many messages should be included to each channel
|
/// message_limit: how many messages should be included to each channel
|
||||||
final PaginationParams pagination;
|
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
|
/// Set it to false to disable the pull-to-refresh widget
|
||||||
final bool pullToRefresh;
|
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
|
/// Set it to true to group users by their first character
|
||||||
///
|
///
|
||||||
/// defaults to false
|
/// defaults to false
|
||||||
final bool groupAlphabetically;
|
final bool groupAlphabetically;
|
||||||
|
|
||||||
/// The number of children in the cross axis.
|
|
||||||
final int crossAxisCount;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_UserListViewState createState() => _UserListViewState();
|
_UserListViewState createState() => _UserListViewState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _UserListViewState extends State<UserListView>
|
class _UserListViewState extends State<UserListView>
|
||||||
with WidgetsBindingObserver {
|
with WidgetsBindingObserver {
|
||||||
bool get _isListView => widget.crossAxisCount == 1;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -150,6 +113,10 @@ class _UserListViewState extends State<UserListView>
|
|||||||
pagination: widget.pagination,
|
pagination: widget.pagination,
|
||||||
options: widget.options,
|
options: widget.options,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (widget.userListController != null) {
|
||||||
|
widget.userListController.paginateData = paginateData;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -214,235 +181,36 @@ class _UserListViewState extends State<UserListView>
|
|||||||
print((snapshot.error as Error).stackTrace);
|
print((snapshot.error as Error).stackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (widget.errorBuilder != null) {
|
return widget.errorBuilder(snapshot.error);
|
||||||
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) {
|
if (!snapshot.hasData) {
|
||||||
return LayoutBuilder(
|
return widget.loadingBuilder(context);
|
||||||
builder: (context, viewportConstraints) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
physics: AlwaysScrollableScrollPhysics(),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
minHeight: viewportConstraints.maxHeight,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final items = snapshot.data;
|
final items = snapshot.data;
|
||||||
|
|
||||||
if (items.isEmpty && widget.emptyBuilder != null) {
|
if (items.isEmpty) {
|
||||||
return widget.emptyBuilder(context);
|
return widget.emptyBuilder(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (items.isEmpty && widget.emptyBuilder == null) {
|
if (items.isEmpty) {
|
||||||
return LayoutBuilder(
|
return widget.emptyBuilder(context);
|
||||||
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
|
return widget.listBuilder(context, items);
|
||||||
? 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) {
|
void paginateData() {
|
||||||
final usersProvider = UsersBloc.of(context);
|
final usersBloc = 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) {
|
usersBloc.queryUsers(
|
||||||
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,
|
filter: widget.filter,
|
||||||
sort: widget.sort,
|
sort: widget.sort,
|
||||||
pagination: widget.pagination.copyWith(
|
pagination: widget.pagination.copyWith(
|
||||||
offset: usersProvider.users?.length ?? 0,
|
offset: usersBloc.users?.length ?? 0,
|
||||||
),
|
),
|
||||||
options: widget.options,
|
options: widget.options,
|
||||||
);
|
);
|
||||||
@@ -505,3 +273,8 @@ class ListUserItem extends ListItem {
|
|||||||
|
|
||||||
ListUserItem(this.user);
|
ListUserItem(this.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Controller used for paginating data in [ChannelListView]
|
||||||
|
class UserListController {
|
||||||
|
VoidCallback paginateData;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user