mirror of
https://gitlab.com/famedly/fluffychat.git
synced 2025-01-11 18:22:49 +01:00
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:
parent
42267f263e
commit
26983a15a8
@ -1311,6 +1311,7 @@
|
||||
"type": "text",
|
||||
"placeholders": {}
|
||||
},
|
||||
"showSpaces": "Show spaces list",
|
||||
"loadMore": "Load more…",
|
||||
"@loadMore": {
|
||||
"type": "text",
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -12,7 +12,6 @@ import desktop_lifecycle
|
||||
import device_info_plus_macos
|
||||
import emoji_picker_flutter
|
||||
import file_selector_macos
|
||||
import flutter_app_badger
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import flutter_web_auth
|
||||
@ -36,7 +35,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
|
||||
EmojiPickerFlutterPlugin.register(with: registry.registrar(forPlugin: "EmojiPickerFlutterPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStorageMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageMacosPlugin"))
|
||||
FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin"))
|
||||
|
70
pubspec.lock
70
pubspec.lock
@ -14,7 +14,7 @@ packages:
|
||||
name: adaptive_dialog
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.5.0+1"
|
||||
version: "1.4.0"
|
||||
adaptive_theme:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -289,14 +289,14 @@ packages:
|
||||
name: dart_webrtc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.4"
|
||||
dbus:
|
||||
dependency: "direct overridden"
|
||||
description:
|
||||
name: dbus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.3"
|
||||
version: "0.7.2"
|
||||
desktop_drop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -483,7 +483,7 @@ packages:
|
||||
name: flutter_app_badger
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
version: "1.3.0"
|
||||
flutter_app_lock:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -551,7 +551,7 @@ packages:
|
||||
name: flutter_local_notifications
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "9.4.0"
|
||||
version: "9.4.1"
|
||||
flutter_local_notifications_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -598,7 +598,7 @@ packages:
|
||||
name: flutter_native_splash
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.5"
|
||||
version: "2.1.2+1"
|
||||
flutter_olm:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -626,7 +626,7 @@ packages:
|
||||
name: flutter_ringtone_player
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
version: "3.1.1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -713,7 +713,7 @@ packages:
|
||||
name: flutter_webrtc
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.5"
|
||||
version: "0.8.4"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -851,13 +851,6 @@ packages:
|
||||
name: image_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
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"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
@ -866,13 +859,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -957,13 +943,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.8.1"
|
||||
lint:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lint
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.2"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1140,14 +1119,14 @@ packages:
|
||||
name: package_info_plus
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
version: "1.4.0"
|
||||
package_info_plus_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_linux
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.3"
|
||||
package_info_plus_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1168,14 +1147,14 @@ packages:
|
||||
name: package_info_plus_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.4"
|
||||
package_info_plus_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
version: "1.0.4"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1273,7 +1252,7 @@ packages:
|
||||
name: permission_handler_apple
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "9.0.4"
|
||||
version: "9.0.3"
|
||||
permission_handler_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1420,7 +1399,7 @@ packages:
|
||||
name: record
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.0.4"
|
||||
version: "3.0.3"
|
||||
record_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1539,7 +1518,7 @@ packages:
|
||||
name: shelf
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.2.0"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1573,6 +1552,15 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1607,7 +1595,7 @@ packages:
|
||||
name: sqflite_common
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.2.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1831,7 +1819,7 @@ packages:
|
||||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.9"
|
||||
version: "2.0.6"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1999,14 +1987,14 @@ packages:
|
||||
name: webrtc_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "1.0.2"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.5.0"
|
||||
wkt_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2037,4 +2025,4 @@ packages:
|
||||
version: "3.1.0"
|
||||
sdks:
|
||||
dart: ">=2.16.1 <3.0.0"
|
||||
flutter: ">=2.10.0"
|
||||
flutter: ">=2.8.0"
|
||||
|
@ -78,6 +78,7 @@ dependencies:
|
||||
share: ^2.0.4
|
||||
shared_preferences: ^2.0.13
|
||||
slugify: ^2.0.0
|
||||
snapping_sheet: ^3.1.0
|
||||
swipe_to_action: ^0.2.0
|
||||
uni_links: ^0.5.1
|
||||
unifiedpush: ^4.0.0
|
||||
@ -146,3 +147,9 @@ dependency_overrides:
|
||||
url: https://github.com/TheOneWithTheBraid/keyboard_shortcuts.git
|
||||
ref: null-safety
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user