Merge pull request #540 from GetStream/feat/channel-pagination-error-retry

fix: channel pagination error retry
This commit is contained in:
Salvatore Giordano
2021-07-14 13:48:16 +02:00
committed by GitHub
5 changed files with 149 additions and 145 deletions
@@ -202,6 +202,7 @@ class _ChannelListViewState extends State<ChannelListView> {
final _slideController = SlidableController(); final _slideController = SlidableController();
late final _defaultController = ChannelListController(); late final _defaultController = ChannelListController();
ChannelListController get _channelListController => ChannelListController get _channelListController =>
widget.channelListController ?? _defaultController; widget.channelListController ?? _defaultController;
@@ -237,39 +238,32 @@ class _ChannelListViewState extends State<ChannelListView> {
} }
Widget _buildListView(BuildContext context, List<Channel> channels) { Widget _buildListView(BuildContext context, List<Channel> channels) {
late Widget child; if (widget.crossAxisCount > 1) {
return GridView.builder(
if (channels.isNotEmpty) { padding: widget.padding,
if (widget.crossAxisCount > 1) { gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
child = GridView.builder( crossAxisCount: widget.crossAxisCount,
padding: widget.padding, ),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( itemCount: channels.length,
crossAxisCount: widget.crossAxisCount, physics: const AlwaysScrollableScrollPhysics(),
), itemBuilder: (context, index) =>
itemCount: channels.length, _gridItemBuilder(context, index, channels),
physics: const AlwaysScrollableScrollPhysics(), );
itemBuilder: (context, index) =>
_gridItemBuilder(context, index, channels),
);
} else {
child = ListView.separated(
padding: widget.padding,
physics: const AlwaysScrollableScrollPhysics(),
itemCount:
channels.isNotEmpty ? channels.length + 1 : channels.length,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder!(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) =>
_listItemBuilder(context, index, channels),
);
}
} }
return ListView.separated(
return child; padding: widget.padding,
physics: const AlwaysScrollableScrollPhysics(),
// all channels + progress loader
itemCount: channels.length + 1,
separatorBuilder: (_, index) {
if (widget.separatorBuilder != null) {
return widget.separatorBuilder!(context, index);
}
return _separatorBuilder(context, index);
},
itemBuilder: (context, index) =>
_listItemBuilder(context, index, channels),
);
} }
Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder( Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder(
@@ -486,111 +480,111 @@ class _ChannelListViewState extends State<ChannelListView> {
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) { Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
final channelsBloc = ChannelsBloc.of(context); final channelsBloc = ChannelsBloc.of(context);
if (i == channels.length) {
return _buildQueryProgressIndicator(context, channelsBloc);
}
final onTap = _getChannelTap(context); final onTap = _getChannelTap(context);
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
final backgroundColor = chatThemeData.colorTheme.inputBg; final backgroundColor = chatThemeData.colorTheme.inputBg;
final channel = channels[i];
if (i < channels.length) { return StreamChannel(
final channel = channels[i]; key: ValueKey<String>('CHANNEL-${channel.cid}'),
channel: channel,
return StreamChannel( child: Slidable(
key: ValueKey<String>('CHANNEL-${channel.cid}'), controller: _slideController,
channel: channel, enabled: widget.swipeToAction,
child: Slidable( actionPane: const SlidableBehindActionPane(),
controller: _slideController, actionExtentRatio: 0.12,
enabled: widget.swipeToAction, secondaryActions: widget.swipeActions
actionPane: const SlidableBehindActionPane(), ?.map((e) => IconSlideAction(
actionExtentRatio: 0.12, color: e.color,
secondaryActions: widget.swipeActions iconWidget: e.iconWidget,
?.map((e) => IconSlideAction( onTap: () {
color: e.color, e.onTap?.call(channel);
iconWidget: e.iconWidget, },
onTap: () { ))
e.onTap?.call(channel); .toList() ??
}, <Widget>[
)) IconSlideAction(
.toList() ?? color: backgroundColor,
<Widget>[ icon: Icons.more_horiz,
onTap: widget.onMoreDetailsPressed != null
? () {
widget.onMoreDetailsPressed!(channel);
}
: () {
showModalBottomSheet(
clipBehavior: Clip.hardEdge,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(32),
topRight: Radius.circular(32),
),
),
context: context,
builder: (context) => StreamChannel(
channel: channel,
child: ChannelBottomSheet(
onViewInfoTap: () {
widget.onViewInfoTap?.call(channel);
},
),
),
);
},
),
if ([
'admin',
'owner',
].contains(channel.state!.members
.firstWhereOrNull(
(m) => m.userId == channel.client.state.user?.id)
?.role))
IconSlideAction( IconSlideAction(
color: backgroundColor, color: backgroundColor,
icon: Icons.more_horiz, iconWidget: StreamSvgIcon.delete(
onTap: widget.onMoreDetailsPressed != null color: chatThemeData.colorTheme.accentError,
),
onTap: widget.onDeletePressed != null
? () { ? () {
widget.onMoreDetailsPressed!(channel); widget.onDeletePressed!(channel);
} }
: () { : () async {
showModalBottomSheet( final res = await showConfirmationDialog(
clipBehavior: Clip.hardEdge, context,
shape: const RoundedRectangleBorder( title: 'Delete Conversation',
borderRadius: BorderRadius.only( okText: 'DELETE',
topLeft: Radius.circular(32), question:
topRight: Radius.circular(32), // ignore: lines_longer_than_80_chars
), 'Are you sure you want to delete this conversation?',
), cancelText: 'CANCEL',
context: context, icon: StreamSvgIcon.delete(
builder: (context) => StreamChannel( color: chatThemeData.colorTheme.accentError,
channel: channel,
child: ChannelBottomSheet(
onViewInfoTap: () {
widget.onViewInfoTap?.call(channel);
},
),
), ),
); );
if (res == true) {
await channel.delete();
}
}, },
), ),
if ([ ],
'admin', child: DecoratedBox(
'owner', decoration: BoxDecoration(
].contains(channel.state!.members color: chatThemeData.colorTheme.appBg,
.firstWhereOrNull(
(m) => m.userId == channel.client.state.user?.id)
?.role))
IconSlideAction(
color: backgroundColor,
iconWidget: StreamSvgIcon.delete(
color: chatThemeData.colorTheme.accentError,
),
onTap: widget.onDeletePressed != null
? () {
widget.onDeletePressed!(channel);
}
: () async {
final res = await showConfirmationDialog(
context,
title: 'Delete Conversation',
okText: 'DELETE',
question:
// ignore: lines_longer_than_80_chars
'Are you sure you want to delete this conversation?',
cancelText: 'CANCEL',
icon: StreamSvgIcon.delete(
color: chatThemeData.colorTheme.accentError,
),
);
if (res == true) {
await channel.delete();
}
},
),
],
child: DecoratedBox(
decoration: BoxDecoration(
color: chatThemeData.colorTheme.appBg,
),
child: widget.channelPreviewBuilder?.call(context, channel) ??
ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: () => widget.onImageTap?.call(channel),
onTap: (channel) => onTap(channel, widget.channelWidget),
),
), ),
child: widget.channelPreviewBuilder?.call(context, channel) ??
ChannelPreview(
onLongPress: widget.onChannelLongPress,
channel: channel,
onImageTap: () => widget.onImageTap?.call(channel),
onTap: (channel) => onTap(channel, widget.channelWidget),
),
), ),
); ),
} else { );
return _buildQueryProgressIndicator(context, channelsBloc);
}
} }
ChannelTapCallback _getChannelTap(BuildContext context) { ChannelTapCallback _getChannelTap(BuildContext context) {
@@ -659,28 +653,33 @@ class _ChannelListViewState extends State<ChannelListView> {
ChannelsBlocState channelsProvider, ChannelsBlocState channelsProvider,
) => ) =>
BetterStreamBuilder<bool>( BetterStreamBuilder<bool>(
stream: channelsProvider.queryChannelsLoading, stream: channelsProvider.queryChannelsLoading,
initialData: false, initialData: false,
errorBuilder: (context, err) => Container( errorBuilder: (context, err) {
color: StreamChatTheme.of(context) final theme = StreamChatTheme.of(context);
.colorTheme return Container(
.accentError color: theme.colorTheme.textLowEmphasis.withOpacity(0.9),
.withOpacity(.2), child: Padding(
child: const Padding( padding: const EdgeInsets.all(16),
padding: EdgeInsets.symmetric(vertical: 16), child: Text(
child: Center( 'Error loading channels',
child: Text('Error loading channels'), style: theme.textTheme.body.copyWith(
), color: Colors.white,
), ),
), ),
builder: (context, data) => data ),
? const Center( );
child: Padding( },
padding: EdgeInsets.all(16), builder: (context, showLoading) {
child: CircularProgressIndicator(), if (!showLoading) return const Offstage();
), return const Center(
) child: Padding(
: const Offstage()); padding: EdgeInsets.all(16),
child: CircularProgressIndicator(),
),
);
},
);
Widget _separatorBuilder(context, i) { Widget _separatorBuilder(context, i) {
final effect = StreamChatTheme.of(context).colorTheme.borderBottom; final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
@@ -146,6 +146,8 @@ class ChannelsBlocState extends State<ChannelsBloc>
_paginationEnded = true; _paginationEnded = true;
} }
} catch (e, stk) { } catch (e, stk) {
// reset loading controller
_queryChannelsLoadingController.sink.add(false);
if (_channelsController.hasValue) { if (_channelsController.hasValue) {
_queryChannelsLoadingController.addError(e, stk); _queryChannelsLoadingController.addError(e, stk);
} else { } else {
@@ -85,6 +85,7 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
final extentBefore = notification.metrics.extentBefore; final extentBefore = notification.metrics.extentBefore;
final extentAfter = notification.metrics.extentAfter; final extentAfter = notification.metrics.extentAfter;
final scrollingDown = _scrollPosition < pixels; final scrollingDown = _scrollPosition < pixels;
_scrollPosition = pixels;
if (scrollingDown) { if (scrollingDown) {
if (extentAfter <= scrollOffset) { if (extentAfter <= scrollOffset) {
@@ -97,8 +98,6 @@ class _LazyLoadScrollViewState extends State<LazyLoadScrollView> {
return true; return true;
} }
} }
_scrollPosition = pixels;
} }
if (notification is OverscrollNotification) { if (notification is OverscrollNotification) {
if (notification.overscroll > 0) { if (notification.overscroll > 0) {
@@ -98,6 +98,8 @@ class MessageSearchBlocState extends State<MessageSearchBloc>
_queryMessagesLoadingController.add(false); _queryMessagesLoadingController.add(false);
} }
} catch (e, stk) { } catch (e, stk) {
// reset loading controller
_queryMessagesLoadingController.add(false);
if (_messageResponses.hasValue) { if (_messageResponses.hasValue) {
_queryMessagesLoadingController.addError(e, stk); _queryMessagesLoadingController.addError(e, stk);
} else { } else {
@@ -96,6 +96,8 @@ class UsersBlocState extends State<UsersBloc>
_queryUsersLoadingController.add(false); _queryUsersLoadingController.add(false);
} }
} catch (e, stk) { } catch (e, stk) {
// reset loading controller
_queryUsersLoadingController.add(false);
if (_usersController.hasValue) { if (_usersController.hasValue) {
_queryUsersLoadingController.addError(e, stk); _queryUsersLoadingController.addError(e, stk);
} else { } else {