feat: New navigation design

This commit is contained in:
Christian Pauly 2022-08-30 20:24:36 +02:00
parent 4490c598ff
commit e7af925f2a
10 changed files with 495 additions and 355 deletions

View file

@ -1,13 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import '../widgets/matrix.dart';
import 'app_config.dart';
abstract class FluffyThemes {
static const double columnWidth = 360.0;
static bool isColumnModeByWidth(double width) => width > columnWidth * 2 + 64;
static bool isColumnMode(BuildContext context) =>
MediaQuery.of(context).size.width > columnWidth * 2;
isColumnModeByWidth(MediaQuery.of(context).size.width);
static bool getDisplayNavigationRail(BuildContext context) =>
VRouter.of(context).path.startsWith('/rooms') &&
(Matrix.of(context).client.rooms.any((room) => room.isSpace) ||
AppConfig.separateChatTypes);
static const fallbackTextStyle = TextStyle(
fontFamily: 'Roboto',

View file

@ -13,10 +13,12 @@ import 'package:uni_links/uni_links.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list_view.dart';
import 'package:fluffychat/pages/chat_list/spaces_entry.dart';
import 'package:fluffychat/utils/famedlysdk_store.dart';
import 'package:fluffychat/utils/localized_exception_extension.dart';
import 'package:fluffychat/utils/matrix_sdk_extensions.dart/client_stories_extension.dart';
import 'package:fluffychat/utils/platform_infos.dart';
import 'package:fluffychat/utils/space_navigator.dart';
import '../../../utils/account_bundles.dart';
@ -30,7 +32,11 @@ import '../settings_account/settings_account.dart';
import 'package:fluffychat/utils/tor_stub.dart'
if (dart.library.html) 'package:tor_detector_web/tor_detector_web.dart';
enum SelectMode { normal, share, select }
enum SelectMode {
normal,
share,
select,
}
enum PopupMenuAction {
settings,
@ -41,6 +47,13 @@ enum PopupMenuAction {
archive,
}
enum ActiveFilter {
allChats,
groups,
messages,
spaces,
}
class ChatList extends StatefulWidget {
const ChatList({Key? key}) : super(key: key);
@ -58,6 +71,92 @@ class ChatListController extends State<ChatList>
SpacesEntry? _activeSpacesEntry;
bool get displayNavigationBar =>
!FluffyThemes.isColumnMode(context) &&
(spaces.isNotEmpty || AppConfig.separateChatTypes);
int get selectedIndex {
switch (activeFilter) {
case ActiveFilter.allChats:
return 0;
case ActiveFilter.groups:
return 0;
case ActiveFilter.messages:
return 1;
case ActiveFilter.spaces:
return AppConfig.separateChatTypes ? 2 : 1;
}
}
void onDestinationSelected(int? i) {
switch (i) {
case 0:
if (AppConfig.separateChatTypes) {
setState(() {
activeFilter = ActiveFilter.groups;
});
} else {
setState(() {
activeFilter = ActiveFilter.allChats;
});
}
break;
case 1:
if (AppConfig.separateChatTypes) {
setState(() {
activeFilter = ActiveFilter.messages;
});
} else {
setState(() {
activeFilter = ActiveFilter.spaces;
});
}
break;
case 2:
setState(() {
activeFilter = ActiveFilter.spaces;
});
break;
}
}
ActiveFilter activeFilter = AppConfig.separateChatTypes
? ActiveFilter.messages
: ActiveFilter.allChats;
List<Room> get filteredRooms {
final rooms = Matrix.of(context).client.rooms;
switch (activeFilter) {
case ActiveFilter.allChats:
return rooms
.where((room) =>
!room.isSpace && room.spaceParents.isEmpty && !room.isStoryRoom)
.toList();
case ActiveFilter.groups:
return rooms
.where((room) =>
!room.isSpace &&
room.spaceParents.isEmpty &&
!room.isDirectChat &&
!room.isStoryRoom)
.toList();
case ActiveFilter.messages:
return rooms
.where((room) =>
!room.isSpace &&
room.spaceParents.isEmpty &&
room.isDirectChat &&
!room.isStoryRoom)
.toList();
case ActiveFilter.spaces:
return rooms
.where((room) =>
(room.isSpace || room.spaceParents.isNotEmpty) &&
!room.isStoryRoom)
.toList();
}
}
bool isSearchMode = false;
Future<QueryPublicRoomsResponse>? publicRoomsResponse;
String? searchServer;
@ -154,6 +253,7 @@ class ChatListController extends State<ChatList>
bool isTorBrowser = false;
@Deprecated('')
SpacesEntry get activeSpacesEntry {
final id = _activeSpacesEntry;
return (id == null || !id.stillValid(context)) ? defaultSpacesEntry : id;

View file

@ -17,6 +17,7 @@ import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/profile_bottom_sheet.dart';
import 'package:fluffychat/widgets/public_room_bottom_sheet.dart';
import '../../utils/stream_extension.dart';
import '../../widgets/connection_status_header.dart';
import '../../widgets/matrix.dart';
import 'spaces_hierarchy_proposal.dart';
@ -57,146 +58,148 @@ class _ChatListViewBodyState extends State<ChatListViewBody> {
Widget child;
if (widget.controller.waitForFirstSync &&
Matrix.of(context).client.prevBatch != null) {
final rooms = widget.controller.activeSpacesEntry.getRooms(context);
final rooms = widget.controller.filteredRooms;
final displayStoriesHeader = widget.controller.activeSpacesEntry
.shouldShowStoriesHeader(context) ||
rooms.isEmpty;
final displayStoriesHeader = {
ActiveFilter.allChats,
ActiveFilter.messages,
}.contains(widget.controller.activeFilter);
child = ListView.builder(
key: ValueKey(Matrix.of(context).client.userID.toString() +
widget.controller.activeSpaceId.toString() +
widget.controller.activeSpacesEntry.runtimeType.toString()),
widget.controller.activeFilter.toString()),
controller: widget.controller.scrollController,
// add +1 space below in order to properly scroll below the spaces bar
itemCount: rooms.length + (displayStoriesHeader ? 2 : 1),
itemCount: rooms.length + 2,
itemBuilder: (BuildContext context, int i) {
if (displayStoriesHeader) {
if (i == 0) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SpaceRoomListTopBar(widget.controller),
if (roomSearchResult != null) ...[
SearchTitle(
title: L10n.of(context)!.publicRooms,
icon: const Icon(Icons.explore_outlined),
),
AnimatedContainer(
height: roomSearchResult.chunk.isEmpty ? 0 : 106,
duration: const Duration(milliseconds: 250),
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: roomSearchResult.chunk.length,
itemBuilder: (context, i) => _SearchItem(
title: roomSearchResult.chunk[i].name ??
roomSearchResult
.chunk[i].canonicalAlias?.localpart ??
L10n.of(context)!.group,
avatar: roomSearchResult.chunk[i].avatarUrl,
onPressed: () => showModalBottomSheet(
context: context,
builder: (c) => PublicRoomBottomSheet(
roomAlias:
roomSearchResult.chunk[i].canonicalAlias ??
roomSearchResult.chunk[i].roomId,
outerContext: context,
chunk: roomSearchResult.chunk[i],
),
if (i == 0) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SpaceRoomListTopBar(widget.controller),
if (roomSearchResult != null) ...[
SearchTitle(
title: L10n.of(context)!.publicRooms,
icon: const Icon(Icons.explore_outlined),
),
AnimatedContainer(
height: roomSearchResult.chunk.isEmpty ? 0 : 106,
duration: const Duration(milliseconds: 250),
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: roomSearchResult.chunk.length,
itemBuilder: (context, i) => _SearchItem(
title: roomSearchResult.chunk[i].name ??
roomSearchResult
.chunk[i].canonicalAlias?.localpart ??
L10n.of(context)!.group,
avatar: roomSearchResult.chunk[i].avatarUrl,
onPressed: () => showModalBottomSheet(
context: context,
builder: (c) => PublicRoomBottomSheet(
roomAlias:
roomSearchResult.chunk[i].canonicalAlias ??
roomSearchResult.chunk[i].roomId,
outerContext: context,
chunk: roomSearchResult.chunk[i],
),
),
),
),
],
if (userSearchResult != null) ...[
SearchTitle(
title: L10n.of(context)!.users,
icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
height: userSearchResult.results.isEmpty ? 0 : 106,
duration: const Duration(milliseconds: 250),
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title: userSearchResult.results[i].displayName ??
userSearchResult.results[i].userId.localpart ??
L10n.of(context)!.unknownDevice,
avatar: userSearchResult.results[i].avatarUrl,
onPressed: () => showModalBottomSheet(
context: context,
builder: (c) => ProfileBottomSheet(
userId: userSearchResult.results[i].userId,
outerContext: context,
),
),
],
if (userSearchResult != null) ...[
SearchTitle(
title: L10n.of(context)!.users,
icon: const Icon(Icons.group_outlined),
),
AnimatedContainer(
height: userSearchResult.results.isEmpty ? 0 : 106,
duration: const Duration(milliseconds: 250),
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: userSearchResult.results.length,
itemBuilder: (context, i) => _SearchItem(
title: userSearchResult.results[i].displayName ??
userSearchResult.results[i].userId.localpart ??
L10n.of(context)!.unknownDevice,
avatar: userSearchResult.results[i].avatarUrl,
onPressed: () => showModalBottomSheet(
context: context,
builder: (c) => ProfileBottomSheet(
userId: userSearchResult.results[i].userId,
outerContext: context,
),
),
),
),
],
if (widget.controller.isSearchMode)
SearchTitle(
title: L10n.of(context)!.stories,
icon: const Icon(Icons.camera_alt_outlined),
),
),
],
if (widget.controller.isSearchMode)
SearchTitle(
title: L10n.of(context)!.stories,
icon: const Icon(Icons.camera_alt_outlined),
),
if (displayStoriesHeader)
StoriesHeader(
filter: widget.controller.searchController.text,
),
AnimatedContainer(
height: widget.controller.isTorBrowser ? 64 : 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: const Icon(Icons.vpn_key),
title: Text(L10n.of(context)!.dehydrateTor),
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: widget.controller.dehydrate,
),
const ConnectionStatusHeader(),
AnimatedContainer(
height: widget.controller.isTorBrowser ? 64 : 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: const Icon(Icons.vpn_key),
title: Text(L10n.of(context)!.dehydrateTor),
subtitle: Text(L10n.of(context)!.dehydrateTorLong),
trailing: const Icon(Icons.chevron_right_outlined),
onTap: widget.controller.dehydrate,
),
),
if (widget.controller.isSearchMode)
SearchTitle(
title: L10n.of(context)!.chats,
icon: const Icon(Icons.chat_outlined),
),
if (rooms.isEmpty && !widget.controller.isSearchMode)
Column(
key: const ValueKey(null),
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image.asset(
'assets/private_chat_wallpaper.png',
width: 160,
height: 160,
),
Center(
child: Text(
L10n.of(context)!.startYourFirstChat,
textAlign: TextAlign.start,
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
if (widget.controller.isSearchMode)
SearchTitle(
title: L10n.of(context)!.chats,
icon: const Icon(Icons.chat_outlined),
),
if (rooms.isEmpty && !widget.controller.isSearchMode)
Column(
key: const ValueKey(null),
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Image.asset(
'assets/private_chat_wallpaper.png',
width: 160,
height: 160,
),
Center(
child: Text(
L10n.of(context)!.startYourFirstChat,
textAlign: TextAlign.start,
style: const TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
const SizedBox(height: 16),
],
),
],
);
}
i--;
),
const SizedBox(height: 16),
],
),
],
);
}
i--;
if (i >= rooms.length) {
return SpacesHierarchyProposals(
space: widget.controller.activeSpacesEntry.getSpace(context)?.id,

View file

@ -1,85 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/spaces_drawer.dart';
import 'package:fluffychat/utils/fluffy_share.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../config/app_config.dart';
class ChatListDrawer extends StatelessWidget {
final ChatListController controller;
const ChatListDrawer(this.controller, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context) => Drawer(
child: SafeArea(
child: Column(
children: [
ListTile(
leading: const CircleAvatar(
radius: Avatar.defaultSize / 2,
backgroundImage: AssetImage('assets/logo.png'),
),
title: Text(AppConfig.applicationName),
trailing: Icon(
Icons.adaptive.share_outlined,
color: Theme.of(context).colorScheme.onBackground,
),
onTap: () {
Scaffold.of(context).closeDrawer();
FluffyShare.share(
L10n.of(context)!.inviteText(
Matrix.of(context).client.userID!,
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat'),
context);
},
),
const Divider(thickness: 1),
Expanded(
child: SpacesDrawer(
controller: controller,
),
),
const Divider(thickness: 1),
ListTile(
leading: Icon(
Icons.group_add_outlined,
color: Theme.of(context).colorScheme.onBackground,
),
title: Text(L10n.of(context)!.createNewGroup),
onTap: () {
Scaffold.of(context).closeDrawer();
VRouter.of(context).to('/newgroup');
},
),
ListTile(
leading: Icon(
Icons.group_work_outlined,
color: Theme.of(context).colorScheme.onBackground,
),
title: Text(L10n.of(context)!.createNewSpace),
onTap: () {
Scaffold.of(context).closeDrawer();
VRouter.of(context).to('/newspace');
},
),
ListTile(
leading: Icon(
Icons.settings_outlined,
color: Theme.of(context).colorScheme.onBackground,
),
title: Text(L10n.of(context)!.settings),
onTap: () {
Scaffold.of(context).closeDrawer();
VRouter.of(context).to('/settings');
},
),
],
),
),
);
}

View file

@ -1,12 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/client_chooser_button.dart';
import 'package:fluffychat/widgets/matrix.dart';
class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
final ChatListController controller;
@ -53,68 +51,22 @@ class ChatListHeader extends StatelessWidget implements PreferredSizeWidget {
borderRadius:
BorderRadius.circular(AppConfig.borderRadius),
),
hintText: controller.activeSpacesEntry.getName(context),
prefixIcon: Padding(
padding: const EdgeInsets.only(
left: 8.0,
right: 4,
),
child: controller.isSearchMode
? IconButton(
tooltip: L10n.of(context)!.cancel,
icon: const Icon(Icons.close_outlined),
onPressed: controller.cancelSearch,
color:
Theme.of(context).colorScheme.onBackground,
)
: IconButton(
onPressed: Scaffold.of(context).openDrawer,
icon: Icon(
Icons.menu,
color: Theme.of(context)
.colorScheme
.onBackground,
),
),
),
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: controller.isSearchMode
? [
if (controller.isSearching)
const CircularProgressIndicator.adaptive(
strokeWidth: 2,
),
TextButton(
onPressed: controller.setServer,
style: TextButton.styleFrom(
textStyle: const TextStyle(fontSize: 12),
),
child: Text(
controller.searchServer ??
Matrix.of(context)
.client
.homeserver!
.host,
maxLines: 2,
),
),
]
: [
IconButton(
icon: Icon(
Icons.camera_alt_outlined,
color: Theme.of(context)
.colorScheme
.onBackground,
),
tooltip: L10n.of(context)!.addToStory,
onPressed: () =>
VRouter.of(context).to('/stories/create'),
),
ClientChooserButton(controller),
const SizedBox(width: 12),
],
hintText: L10n.of(context)!.search,
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: controller.isSearchMode
? IconButton(
tooltip: L10n.of(context)!.cancel,
icon: const Icon(Icons.close_outlined),
onPressed: controller.cancelSearch,
color: Theme.of(context).colorScheme.onBackground,
)
: Icon(
Icons.search_outlined,
color: Theme.of(context).colorScheme.onBackground,
),
suffixIcon: SizedBox(
width: 0,
child: ClientChooserButton(controller),
),
),
),

View file

@ -5,9 +5,9 @@ import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/config/themes.dart';
import 'package:fluffychat/pages/chat_list/chat_list.dart';
import 'package:fluffychat/pages/chat_list/chat_list_drawer.dart';
import 'package:fluffychat/widgets/connection_status_header.dart';
import '../../widgets/matrix.dart';
import 'chat_list_body.dart';
import 'chat_list_header.dart';
@ -18,6 +18,29 @@ class ChatListView extends StatelessWidget {
const ChatListView(this.controller, {Key? key}) : super(key: key);
List<NavigationDestination> getNavigationDestinations(BuildContext context) =>
[
if (AppConfig.separateChatTypes) ...[
NavigationDestination(
icon: const Icon(Icons.group_outlined),
label: L10n.of(context)!.groups,
),
NavigationDestination(
icon: const Icon(Icons.chat_bubble_outline),
label: L10n.of(context)!.messages,
),
] else
NavigationDestination(
icon: const Icon(Icons.chat_bubble),
label: L10n.of(context)!.allChats,
),
if (controller.spaces.isNotEmpty)
const NavigationDestination(
icon: Icon(Icons.group_work_outlined),
label: 'Spaces',
),
];
@override
Widget build(BuildContext context) {
return StreamBuilder<Object?>(
@ -30,24 +53,56 @@ class ChatListView extends StatelessWidget {
if (selMode != SelectMode.normal) controller.cancelAction();
if (selMode == SelectMode.select) redirector.stopRedirection();
},
child: Scaffold(
appBar: ChatListHeader(controller: controller),
body: ChatListViewBody(controller),
drawer: ChatListDrawer(controller),
bottomNavigationBar: const ConnectionStatusHeader(),
floatingActionButton: selectMode == SelectMode.normal
? KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyN
},
onKeysPressed: () =>
VRouter.of(context).to('/newprivatechat'),
helpLabel: L10n.of(context)!.newChat,
child:
StartChatFloatingActionButton(controller: controller),
)
: null,
child: Row(
children: [
if (FluffyThemes.isColumnMode(context) &&
FluffyThemes.getDisplayNavigationRail(context)) ...[
NavigationRail(
selectedIndex: controller.selectedIndex,
onDestinationSelected: controller.onDestinationSelected,
labelType: NavigationRailLabelType.all,
destinations: getNavigationDestinations(context)
.map(
(destination) => NavigationRailDestination(
icon: destination.icon,
label: Text(destination.label),
),
)
.toList(),
),
Container(
color: Theme.of(context).dividerColor,
width: 1,
),
],
Expanded(
child: Scaffold(
appBar: ChatListHeader(controller: controller),
body: ChatListViewBody(controller),
bottomNavigationBar: controller.displayNavigationBar
? NavigationBar(
selectedIndex: controller.selectedIndex,
onDestinationSelected:
controller.onDestinationSelected,
destinations: getNavigationDestinations(context),
)
: null,
floatingActionButton: selectMode == SelectMode.normal
? KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.keyN
},
onKeysPressed: () =>
VRouter.of(context).to('/newprivatechat'),
helpLabel: L10n.of(context)!.newChat,
child: StartChatFloatingActionButton(
controller: controller),
)
: null,
),
),
],
),
);
},

View file

@ -4,9 +4,11 @@ import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:keyboard_shortcuts/keyboard_shortcuts.dart';
import 'package:matrix/matrix.dart';
import 'package:vrouter/vrouter.dart';
import 'package:fluffychat/widgets/avatar.dart';
import 'package:fluffychat/widgets/matrix.dart';
import '../../utils/fluffy_share.dart';
import 'chat_list.dart';
class ClientChooserButton extends StatelessWidget {
@ -23,6 +25,60 @@ class ClientChooserButton extends StatelessWidget {
? -1
: 1);
return <PopupMenuEntry<Object>>[
PopupMenuItem(
value: SettingsAction.newStory,
child: Row(
children: [
const Icon(Icons.camera_outlined),
const SizedBox(width: 18),
Text(L10n.of(context)!.yourStory),
],
),
),
PopupMenuItem(
value: SettingsAction.newGroup,
child: Row(
children: [
const Icon(Icons.group_add_outlined),
const SizedBox(width: 18),
Text(L10n.of(context)!.createNewGroup),
],
),
),
PopupMenuItem(
value: SettingsAction.newSpace,
child: Row(
children: [
const Icon(Icons.group_work_outlined),
const SizedBox(width: 18),
Text(L10n.of(context)!.createNewSpace),
],
),
),
PopupMenuItem(
value: SettingsAction.invite,
child: Row(
children: [
Icon(Icons.adaptive.share_outlined),
const SizedBox(width: 18),
Text(L10n.of(context)!.inviteContact),
],
),
),
PopupMenuItem(
value: SettingsAction.settings,
child: Row(
children: [
const Icon(Icons.settings_outlined),
const SizedBox(width: 18),
Text(L10n.of(context)!.settings),
],
),
),
const PopupMenuItem(
value: null,
child: Divider(height: 1),
),
for (final bundle in bundles) ...[
if (matrix.accountBundles[bundle]!.length != 1 ||
matrix.accountBundles[bundle]!.single!.userID != bundle)
@ -80,7 +136,7 @@ class ClientChooserButton extends StatelessWidget {
.toList(),
],
PopupMenuItem(
value: AddAccountAction.addAccount,
value: SettingsAction.addAccount,
child: Row(
children: [
const Icon(Icons.person_add_outlined),
@ -98,42 +154,55 @@ class ClientChooserButton extends StatelessWidget {
int clientCount = 0;
matrix.accountBundles.forEach((key, value) => clientCount += value.length);
return Center(
child: FutureBuilder<Profile>(
future: matrix.client.fetchOwnProfile(),
builder: (context, snapshot) => Stack(
alignment: Alignment.center,
children: [
...List.generate(
clientCount,
(index) => KeyBoardShortcuts(
keysToPress: _buildKeyboardShortcut(index + 1),
helpLabel: L10n.of(context)!.switchToAccount(index + 1),
onKeysPressed: () => _handleKeyboardShortcut(matrix, index),
child: Container(),
return FutureBuilder<Profile>(
future: matrix.client.fetchOwnProfile(),
builder: (context, snapshot) => Stack(
alignment: Alignment.center,
children: [
...List.generate(
clientCount,
(index) => KeyBoardShortcuts(
keysToPress: _buildKeyboardShortcut(index + 1),
helpLabel: L10n.of(context)!.switchToAccount(index + 1),
onKeysPressed: () => _handleKeyboardShortcut(
matrix,
index,
context,
),
child: Container(),
),
),
KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.tab
},
helpLabel: L10n.of(context)!.nextAccount,
onKeysPressed: () => _nextAccount(matrix, context),
child: Container(),
),
KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.tab
},
helpLabel: L10n.of(context)!.previousAccount,
onKeysPressed: () => _previousAccount(matrix, context),
child: Container(),
),
Theme(
data: Theme.of(context).copyWith(
useMaterial3: false,
popupMenuTheme: PopupMenuThemeData(
color: Theme.of(context).colorScheme.background,
textStyle: TextStyle(
color: Theme.of(context).colorScheme.onBackground),
elevation: Theme.of(context).appBarTheme.scrolledUnderElevation,
),
),
KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.tab
},
helpLabel: L10n.of(context)!.nextAccount,
onKeysPressed: () => _nextAccount(matrix),
child: Container(),
),
KeyBoardShortcuts(
keysToPress: {
LogicalKeyboardKey.controlLeft,
LogicalKeyboardKey.shiftLeft,
LogicalKeyboardKey.tab
},
helpLabel: L10n.of(context)!.previousAccount,
onKeysPressed: () => _previousAccount(matrix),
child: Container(),
),
PopupMenuButton<Object>(
onSelected: _clientSelected,
child: PopupMenuButton<Object>(
onSelected: (o) => _clientSelected(o, context),
itemBuilder: _bundleMenuItems,
child: Material(
color: Colors.transparent,
@ -147,8 +216,8 @@ class ClientChooserButton extends StatelessWidget {
),
),
),
],
),
),
],
),
);
}
@ -164,17 +233,46 @@ class ClientChooserButton extends StatelessWidget {
}
}
void _clientSelected(Object object) {
void _clientSelected(
Object object,
BuildContext context,
) {
if (object is Client) {
controller.setActiveClient(object);
} else if (object is String) {
controller.setActiveBundle(object);
} else if (object == AddAccountAction.addAccount) {
controller.addAccountAction();
} else if (object is SettingsAction) {
switch (object) {
case SettingsAction.addAccount:
VRouter.of(context).to('/settings/account');
break;
case SettingsAction.newStory:
VRouter.of(context).to('/stories/create');
break;
case SettingsAction.newGroup:
VRouter.of(context).to('/newgroup');
break;
case SettingsAction.newSpace:
VRouter.of(context).to('/newspace');
break;
case SettingsAction.invite:
FluffyShare.share(
L10n.of(context)!.inviteText(Matrix.of(context).client.userID!,
'https://matrix.to/#/${Matrix.of(context).client.userID}?client=im.fluffychat'),
context);
break;
case SettingsAction.settings:
VRouter.of(context).to('/settings');
break;
}
}
}
void _handleKeyboardShortcut(MatrixState matrix, int index) {
void _handleKeyboardShortcut(
MatrixState matrix,
int index,
BuildContext context,
) {
final bundles = matrix.accountBundles.keys.toList()
..sort((a, b) => a!.isValidMatrixId == b!.isValidMatrixId
? 0
@ -186,20 +284,20 @@ class ClientChooserButton extends StatelessWidget {
int clientCount = 0;
matrix.accountBundles
.forEach((key, value) => clientCount += value.length);
_handleKeyboardShortcut(matrix, clientCount);
_handleKeyboardShortcut(matrix, clientCount, context);
}
for (final bundleName in bundles) {
final bundle = matrix.accountBundles[bundleName];
if (bundle != null) {
if (index < bundle.length) {
return _clientSelected(bundle[index]!);
return _clientSelected(bundle[index]!, context);
} else {
index -= bundle.length;
}
}
}
// if index too high, restarting from 0
_handleKeyboardShortcut(matrix, 0);
_handleKeyboardShortcut(matrix, 0, context);
}
int? _shortcutIndexOfClient(MatrixState matrix, Client client) {
@ -223,17 +321,24 @@ class ClientChooserButton extends StatelessWidget {
return null;
}
void _nextAccount(MatrixState matrix) {
void _nextAccount(MatrixState matrix, BuildContext context) {
final client = matrix.client;
final lastIndex = _shortcutIndexOfClient(matrix, client);
_handleKeyboardShortcut(matrix, lastIndex! + 1);
_handleKeyboardShortcut(matrix, lastIndex! + 1, context);
}
void _previousAccount(MatrixState matrix) {
void _previousAccount(MatrixState matrix, BuildContext context) {
final client = matrix.client;
final lastIndex = _shortcutIndexOfClient(matrix, client);
_handleKeyboardShortcut(matrix, lastIndex! - 1);
_handleKeyboardShortcut(matrix, lastIndex! - 1, context);
}
}
enum AddAccountAction { addAccount }
enum SettingsAction {
addAccount,
newStory,
newGroup,
newSpace,
invite,
settings,
}

View file

@ -16,14 +16,8 @@ extension ClientStoriesExtension on Client {
room.unsafeGetUserFromMemoryOrFallback(room.directChatMatrixID!))
.toList();
List<Room> get storiesRooms => rooms
.where((room) =>
room
.getState(EventTypes.RoomCreate)
?.content
.tryGet<String>('type') ==
storiesRoomType)
.toList();
List<Room> get storiesRooms =>
rooms.where((room) => room.isStoryRoom).toList();
Future<List<User>> getUndecidedContactsForStories(Room? storiesRoom) async {
if (storiesRoom == null) return contacts;
@ -96,3 +90,9 @@ extension ClientStoriesExtension on Client {
.toList());
}
}
extension StoryRoom on Room {
bool get isStoryRoom =>
getState(EventTypes.RoomCreate)?.content.tryGet<String>('type') ==
ClientStoriesExtension.storiesRoomType;
}

View file

@ -62,18 +62,14 @@ class FluffyChatAppState extends State<FluffyChatApp> {
initial: AdaptiveThemeMode.system,
builder: (theme, darkTheme) => LayoutBuilder(
builder: (context, constraints) {
const maxColumns = 3;
var newColumns =
(constraints.maxWidth / FluffyThemes.columnWidth).floor();
if (newColumns > maxColumns) newColumns = maxColumns;
columnMode ??= newColumns > 1;
_router ??= GlobalKey<VRouterState>();
if (columnMode != newColumns > 1) {
Logs().v('Set Column Mode = $columnMode');
final isColumnMode =
FluffyThemes.isColumnModeByWidth(constraints.maxWidth);
if (isColumnMode != columnMode) {
Logs().v('Set Column Mode = $isColumnMode');
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_initialUrl = _router?.currentState?.url;
columnMode = newColumns > 1;
columnMode = isColumnMode;
_router = GlobalKey<VRouterState>();
});
});

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import '../../config/themes.dart';
class TwoColumnLayout extends StatelessWidget {
final Widget mainView;
final Widget sideView;
@ -18,7 +20,8 @@ class TwoColumnLayout extends StatelessWidget {
Container(
clipBehavior: Clip.antiAlias,
decoration: const BoxDecoration(),
width: 360.0,
width: 360.0 +
(FluffyThemes.getDisplayNavigationRail(context) ? 64 : 0),
child: mainView,
),
Container(