Merge branch 'braid/spaces-advanced' into 'main'

feat: improve spaces

See merge request famedly/fluffychat!799
This commit is contained in:
Krille Fear 2022-04-30 06:47:05 +00:00
commit 10798ac7ff
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