Merge branch 'braid/spaces-advanced' into 'main'
feat: improve spaces See merge request famedly/fluffychat!799
This commit is contained in:
commit
14a6047fef
8 changed files with 488 additions and 269 deletions
|
|
@ -8,11 +8,13 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
|
|||
import 'package:future_loading_dialog/future_loading_dialog.dart';
|
||||
import 'package:matrix/matrix.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:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.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/utils/fluffy_share.dart';
|
||||
import 'package:fluffychat/utils/platform_infos.dart';
|
||||
|
|
@ -40,7 +42,7 @@ class ChatList extends StatefulWidget {
|
|||
ChatListController createState() => ChatListController();
|
||||
}
|
||||
|
||||
class ChatListController extends State<ChatList> {
|
||||
class ChatListController extends State<ChatList> with TickerProviderStateMixin {
|
||||
StreamSubscription? _intentDataStreamSubscription;
|
||||
|
||||
StreamSubscription? _intentFileStreamSubscription;
|
||||
|
|
@ -54,6 +56,8 @@ class ChatListController extends State<ChatList> {
|
|||
return (id == null || !id.stillValid(context)) ? defaultSpacesEntry : id;
|
||||
}
|
||||
|
||||
BoxConstraints? snappingSheetContainerSize;
|
||||
|
||||
String? get activeSpaceId => activeSpacesEntry.getSpace(context)?.id;
|
||||
|
||||
final ScrollController scrollController = ScrollController();
|
||||
|
|
@ -61,6 +65,10 @@ class ChatListController extends State<ChatList> {
|
|||
|
||||
final StreamController<Client> _clientStream = StreamController.broadcast();
|
||||
|
||||
SnappingSheetController snappingSheetController = SnappingSheetController();
|
||||
|
||||
ScrollController snappingSheetScrollContentController = ScrollController();
|
||||
|
||||
Stream<Client> get clientStream => _clientStream.stream;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -480,6 +491,8 @@ class ChatListController extends State<ChatList> {
|
|||
VRouter.of(context).to('/rooms');
|
||||
setState(() {
|
||||
_activeSpacesEntry = null;
|
||||
snappingSheetController = SnappingSheetController();
|
||||
snappingSheetScrollContentController = ScrollController();
|
||||
selectedRoomIds.clear();
|
||||
Matrix.of(context).setActiveClient(client);
|
||||
});
|
||||
|
|
@ -575,6 +588,21 @@ class ChatListController extends State<ChatList> {
|
|||
void _hackyWebRTCFixForWeb() {
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import 'package:animations/animations.dart';
|
|||
import 'package:flutter_gen/gen_l10n/l10n.dart';
|
||||
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:snapping_sheet/snapping_sheet.dart';
|
||||
import 'package:vrouter/vrouter.dart';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
|
|
@ -28,203 +29,240 @@ class ChatListView extends StatelessWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<Object?>(
|
||||
stream: Matrix.of(context).onShareContentChanged.stream,
|
||||
builder: (_, __) {
|
||||
final selectMode = controller.selectMode;
|
||||
return VWidgetGuard(
|
||||
onSystemPop: (redirector) async {
|
||||
final selMode = controller.selectMode;
|
||||
if (selMode != SelectMode.normal) controller.cancelAction();
|
||||
if (selMode == SelectMode.select) redirector.stopRedirection();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: controller.scrolledToTop ? 0 : null,
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: controller.selectedRoomIds.isEmpty
|
||||
? null
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
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
|
||||
stream: Matrix.of(context).onShareContentChanged.stream,
|
||||
builder: (_, __) {
|
||||
final selectMode = controller.selectMode;
|
||||
final showSpaces =
|
||||
controller.spaces.isNotEmpty && controller.selectedRoomIds.isEmpty;
|
||||
return VWidgetGuard(
|
||||
onSystemPop: (redirector) async {
|
||||
final selMode = controller.selectMode;
|
||||
if (selMode != SelectMode.normal) controller.cancelAction();
|
||||
if (selMode == SelectMode.select) redirector.stopRedirection();
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
elevation: controller.scrolledToTop ? 0 : null,
|
||||
actionsIconTheme: IconThemeData(
|
||||
color: controller.selectedRoomIds.isEmpty
|
||||
? null
|
||||
: 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),
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
body: 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,
|
||||
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
|
||||
: 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),
|
||||
trailing: const Icon(Icons.chevron_right_outlined),
|
||||
onTap: controller.firstRunBootstrapAction,
|
||||
),
|
||||
Expanded(child: _ChatListViewBody(controller)),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: _ChatListViewBody(controller)),
|
||||
]),
|
||||
floatingActionButton: selectMode == SelectMode.normal
|
||||
? KeyBoardShortcuts(
|
||||
initialSnappingPosition: showSpaces
|
||||
? const SnappingPosition.pixels(
|
||||
positionPixels: kSpacesBottomBarHeight)
|
||||
: const SnappingPosition.factor(positionFactor: 0.0),
|
||||
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(
|
||||
isExtended: controller.scrolledToTop,
|
||||
onPressed: () =>
|
||||
|
|
@ -239,20 +277,14 @@ class ChatListView extends StatelessWidget {
|
|||
onKeysPressed: () =>
|
||||
VRouter.of(context).to('/newprivatechat'),
|
||||
helpLabel: L10n.of(context)!.newChat,
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ConnectionStatusHeader(),
|
||||
if (controller.spaces.isNotEmpty &&
|
||||
controller.selectedRoomIds.isEmpty)
|
||||
SpacesBottomBar(controller),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
),
|
||||
)
|
||||
: null,
|
||||
bottomNavigationBar: const ConnectionStatusHeader(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,53 +1,135 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/l10n.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/spaces_drawer.dart';
|
||||
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
import 'package:fluffychat/widgets/matrix.dart';
|
||||
|
||||
const kSpacesBottomBarHeight = 56.0;
|
||||
|
||||
class SpacesBottomBar extends StatelessWidget {
|
||||
final ChatListController controller;
|
||||
|
||||
const SpacesBottomBar(this.controller, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final foundIndex = controller.spacesEntries.indexWhere(
|
||||
(se) => spacesEntryRoughEquivalence(se, controller.activeSpacesEntry));
|
||||
final currentIndex = foundIndex == -1 ? 0 : foundIndex;
|
||||
return Material(
|
||||
color: Theme.of(context).appBarTheme.backgroundColor,
|
||||
color: Theme.of(context).navigationBarTheme.backgroundColor,
|
||||
elevation: 6,
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppConfig.borderRadius)),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
child: SafeArea(
|
||||
child: StreamBuilder<Object>(
|
||||
stream: Matrix.of(context).client.onSync.stream.where((sync) =>
|
||||
(sync.rooms?.join?.values.any((r) =>
|
||||
r.state?.any((s) => s.type.startsWith('m.space')) ??
|
||||
false) ??
|
||||
false) ||
|
||||
(sync.rooms?.leave?.isNotEmpty ?? false)),
|
||||
builder: (context, snapshot) {
|
||||
return Container(
|
||||
height: 56,
|
||||
alignment: Alignment.center,
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SalomonBottomBar(
|
||||
itemPadding: const EdgeInsets.all(8),
|
||||
currentIndex: currentIndex,
|
||||
onTap: (i) => controller.setActiveSpacesEntry(
|
||||
stream: Matrix.of(context).client.onSync.stream.where((sync) =>
|
||||
(sync.rooms?.join?.values.any((r) =>
|
||||
r.state?.any((s) => s.type.startsWith('m.space')) ??
|
||||
false) ??
|
||||
false) ||
|
||||
(sync.rooms?.leave?.isNotEmpty ?? false)),
|
||||
builder: (context, snapshot) {
|
||||
return SingleChildScrollView(
|
||||
controller: controller.snappingSheetScrollContentController,
|
||||
child: AnimatedBuilder(
|
||||
child: _SpacesBottomNavigation(controller: controller),
|
||||
builder: (context, child) {
|
||||
if (controller.snappingSheetContainerSize == null) {
|
||||
return child!;
|
||||
}
|
||||
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,
|
||||
controller.spacesEntries[i],
|
||||
),
|
||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
||||
items: controller.spacesEntries
|
||||
.map((entry) => _buildSpacesEntryUI(context, entry))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
||||
items: [
|
||||
SalomonBottomBarItem(
|
||||
icon: const Icon(Icons.keyboard_arrow_up),
|
||||
title: Text(L10n.of(context)!.showSpaces),
|
||||
),
|
||||
...controller.spacesEntries
|
||||
.map((space) => _buildSpacesEntryUI(context, space))
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
83
lib/pages/chat_list/spaces_drawer.dart
Normal file
83
lib/pages/chat_list/spaces_drawer.dart
Normal 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),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue