Merge pull request #568 from GetStream/hotfix/listBackground

fix(ui): list background
This commit is contained in:
Salvatore Giordano
2021-07-21 12:32:18 +02:00
committed by GitHub
4 changed files with 311 additions and 275 deletions
@@ -231,13 +231,21 @@ class _ChannelListViewState extends State<ChannelListView> {
); );
} }
return ColoredBox( child = LazyLoadScrollView(
color: ChannelListViewTheme.of(context).backgroundColor!, onEndOfPage: () => _channelListController.paginateData!(),
child: LazyLoadScrollView( child: child,
onEndOfPage: () => _channelListController.paginateData!(),
child: child,
),
); );
final backgroundColor = ChannelListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
child: child,
);
}
return child;
} }
Widget _buildListView(BuildContext context, List<Channel> channels) { Widget _buildListView(BuildContext context, List<Channel> channels) {
@@ -386,210 +386,217 @@ class _MessageListViewState extends State<MessageListView> {
1 // parent message 1 // parent message
; ;
return ColoredBox( final child = Stack(
color: MessageListViewTheme.of(context).backgroundColor!, alignment: Alignment.center,
child: Stack( children: [
alignment: Alignment.center, ConnectionStatusBuilder(
children: [ statusBuilder: (context, status) {
ConnectionStatusBuilder( var statusString = '';
statusBuilder: (context, status) { var showStatus = true;
var statusString = ''; switch (status) {
var showStatus = true; case ConnectionStatus.connected:
switch (status) { statusString = 'Connected';
case ConnectionStatus.connected: showStatus = false;
statusString = 'Connected'; break;
showStatus = false; case ConnectionStatus.connecting:
break; statusString = 'Reconnecting...';
case ConnectionStatus.connecting: break;
statusString = 'Reconnecting...'; case ConnectionStatus.disconnected:
break; statusString = 'Disconnected';
case ConnectionStatus.disconnected: break;
statusString = 'Disconnected'; }
break;
}
return InfoTile( return InfoTile(
showMessage: widget.showConnectionStateTile && showStatus, showMessage: widget.showConnectionStateTile && showStatus,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
message: statusString, message: statusString,
child: LazyLoadScrollView( child: LazyLoadScrollView(
onPageScrollStart: () { onPageScrollStart: () {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
},
onStartOfPage: () async {
_inBetweenList = false;
if (!_upToDate) {
_topPaginationActive = false;
_bottomPaginationActive = true;
return _paginateData(
streamChannel,
QueryDirection.bottom,
);
}
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData(
streamChannel,
QueryDirection.top,
);
},
onInBetweenOfPage: () {
_inBetweenList = true;
},
child: ScrollablePositionedList.separated(
key: ValueKey(initialIndex! + initialAlignment!),
itemPositionsListener: _itemPositionListener,
initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
addAutomaticKeepAlives: false,
itemCount: itemCount,
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages)
// eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)|
// ParentMessage -> 7 (count-1)
// Separator(ThreadSeparator) -> 6 (count-2)
// Header -> 6 (count-2)
// Separator(Header -> 8??T -> 0||52) -> 5 (count-3)
// TopLoader -> 5 (count-3)
// Separator(0) -> 4 (count-4)
// Message -> 4 (count-4)
// Separator(2||8) -> 3 (count-5)
// Message -> 3 (count-5)
// Separator(2||8) -> 2 (count-6)
// Message -> 2 (count-6)
// Separator(0) -> 1 (count-7)
// BottomLoader -> 1 (count-7)
// Separator(Footer -> 8??30) -> 0 (count-8)
// Footer -> 0 (count-8)
separatorBuilder: (context, i) {
if (i == itemCount - 2) {
if (widget.parentMessage == null) {
return const Offstage();
}
return _buildThreadSeparator();
}
if (i == itemCount - 3) {
if (widget.headerBuilder == null) {
if (_isThreadConversation) return const Offstage();
return const SizedBox(height: 52);
}
return const SizedBox(height: 8);
}
if (i == 0) {
if (widget.footerBuilder == null) {
return const SizedBox(height: 30);
}
return const SizedBox(height: 8);
}
if (i == 1 || i == itemCount - 4) return const Offstage();
final message = messages[i - 1];
final nextMessage = messages[i - 2];
if (!Jiffy(message.createdAt.toLocal()).isSame(
nextMessage.createdAt.toLocal(),
Units.DAY,
)) {
final divider = widget.dateDividerBuilder != null
? widget.dateDividerBuilder!(
nextMessage.createdAt.toLocal(),
)
: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: divider,
);
}
final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff(
message.createdAt.toLocal(),
Units.MINUTE,
);
final isNextUserSame =
message.user!.id == nextMessage.user?.id;
final isThread = message.replyCount! > 0;
final isDeleted = message.isDeleted;
if (timeDiff >= 1 ||
!isNextUserSame ||
isThread ||
isDeleted) {
return const SizedBox(height: 8);
}
return const SizedBox(height: 2);
}, },
onStartOfPage: () async { itemBuilder: (context, i) {
_inBetweenList = false; if (i == itemCount - 1) {
if (!_upToDate) { if (widget.parentMessage == null) {
_topPaginationActive = false; return const Offstage();
_bottomPaginationActive = true; }
return _paginateData( return buildParentMessage(widget.parentMessage!);
streamChannel, }
if (i == itemCount - 2) {
return widget.headerBuilder?.call(context) ??
const Offstage();
}
if (i == itemCount - 3) {
return _buildLoadingIndicator(
streamChannel!,
QueryDirection.top,
);
}
if (i == 1) {
return _buildLoadingIndicator(
streamChannel!,
QueryDirection.bottom, QueryDirection.bottom,
); );
} }
},
onEndOfPage: () async {
_inBetweenList = false;
_topPaginationActive = true;
_bottomPaginationActive = false;
return _paginateData(
streamChannel,
QueryDirection.top,
);
},
onInBetweenOfPage: () {
_inBetweenList = true;
},
child: ScrollablePositionedList.separated(
key: ValueKey(initialIndex! + initialAlignment!),
itemPositionsListener: _itemPositionListener,
initialScrollIndex: initialIndex ?? 0,
initialAlignment: initialAlignment ?? 0,
physics: widget.scrollPhysics,
itemScrollController: _scrollController,
reverse: true,
addAutomaticKeepAlives: false,
itemCount: itemCount,
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) if (i == 0) {
// eg: |Type| rev(|Index(item)|) rev(|Index(separator)|) |Index(item)| |Index(separator)| return widget.footerBuilder?.call(context) ??
// ParentMessage -> 7 (count-1) const Offstage();
// Separator(ThreadSeparator) -> 6 (count-2) }
// Header -> 6 (count-2)
// Separator(Header -> 8??T -> 0||52) -> 5 (count-3)
// TopLoader -> 5 (count-3)
// Separator(0) -> 4 (count-4)
// Message -> 4 (count-4)
// Separator(2||8) -> 3 (count-5)
// Message -> 3 (count-5)
// Separator(2||8) -> 2 (count-6)
// Message -> 2 (count-6)
// Separator(0) -> 1 (count-7)
// BottomLoader -> 1 (count-7)
// Separator(Footer -> 8??30) -> 0 (count-8)
// Footer -> 0 (count-8)
separatorBuilder: (context, i) { const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer
if (i == itemCount - 2) {
if (widget.parentMessage == null) {
return const Offstage();
}
return _buildThreadSeparator();
}
if (i == itemCount - 3) {
if (widget.headerBuilder == null) {
if (_isThreadConversation) return const Offstage();
return const SizedBox(height: 52);
}
return const SizedBox(height: 8);
}
if (i == 0) {
if (widget.footerBuilder == null) {
return const SizedBox(height: 30);
}
return const SizedBox(height: 8);
}
if (i == 1 || i == itemCount - 4) return const Offstage(); final message = messages[i - 2];
Widget messageWidget;
final message = messages[i - 1]; if (i == bottomMessageIndex) {
final nextMessage = messages[i - 2]; messageWidget = _buildBottomMessage(
if (!Jiffy(message.createdAt.toLocal()).isSame( context,
nextMessage.createdAt.toLocal(), message,
Units.DAY, messages,
)) { streamChannel!,
final divider = widget.dateDividerBuilder != null i - 2,
? widget.dateDividerBuilder!(
nextMessage.createdAt.toLocal(),
)
: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: divider,
);
}
final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff(
message.createdAt.toLocal(),
Units.MINUTE,
); );
} else {
final isNextUserSame = messageWidget = buildMessage(message, messages, i - 2);
message.user!.id == nextMessage.user?.id; }
final isThread = message.replyCount! > 0; return messageWidget;
final isDeleted = message.isDeleted; },
if (timeDiff >= 1 ||
!isNextUserSame ||
isThread ||
isDeleted) {
return const SizedBox(height: 8);
}
return const SizedBox(height: 2);
},
itemBuilder: (context, i) {
if (i == itemCount - 1) {
if (widget.parentMessage == null) {
return const Offstage();
}
return buildParentMessage(widget.parentMessage!);
}
if (i == itemCount - 2) {
return widget.headerBuilder?.call(context) ??
const Offstage();
}
if (i == itemCount - 3) {
return _buildLoadingIndicator(
streamChannel!,
QueryDirection.top,
);
}
if (i == 1) {
return _buildLoadingIndicator(
streamChannel!,
QueryDirection.bottom,
);
}
if (i == 0) {
return widget.footerBuilder?.call(context) ??
const Offstage();
}
const bottomMessageIndex =
2; // 1 -> loader // 0 -> footer
final message = messages[i - 2];
Widget messageWidget;
if (i == bottomMessageIndex) {
messageWidget = _buildBottomMessage(
context,
message,
messages,
streamChannel!,
i - 2,
);
} else {
messageWidget = buildMessage(message, messages, i - 2);
}
return messageWidget;
},
),
), ),
); ),
}, );
), },
if (widget.showScrollToBottom) _buildScrollToBottom(), ),
if (widget.showFloatingDateDivider) if (widget.showScrollToBottom) _buildScrollToBottom(),
_buildFloatingDateDivider(itemCount), if (widget.showFloatingDateDivider)
], _buildFloatingDateDivider(itemCount),
), ],
); );
final backgroundColor = MessageListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
child: child,
);
}
return child;
} }
Widget _buildThreadSeparator() { Widget _buildThreadSeparator() {
@@ -144,61 +144,72 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
widget.messageSearchListController ?? _defaultController; widget.messageSearchListController ?? _defaultController;
@override @override
Widget build(BuildContext context) => ColoredBox( Widget build(BuildContext context) {
color: MessageSearchListViewTheme.of(context).backgroundColor!, final messageSearchListCore = MessageSearchListCore(
child: MessageSearchListCore( filters: widget.filters,
filters: widget.filters, sortOptions: widget.sortOptions,
sortOptions: widget.sortOptions, messageQuery: widget.messageQuery,
messageQuery: widget.messageQuery, paginationParams: widget.paginationParams,
paginationParams: widget.paginationParams, messageFilters: widget.messageFilters,
messageFilters: widget.messageFilters, messageSearchListController: _messageSearchListController,
messageSearchListController: _messageSearchListController, emptyBuilder: widget.emptyBuilder ??
emptyBuilder: widget.emptyBuilder ?? (context) => LayoutBuilder(
(context) => LayoutBuilder( builder: (context, viewportConstraints) =>
builder: (context, viewportConstraints) => SingleChildScrollView(
SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(), child: ConstrainedBox(
child: ConstrainedBox( constraints: BoxConstraints(
constraints: BoxConstraints( minHeight: viewportConstraints.maxHeight,
minHeight: viewportConstraints.maxHeight, ),
), child: const Center(
child: const Center( child: Text('There are no messages currently'),
child: Text('There are no messages currently'),
),
),
), ),
), ),
errorBuilder: widget.errorBuilder ?? ),
(BuildContext context, dynamic error) { ),
if (error is Error) { errorBuilder: widget.errorBuilder ??
print(error.stackTrace); (BuildContext context, dynamic error) {
} if (error is Error) {
return InfoTile( print(error.stackTrace);
showMessage: widget.showErrorTile, }
tileAnchor: Alignment.topCenter, return InfoTile(
childAnchor: Alignment.topCenter, showMessage: widget.showErrorTile,
message: 'An error occurred.', tileAnchor: Alignment.topCenter,
child: Container(), childAnchor: Alignment.topCenter,
); message: 'An error occurred.',
}, child: Container(),
loadingBuilder: widget.loadingBuilder ?? );
(context) => LayoutBuilder( },
builder: (context, viewportConstraints) => loadingBuilder: widget.loadingBuilder ??
SingleChildScrollView( (context) => LayoutBuilder(
physics: const AlwaysScrollableScrollPhysics(), builder: (context, viewportConstraints) =>
child: ConstrainedBox( SingleChildScrollView(
constraints: BoxConstraints( physics: const AlwaysScrollableScrollPhysics(),
minHeight: viewportConstraints.maxHeight, child: ConstrainedBox(
), constraints: BoxConstraints(
child: const Center( minHeight: viewportConstraints.maxHeight,
child: CircularProgressIndicator(), ),
), child: const Center(
), child: CircularProgressIndicator(),
), ),
), ),
childBuilder: widget.childBuilder ?? _buildListView, ),
), ),
childBuilder: widget.childBuilder ?? _buildListView,
);
final backgroundColor =
MessageSearchListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
color: backgroundColor,
child: messageSearchListCore,
); );
}
return messageSearchListCore;
}
Widget _separatorBuilder(BuildContext context, int index) => Container( Widget _separatorBuilder(BuildContext context, int index) => Container(
height: 1, height: 1,
@@ -160,38 +160,48 @@ class _UserListViewState extends State<UserListView>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final child = ColoredBox( final userListCore = UserListCore(
color: UserListViewTheme.of(context).backgroundColor!, errorBuilder: widget.errorBuilder ??
child: UserListCore( (BuildContext context, Object err) => _buildError(err),
errorBuilder: widget.errorBuilder ?? emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(),
(BuildContext context, Object err) => _buildError(err), loadingBuilder: widget.loadingBuilder ??
emptyBuilder: widget.emptyBuilder ?? (context) => _buildEmpty(), (context) => LayoutBuilder(
loadingBuilder: widget.loadingBuilder ?? builder: (context, viewportConstraints) =>
(context) => LayoutBuilder( SingleChildScrollView(
builder: (context, viewportConstraints) => physics: const AlwaysScrollableScrollPhysics(),
SingleChildScrollView( child: ConstrainedBox(
physics: const AlwaysScrollableScrollPhysics(), constraints: BoxConstraints(
child: ConstrainedBox( minHeight: viewportConstraints.maxHeight,
constraints: BoxConstraints( ),
minHeight: viewportConstraints.maxHeight, child: const Center(
), child: CircularProgressIndicator(),
child: const Center(
child: CircularProgressIndicator(),
),
), ),
), ),
), ),
listBuilder: ),
widget.listBuilder ?? (context, list) => _buildListView(list), listBuilder:
pagination: widget.pagination, widget.listBuilder ?? (context, list) => _buildListView(list),
sort: widget.sort, pagination: widget.pagination,
filter: widget.filter, sort: widget.sort,
presence: widget.presence, filter: widget.filter,
groupAlphabetically: widget.groupAlphabetically, presence: widget.presence,
userListController: _userListController, groupAlphabetically: widget.groupAlphabetically,
), userListController: _userListController,
); );
final backgroundColor = UserListViewTheme.of(context).backgroundColor;
Widget child;
if (backgroundColor != null) {
child = ColoredBox(
color: backgroundColor,
child: userListCore,
);
} else {
child = userListCore;
}
if (!widget.pullToRefresh) { if (!widget.pullToRefresh) {
return child; return child;
} else { } else {