feat: improve spaces

- support to show spaces in a list
- add a beautiful animation

This MR makes Spaces much easier to use on desktops and allows to better
find the right space in case they have no avatar.

There will be another MR builting on this work as soon as
https://gitlab.com/famedly/company/frontend/libraries/matrix_api_lite/-/merge_requests/58
is merged.

Signed-off-by: TheOneWithTheBraid <the-one@with-the-braid.cf>
This commit is contained in:
TheOneWithTheBraid 2022-04-02 16:18:36 +02:00
parent 42267f263e
commit 26983a15a8
8 changed files with 499 additions and 280 deletions

View File

@ -1311,6 +1311,7 @@
"type": "text", "type": "text",
"placeholders": {} "placeholders": {}
}, },
"showSpaces": "Show spaces list",
"loadMore": "Load more…", "loadMore": "Load more…",
"@loadMore": { "@loadMore": {
"type": "text", "type": "text",

View File

@ -8,11 +8,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:snapping_sheet/snapping_sheet.dart';
import 'package:uni_links/uni_links.dart'; import 'package:uni_links/uni_links.dart';
import 'package:vrouter/vrouter.dart'; import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
import 'package:fluffychat/pages/chat_list/spaces_bottom_bar.dart';
import 'package:fluffychat/pages/chat_list/spaces_entry.dart'; import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
import 'package:fluffychat/utils/fluffy_share.dart'; import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/platform_infos.dart';
@ -40,7 +42,7 @@ class ChatList extends StatefulWidget {
ChatListController createState() => ChatListController(); ChatListController createState() => ChatListController();
} }
class ChatListController extends State<ChatList> { class ChatListController extends State<ChatList> with TickerProviderStateMixin {
StreamSubscription? _intentDataStreamSubscription; StreamSubscription? _intentDataStreamSubscription;
StreamSubscription? _intentFileStreamSubscription; StreamSubscription? _intentFileStreamSubscription;
@ -54,6 +56,8 @@ class ChatListController extends State<ChatList> {
return (id == null || !id.stillValid(context)) ? defaultSpacesEntry : id; return (id == null || !id.stillValid(context)) ? defaultSpacesEntry : id;
} }
BoxConstraints? snappingSheetContainerSize;
String? get activeSpaceId => activeSpacesEntry.getSpace(context)?.id; String? get activeSpaceId => activeSpacesEntry.getSpace(context)?.id;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
@ -61,6 +65,10 @@ class ChatListController extends State<ChatList> {
final StreamController<Client> _clientStream = StreamController.broadcast(); final StreamController<Client> _clientStream = StreamController.broadcast();
SnappingSheetController snappingSheetController = SnappingSheetController();
ScrollController snappingSheetScrollContentController = ScrollController();
Stream<Client> get clientStream => _clientStream.stream; Stream<Client> get clientStream => _clientStream.stream;
void _onScroll() { void _onScroll() {
@ -72,7 +80,10 @@ class ChatListController extends State<ChatList> {
} }
} }
void setActiveSpacesEntry(BuildContext context, SpacesEntry spaceId) { void setActiveSpacesEntry(BuildContext context, SpacesEntry? spaceId) {
if (snappingSheetController.currentPosition != kSpacesBottomBarHeight) {
snapBackSpacesSheet();
}
setState(() => _activeSpacesEntry = spaceId); setState(() => _activeSpacesEntry = spaceId);
} }
@ -480,6 +491,8 @@ class ChatListController extends State<ChatList> {
VRouter.of(context).to('/rooms'); VRouter.of(context).to('/rooms');
setState(() { setState(() {
_activeSpacesEntry = null; _activeSpacesEntry = null;
snappingSheetController = SnappingSheetController();
snappingSheetScrollContentController = ScrollController();
selectedRoomIds.clear(); selectedRoomIds.clear();
Matrix.of(context).setActiveClient(client); Matrix.of(context).setActiveClient(client);
}); });
@ -575,6 +588,21 @@ class ChatListController extends State<ChatList> {
void _hackyWebRTCFixForWeb() { void _hackyWebRTCFixForWeb() {
Matrix.of(context).voipPlugin?.context = context; Matrix.of(context).voipPlugin?.context = context;
} }
void snapBackSpacesSheet() {
snappingSheetController.snapToPosition(
const SnappingPosition.pixels(
positionPixels: kSpacesBottomBarHeight,
snappingDuration: Duration(milliseconds: 500),
),
);
}
expandSpaces() {
snappingSheetController.snapToPosition(
const SnappingPosition.factor(positionFactor: 0.5),
);
}
} }
enum EditBundleAction { addToBundle, removeFromBundle } enum EditBundleAction { addToBundle, removeFromBundle }

View File

@ -8,6 +8,7 @@ import 'package:animations/animations.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
import 'package:matrix/matrix.dart'; import 'package:matrix/matrix.dart';
import 'package:snapping_sheet/snapping_sheet.dart';
import 'package:vrouter/vrouter.dart'; import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/app_config.dart';
@ -28,203 +29,240 @@ class ChatListView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return StreamBuilder<Object?>( return StreamBuilder<Object?>(
stream: Matrix.of(context).onShareContentChanged.stream, stream: Matrix.of(context).onShareContentChanged.stream,
builder: (_, __) { builder: (_, __) {
final selectMode = controller.selectMode; final selectMode = controller.selectMode;
return VWidgetGuard( final showSpaces =
onSystemPop: (redirector) async { controller.spaces.isNotEmpty && controller.selectedRoomIds.isEmpty;
final selMode = controller.selectMode; return VWidgetGuard(
if (selMode != SelectMode.normal) controller.cancelAction(); onSystemPop: (redirector) async {
if (selMode == SelectMode.select) redirector.stopRedirection(); final selMode = controller.selectMode;
}, if (selMode != SelectMode.normal) controller.cancelAction();
child: Scaffold( if (selMode == SelectMode.select) redirector.stopRedirection();
appBar: AppBar( },
elevation: controller.scrolledToTop ? 0 : null, child: Scaffold(
actionsIconTheme: IconThemeData( appBar: AppBar(
color: controller.selectedRoomIds.isEmpty elevation: controller.scrolledToTop ? 0 : null,
? null actionsIconTheme: IconThemeData(
: Theme.of(context).colorScheme.primary, color: controller.selectedRoomIds.isEmpty
),
leading: Matrix.of(context).isMultiAccount
? ClientChooserButton(controller)
: selectMode == SelectMode.normal
? null
: IconButton(
tooltip: L10n.of(context)!.cancel,
icon: const Icon(Icons.close_outlined),
onPressed: controller.cancelAction,
color: Theme.of(context).colorScheme.primary,
),
centerTitle: false,
actions: selectMode == SelectMode.share
? null ? null
: selectMode == SelectMode.select : Theme.of(context).colorScheme.primary,
? [
if (controller.spaces.isNotEmpty)
IconButton(
tooltip: L10n.of(context)!.addToSpace,
icon: const Icon(Icons.group_work_outlined),
onPressed: controller.addOrRemoveToSpace,
),
IconButton(
tooltip: L10n.of(context)!.toggleUnread,
icon: Icon(
controller.anySelectedRoomNotMarkedUnread
? Icons.mark_chat_read_outlined
: Icons.mark_chat_unread_outlined),
onPressed: controller.toggleUnread,
),
IconButton(
tooltip: L10n.of(context)!.toggleFavorite,
icon: Icon(controller.anySelectedRoomNotFavorite
? Icons.push_pin_outlined
: Icons.push_pin),
onPressed: controller.toggleFavouriteRoom,
),
IconButton(
icon: Icon(controller.anySelectedRoomNotMuted
? Icons.notifications_off_outlined
: Icons.notifications_outlined),
tooltip: L10n.of(context)!.toggleMuted,
onPressed: controller.toggleMuted,
),
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: L10n.of(context)!.archive,
onPressed: controller.archiveAction,
),
]
: [
KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyF
},
onKeysPressed: () =>
VRouter.of(context).to('/search'),
helpLabel: L10n.of(context)!.search,
child: IconButton(
icon: const Icon(Icons.search_outlined),
tooltip: L10n.of(context)!.search,
onPressed: () =>
VRouter.of(context).to('/search'),
),
),
if (selectMode == SelectMode.normal)
IconButton(
icon: const Icon(Icons.camera_alt_outlined),
tooltip: L10n.of(context)!.addToStory,
onPressed: () =>
VRouter.of(context).to('/stories/create'),
),
PopupMenuButton<PopupMenuAction>(
onSelected: controller.onPopupMenuSelect,
itemBuilder: (_) => [
PopupMenuItem(
value: PopupMenuAction.setStatus,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.edit_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.setStatus),
],
),
),
PopupMenuItem(
value: PopupMenuAction.newGroup,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.group_add_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.createNewGroup),
],
),
),
PopupMenuItem(
value: PopupMenuAction.newSpace,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.group_work_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.createNewSpace),
],
),
),
PopupMenuItem(
value: PopupMenuAction.invite,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.share_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.inviteContact),
],
),
),
PopupMenuItem(
value: PopupMenuAction.archive,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.archive_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.archive),
],
),
),
PopupMenuItem(
value: PopupMenuAction.settings,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.settings_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.settings),
],
),
),
],
),
],
title: Text(selectMode == SelectMode.share
? L10n.of(context)!.share
: selectMode == SelectMode.select
? controller.selectedRoomIds.length.toString()
: controller.activeSpaceId == null
? AppConfig.applicationName
: Matrix.of(context)
.client
.getRoomById(controller.activeSpaceId!)!
.displayname),
), ),
body: Column(children: [ leading: Matrix.of(context).isMultiAccount
AnimatedContainer( ? ClientChooserButton(controller)
height: controller.showChatBackupBanner ? 54 : 0, : selectMode == SelectMode.normal
duration: const Duration(milliseconds: 300), ? null
clipBehavior: Clip.hardEdge, : IconButton(
curve: Curves.bounceInOut, tooltip: L10n.of(context)!.cancel,
decoration: const BoxDecoration(), icon: const Icon(Icons.close_outlined),
child: Material( onPressed: controller.cancelAction,
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.primary,
child: ListTile( ),
leading: Image.asset( centerTitle: false,
'assets/backup.png', actions: selectMode == SelectMode.share
fit: BoxFit.contain, ? null
width: 44, : selectMode == SelectMode.select
? [
if (controller.spaces.isNotEmpty)
IconButton(
tooltip: L10n.of(context)!.addToSpace,
icon: const Icon(Icons.group_work_outlined),
onPressed: controller.addOrRemoveToSpace,
),
IconButton(
tooltip: L10n.of(context)!.toggleUnread,
icon: Icon(controller.anySelectedRoomNotMarkedUnread
? Icons.mark_chat_read_outlined
: Icons.mark_chat_unread_outlined),
onPressed: controller.toggleUnread,
),
IconButton(
tooltip: L10n.of(context)!.toggleFavorite,
icon: Icon(controller.anySelectedRoomNotFavorite
? Icons.push_pin_outlined
: Icons.push_pin),
onPressed: controller.toggleFavouriteRoom,
),
IconButton(
icon: Icon(controller.anySelectedRoomNotMuted
? Icons.notifications_off_outlined
: Icons.notifications_outlined),
tooltip: L10n.of(context)!.toggleMuted,
onPressed: controller.toggleMuted,
),
IconButton(
icon: const Icon(Icons.delete_outlined),
tooltip: L10n.of(context)!.archive,
onPressed: controller.archiveAction,
),
]
: [
KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyF
},
onKeysPressed: () =>
VRouter.of(context).to('/search'),
helpLabel: L10n.of(context)!.search,
child: IconButton(
icon: const Icon(Icons.search_outlined),
tooltip: L10n.of(context)!.search,
onPressed: () =>
VRouter.of(context).to('/search'),
),
),
if (selectMode == SelectMode.normal)
IconButton(
icon: const Icon(Icons.camera_alt_outlined),
tooltip: L10n.of(context)!.addToStory,
onPressed: () =>
VRouter.of(context).to('/stories/create'),
),
PopupMenuButton<PopupMenuAction>(
onSelected: controller.onPopupMenuSelect,
itemBuilder: (_) => [
PopupMenuItem(
value: PopupMenuAction.setStatus,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.edit_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.setStatus),
],
),
),
PopupMenuItem(
value: PopupMenuAction.newGroup,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.group_add_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.createNewGroup),
],
),
),
PopupMenuItem(
value: PopupMenuAction.newSpace,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.group_work_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.createNewSpace),
],
),
),
PopupMenuItem(
value: PopupMenuAction.invite,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.share_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.inviteContact),
],
),
),
PopupMenuItem(
value: PopupMenuAction.archive,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.archive_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.archive),
],
),
),
PopupMenuItem(
value: PopupMenuAction.settings,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.settings_outlined),
const SizedBox(width: 12),
Text(L10n.of(context)!.settings),
],
),
),
],
),
],
title: Text(selectMode == SelectMode.share
? L10n.of(context)!.share
: selectMode == SelectMode.select
? controller.selectedRoomIds.length.toString()
: controller.activeSpaceId == null
? AppConfig.applicationName
: Matrix.of(context)
.client
.getRoomById(controller.activeSpaceId!)!
.displayname),
),
body: LayoutBuilder(
builder: (context, size) {
controller.snappingSheetContainerSize = size;
return SnappingSheet(
key: ValueKey(Matrix.of(context).client.userID.toString() +
showSpaces.toString()),
controller: controller.snappingSheetController,
child: Column(
children: [
AnimatedContainer(
height: controller.showChatBackupBanner ? 54 : 0,
duration: const Duration(milliseconds: 300),
clipBehavior: Clip.hardEdge,
curve: Curves.bounceInOut,
decoration: const BoxDecoration(),
child: Material(
color: Theme.of(context).colorScheme.surface,
child: ListTile(
leading: Image.asset(
'assets/backup.png',
fit: BoxFit.contain,
width: 44,
),
title: Text(L10n.of(context)!.setupChatBackupNow),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: controller.firstRunBootstrapAction,
),
),
), ),
title: Text(L10n.of(context)!.setupChatBackupNow), Expanded(child: _ChatListViewBody(controller)),
trailing: const Icon(Icons.chevron_right_outlined), ],
onTap: controller.firstRunBootstrapAction,
),
), ),
), initialSnappingPosition: showSpaces
Expanded(child: _ChatListViewBody(controller)), ? const SnappingPosition.pixels(
]), positionPixels: kSpacesBottomBarHeight)
floatingActionButton: selectMode == SelectMode.normal : const SnappingPosition.factor(positionFactor: 0.0),
? KeyBoardShortcuts( snappingPositions: showSpaces
? const [
SnappingPosition.pixels(
positionPixels: kSpacesBottomBarHeight),
SnappingPosition.factor(positionFactor: 0.5),
SnappingPosition.factor(positionFactor: 0.9),
]
: [const SnappingPosition.factor(positionFactor: 0.0)],
sheetBelow: showSpaces
? SnappingSheetContent(
childScrollController:
controller.snappingSheetScrollContentController,
draggable: true,
child: SpacesBottomBar(controller),
)
: null,
);
},
),
floatingActionButton: selectMode == SelectMode.normal
? Padding(
padding: showSpaces
? const EdgeInsets.only(bottom: 64.0)
: const EdgeInsets.all(0),
child: KeyBoardShortcuts(
child: FloatingActionButton.extended( child: FloatingActionButton.extended(
isExtended: controller.scrolledToTop, isExtended: controller.scrolledToTop,
onPressed: () => onPressed: () =>
@ -239,20 +277,14 @@ class ChatListView extends StatelessWidget {
onKeysPressed: () => onKeysPressed: () =>
VRouter.of(context).to('/newprivatechat'), VRouter.of(context).to('/newprivatechat'),
helpLabel: L10n.of(context)!.newChat, helpLabel: L10n.of(context)!.newChat,
) ),
: null, )
bottomNavigationBar: Column( : null,
mainAxisSize: MainAxisSize.min, bottomNavigationBar: const ConnectionStatusHeader(),
children: [ ),
const ConnectionStatusHeader(), );
if (controller.spaces.isNotEmpty && },
controller.selectedRoomIds.isEmpty) );
SpacesBottomBar(controller),
],
),
),
);
});
} }
} }

View File

@ -1,53 +1,135 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:salomon_bottom_bar/salomon_bottom_bar.dart'; import 'package:salomon_bottom_bar/salomon_bottom_bar.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/spaces_drawer.dart';
import 'package:fluffychat/pages/chat_list/spaces_entry.dart'; import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/matrix.dart';
const kSpacesBottomBarHeight = 56.0;
class SpacesBottomBar extends StatelessWidget { class SpacesBottomBar extends StatelessWidget {
final ChatListController controller; final ChatListController controller;
const SpacesBottomBar(this.controller, {Key? key}) : super(key: key); const SpacesBottomBar(this.controller, {Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final foundIndex = controller.spacesEntries.indexWhere(
(se) => spacesEntryRoughEquivalence(se, controller.activeSpacesEntry));
final currentIndex = foundIndex == -1 ? 0 : foundIndex;
return Material( return Material(
color: Theme.of(context).appBarTheme.backgroundColor, color: Theme.of(context).navigationBarTheme.backgroundColor,
elevation: 6, elevation: 6,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppConfig.borderRadius)),
clipBehavior: Clip.hardEdge,
child: SafeArea( child: SafeArea(
child: StreamBuilder<Object>( child: StreamBuilder<Object>(
stream: Matrix.of(context).client.onSync.stream.where((sync) => stream: Matrix.of(context).client.onSync.stream.where((sync) =>
(sync.rooms?.join?.values.any((r) => (sync.rooms?.join?.values.any((r) =>
r.state?.any((s) => s.type.startsWith('m.space')) ?? r.state?.any((s) => s.type.startsWith('m.space')) ??
false) ?? false) ??
false) || false) ||
(sync.rooms?.leave?.isNotEmpty ?? false)), (sync.rooms?.leave?.isNotEmpty ?? false)),
builder: (context, snapshot) { builder: (context, snapshot) {
return Container( return SingleChildScrollView(
height: 56, controller: controller.snappingSheetScrollContentController,
alignment: Alignment.center, child: AnimatedBuilder(
child: SingleChildScrollView( child: _SpacesBottomNavigation(controller: controller),
scrollDirection: Axis.horizontal, builder: (context, child) {
child: SalomonBottomBar( if (controller.snappingSheetContainerSize == null) {
itemPadding: const EdgeInsets.all(8), return child!;
currentIndex: currentIndex, }
onTap: (i) => controller.setActiveSpacesEntry( final rawPosition =
controller.snappingSheetController.currentPosition;
final position = rawPosition /
controller.snappingSheetContainerSize!.maxHeight;
if (rawPosition <= kSpacesBottomBarHeight) {
return child!;
} else if (position >= 0.5) {
return SpacesDrawer(controller: controller);
} else {
final normalized = (rawPosition - kSpacesBottomBarHeight) /
(controller.snappingSheetContainerSize!.maxHeight -
kSpacesBottomBarHeight) *
2;
var boxHeight = (1 - normalized) * kSpacesBottomBarHeight;
if (boxHeight < 0) boxHeight = 0;
return Column(
children: [
SizedBox(
height: boxHeight,
child: ClipRect(
clipBehavior: Clip.hardEdge,
child: Opacity(
opacity: 1 - normalized, child: child!)),
),
Opacity(
opacity: normalized,
child: SpacesDrawer(controller: controller),
),
],
);
}
},
animation: controller.snappingSheetController,
),
);
},
),
),
);
}
}
class _SpacesBottomNavigation extends StatelessWidget {
final ChatListController controller;
const _SpacesBottomNavigation({Key? key, required this.controller})
: super(key: key);
@override
Widget build(BuildContext context) {
final currentIndex = controller.activeSpaceId == null
? 1
: controller.spaces
.indexWhere((space) => controller.activeSpaceId == space.id) +
2;
return Container(
height: 56,
alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SalomonBottomBar(
itemPadding: const EdgeInsets.all(8),
currentIndex: currentIndex,
onTap: (i) => i == 0
? controller.expandSpaces()
: i == 1
? controller.setActiveSpacesEntry(
context,
null,
)
: controller.setActiveSpacesEntry(
context, context,
controller.spacesEntries[i], controller.spacesEntries[i],
), ),
selectedItemColor: Theme.of(context).colorScheme.primary, selectedItemColor: Theme.of(context).colorScheme.primary,
items: controller.spacesEntries items: [
.map((entry) => _buildSpacesEntryUI(context, entry)) SalomonBottomBarItem(
.toList(), icon: const Icon(Icons.keyboard_arrow_up),
), title: Text(L10n.of(context)!.showSpaces),
), ),
); ...controller.spacesEntries
}), .map((space) => _buildSpacesEntryUI(context, space))
.toList(),
],
),
), ),
); );
} }

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'chat_list.dart';
class SpacesDrawer extends StatelessWidget {
final ChatListController controller;
const SpacesDrawer({Key? key, required this.controller}) : super(key: key);
@override
Widget build(BuildContext context) {
final currentIndex = controller.activeSpaceId == null
? 0
: controller.spaces
.indexWhere((space) => controller.activeSpaceId == space.id) +
1;
final Map<SpacesEntry, dynamic> spaceHierarchy =
Map.fromEntries(controller.spacesEntries.map((e) => MapEntry(e, null)));
// TODO(TheOeWithTheBraid): wait for space hierarchy https://gitlab.com/famedly/company/frontend/libraries/matrix_api_lite/-/merge_requests/58
return WillPopScope(
onWillPop: () async {
controller.snapBackSpacesSheet();
return false;
},
child: ListView.builder(
shrinkWrap: true,
itemCount: spaceHierarchy.length,
itemBuilder: (BuildContext context, int index) {
if (index == 0) {
return ListTile(
selected: currentIndex == index,
leading: const Icon(Icons.keyboard_arrow_down),
title: Text(L10n.of(context)!.allChats),
onTap: () => controller.setActiveSpacesEntry(
context,
null,
),
);
} else {
final space = spaceHierarchy.keys.toList()[index];
final room = space.getSpace(context)!;
return ListTile(
selected: currentIndex == index,
leading: Avatar(
mxContent: room.avatar,
name: space.getName(context),
size: 24,
fontSize: 12,
),
title: Text(space.getName(context)),
subtitle: room.topic.isEmpty
? null
: Tooltip(
message: room.topic,
child: Text(
room.topic.replaceAll('\n', ' '),
softWrap: false,
overflow: TextOverflow.fade,
),
),
onTap: () => controller.setActiveSpacesEntry(
context,
space,
),
trailing: IconButton(
icon: const Icon(Icons.edit),
tooltip: L10n.of(context)!.edit,
onPressed: () => controller.editSpace(context, room.id),
),
);
}
},
),
);
}
}

View File

@ -12,7 +12,6 @@ import desktop_lifecycle
import device_info_plus_macos import device_info_plus_macos
import emoji_picker_flutter import emoji_picker_flutter
import file_selector_macos import file_selector_macos
import flutter_app_badger
import flutter_local_notifications import flutter_local_notifications
import flutter_secure_storage_macos import flutter_secure_storage_macos
import flutter_web_auth import flutter_web_auth
@ -36,7 +35,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin")) EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin")) FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin"))
FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin")) FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin"))

View File

@ -14,7 +14,7 @@ packages:
name: adaptive_dialog name: adaptive_dialog
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.5.0+1" version: "1.4.0"
adaptive_theme: adaptive_theme:
dependency: "direct main" dependency: "direct main"
description: description:
@ -289,14 +289,14 @@ packages:
name: dart_webrtc name: dart_webrtc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.4"
dbus: dbus:
dependency: "direct overridden" dependency: "direct overridden"
description: description:
name: dbus name: dbus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.7.3" version: "0.7.2"
desktop_drop: desktop_drop:
dependency: "direct main" dependency: "direct main"
description: description:
@ -483,7 +483,7 @@ packages:
name: flutter_app_badger name: flutter_app_badger
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.0" version: "1.3.0"
flutter_app_lock: flutter_app_lock:
dependency: "direct main" dependency: "direct main"
description: description:
@ -551,7 +551,7 @@ packages:
name: flutter_local_notifications name: flutter_local_notifications
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "9.4.0" version: "9.4.1"
flutter_local_notifications_linux: flutter_local_notifications_linux:
dependency: transitive dependency: transitive
description: description:
@ -598,7 +598,7 @@ packages:
name: flutter_native_splash name: flutter_native_splash
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.5" version: "2.1.2+1"
flutter_olm: flutter_olm:
dependency: "direct main" dependency: "direct main"
description: description:
@ -626,7 +626,7 @@ packages:
name: flutter_ringtone_player name: flutter_ringtone_player
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.2.0" version: "3.1.1"
flutter_secure_storage: flutter_secure_storage:
dependency: "direct main" dependency: "direct main"
description: description:
@ -713,7 +713,7 @@ packages:
name: flutter_webrtc name: flutter_webrtc
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.8.5" version: "0.8.4"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -851,13 +851,6 @@ packages:
name: image_picker name: image_picker
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.8.5"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4+11" version: "0.8.4+11"
image_picker_for_web: image_picker_for_web:
dependency: transitive dependency: transitive
@ -866,13 +859,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.6" version: "2.1.6"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.4+11"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -957,13 +943,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.8.1" version: "0.8.1"
lint:
dependency: transitive
description:
name: lint
url: "https://pub.dartlang.org"
source: hosted
version: "1.8.2"
lints: lints:
dependency: transitive dependency: transitive
description: description:
@ -1140,14 +1119,14 @@ packages:
name: package_info_plus name: package_info_plus
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.4.2" version: "1.4.0"
package_info_plus_linux: package_info_plus_linux:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_linux name: package_info_plus_linux
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.3"
package_info_plus_macos: package_info_plus_macos:
dependency: transitive dependency: transitive
description: description:
@ -1168,14 +1147,14 @@ packages:
name: package_info_plus_web name: package_info_plus_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.4"
package_info_plus_windows: package_info_plus_windows:
dependency: transitive dependency: transitive
description: description:
name: package_info_plus_windows name: package_info_plus_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.5" version: "1.0.4"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -1273,7 +1252,7 @@ packages:
name: permission_handler_apple name: permission_handler_apple
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "9.0.4" version: "9.0.3"
permission_handler_platform_interface: permission_handler_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1420,7 +1399,7 @@ packages:
name: record name: record
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.4" version: "3.0.3"
record_platform_interface: record_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -1539,7 +1518,7 @@ packages:
name: shelf name: shelf
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.0" version: "1.2.0"
shelf_packages_handler: shelf_packages_handler:
dependency: transitive dependency: transitive
description: description:
@ -1573,6 +1552,15 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
snapping_sheet:
dependency: "direct main"
description:
path: "."
ref: listenable
resolved-ref: "3da78eea5d222baa1b266c19284acafee090f6be"
url: "https://github.com/TheOneWithTheBraid/snapping_sheet.git"
source: git
version: "3.1.0"
source_map_stack_trace: source_map_stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -1607,7 +1595,7 @@ packages:
name: sqflite_common name: sqflite_common
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.2.1" version: "2.2.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -1831,7 +1819,7 @@ packages:
name: url_launcher_web name: url_launcher_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.9" version: "2.0.6"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
@ -1999,14 +1987,14 @@ packages:
name: webrtc_interface name: webrtc_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.3" version: "1.0.2"
win32: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.5.1" version: "2.5.0"
wkt_parser: wkt_parser:
dependency: transitive dependency: transitive
description: description:
@ -2037,4 +2025,4 @@ packages:
version: "3.1.0" version: "3.1.0"
sdks: sdks:
dart: ">=2.16.1 <3.0.0" dart: ">=2.16.1 <3.0.0"
flutter: ">=2.10.0" flutter: ">=2.8.0"

View File

@ -78,6 +78,7 @@ dependencies:
share: ^2.0.4 share: ^2.0.4
shared_preferences: ^2.0.13 shared_preferences: ^2.0.13
slugify: ^2.0.0 slugify: ^2.0.0
snapping_sheet: ^3.1.0
swipe_to_action: ^0.2.0 swipe_to_action: ^0.2.0
uni_links: ^0.5.1 uni_links: ^0.5.1
unifiedpush: ^4.0.0 unifiedpush: ^4.0.0
@ -146,3 +147,9 @@ dependency_overrides:
url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
ref: null-safety ref: null-safety
provider: 5.0.0 provider: 5.0.0
# wating for `Listenable` implementation
# Upstream pull request: https://github.com/AdamJonsson/snapping_sheet/pull/84
snapping_sheet:
git:
url: https://github.com/TheOneWithTheBraid/snapping_sheet.git
ref: listenable