style: Use SliverList for chatlist

This commit is contained in:
Krille 2023-03-19 19:59:50 +01:00
parent 46e0e4b65e
commit 61c4d0e61f
7 changed files with 336 additions and 275 deletions

View file

@ -16,7 +16,7 @@ abstract class AppConfig {
static const double messageFontSize = 15.75;
static const bool allowOtherHomeservers = true;
static const bool enableRegistration = true;
static const Color primaryColor = Color.fromARGB(255, 135, 103, 172);
static const Color primaryColor = Color(0xFF5625BA);
static const Color primaryColorLight = Color(0xFFCCBDEA);
static const Color secondaryColor = Color(0xFF41a2bc);
static String _privacyUrl =

View file

@ -8,6 +8,7 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/chat_list_item.dart';
import 'package:fluffychat/pages/chat_list/search_title.dart';
import 'package:fluffychat/pages/chat_list/space_view.dart';
import 'package:fluffychat/pages/chat_list/start_chat_fab.dart';
import 'package:fluffychat/pages/chat_list/stories_header.dart';
import 'package:fluffychat/utils/adaptive_bottom_sheet.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart';
@ -19,6 +20,7 @@ import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
import '../../config/themes.dart';
import '../../widgets/connection_status_header.dart';
import '../../widgets/matrix.dart';
import 'chat_list_header.dart';
class ChatListViewBody extends StatelessWidget {
final ChatListController controller;
@ -70,15 +72,13 @@ class ChatListViewBody extends StatelessWidget {
ActiveFilter.messages,
}.contains(controller.activeFilter) &&
client.storiesRooms.isNotEmpty;
return ListView.builder(
return CustomScrollView(
controller: controller.scrollController,
// add +1 space below in order to properly scroll below the spaces bar
itemCount: rooms.length + 1,
itemBuilder: (BuildContext context, int i) {
if (i == 0) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
slivers: [
ChatListHeader(controller: controller),
SliverList(
delegate: SliverChildListDelegate(
[
if (controller.isSearchMode) ...[
SearchTitle(
title: L10n.of(context)!.publicRooms,
@ -187,7 +187,7 @@ class ChatListViewBody extends StatelessWidget {
title: L10n.of(context)!.chats,
icon: const Icon(Icons.forum_outlined),
),
if (rooms.isEmpty && !controller.isSearchMode)
if (rooms.isEmpty && !controller.isSearchMode) ...[
Padding(
padding: const EdgeInsets.all(32.0),
child: Column(
@ -201,29 +201,47 @@ class ChatListViewBody extends StatelessWidget {
],
),
),
Center(
child: StartChatFloatingActionButton(
activeFilter: controller.activeFilter,
roomsIsEmpty: true,
scrolledToTop: controller.scrolledToTop,
),
),
],
],
);
}
i--;
if (!rooms[i]
.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!))
.toLowerCase()
.contains(
controller.searchController.text.toLowerCase(),
)) {
return Container();
}
return ChatListItem(
rooms[i],
key: Key('chat_list_item_${rooms[i].id}'),
selected: controller.selectedRoomIds.contains(rooms[i].id),
onTap: controller.selectMode == SelectMode.select
? () => controller.toggleSelection(rooms[i].id)
: null,
onLongPress: () => controller.toggleSelection(rooms[i].id),
activeChat: controller.activeChat == rooms[i].id,
);
},
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
if (!rooms[i]
.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
)
.toLowerCase()
.contains(
controller.searchController.text.toLowerCase(),
)) {
return Container();
}
return ChatListItem(
rooms[i],
key: Key('chat_list_item_${rooms[i].id}'),
selected:
controller.selectedRoomIds.contains(rooms[i].id),
onTap: controller.selectMode == SelectMode.select
? () => controller.toggleSelection(rooms[i].id)
: null,
onLongPress: () =>
controller.toggleSelection(rooms[i].id),
activeChat: controller.activeChat == rooms[i].id,
);
},
childCount: rooms.length,
),
),
],
);
}
const dummyChatCount = 5;

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/client_chooser_button.dart';
import '../../widgets/matrix.dart';
@ -16,7 +17,12 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
Widget build(BuildContext context) {
final selectMode = controller.selectMode;
return AppBar(
return SliverAppBar(
floating: true,
pinned: FluffyThemes.isColumnMode(context),
elevation: 0,
scrolledUnderElevation: 0,
backgroundColor: Colors.transparent,
automaticallyImplyLeading: false,
leading: selectMode == SelectMode.normal
? null
@ -38,65 +44,70 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
)
: SizedBox(
height: 44,
child: TextField(
controller: controller.searchController,
textInputAction: TextInputAction.search,
onChanged: controller.onSearchEnter,
decoration: InputDecoration(
fillColor: Theme.of(context)
.colorScheme
.secondaryContainer
.withAlpha(128),
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
hintText: L10n.of(context)!.search,
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: controller.isSearchMode
? IconButton(
tooltip: L10n.of(context)!.cancel,
icon: const Icon(Icons.close_outlined),
onPressed: controller.cancelSearch,
color: Theme.of(context).colorScheme.onBackground,
)
: Icon(
Icons.search_outlined,
color: Theme.of(context).colorScheme.onBackground,
),
suffixIcon: controller.isSearchMode
? controller.isSearching
? const Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 12,
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
child: Material(
elevation:
Theme.of(context).appBarTheme.scrolledUnderElevation ??
4,
shadowColor: Theme.of(context).appBarTheme.shadowColor,
borderRadius: BorderRadius.circular(AppConfig.borderRadius),
child: TextField(
controller: controller.searchController,
textInputAction: TextInputAction.search,
onChanged: controller.onSearchEnter,
decoration: InputDecoration(
border: UnderlineInputBorder(
borderSide: BorderSide.none,
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
hintText: L10n.of(context)!.search,
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: controller.isSearchMode
? IconButton(
tooltip: L10n.of(context)!.cancel,
icon: const Icon(Icons.close_outlined),
onPressed: controller.cancelSearch,
color:
Theme.of(context).colorScheme.onBackground,
)
: Icon(
Icons.search_outlined,
color:
Theme.of(context).colorScheme.onBackground,
),
suffixIcon: controller.isSearchMode
? controller.isSearching
? const Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0,
horizontal: 12,
),
),
)
: TextButton(
onPressed: controller.setServer,
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 12),
),
child: Text(
controller.searchServer ??
Matrix.of(context)
.client
.homeserver!
.host,
maxLines: 2,
),
)
: SizedBox(
width: 0,
child: ClientChooserButton(controller),
),
child: SizedBox.square(
dimension: 24,
child: CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
),
)
: TextButton(
onPressed: controller.setServer,
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 12),
),
child: Text(
controller.searchServer ??
Matrix.of(context)
.client
.homeserver!
.host,
maxLines: 2,
),
)
: SizedBox(
width: 0,
child: ClientChooserButton(controller),
),
),
),
),
),

View file

@ -15,7 +15,6 @@ import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/unread_rooms_badge.dart';
import '../../widgets/matrix.dart';
import 'chat_list_body.dart';
import 'chat_list_header.dart';
import 'start_chat_fab.dart';
class ChatListView extends StatelessWidget {
@ -46,12 +45,12 @@ class ChatListView extends StatelessWidget {
icon: UnreadRoomsBadge(
badgePosition: badgePosition,
filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups),
child: const Icon(Icons.groups_outlined),
child: const Icon(Icons.group_outlined),
),
selectedIcon: UnreadRoomsBadge(
badgePosition: badgePosition,
filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups),
child: const Icon(Icons.groups),
child: const Icon(Icons.group),
),
label: L10n.of(context)!.groups,
),
@ -174,7 +173,6 @@ class ChatListView extends StatelessWidget {
excludeFromSemantics: true,
behavior: HitTestBehavior.translucent,
child: Scaffold(
appBar: ChatListHeader(controller: controller),
body: ChatListViewBody(controller),
bottomNavigationBar: controller.displayNavigationBar
? NavigationBar(
@ -185,24 +183,24 @@ class ChatListView extends StatelessWidget {
destinations: getNavigationDestinations(context),
)
: null,
floatingActionButtonLocation:
controller.filteredRooms.isEmpty
? FloatingActionButtonLocation.centerFloat
: null,
floatingActionButton: selectMode == SelectMode.normal
? KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyN
},
onKeysPressed: () =>
VRouter.of(context).to('/newprivatechat'),
helpLabel: L10n.of(context)!.newChat,
child: StartChatFloatingActionButton(
controller: controller,
),
)
: null,
floatingActionButton: KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyN
},
onKeysPressed: () =>
VRouter.of(context).to('/newprivatechat'),
helpLabel: L10n.of(context)!.newChat,
child: selectMode == SelectMode.normal &&
controller.filteredRooms.isNotEmpty &&
!controller.isSearchMode
? StartChatFloatingActionButton(
activeFilter: controller.activeFilter,
roomsIsEmpty: false,
scrolledToTop: controller.scrolledToTop,
)
: const SizedBox.shrink(),
),
),
),
),

View file

@ -14,6 +14,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart';
import 'package:fluffychat/widgets/avatar.dart';
import '../../utils/localized_exception_extension.dart';
import '../../widgets/matrix.dart';
import 'chat_list_header.dart';
class SpaceView extends StatefulWidget {
final ChatListController controller;
@ -154,36 +155,44 @@ class _SpaceViewState extends State<SpaceView> {
)
.toList();
return ListView.builder(
itemCount: rootSpaces.length,
return CustomScrollView(
controller: widget.scrollController,
itemBuilder: (context, i) {
final rootSpace = rootSpaces[i];
final displayname = rootSpace.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
);
return Material(
color: Theme.of(context).colorScheme.background,
child: ListTile(
leading: Avatar(
mxContent: rootSpace.avatar,
name: displayname,
),
title: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
L10n.of(context)!
.numChats(rootSpace.spaceChildren.length.toString()),
),
onTap: () => widget.controller.setActiveSpace(rootSpace.id),
onLongPress: () => _onSpaceChildContextMenu(null, rootSpace),
trailing: const Icon(Icons.chevron_right_outlined),
slivers: [
ChatListHeader(controller: widget.controller),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) {
final rootSpace = rootSpaces[i];
final displayname = rootSpace.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
);
return Material(
color: Theme.of(context).colorScheme.background,
child: ListTile(
leading: Avatar(
mxContent: rootSpace.avatar,
name: displayname,
),
title: Text(
displayname,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
L10n.of(context)!
.numChats(rootSpace.spaceChildren.length.toString()),
),
onTap: () => widget.controller.setActiveSpace(rootSpace.id),
onLongPress: () =>
_onSpaceChildContextMenu(null, rootSpace),
trailing: const Icon(Icons.chevron_right_outlined),
),
);
},
childCount: rootSpaces.length,
),
);
},
),
],
);
}
return FutureBuilder<GetSpaceHierarchyResponse>(
@ -208,7 +217,16 @@ class _SpaceViewState extends State<SpaceView> {
);
}
if (response == null) {
return const Center(child: CircularProgressIndicator.adaptive());
return CustomScrollView(
slivers: [
ChatListHeader(controller: widget.controller),
const SliverFillRemaining(
child: Center(
child: CircularProgressIndicator.adaptive(),
),
),
],
);
}
final parentSpace = allSpaces.firstWhereOrNull(
(space) =>
@ -224,125 +242,139 @@ class _SpaceViewState extends State<SpaceView> {
return;
}
},
child: ListView.builder(
itemCount: spaceChildren.length + 1 + (canLoadMore ? 1 : 0),
child: CustomScrollView(
controller: widget.scrollController,
itemBuilder: (context, i) {
if (i == 0) {
return ListTile(
leading: BackButton(
onPressed: () =>
widget.controller.setActiveSpace(parentSpace?.id),
),
title: Text(
parentSpace == null
? L10n.of(context)!.allSpaces
: parentSpace.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
),
),
trailing: IconButton(
icon: snapshot.connectionState != ConnectionState.done
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.refresh_outlined),
onPressed: snapshot.connectionState != ConnectionState.done
slivers: [
ChatListHeader(controller: widget.controller),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, i) {
if (i == 0) {
return ListTile(
leading: BackButton(
onPressed: () =>
widget.controller.setActiveSpace(parentSpace?.id),
),
title: Text(
parentSpace == null
? L10n.of(context)!.allSpaces
: parentSpace.getLocalizedDisplayname(
MatrixLocals(L10n.of(context)!),
),
),
trailing: IconButton(
icon: snapshot.connectionState != ConnectionState.done
? const CircularProgressIndicator.adaptive()
: const Icon(Icons.refresh_outlined),
onPressed:
snapshot.connectionState != ConnectionState.done
? null
: _refresh,
),
);
}
i--;
if (canLoadMore && i == spaceChildren.length) {
return ListTile(
title: Text(L10n.of(context)!.loadMore),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: () {
prevBatch = response.nextBatch;
_refresh();
},
);
}
final spaceChild = spaceChildren[i];
final room = client.getRoomById(spaceChild.roomId);
if (room != null && !room.isSpace) {
return ChatListItem(
room,
onLongPress: () =>
_onSpaceChildContextMenu(spaceChild, room),
activeChat: widget.controller.activeChat == room.id,
);
}
final isSpace = spaceChild.roomType == 'm.space';
final topic = spaceChild.topic?.isEmpty ?? true
? null
: _refresh,
),
);
}
i--;
if (canLoadMore && i == spaceChildren.length) {
return ListTile(
title: Text(L10n.of(context)!.loadMore),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: () {
prevBatch = response.nextBatch;
_refresh();
},
);
}
final spaceChild = spaceChildren[i];
final room = client.getRoomById(spaceChild.roomId);
if (room != null && !room.isSpace) {
return ChatListItem(
room,
onLongPress: () => _onSpaceChildContextMenu(spaceChild, room),
activeChat: widget.controller.activeChat == room.id,
);
}
final isSpace = spaceChild.roomType == 'm.space';
final topic =
spaceChild.topic?.isEmpty ?? true ? null : spaceChild.topic;
if (spaceChild.roomId == activeSpaceId) {
return SearchTitle(
title:
spaceChild.name ?? spaceChild.canonicalAlias ?? 'Space',
icon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Avatar(
size: 24,
mxContent: spaceChild.avatarUrl,
name: spaceChild.name,
fontSize: 9,
),
),
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withAlpha(128),
trailing: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.edit_outlined),
),
onTap: () => _onJoinSpaceChild(spaceChild),
);
}
return ListTile(
leading: Avatar(
mxContent: spaceChild.avatarUrl,
name: spaceChild.name,
),
title: Row(
children: [
Expanded(
child: Text(
spaceChild.name ??
: spaceChild.topic;
if (spaceChild.roomId == activeSpaceId) {
return SearchTitle(
title: spaceChild.name ??
spaceChild.canonicalAlias ??
L10n.of(context)!.chat,
'Space',
icon: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10.0),
child: Avatar(
size: 24,
mxContent: spaceChild.avatarUrl,
name: spaceChild.name,
fontSize: 9,
),
),
color: Theme.of(context)
.colorScheme
.secondaryContainer
.withAlpha(128),
trailing: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16.0),
child: Icon(Icons.edit_outlined),
),
onTap: () => _onJoinSpaceChild(spaceChild),
);
}
return ListTile(
leading: Avatar(
mxContent: spaceChild.avatarUrl,
name: spaceChild.name,
),
title: Row(
children: [
Expanded(
child: Text(
spaceChild.name ??
spaceChild.canonicalAlias ??
L10n.of(context)!.chat,
maxLines: 1,
style:
const TextStyle(fontWeight: FontWeight.bold),
),
),
if (!isSpace) ...[
const Icon(
Icons.people_outline,
size: 16,
),
const SizedBox(width: 4),
Text(
spaceChild.numJoinedMembers.toString(),
style: const TextStyle(fontSize: 14),
),
],
],
),
onTap: () => _onJoinSpaceChild(spaceChild),
onLongPress: () =>
_onSpaceChildContextMenu(spaceChild, room),
subtitle: Text(
topic ??
(isSpace
? L10n.of(context)!.enterSpace
: L10n.of(context)!.enterRoom),
maxLines: 1,
style: const TextStyle(fontWeight: FontWeight.bold),
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground,
),
),
),
if (!isSpace) ...[
const Icon(
Icons.people_outline,
size: 16,
),
const SizedBox(width: 4),
Text(
spaceChild.numJoinedMembers.toString(),
style: const TextStyle(fontSize: 14),
),
],
],
trailing: isSpace
? const Icon(Icons.chevron_right_outlined)
: null,
);
},
childCount: spaceChildren.length + 1 + (canLoadMore ? 1 : 0),
),
onTap: () => _onJoinSpaceChild(spaceChild),
onLongPress: () => _onSpaceChildContextMenu(spaceChild, room),
subtitle: Text(
topic ??
(isSpace
? L10n.of(context)!.enterSpace
: L10n.of(context)!.enterRoom),
maxLines: 1,
style: TextStyle(
color: Theme.of(context).colorScheme.onBackground,
),
),
trailing:
isSpace ? const Icon(Icons.chevron_right_outlined) : null,
);
},
),
],
),
);
},

View file

@ -7,13 +7,19 @@ import '../../config/themes.dart';
import 'chat_list.dart';
class StartChatFloatingActionButton extends StatelessWidget {
final ChatListController controller;
final ActiveFilter activeFilter;
final bool scrolledToTop;
final bool roomsIsEmpty;
const StartChatFloatingActionButton({Key? key, required this.controller})
: super(key: key);
const StartChatFloatingActionButton({
Key? key,
required this.activeFilter,
required this.scrolledToTop,
required this.roomsIsEmpty,
}) : super(key: key);
void _onPressed(BuildContext context) {
switch (controller.activeFilter) {
switch (activeFilter) {
case ActiveFilter.allChats:
case ActiveFilter.messages:
VRouter.of(context).to('/newprivatechat');
@ -28,10 +34,10 @@ class StartChatFloatingActionButton extends StatelessWidget {
}
IconData get icon {
switch (controller.activeFilter) {
switch (activeFilter) {
case ActiveFilter.allChats:
case ActiveFilter.messages:
return Icons.edit_outlined;
return Icons.add_outlined;
case ActiveFilter.groups:
return Icons.group_add_outlined;
case ActiveFilter.spaces:
@ -40,10 +46,10 @@ class StartChatFloatingActionButton extends StatelessWidget {
}
String getLabel(BuildContext context) {
switch (controller.activeFilter) {
switch (activeFilter) {
case ActiveFilter.allChats:
case ActiveFilter.messages:
return controller.filteredRooms.isEmpty
return roomsIsEmpty
? L10n.of(context)!.startFirstChat
: L10n.of(context)!.newChat;
case ActiveFilter.groups:
@ -58,15 +64,13 @@ class StartChatFloatingActionButton extends StatelessWidget {
return AnimatedContainer(
duration: FluffyThemes.animationDuration,
curve: FluffyThemes.animationCurve,
width: controller.filteredRooms.isEmpty
width: roomsIsEmpty
? null
: controller.scrolledToTop
: scrolledToTop
? 144
: 56,
child: controller.scrolledToTop
child: scrolledToTop
? FloatingActionButton.extended(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
onPressed: () => _onPressed(context),
icon: Icon(icon),
label: Text(
@ -75,8 +79,6 @@ class StartChatFloatingActionButton extends StatelessWidget {
),
)
: FloatingActionButton(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary,
onPressed: () => _onPressed(context),
child: Icon(icon),
),

View file

@ -47,11 +47,11 @@ class SettingsStyleController extends State<SettingsStyle> {
static final List<Color?> customColors = [
AppConfig.chatColor,
Colors.blue.shade800,
Colors.green.shade800,
Colors.orange.shade700,
Colors.pink.shade700,
Colors.blueGrey.shade600,
Colors.indigo,
Colors.green,
Colors.orange,
Colors.pink,
Colors.blueGrey,
null,
];