chore: 1.5.1 (#387)

* lint(persistence): Apply team linter

Signed-off-by: Sahil Kumar <[email protected]>

* style: flutter format

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): Add tests for channel dao

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(persistence): remove dummy comments

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): Add test for channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for connection event dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for member dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for message dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for pinned message dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for reaction dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for read dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): minor improvement in update function tests

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for user dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for list converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for map converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for message sending status converter

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for channel mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add test for event mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for member mapper

Signed-off-by: Sahil Kumar <[email protected]>

* remove useless dependencies

* test(persistence): add tests for message mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for pinned message mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for reaction mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for read mapper

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for user mapper

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(persistence): remove redundant UsersTable getter from ConnectionEventDao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests for stream chat persistence client

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add tests moor chat database

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add moor schemas for future migration tests

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add clear queryCache test in channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add getThreadMessagesByParentId along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add getMessagesByCid along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* update ci

* apply locale to jiffy

* fix test

* test(persistence): add getMessagesByCid along with quotedMessage test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add extra checks in getMessagesByCid along with pagination test in message, pinned message dao.

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add return channel with pagination applies test in channel query dao

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): improve message mapper, pinned message mapper test

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add client constructor test in stream chat persistence client

Signed-off-by: Sahil Kumar <[email protected]>

* test(persistence): add coverage:ignore-file for entities and db/shared directory

Signed-off-by: Sahil Kumar <[email protected]>

* skip pub publish --dry-run for develop branch

Signed-off-by: Sahil Kumar <[email protected]>

* add remove from coverage script

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix

Signed-off-by: Sahil Kumar <[email protected]>

* fix locale

* fix message input _parseExistingMessage

* fix(UI-Kit): Show selected attachment file as file in message input widget

Signed-off-by: Sahil Kumar <[email protected]>

* fix: ws disconnection (#345)

* update ws removing custom implementation; fixed disconnection while connecting

* add test

* feat: Apply team lints to core package (#334)

* Add analyzer file to core package

* Apply team lint rules to core package

* clean up analyzer warnings in stream chat flutter

* remove unused line

* add ignore

* sync analysis options

* [Core -> MessageWidget] Fix variable dynamic types

Signed-off-by: Sahil Kumar <[email protected]>

Co-authored-by: Salvatore Giordano <[email protected]>
Co-authored-by: Sahil Kumar <[email protected]>

* test: add tests for stream chat flutter core (#354)

* test(core): add tests for channels bloc

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): refactor some tests due to changes in channels bloc

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for channel list core

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for users bloc, minor improvements

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for user list core, minor improvements

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for message search bloc, refactor

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for message search list core, refactor

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for lazy load scroll view

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for stream chat core

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for message list core

Signed-off-by: Sahil Kumar <[email protected]>

* test(core): add tests for stream channel

Signed-off-by: Sahil Kumar <[email protected]>

* Fix merge conflicts

Signed-off-by: Sahil Kumar <[email protected]>

* flutter format

Signed-off-by: Sahil Kumar <[email protected]>

* fixes

Signed-off-by: Sahil Kumar <[email protected]>

* update workflow

Signed-off-by: Sahil Kumar <[email protected]>

* chore(core): fix analyzer issues

Signed-off-by: Sahil Kumar <[email protected]>

* chore(ui-kit): fix analyzer issues

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(ui-kit): update giphy interaction (#361)

* refactor(ui-kit): update giphy interaction

Signed-off-by: Sahil Kumar <[email protected]>

* fix overflow

Co-authored-by: Salvatore Giordano <[email protected]>

* fix: Fixed sendingbutton.outside case (#355)

* fix: Fixed sendingbutton.outside case

* fix: Fixed all actionbutton cases

* fix review

Co-authored-by: Salvatore Giordano <[email protected]>

* fix(ui-kit): fix accessibility text size scaling in message widget

Signed-off-by: Sahil Kumar <[email protected]>

* fix(ui-kit): fix message widget long username overflow

Signed-off-by: Sahil Kumar <[email protected]>

* fix(ui-kit): fix message input dm checkbox accessibility scaling issue

Signed-off-by: Sahil Kumar <[email protected]>

* feat: exposed textbuilder callback (#367)

* exposed textbuilder callback

* exposed textbuilder callback

* fixes

Signed-off-by: Sahil Kumar <[email protected]>

* check if user.mutes is null

* check if user.mutes is null

* fixes #370: remove all controls on full screen media if message is of type ephemeral (#373)

* fix: fix channel list pagination spinner (#375)

Signed-off-by: Sahil Kumar <[email protected]>

* fix: attachments upload when size exceeds (#377)

* fix: Fixed file upload error

* fix: Fixed file upload error

* fix coverage script

Signed-off-by: Sahil Kumar <[email protected]>

* fix: message input (#382)

* feat: Added mentions suffix builder

* feat: Added builders for mentions and theme element for message input expanded button

* refactor: refactored PR to suit other style

* fix: fixes trailing commas and changes names

* doc: added documentation to new elements

* doc: added documentation to new elements

* bump version

* add meta dep to core

* sort deps

Co-authored-by: Sahil Kumar <[email protected]>
Co-authored-by: Neevash Ramdial (Nash) <[email protected]>
Co-authored-by: Deven Joshi <[email protected]>
This commit is contained in:
Salvatore Giordano
2021-04-07 12:24:29 +02:00
committed by GitHub
co-authored by Sahil Kumar Neevash Ramdial Deven Joshi
parent aeb6da8d38
commit 87ca818a9a
119 changed files with 11223 additions and 1240 deletions
@@ -5,11 +5,11 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/channels_bloc.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
import 'stream_chat_core.dart';
/// [ChannelListCore] is a simplified class that allows fetching a list of channels while exposing UI builders.
/// [ChannelListCore] is a simplified class that allows fetching a list of
/// channels while exposing UI builders.
/// A [ChannelListController] is used to reload and paginate data.
///
///
@@ -52,10 +52,11 @@ import 'stream_chat_core.dart';
/// }
/// ```
///
/// Make sure to have a [StreamChatCore] ancestor in order to provide the information about the channels.
/// Make sure to have a [StreamChatCore] ancestor in order to provide the
/// information about the channels.
class ChannelListCore extends StatefulWidget {
/// Instantiate a new ChannelListView
ChannelListCore({
const ChannelListCore({
Key key,
@required this.errorBuilder,
@required this.emptyBuilder,
@@ -65,18 +66,31 @@ class ChannelListCore extends StatefulWidget {
this.options,
this.sort,
this.pagination = const PaginationParams(
offset: 0,
limit: 25,
),
this.channelListController,
}) : assert(errorBuilder != null),
assert(emptyBuilder != null),
assert(loadingBuilder != null),
assert(listBuilder != null),
}) : assert(
errorBuilder != null,
'Parameter errorBuilder should not be null',
),
assert(
emptyBuilder != null,
'Parameter emptyBuilder should not be null',
),
assert(
loadingBuilder != null,
'Parameter loadingBuilder should not be null',
),
assert(
listBuilder != null,
'Parameter listBuilder should not be null',
),
super(key: key);
/// A [ChannelListController] allows reloading and pagination.
/// Use [ChannelListController.loadData] and [ChannelListController.paginateData] respectively for reloading and pagination.
/// Use [ChannelListController.loadData] and
/// [ChannelListController.paginateData] respectively for reloading and
/// pagination.
final ChannelListController channelListController;
/// The builder that will be used in case of error
@@ -103,9 +117,10 @@ class ChannelListCore extends StatefulWidget {
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.
/// 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<ChannelModel>> sort;
/// Pagination parameters
@@ -115,10 +130,11 @@ class ChannelListCore extends StatefulWidget {
final PaginationParams pagination;
@override
_ChannelListCoreState createState() => _ChannelListCoreState();
ChannelListCoreState createState() => ChannelListCoreState();
}
class _ChannelListCoreState extends State<ChannelListCore> {
/// The current state of the [ChannelListCore].
class ChannelListCoreState extends State<ChannelListCore> {
@override
Widget build(BuildContext context) {
final channelsBloc = ChannelsBloc.of(context);
@@ -128,37 +144,25 @@ class _ChannelListCoreState extends State<ChannelListCore> {
StreamBuilder<List<Channel>> _buildListView(
ChannelsBlocState channelsBlocState,
) {
return StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return _buildErrorWidget(snapshot, context, channelsBlocState);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final channels = snapshot.data;
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
},
);
}
Widget _buildErrorWidget(
AsyncSnapshot<List<Channel>> snapshot,
BuildContext context,
ChannelsBlocState channelsBlocState,
) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
return widget.errorBuilder(context, snapshot.error);
}
) =>
StreamBuilder<List<Channel>>(
stream: channelsBlocState.channelsStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final channels = snapshot.data;
if (channels.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, channels);
},
);
/// Fetches initial channels and updates the widget
Future<void> loadData() {
final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
@@ -169,6 +173,7 @@ class _ChannelListCoreState extends State<ChannelListCore> {
);
}
/// Fetches more channels with updated pagination and updates the widget
Future<void> paginateData() {
final channelsBloc = ChannelsBloc.of(context);
return channelsBloc.queryChannels(
@@ -209,9 +214,9 @@ class _ChannelListCoreState extends State<ChannelListCore> {
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.options?.toString() != oldWidget.options?.toString() ||
widget.pagination?.toJson()?.toString() !=
oldWidget.pagination?.toJson()?.toString() ||
widget.options?.toString() != oldWidget.options?.toString()) {
oldWidget.pagination?.toJson()?.toString()) {
loadData();
}
}
@@ -223,14 +228,15 @@ class _ChannelListCoreState extends State<ChannelListCore> {
}
}
/// Controller used for loading more data and controlling pagination in [ChannelListCore].
/// Controller used for loading more data and controlling pagination in
/// [ChannelListCore].
class ChannelListController {
/// This function calls Stream's servers to load a list of channels. If there is existing data,
/// calling this function causes a reload.
/// This function calls Stream's servers to load a list of channels.
/// If there is existing data, calling this function causes a reload.
AsyncCallback loadData;
/// This function is used to load another page of data. Note, [loadData] should be
/// used to populate the initial page of data. Calling [paginateData] performs a query
/// to load subsequent pages.
/// This function is used to load another page of data. Note, [loadData]
/// should be used to populate the initial page of data. Calling
/// [paginateData] performs a query to load subsequent pages.
AsyncCallback paginateData;
}
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -16,26 +17,29 @@ import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
///
/// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/
class ChannelsBloc extends StatefulWidget {
/// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and not null.
/// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and
/// not null.
const ChannelsBloc({
Key key,
@required this.child,
this.lockChannelsOrder = false,
this.channelsComparator,
this.shouldAddChannel,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null.'),
super(key: key);
/// The widget child
final Widget child;
/// Set this to true to prevent channels to be brought to the top of the list when a new message arrives
/// Set this to true to prevent channels to be brought to the top of the list
/// when a new message arrives
final bool lockChannelsOrder;
/// Comparator used to sort the channels when a message.new event is received
final Comparator<Channel> channelsComparator;
/// Function used to evaluate if a channel should be added to the list when a message.new event is received
/// Function used to evaluate if a channel should be added to the list when a
/// message.new event is received
final bool Function(Event) shouldAddChannel;
@override
@@ -70,10 +74,9 @@ class ChannelsBlocState extends State<ChannelsBloc>
/// The current channel list as a stream
Stream<List<Channel>> get channelsStream => _channelsController.stream;
final BehaviorSubject<bool> _queryChannelsLoadingController =
BehaviorSubject.seeded(false);
final _queryChannelsLoadingController = BehaviorSubject.seeded(false);
final BehaviorSubject<List<Channel>> _channelsController = BehaviorSubject();
final _channelsController = BehaviorSubject<List<Channel>>();
/// The stream notifying the state of queryChannel call
Stream<bool> get queryChannelsLoading =>
@@ -81,45 +84,58 @@ class ChannelsBlocState extends State<ChannelsBloc>
final List<Channel> _hiddenChannels = [];
bool _paginationEnded = false;
/// Calls [client.queryChannels] updating [queryChannelsLoading] stream
Future<void> queryChannels({
Map<String, dynamic> filter,
List<SortOption<ChannelModel>> sortOptions,
PaginationParams paginationParams,
Map<String, dynamic> options,
bool onlyOffline = false,
}) async {
final client = StreamChatCore.of(context).client;
if (client.state?.user == null ||
_queryChannelsLoadingController.value == true) {
if (_paginationEnded || _queryChannelsLoadingController.value == true) {
return;
}
_queryChannelsLoadingController.sink.add(true);
if (_channelsController.hasValue) {
_queryChannelsLoadingController.add(true);
}
try {
final clear = paginationParams == null ||
paginationParams.offset == null ||
paginationParams.offset == 0;
final oldChannels = List<Channel>.from(channels ?? []);
var newChannels = <Channel>[];
await for (final channels in client.queryChannels(
filter: filter,
sort: sortOptions,
options: options,
paginationParams: paginationParams,
)) {
newChannels = channels;
if (clear) {
_channelsController.add(channels);
} else {
final l = oldChannels + channels;
_channelsController.add(l);
final temp = oldChannels + channels;
_channelsController.add(temp);
}
if (_channelsController.hasValue &&
_queryChannelsLoadingController.value) {
_queryChannelsLoadingController.sink.add(false);
}
_queryChannelsLoadingController.sink.add(false);
}
} catch (err, stackTrace) {
print(err);
print(stackTrace);
_queryChannelsLoadingController.addError(err, stackTrace);
if (newChannels.isEmpty || newChannels.length < paginationParams.limit) {
_paginationEnded = true;
}
} catch (e, stk) {
if (_channelsController.hasValue) {
_queryChannelsLoadingController.addError(e, stk);
} else {
_channelsController.addError(e, stk);
}
}
}
@@ -135,15 +151,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
_subscriptions.add(client.on(EventType.messageNew).listen((e) {
final newChannels = List<Channel>.from(channels ?? []);
final index = newChannels.indexWhere((c) => c.cid == e.cid);
if (index > -1) {
if (index != -1) {
if (index > 0) {
final channel = newChannels.removeAt(index);
newChannels.insert(0, channel);
}
} else if (widget.shouldAddChannel != null &&
widget.shouldAddChannel(e)) {
} else if (widget.shouldAddChannel?.call(e) == true) {
final hiddenIndex = _hiddenChannels.indexWhere((c) => c.cid == e.cid);
if (hiddenIndex > -1) {
if (hiddenIndex != -1) {
newChannels.insert(0, _hiddenChannels[hiddenIndex]);
_hiddenChannels.removeAt(hiddenIndex);
} else {
@@ -170,13 +185,14 @@ class ChannelsBlocState extends State<ChannelsBloc>
_channelsController.add(newChannels);
}
}));
// ignore: cascade_invocations
_subscriptions.add(client
.on(
EventType.channelDeleted,
EventType.notificationRemovedFromChannel,
)
.listen((e) {
// ignore: cascade_invocations
final channel = e.channel;
_channelsController.add(List.from(
(channels ?? [])..removeWhere((c) => c.cid == channel.cid)));
@@ -1,13 +1,13 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
enum _LoadingStatus { LOADING, STABLE }
enum _LoadingStatus { loading, stable }
/// Wrapper around a [Scrollable] which triggers [onEndOfPage]/[onStartOfPage] the Scrollable
/// reaches to the start or end of the view extent.
class LazyLoadScrollView extends StatefulWidget {
/// Creates a new instance of [LazyLoadScrollView]. The parameter [child] must be
/// supplied and not null.
/// Creates a new instance of [LazyLoadScrollView]. The parameter [child]
/// must be supplied and not null.
const LazyLoadScrollView({
Key key,
@required this.child,
@@ -17,7 +17,7 @@ class LazyLoadScrollView extends StatefulWidget {
this.onPageScrollEnd,
this.onInBetweenOfPage,
this.scrollOffset = 100,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null'),
super(key: key);
/// The [Widget] that this widget watches for changes on
@@ -46,18 +46,17 @@ class LazyLoadScrollView extends StatefulWidget {
}
class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
_LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE;
double _scrollPosition = 0.0;
_LoadingStatus _loadMoreStatus = _LoadingStatus.stable;
double _scrollPosition = 0;
@override
Widget build(BuildContext context) {
return NotificationListener(
child: widget.child,
onNotification: _onNotification,
);
}
Widget build(BuildContext context) =>
NotificationListener<ScrollNotification>(
onNotification: _onNotification,
child: widget.child,
);
bool _onNotification(Notification notification) {
bool _onNotification(ScrollNotification notification) {
if (notification is ScrollStartNotification) {
if (widget.onPageScrollStart != null) {
widget.onPageScrollStart();
@@ -74,7 +73,7 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final pixels = notification.metrics.pixels;
final maxScrollExtent = notification.metrics.maxScrollExtent;
final minScrollExtent = notification.metrics.minScrollExtent;
final scrollOffset = widget.scrollOffset;
final scrollOffset = widget.scrollOffset ?? 0;
if (pixels > (minScrollExtent + scrollOffset) &&
pixels < (maxScrollExtent - scrollOffset)) {
@@ -88,26 +87,19 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels;
if (scrollOffset == null || scrollOffset == 0) {
if (extentAfter == 0) {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
if (extentBefore == 0) {
_onStartOfPage();
return true;
}
} else {
if (scrollingDown) {
if (extentAfter <= scrollOffset) {
_onEndOfPage();
}
} else {
if (extentBefore <= scrollOffset) {
_onStartOfPage();
}
if (extentBefore <= scrollOffset) {
_onStartOfPage();
return true;
}
}
_scrollPosition = pixels;
return true;
}
if (notification is OverscrollNotification) {
if (notification.overscroll > 0) {
@@ -122,22 +114,22 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
}
void _onEndOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
if (widget.onEndOfPage != null) {
_loadMoreStatus = _LoadingStatus.loading;
widget.onEndOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
_loadMoreStatus = _LoadingStatus.stable;
});
}
}
}
void _onStartOfPage() {
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) {
_loadMoreStatus = _LoadingStatus.LOADING;
if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.stable) {
if (widget.onStartOfPage != null) {
_loadMoreStatus = _LoadingStatus.loading;
widget.onStartOfPage().whenComplete(() {
_loadMoreStatus = _LoadingStatus.STABLE;
_loadMoreStatus = _LoadingStatus.stable;
});
}
}
@@ -1,12 +1,15 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
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_channel.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
import 'stream_channel.dart';
/// [MessageListCore] is a simplified class that allows fetching a list of messages while exposing UI builders.
/// [MessageListCore] is a simplified class that allows fetching a list of
/// messages while exposing UI builders.
///
/// A [MessageListController] is used to paginate data.
///
/// ```dart
@@ -50,12 +53,14 @@ import 'stream_channel.dart';
/// ```
///
///
/// Make sure to have a [StreamChannel] ancestor in order to provide the information about the channels.
/// 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.
///
class MessageListCore extends StatefulWidget {
/// Instantiate a new [MessageListView].
MessageListCore({
const MessageListCore({
Key key,
@required this.loadingBuilder,
@required this.emptyBuilder,
@@ -65,10 +70,16 @@ class MessageListCore extends StatefulWidget {
this.parentMessage,
this.messageListController,
this.messageFilter,
}) : assert(loadingBuilder != null),
assert(emptyBuilder != null),
assert(messageListBuilder != null),
assert(errorWidgetBuilder != null),
}) : assert(loadingBuilder != null, 'loadingBuilder should not be null'),
assert(emptyBuilder != null, 'emptyBuilder should not be null'),
assert(
messageListBuilder != null,
'messageListBuilder should not be null',
),
assert(
errorWidgetBuilder != null,
'errorWidgetBuilder should not be null',
),
super(key: key);
/// A [MessageListController] allows pagination.
@@ -84,12 +95,15 @@ class MessageListCore extends StatefulWidget {
/// Function used to build an empty widget
final WidgetBuilder emptyBuilder;
/// Callback triggered when an error occurs while performing the given request.
/// This parameter can be used to display an error message to users in the event
/// of a connection failure.
/// Callback triggered when an error occurs while performing the given
/// request.
///
/// This parameter can be used to display an error message to users in the
/// event of a connection failure.
final ErrorBuilder errorWidgetBuilder;
/// If true will show a scroll to bottom message when there are new messages and the scroll offset is not zero
/// If true will show a scroll to bottom message when there are new messages
/// and the scroll offset is not zero.
final bool showScrollToBottom;
/// If the current message belongs to a `thread`, this property represents the
@@ -100,32 +114,28 @@ class MessageListCore extends StatefulWidget {
final bool Function(Message) messageFilter;
@override
_MessageListCoreState createState() => _MessageListCoreState();
MessageListCoreState createState() => MessageListCoreState();
}
class _MessageListCoreState extends State<MessageListCore> {
StreamChannelState streamChannel;
/// The current state of the [MessageListCore].
class MessageListCoreState extends State<MessageListCore> {
StreamChannelState _streamChannel;
bool get _upToDate => streamChannel.channel.state.isUpToDate;
bool get _upToDate => _streamChannel.channel.state.isUpToDate;
bool get _isThreadConversation => widget.parentMessage != null;
OwnUser get _currentUser => streamChannel.channel.client.state.user;
OwnUser get _currentUser => _streamChannel.channel.client.state.user;
int initialIndex;
double initialAlignment;
List<Message> messages = <Message>[];
bool initialMessageHighlightComplete = false;
var _messages = <Message>[];
@override
Widget build(BuildContext context) {
final messagesStream = _isThreadConversation
? streamChannel.channel.state.threadsStream
? _streamChannel.channel.state.threadsStream
.where((threads) => threads.containsKey(widget.parentMessage.id))
.map((threads) => threads[widget.parentMessage.id])
: streamChannel.channel.state?.messagesStream;
: _streamChannel.channel.state?.messagesStream;
bool defaultFilter(Message m) {
final isMyMessage = m.user.id == _currentUser.id;
@@ -138,10 +148,10 @@ class _MessageListCoreState extends State<MessageListCore> {
stream: messagesStream?.map((messages) =>
messages?.where(widget.messageFilter ?? defaultFilter)?.toList()),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
} else if (snapshot.hasError) {
if (snapshot.hasError) {
return widget.errorWidgetBuilder(context, snapshot.error);
} else if (!snapshot.hasData) {
return widget.loadingBuilder(context);
} else {
final messageList = snapshot.data?.reversed?.toList() ?? [];
if (messageList.isEmpty && !_isThreadConversation) {
@@ -149,29 +159,32 @@ class _MessageListCoreState extends State<MessageListCore> {
return widget.emptyBuilder(context);
}
} else {
messages = messageList;
_messages = messageList;
}
return widget.messageListBuilder(context, messages);
return widget.messageListBuilder(context, _messages);
}
},
);
}
/// Fetches more messages with updated pagination and updates the widget.
///
/// Optionally pass the fetch direction, defaults to [QueryDirection.bottom]
Future<void> paginateData(
{QueryDirection direction = QueryDirection.bottom}) {
if (!_isThreadConversation) {
return streamChannel.queryMessages(direction: direction);
return _streamChannel.queryMessages(direction: direction);
} else {
return streamChannel.getReplies(widget.parentMessage.id);
return _streamChannel.getReplies(widget.parentMessage.id);
}
}
@override
void initState() {
streamChannel = StreamChannel.of(context);
_streamChannel = StreamChannel.of(context);
if (_isThreadConversation) {
streamChannel.getReplies(widget.parentMessage.id);
_streamChannel.getReplies(widget.parentMessage.id);
}
if (widget.messageListController != null) {
@@ -184,7 +197,7 @@ class _MessageListCoreState extends State<MessageListCore> {
@override
void dispose() {
if (!_upToDate) {
streamChannel.reloadChannel();
_streamChannel.reloadChannel();
}
super.dispose();
}
@@ -193,5 +206,5 @@ class _MessageListCoreState extends State<MessageListCore> {
/// Controller used for paginating data in [ChannelListView]
class MessageListController {
/// Call this function to load further data
Function({QueryDirection direction}) paginateData;
Future<void> Function({QueryDirection direction}) paginateData;
}
@@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_core.dart';
import 'package:stream_chat_flutter_core/src/stream_chat_core.dart';
/// [MessageSearchBloc] is used to manage a list of messages with pagination.
/// This class can be used to load messages, perform queries, etc.
@@ -16,7 +15,7 @@ class MessageSearchBloc extends StatefulWidget {
const MessageSearchBloc({
Key key,
@required this.child,
}) : assert(child != null),
}) : assert(child != null, 'Parameter child should not be null.'),
super(key: key);
/// The widget child
@@ -49,17 +48,16 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
Stream<List<GetMessageResponse>> get messagesStream =>
_messageResponses.stream;
final BehaviorSubject<List<GetMessageResponse>> _messageResponses =
BehaviorSubject();
final _messageResponses = BehaviorSubject<List<GetMessageResponse>>();
final BehaviorSubject<bool> _queryMessagesLoadingController =
BehaviorSubject.seeded(false);
final _queryMessagesLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryMessagesLoading =>
_queryMessagesLoadingController.stream;
/// Calls [StreamChatClient.search] updating [messageResponses] stream
/// Calls [StreamChatClient.search] updating
/// [messagesStream] and [queryMessagesLoading] stream
Future<void> search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
@@ -67,33 +65,13 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
String query,
PaginationParams pagination,
}) async {
_messageResponses.add(null);
try {
final messages = await _search(
filter: filter,
messageFilter: messageFilter,
sort: sort,
query: query,
pagination: pagination,
);
_messageResponses.add(messages.results);
} catch (err, stk) {
_messageResponses.addError(err, stk);
}
}
final client = StreamChatCore.of(context).client;
/// Calls [StreamChatClient.search] updating [queryMessagesLoading] stream
Future<void> loadMore({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) async {
if (_queryMessagesLoadingController.value == true) {
return;
if (_queryMessagesLoadingController.value == true) return;
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.add(true);
}
_queryMessagesLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
@@ -101,12 +79,12 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
final oldMessages = List<GetMessageResponse>.from(messageResponses ?? []);
final messages = await _search(
filter: filter,
messageFilter: messageFilter,
final messages = await client.search(
filter,
sort: sort,
query: query,
pagination: pagination,
paginationParams: pagination,
messageFilters: messageFilter,
);
if (clear) {
@@ -115,30 +93,18 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
final temp = oldMessages + messages.results;
_messageResponses.add(temp);
}
_queryMessagesLoadingController.add(false);
} catch (err, stackTrace) {
_queryMessagesLoadingController.addError(err, stackTrace);
if (_messageResponses.hasValue && _queryMessagesLoadingController.value) {
_queryMessagesLoadingController.add(false);
}
} catch (e, stk) {
if (_messageResponses.hasValue) {
_queryMessagesLoadingController.addError(e, stk);
} else {
_messageResponses.addError(e, stk);
}
}
}
Future<SearchMessagesResponse> _search({
Map<String, dynamic> filter,
Map<String, dynamic> messageFilter,
List<SortOption> sort,
String query,
PaginationParams pagination,
}) {
final client = StreamChatCore.of(context).client;
return client.search(
filter,
sort: sort,
query: query,
paginationParams: pagination,
messageFilters: messageFilter,
);
}
@override
Widget build(BuildContext context) {
super.build(context);
@@ -1,13 +1,15 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/message_search_bloc.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
import 'message_search_bloc.dart';
///
/// [MessageSearchListCore] is a simplified class that allows searching for messages across channels while exposing UI builders.
/// A [MessageSearchListController] is used to load and paginate data.
/// [MessageSearchListCore] is a simplified class that allows searching for
/// messages across channels while exposing UI builders.
/// A [MessageSearchListController] is used to load and paginate data.
///
/// ```dart
/// class MessageSearchPage extends StatelessWidget {
@@ -28,7 +30,8 @@ import 'message_search_bloc.dart';
/// }
/// ```
///
/// Make sure to have a [MessageSearchBloc] ancestor in order to provide the information about the messages.
/// 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.
///
class MessageSearchListCore extends StatefulWidget {
@@ -50,14 +53,16 @@ class MessageSearchListCore extends StatefulWidget {
this.paginationParams,
this.messageFilters,
this.messageSearchListController,
}) : assert(emptyBuilder != null),
assert(errorBuilder != null),
assert(loadingBuilder != null),
assert(childBuilder != null),
}) : assert(emptyBuilder != null, 'emptyBuilder should not be null'),
assert(errorBuilder != null, 'errorBuilder should not be null'),
assert(loadingBuilder != null, 'loadingBuilder should not be null'),
assert(childBuilder != null, 'childBuilder should not be null'),
super(key: key);
/// A [MessageSearchListController] allows reloading and pagination.
/// Use [MessageSearchListController.loadData] and [MessageSearchListController.paginateData] respectively for reloading and pagination.
/// Use [MessageSearchListController.loadData] and
/// [MessageSearchListController.paginateData] respectively for reloading and
/// pagination.
final MessageSearchListController messageSearchListController;
/// Message String to search on
@@ -69,9 +74,10 @@ class MessageSearchListCore extends StatefulWidget {
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.
/// 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
@@ -98,22 +104,15 @@ class MessageSearchListCore extends StatefulWidget {
final WidgetBuilder loadingBuilder;
@override
_MessageSearchListCoreState createState() => _MessageSearchListCoreState();
MessageSearchListCoreState createState() => MessageSearchListCoreState();
}
class _MessageSearchListCoreState extends State<MessageSearchListCore> {
/// The current state of the [MessageSearchListCore].
class MessageSearchListCoreState extends State<MessageSearchListCore> {
@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,
);
void didChangeDependencies() {
super.didChangeDependencies();
loadData();
if (widget.messageSearchListController != null) {
widget.messageSearchListController.loadData = loadData;
widget.messageSearchListController.paginateData = paginateData;
@@ -126,37 +125,28 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
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);
Widget _buildListView(MessageSearchBlocState messageSearchBloc) =>
StreamBuilder<List<GetMessageResponse>>(
stream: messageSearchBloc.messagesStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.childBuilder(snapshot.data);
},
);
return widget.errorBuilder(context, snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.childBuilder(snapshot.data);
},
);
}
void loadData() {
/// Fetches initial messages and updates the widget
Future<void> loadData() {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.search(
return messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
query: widget.messageQuery,
@@ -165,10 +155,10 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
);
}
void paginateData() {
/// Fetches more messages with updated pagination and updates the widget
Future<void> paginateData() {
final messageSearchBloc = MessageSearchBloc.of(context);
messageSearchBloc.loadMore(
return messageSearchBloc.search(
filter: widget.filters,
sort: widget.sortOptions,
pagination: widget.paginationParams.copyWith(
@@ -184,19 +174,12 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
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,
);
oldWidget.messageFilters?.toString() ||
widget.paginationParams?.toJson()?.toString() !=
oldWidget.paginationParams?.toJson()?.toString()) {
loadData();
}
}
}
@@ -204,8 +187,8 @@ class _MessageSearchListCoreState extends State<MessageSearchListCore> {
/// Controller used for paginating data in [ChannelListView]
class MessageSearchListController {
/// Call this function to reload data
VoidCallback loadData;
AsyncCallback loadData;
/// Call this function to load further data
VoidCallback paginateData;
AsyncCallback paginateData;
}
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
@@ -25,8 +26,8 @@ class StreamChannel extends StatefulWidget {
@required this.channel,
this.showLoading = true,
this.initialMessageId,
}) : assert(child != null),
assert(channel != null),
}) : assert(child != null, 'Child should not be null'),
assert(channel != null, 'Channel should not be null'),
super(key: key);
// ignore: public_member_api_docs
@@ -49,7 +50,8 @@ class StreamChannel extends StatefulWidget {
if (streamChannelState == null) {
throw Exception(
'You must have a StreamChannel widget at the top of your widget tree');
'You must have a StreamChannel widget at the top of your widget tree',
);
}
return streamChannelState;
@@ -204,17 +206,15 @@ class StreamChannelState extends State<StreamChannel> {
int before = 20,
int after = 20,
bool preferOffline = false,
}) {
return queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
}
}) =>
_queryAtMessage(
messageId: messageId,
before: before,
after: after,
preferOffline: preferOffline,
);
///
Future<void> queryAtMessage({
Future<void> _queryAtMessage({
String messageId,
int before = 20,
int after = 20,
@@ -254,15 +254,14 @@ class StreamChannelState extends State<StreamChannel> {
String messageId, {
int limit = 20,
bool preferOffline = false,
}) {
return channel.query(
messagesPagination: PaginationParams(
lessThan: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
}
}) =>
channel.query(
messagesPagination: PaginationParams(
lessThan: messageId,
limit: limit,
),
preferOffline: preferOffline,
);
///
Future<ChannelState> queryAfterMessage(
@@ -297,7 +296,7 @@ class StreamChannelState extends State<StreamChannel> {
}
/// Reloads the channel with latest message
Future<void> reloadChannel() => queryAtMessage(before: 30);
Future<void> reloadChannel() => _queryAtMessage(before: 30);
List<Future<bool>> _futures;
@@ -305,8 +304,7 @@ class StreamChannelState extends State<StreamChannel> {
try {
await loadChannelAtMessage(initialMessageId);
return true;
} catch (e, stk) {
print('Error: $e\nStack: $stk');
} catch (_) {
rethrow;
}
}
@@ -349,9 +347,6 @@ class StreamChannelState extends State<StreamChannel> {
],
builder: (context, snapshot) {
if (snapshot.hasError) {
if (snapshot.error is Error) {
print((snapshot.error as Error).stackTrace);
}
var message = snapshot.error.toString();
if (snapshot.error is DioError) {
final dioError = snapshot.error as DioError;
@@ -361,14 +356,13 @@ class StreamChannelState extends State<StreamChannel> {
message = 'Check your connection and retry';
}
}
return Center(
child: Text(message),
);
return Center(child: Text(message));
}
final initialized = snapshot.data[0];
// ignore: avoid_bool_literals_in_conditional_expressions
final dataLoaded = initialMessageId == null ? true : snapshot.data[1];
if (widget.showLoading && (!initialized || !dataLoaded)) {
return Center(
return const Center(
child: CircularProgressIndicator(),
);
}
@@ -3,13 +3,12 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'typedef.dart';
import 'package:stream_chat_flutter_core/src/typedef.dart';
/// Widget used to provide information about the chat to the widget tree.
/// This Widget is used to react to life cycle changes and system updates.
/// When the app goes into the background, the websocket connection is kept alive
/// for two minutes before being terminated.
/// When the app goes into the background, the websocket connection is kept
/// alive for two minutes before being terminated.
///
/// Conversely, when app is resumed or restarted, a new connection is initiated.
///
@@ -36,16 +35,16 @@ import 'typedef.dart';
class StreamChatCore extends StatefulWidget {
/// Constructor used for creating a new instance of [StreamChatCore].
///
/// [StreamChatCore] is a stateful widget which reacts to system events and updates
/// Stream's connection status accordingly.
StreamChatCore({
/// [StreamChatCore] is a stateful widget which reacts to system events and
/// updates Stream's connection status accordingly.
const StreamChatCore({
Key key,
@required this.client,
@required this.child,
this.onBackgroundEventReceived,
this.backgroundKeepAlive = const Duration(minutes: 1),
}) : assert(client != null),
assert(child != null),
}) : assert(client != null, 'Stream Chat Client should not be null'),
assert(child != null, 'Child should not be null'),
super(key: key);
/// Instance of Stream Chat Client containing information about the current
@@ -55,7 +54,8 @@ class StreamChatCore extends StatefulWidget {
/// Widget descendant.
final Widget child;
/// The amount of time that will pass before disconnecting the client in the background
/// The amount of time that will pass before disconnecting the client in
/// the background
final Duration backgroundKeepAlive;
/// Handler called whenever the [client] receives a new [Event] while the app
@@ -90,15 +90,13 @@ class StreamChatCoreState extends State<StreamChatCore>
Timer _disconnectTimer;
@override
Widget build(BuildContext context) {
return widget.child;
}
Widget build(BuildContext context) => widget.child;
/// The current user
User get user => widget.client.state.user;
User get user => client.state?.user;
/// The current user as a stream
Stream<User> get userStream => widget.client.state.userStream;
Stream<User> get userStream => client.state?.userStream;
@override
void initState() {
@@ -110,21 +108,25 @@ class StreamChatCoreState extends State<StreamChatCore>
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (client.state?.user != null) {
if (user != null) {
if (state == AppLifecycleState.paused) {
if (widget.onBackgroundEventReceived != null) {
_eventSubscription =
client.on().listen(widget.onBackgroundEventReceived);
_disconnectTimer = Timer(
widget.backgroundKeepAlive,
client.disconnect,
);
} else {
if (widget.onBackgroundEventReceived == null) {
client.disconnect();
return;
}
_eventSubscription = client.on().listen(
widget.onBackgroundEventReceived,
);
void onTimerComplete() {
_eventSubscription.cancel();
client.disconnect();
}
_disconnectTimer = Timer(widget.backgroundKeepAlive, onTimerComplete);
} else if (state == AppLifecycleState.resumed) {
_eventSubscription?.cancel();
if (_disconnectTimer?.isActive == true) {
_eventSubscription.cancel();
_disconnectTimer.cancel();
} else {
if (client.wsConnectionStatus == ConnectionStatus.disconnected) {
@@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart';
import 'package:stream_chat/stream_chat.dart';
/// A signature for a callback which exposes an error and returns a function.
/// This Callback can be used in cases where an API failure occurs and the widget
/// is unable to render data.
/// This Callback can be used in cases where an API failure occurs and the
/// widget is unable to render data.
typedef ErrorBuilder = Widget Function(BuildContext context, Object error);
/// A Signature for a handler function which will expose a [event].
@@ -1,11 +1,13 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_flutter_core/src/users_bloc.dart';
///
/// [UserListCore] is a simplified class that allows fetching users while exposing UI builders.
/// [UserListCore] is a simplified class that allows fetching users while
/// exposing UI builders.
/// A [UserListController] is used to load and paginate data.
///
/// ```dart
@@ -50,34 +52,35 @@ import 'package:stream_chat_flutter_core/src/users_bloc.dart';
/// [UsersBloc] must be the ancestor of this widget. This is necessary since
/// [UserListCore] depends on functionality contained within [UsersBloc].
///
/// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and [errorBuilder] must all be supplied
/// and not null.
/// The parameters [listBuilder], [loadingBuilder], [emptyBuilder] and
/// [errorBuilder] must all be supplied and not null.
class UserListCore extends StatefulWidget {
/// Instantiate a new [UserListCore]
const UserListCore({
Key key,
@required this.errorBuilder,
@required this.emptyBuilder,
@required this.loadingBuilder,
@required this.listBuilder,
Key key,
this.filter,
this.options,
this.sort,
this.pagination,
this.groupAlphabetically = false,
this.userListController,
}) : assert(errorBuilder != null),
assert(emptyBuilder != null),
assert(loadingBuilder != null),
assert(listBuilder != null),
}) : assert(errorBuilder != null, ''),
assert(emptyBuilder != null, ''),
assert(loadingBuilder != null, ''),
assert(listBuilder != null, ''),
super(key: key);
/// A [UserListController] allows reloading and pagination.
/// Use [UserListController.loadData] and [UserListController.paginateData] respectively for reloading and pagination.
/// Use [UserListController.loadData] and [UserListController.paginateData]
/// respectively for reloading and pagination.
final UserListController userListController;
/// The builder that will be used in case of error
final Widget Function(Error error) errorBuilder;
final Widget Function(Object error) errorBuilder;
/// The builder that will be used to build the list
final Widget Function(BuildContext context, List<ListItem> users) listBuilder;
@@ -100,9 +103,9 @@ class UserListCore extends StatefulWidget {
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.
/// 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
@@ -117,22 +120,16 @@ class UserListCore extends StatefulWidget {
final bool groupAlphabetically;
@override
_UserListCoreState createState() => _UserListCoreState();
UserListCoreState createState() => UserListCoreState();
}
class _UserListCoreState extends State<UserListCore>
/// The current state of the [UserListCore].
class UserListCoreState extends State<UserListCore>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
final usersBloc = UsersBloc.of(context);
usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
options: widget.options,
);
void didChangeDependencies() {
super.didChangeDependencies();
loadData();
if (widget.userListController != null) {
widget.userListController.loadData = loadData;
widget.userListController.paginateData = paginateData;
@@ -142,77 +139,64 @@ class _UserListCoreState extends State<UserListCore>
@override
Widget build(BuildContext context) {
final _usersBloc = UsersBloc.of(context);
return _buildListView(_usersBloc);
}
bool get isListAlreadySorted =>
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));
) =>
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 (final e in temp) {
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (final key in groupedUsers.keys) {
items
..add(ListHeaderItem(key))
..addAll(groupedUsers[key].map((e) => ListUserItem(e)));
}
return items;
}
final groupedUsers = <String, List<User>>{};
for (final e in temp) {
final alphabet = e.name[0]?.toUpperCase();
groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e];
}
final items = <ListItem>[];
for (final 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();
},
);
}
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);
) =>
StreamBuilder(
stream: _buildUserStream(usersBlocState),
builder: (context, snapshot) {
if (snapshot.hasError) {
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, items);
},
);
return widget.errorBuilder(snapshot.error);
}
if (!snapshot.hasData) {
return widget.loadingBuilder(context);
}
final items = snapshot.data;
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
if (items.isEmpty) {
return widget.emptyBuilder(context);
}
return widget.listBuilder(context, items);
},
);
}
void loadData() {
// ignore: public_member_api_docs
Future<void> loadData() {
final _usersBloc = UsersBloc.of(context);
_usersBloc.queryUsers(
return _usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination,
@@ -220,10 +204,10 @@ class _UserListCoreState extends State<UserListCore>
);
}
void paginateData() {
// ignore: public_member_api_docs
Future<void> paginateData() {
final _usersBloc = UsersBloc.of(context);
_usersBloc.queryUsers(
return _usersBloc.queryUsers(
filter: widget.filter,
sort: widget.sort,
pagination: widget.pagination.copyWith(
@@ -238,29 +222,23 @@ class _UserListCoreState extends State<UserListCore>
super.didUpdateWidget(oldWidget);
if (widget.filter?.toString() != oldWidget.filter?.toString() ||
jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) ||
widget.options?.toString() != oldWidget.options?.toString() ||
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,
);
oldWidget.pagination?.toJson()?.toString()) {
loadData();
}
}
}
/// Represents an item in a the user stream list.
/// Header items are prefixed with the key `HEADER` While users are prefixed with
/// `USER`.
/// Header items are prefixed with the key `HEADER` While users are prefixed
/// with `USER`.
abstract class ListItem {
// ignore: public_member_api_docs
/// Unique key per list item
String get key {
if (this is ListHeaderItem) {
final header = (this as ListHeaderItem).heading;
return 'HEADER-$header';
return 'HEADER-${header.toLowerCase()}';
}
if (this is ListUserItem) {
final user = (this as ListUserItem).user;
@@ -269,7 +247,8 @@ abstract class ListItem {
return null;
}
// ignore: public_member_api_docs
/// Helper function to build widget based on ListItem type
// ignore: missing_return
Widget when({
@required Widget Function(String heading) headerItem,
@required Widget Function(User user) userItem,
@@ -280,33 +259,32 @@ abstract class ListItem {
if (this is ListUserItem) {
return userItem((this as ListUserItem).user);
}
return SizedBox();
}
}
// ignore: public_member_api_docs
/// Header Item
class ListHeaderItem extends ListItem {
// ignore: public_member_api_docs
final String heading;
// ignore: public_member_api_docs
/// Constructs a new [ListHeaderItem]
ListHeaderItem(this.heading);
/// Heading used to build the item.
final String heading;
}
// ignore: public_member_api_docs
/// User Item
class ListUserItem extends ListItem {
// ignore: public_member_api_docs
final User user;
// ignore: public_member_api_docs
/// Constructs a new [ListUserItem]
ListUserItem(this.user);
/// [User] used to build the item.
final User user;
}
/// Controller used for paginating data in [ChannelListView]
class UserListController {
/// Call this function to reload data
VoidCallback loadData;
AsyncCallback loadData;
/// Call this function to load further data
VoidCallback paginateData;
AsyncCallback paginateData;
}
@@ -1,8 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/stream_chat.dart';
import 'stream_chat_core.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget dedicated to the management of a users list with pagination.
///
@@ -14,9 +14,12 @@ class UsersBloc extends StatefulWidget {
/// Instantiate a new [UsersBloc]. The parameter [child] must be supplied and
/// not null.
const UsersBloc({
Key key,
@required this.child,
}) : assert(child != null),
Key key,
}) : assert(
child != null,
'When constructing a UsersBloc, the parameter '
'child should not be null.'),
super(key: key);
/// The widget child
@@ -48,10 +51,9 @@ class UsersBlocState extends State<UsersBloc>
/// The current users list as a stream
Stream<List<User>> get usersStream => _usersController.stream;
final BehaviorSubject<List<User>> _usersController = BehaviorSubject();
final _usersController = BehaviorSubject<List<User>>();
final BehaviorSubject<bool> _queryUsersLoadingController =
BehaviorSubject.seeded(false);
final _queryUsersLoadingController = BehaviorSubject.seeded(false);
/// The stream notifying the state of queryUsers call
Stream<bool> get queryUsersLoading => _queryUsersLoadingController.stream;
@@ -67,11 +69,12 @@ class UsersBlocState extends State<UsersBloc>
}) async {
final client = StreamChatCore.of(context).client;
if (client.state?.user == null ||
_queryUsersLoadingController.value == true) {
return;
if (_queryUsersLoadingController.value == true) return;
if (_usersController.hasValue) {
_queryUsersLoadingController.add(true);
}
_queryUsersLoadingController.add(true);
try {
final clear = pagination == null ||
pagination.offset == null ||
@@ -92,10 +95,15 @@ class UsersBlocState extends State<UsersBloc>
final temp = oldUsers + usersResponse.users;
_usersController.add(temp);
}
_queryUsersLoadingController.add(false);
} catch (err, stackTrace) {
_queryUsersLoadingController.addError(err, stackTrace);
if (_usersController.hasValue && _queryUsersLoadingController.value) {
_queryUsersLoadingController.add(false);
}
} catch (e, stk) {
if (_usersController.hasValue) {
_queryUsersLoadingController.addError(e, stk);
} else {
_usersController.addError(e, stk);
}
}
}
@@ -1,14 +1,15 @@
library stream_chat_flutter_core;
export 'src/channel_list_core.dart';
export 'package:stream_chat/stream_chat.dart';
export 'src/channel_list_core.dart' hide ChannelListCoreState;
export 'src/channels_bloc.dart';
export 'src/lazy_load_scroll_view.dart';
export 'src/message_list_core.dart';
export 'src/message_list_core.dart' hide MessageListCoreState;
export 'src/message_search_bloc.dart';
export 'src/message_search_list_core.dart';
export 'src/message_search_list_core.dart' hide MessageSearchListCoreState;
export 'src/stream_channel.dart';
export 'src/stream_chat_core.dart';
export 'src/user_list_core.dart';
export 'src/users_bloc.dart';
export 'src/typedef.dart';
export 'package:stream_chat/stream_chat.dart';
export 'src/user_list_core.dart' hide UserListCoreState;
export 'src/users_bloc.dart';