From 1a26880719cd8c841477e9e3ae6cbf355b9280ca Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 12 Jul 2024 21:59:16 +0800 Subject: [PATCH] :recycle: Chat listening on sidebar --- lib/main.dart | 26 +- lib/providers/content/channel.dart | 25 +- lib/router.dart | 28 +- lib/screens/chat.dart | 269 +++++++----------- lib/screens/realms.dart | 113 ++++---- .../{basic_shell.dart => sidebar_shell.dart} | 4 +- lib/widgets/channel/channel_list.dart | 134 +++++---- .../navigation/app_navigation_drawer.dart | 41 ++- 8 files changed, 318 insertions(+), 322 deletions(-) rename lib/shells/{basic_shell.dart => sidebar_shell.dart} (95%) diff --git a/lib/main.dart b/lib/main.dart index e9eaa35..645248f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -95,7 +95,7 @@ class SolianApp extends StatelessWidget { ); } - void _initializeProviders(BuildContext context) { + void _initializeProviders(BuildContext context) async { Get.lazyPut(() => AuthProvider()); Get.lazyPut(() => FriendProvider()); Get.lazyPut(() => FeedProvider()); @@ -108,19 +108,19 @@ class SolianApp extends StatelessWidget { Get.lazyPut(() => ChatCallProvider()); final AuthProvider auth = Get.find(); - auth.isAuthorized.then((value) async { - if (value) { - Get.find().connect(); - Get.find().connect(); + if (await auth.isAuthorized) { + Get.find().connect(); + Get.find().connect(); - try { - Get.find().registerPushNotifications(); - } catch (err) { - context.showSnackbar( - 'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}), - ); - } + Get.find().refreshAvailableChannel(); + + try { + Get.find().registerPushNotifications(); + } catch (err) { + context.showSnackbar( + 'pushNotifyRegisterFailed'.trParams({'reason': err.toString()}), + ); } - }); + } } } diff --git a/lib/providers/content/channel.dart b/lib/providers/content/channel.dart index 23fb16f..d94c4a0 100644 --- a/lib/providers/content/channel.dart +++ b/lib/providers/content/channel.dart @@ -1,10 +1,32 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:solian/models/channel.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/widgets/account/friend_select.dart'; import 'package:uuid/uuid.dart'; class ChannelProvider extends GetxController { + RxBool isLoading = false.obs; + RxList availableChannels = RxList.empty(growable: true); + + List get groupChannels => + availableChannels.where((x) => x.type == 0).toList(); + List get directChannels => + availableChannels.where((x) => x.type == 1).toList(); + + Future refreshAvailableChannel() async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) throw Exception('unauthorized'); + + isLoading.value = true; + final resp = await listAvailableChannel(); + isLoading.value = false; + + availableChannels.value = + resp.body.map((x) => Channel.fromJson(x)).toList().cast(); + availableChannels.refresh(); + } + Future getChannel(String alias, {String realm = 'global'}) async { final AuthProvider auth = Get.find(); if (!await auth.isAuthorized) throw Exception('unauthorized'); @@ -19,7 +41,8 @@ class ChannelProvider extends GetxController { return resp; } - Future getMyChannelProfile(String alias, {String realm = 'global'}) async { + Future getMyChannelProfile(String alias, + {String realm = 'global'}) async { final AuthProvider auth = Get.find(); if (!await auth.isAuthorized) throw Exception('unauthorized'); diff --git a/lib/router.dart b/lib/router.dart index 95359db..c8fbcad 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -19,7 +19,7 @@ import 'package:solian/screens/realms/realm_organize.dart'; import 'package:solian/screens/realms/realm_view.dart'; import 'package:solian/screens/home.dart'; import 'package:solian/screens/posts/post_editor.dart'; -import 'package:solian/shells/basic_shell.dart'; +import 'package:solian/shells/sidebar_shell.dart'; import 'package:solian/shells/root_shell.dart'; import 'package:solian/shells/title_shell.dart'; import 'package:solian/theme.dart'; @@ -115,20 +115,12 @@ abstract class AppRouter { ); static final ShellRoute _chatRoute = ShellRoute( - builder: (context, state, child) => BasicShell( - state: state, - sidebarFirst: true, - showAppBar: false, - sidebar: const ChatScreen(), - child: child, - ), + builder: (context, state, child) => child, routes: [ GoRoute( path: '/chat', name: 'chat', - builder: (context, state) => SolianTheme.isExtraLargeScreen(context) - ? const EmptyPagePlaceholder() - : const ChatScreen(), + builder: (context, state) => const ChatScreen(), ), GoRoute( path: '/chat/organize', @@ -170,20 +162,12 @@ abstract class AppRouter { ); static final ShellRoute _realmRoute = ShellRoute( - builder: (context, state, child) => BasicShell( - state: state, - sidebarFirst: true, - showAppBar: false, - sidebar: const RealmListScreen(), - child: child, - ), + builder: (context, state, child) => child, routes: [ GoRoute( path: '/realms', name: 'realms', - builder: (context, state) => SolianTheme.isExtraLargeScreen(context) - ? const EmptyPagePlaceholder() - : const RealmListScreen(), + builder: (context, state) => const RealmListScreen(), ), GoRoute( path: '/realms/:alias/detail', @@ -219,7 +203,7 @@ abstract class AppRouter { ); static final ShellRoute _accountRoute = ShellRoute( - builder: (context, state, child) => BasicShell( + builder: (context, state, child) => SidebarShell( state: state, sidebarFirst: true, showAppBar: false, diff --git a/lib/screens/chat.dart b/lib/screens/chat.dart index 9af84b2..f615175 100644 --- a/lib/screens/chat.dart +++ b/lib/screens/chat.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; -import 'package:solian/models/channel.dart'; +import 'package:solian/exts.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/channel.dart'; import 'package:solian/router.dart'; @@ -12,6 +12,7 @@ import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/chat/call/chat_call_indicator.dart'; import 'package:solian/widgets/current_state_action.dart'; +import 'package:solian/widgets/sized_container.dart'; class ChatScreen extends StatefulWidget { const ChatScreen({super.key}); @@ -21,44 +22,17 @@ class ChatScreen extends StatefulWidget { } class _ChatScreenState extends State { - bool _isBusy = true; - int? _accountId; - - final List _channels = List.empty(growable: true); - - getProfile() async { - final AuthProvider auth = Get.find(); - if (!await auth.isAuthorized) return; - - final prof = await auth.getProfile(); - _accountId = prof.body['id']; - } - - getChannels() async { - final AuthProvider auth = Get.find(); - if (!await auth.isAuthorized) return; - - setState(() => _isBusy = true); - - final ChannelProvider provider = Get.find(); - final resp = await provider.listAvailableChannel(); - - setState(() { - _channels.clear(); - _channels.addAll( - resp.body.map((e) => Channel.fromJson(e)).toList().cast(), - ); - }); - - setState(() => _isBusy = false); - } + late final ChannelProvider _channels; @override void initState() { super.initState(); - - getProfile(); - getChannels(); + try { + _channels = Get.find(); + _channels.refreshAvailableChannel(); + } catch (e) { + context.showErrorDialog(e); + } } @override @@ -67,144 +41,103 @@ class _ChatScreenState extends State { return Material( color: Theme.of(context).colorScheme.surface, - child: FutureBuilder( - future: auth.isAuthorized, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (snapshot.data == false) { - return SigninRequiredOverlay( - onSignedIn: () { - getChannels(); - }, - ); - } + child: Scaffold( + appBar: AppBar( + title: AppBarTitle('chat'.tr), + centerTitle: false, + toolbarHeight: SolianTheme.toolbarHeight(context), + actions: [ + const BackgroundStateWidget(), + const NotificationButton(), + PopupMenuButton( + icon: const Icon(Icons.add_circle), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + child: ListTile( + title: Text('channelOrganizeCommon'.tr), + leading: const Icon(Icons.tag), + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + ), + onTap: () { + AppRouter.instance.pushNamed('channelOrganizing').then( + (value) { + if (value != null) { + _channels.refreshAvailableChannel(); + } + }, + ); + }, + ), + PopupMenuItem( + child: ListTile( + title: Text('channelOrganizeDirect'.tr), + leading: const FaIcon( + FontAwesomeIcons.userGroup, + size: 16, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + ), + onTap: () { + final ChannelProvider provider = Get.find(); + provider + .createDirectChannel(context, 'global') + .then((resp) { + if (resp != null) { + _channels.refreshAvailableChannel(); + } + }); + }, + ), + ], + ), + SizedBox( + width: SolianTheme.isLargeScreen(context) ? 8 : 16, + ), + ], + ), + body: FutureBuilder( + future: auth.getProfileWithCheck(), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (snapshot.data == null) { + return SigninRequiredOverlay( + onSignedIn: () => _channels.refreshAvailableChannel(), + ); + } - return DefaultTabController( - length: 2, - child: NestedScrollView( - headerSliverBuilder: (context, innerBoxIsScrolled) { - return [ - SliverOverlapAbsorber( - handle: NestedScrollView.sliverOverlapAbsorberHandleFor( - context), - sliver: SliverAppBar( - title: AppBarTitle('chat'.tr), - centerTitle: false, - floating: true, - toolbarHeight: SolianTheme.toolbarHeight(context), - actions: [ - const BackgroundStateWidget(), - const NotificationButton(), - PopupMenuButton( - icon: const Icon(Icons.add_circle), - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - child: ListTile( - title: Text('channelOrganizeCommon'.tr), - leading: const Icon(Icons.tag), - contentPadding: - const EdgeInsets.symmetric(horizontal: 8), - ), - onTap: () { - AppRouter.instance - .pushNamed('channelOrganizing') - .then( - (value) { - if (value != null) getChannels(); - }, - ); - }, - ), - PopupMenuItem( - child: ListTile( - title: Text('channelOrganizeDirect'.tr), - leading: const FaIcon( - FontAwesomeIcons.userGroup, - size: 16, - ), - contentPadding: - const EdgeInsets.symmetric(horizontal: 8), - ), - onTap: () { - final ChannelProvider provider = Get.find(); - provider - .createDirectChannel(context, 'global') - .then((resp) { - if (resp != null) { - getChannels(); - } - }); - }, - ), - ], + final selfId = snapshot.data!.body['id']; + + return Column( + children: [ + Obx(() { + if (_channels.isLoading.isFalse) { + return const SizedBox(); + } else { + return const LinearProgressIndicator(); + } + }), + const ChatCallCurrentIndicator(), + Expanded( + child: CenteredContainer( + child: RefreshIndicator( + onRefresh: _channels.refreshAvailableChannel, + child: Obx( + () => ChannelListWidget( + noCategory: true, + channels: _channels.directChannels, + selfId: selfId, ), - SizedBox( - width: SolianTheme.isLargeScreen(context) ? 8 : 16, - ), - ], - bottom: TabBar( - tabs: [ - Tab( - icon: const Icon(Icons.tag), - text: 'channels'.tr, - ), - Tab( - icon: const Icon(Icons.chat), - text: 'channelCategoryDirect'.tr, - ), - ], ), ), ), - ]; - }, - body: Builder(builder: (context) { - if (_isBusy) { - return const Center(child: CircularProgressIndicator()); - } - - return TabBarView( - children: [ - Column( - children: [ - const ChatCallCurrentIndicator(), - Expanded( - child: RefreshIndicator( - onRefresh: () => getChannels(), - child: ChannelListWidget( - channels: - _channels.where((x) => x.type == 0).toList(), - selfId: _accountId ?? 0, - ), - ), - ), - ], - ), - Column( - children: [ - const ChatCallCurrentIndicator(), - Expanded( - child: RefreshIndicator( - onRefresh: () => getChannels(), - child: ChannelListWidget( - channels: - _channels.where((x) => x.type == 1).toList(), - selfId: _accountId ?? 0, - noCategory: true, - ), - ), - ), - ], - ), - ], - ); - }), - ), - ); - }, + ), + ], + ); + }, + ), ), ); } diff --git a/lib/screens/realms.dart b/lib/screens/realms.dart index 8cc8bb1..6680dae 100644 --- a/lib/screens/realms.dart +++ b/lib/screens/realms.dart @@ -11,6 +11,7 @@ import 'package:solian/theme.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart'; import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/current_state_action.dart'; +import 'package:solian/widgets/sized_container.dart'; class RealmListScreen extends StatefulWidget { const RealmListScreen({super.key}); @@ -56,63 +57,65 @@ class _RealmListScreenState extends State { return Material( color: Theme.of(context).colorScheme.surface, - child: FutureBuilder( - future: auth.isAuthorized, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator(), - ); - } else if (snapshot.data == false) { - return SigninRequiredOverlay( - onSignedIn: () { - getRealms(); - }, - ); - } - - return RefreshIndicator( - onRefresh: () => getRealms(), - child: CustomScrollView( - slivers: [ - SliverAppBar( - title: AppBarTitle('realm'.tr), - centerTitle: false, - floating: true, - toolbarHeight: SolianTheme.toolbarHeight(context), - actions: [ - const BackgroundStateWidget(), - const NotificationButton(), - IconButton( - icon: const Icon(Icons.add_circle), - onPressed: () { - AppRouter.instance.pushNamed('realmOrganizing').then( - (value) { - if (value != null) getRealms(); - }, - ); - }, - ), - SizedBox( - width: SolianTheme.isLargeScreen(context) ? 8 : 16, - ), - ], - ), - if (_isBusy) - SliverToBoxAdapter( - child: const LinearProgressIndicator().animate().scaleX(), - ), - SliverList.builder( - itemCount: _realms.length, - itemBuilder: (context, index) { - final element = _realms[index]; - return buildRealm(element); + child: Scaffold( + appBar: AppBar( + title: AppBarTitle('realm'.tr), + centerTitle: false, + toolbarHeight: SolianTheme.toolbarHeight(context), + actions: [ + const BackgroundStateWidget(), + const NotificationButton(), + IconButton( + icon: const Icon(Icons.add_circle), + onPressed: () { + AppRouter.instance.pushNamed('realmOrganizing').then( + (value) { + if (value != null) getRealms(); }, - ) - ], + ); + }, ), - ); - }, + SizedBox( + width: SolianTheme.isLargeScreen(context) ? 8 : 16, + ), + ], + ), + body: FutureBuilder( + future: auth.isAuthorized, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator(), + ); + } else if (snapshot.data == false) { + return SigninRequiredOverlay( + onSignedIn: () { + getRealms(); + }, + ); + } + + return Column( + children: [ + if (_isBusy) const LinearProgressIndicator().animate().scaleX(), + Expanded( + child: CenteredContainer( + child: RefreshIndicator( + onRefresh: () => getRealms(), + child: ListView.builder( + itemCount: _realms.length, + itemBuilder: (context, index) { + final element = _realms[index]; + return buildRealm(element); + }, + ), + ), + ), + ), + ], + ); + }, + ), ), ); } diff --git a/lib/shells/basic_shell.dart b/lib/shells/sidebar_shell.dart similarity index 95% rename from lib/shells/basic_shell.dart rename to lib/shells/sidebar_shell.dart index 6657589..2f307b4 100644 --- a/lib/shells/basic_shell.dart +++ b/lib/shells/sidebar_shell.dart @@ -5,7 +5,7 @@ import 'package:solian/theme.dart'; import 'package:solian/widgets/app_bar_title.dart'; import 'package:solian/widgets/sidebar/sidebar_placeholder.dart'; -class BasicShell extends StatelessWidget { +class SidebarShell extends StatelessWidget { final bool showAppBar; final GoRouterState state; final Widget child; @@ -13,7 +13,7 @@ class BasicShell extends StatelessWidget { final bool sidebarFirst; final Widget? sidebar; - const BasicShell({ + const SidebarShell({ super.key, required this.child, required this.state, diff --git a/lib/widgets/channel/channel_list.dart b/lib/widgets/channel/channel_list.dart index cd4280f..12a8154 100644 --- a/lib/widgets/channel/channel_list.dart +++ b/lib/widgets/channel/channel_list.dart @@ -8,13 +8,17 @@ import 'package:solian/widgets/account/account_avatar.dart'; class ChannelListWidget extends StatefulWidget { final List channels; final int selfId; + final bool isDense; final bool noCategory; + final bool useReplace; const ChannelListWidget({ super.key, required this.channels, required this.selfId, + this.isDense = false, this.noCategory = false, + this.useReplace = false, }); @override @@ -48,68 +52,87 @@ class _ChannelListWidgetState extends State { @override void didUpdateWidget(covariant ChannelListWidget oldWidget) { - setState(() => mapChannels()); super.didUpdateWidget(oldWidget); + setState(() => mapChannels()); } @override void initState() { - mapChannels(); super.initState(); + mapChannels(); } - Widget buildItem(Channel element) { - if (element.type == 1) { - final otherside = element.members! + void gotoChannel(Channel item) { + if (widget.useReplace) { + AppRouter.instance.pushReplacementNamed( + 'channelChat', + pathParameters: {'alias': item.alias}, + queryParameters: { + if (item.realmId != null) 'realm': item.realm!.alias, + }, + ); + } else { + AppRouter.instance.pushNamed( + 'channelChat', + pathParameters: {'alias': item.alias}, + queryParameters: { + if (item.realmId != null) 'realm': item.realm!.alias, + }, + ); + } + } + + Widget buildItem(Channel item) { + final padding = widget.isDense + ? const EdgeInsets.symmetric(horizontal: 20) + : const EdgeInsets.symmetric(horizontal: 24); + + if (item.type == 1) { + final otherside = item.members! .where((e) => e.account.externalId != widget.selfId) .first; return ListTile( leading: AccountAvatar( content: otherside.account.avatar, + radius: widget.isDense ? 12 : 24, bgColor: Colors.indigo, feColor: Colors.white, ), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), + contentPadding: padding, title: Text(otherside.account.nick), - subtitle: Text( - 'channelDirectDescription' - .trParams({'username': '@${otherside.account.name}'}), + subtitle: !widget.isDense + ? Text( + 'channelDirectDescription'.trParams( + {'username': '@${otherside.account.name}'}, + ), + ) + : null, + onTap: () => gotoChannel(item), + ); + } else { + return ListTile( + minTileHeight: item.realmId == null + ? 48 + : widget.isDense + ? 24 + : null, + leading: CircleAvatar( + backgroundColor: + item.realmId == null ? Colors.indigo : Colors.transparent, + radius: widget.isDense ? 12 : 24, + child: FaIcon( + FontAwesomeIcons.hashtag, + color: item.realmId == null ? Colors.white : Colors.indigo, + size: widget.isDense ? 12 : 16, + ), ), - onTap: () { - AppRouter.instance.pushNamed( - 'channelChat', - pathParameters: {'alias': element.alias}, - queryParameters: { - if (element.realmId != null) 'realm': element.realm!.alias, - }, - ); - }, + contentPadding: padding, + title: Text(item.name), + subtitle: !widget.isDense ? Text(item.description) : null, + onTap: () => gotoChannel(item), ); } - - return ListTile( - leading: const CircleAvatar( - backgroundColor: Colors.indigo, - child: FaIcon( - FontAwesomeIcons.hashtag, - color: Colors.white, - size: 16, - ), - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - title: Text(element.name), - subtitle: Text(element.description), - onTap: () { - AppRouter.instance.pushNamed( - 'channelChat', - pathParameters: {'alias': element.alias}, - queryParameters: { - if (element.realmId != null) 'realm': element.realm!.alias, - }, - ); - }, - ); } @override @@ -137,25 +160,16 @@ class _ChannelListWidgetState extends State { return buildItem(element); }, ), - ..._inRealms.entries.map((element) { - return SliverList.builder( - itemCount: element.value.length + 1, - itemBuilder: (context, index) { - if (index == 0) { - return ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 28), - leading: const Icon(Icons.workspaces, size: 20) - .paddingOnly(left: 6, right: 10), - tileColor: Theme.of(context).colorScheme.surfaceContainerHigh, - title: Text(element.value.first.realm!.name), - ); - } - - final item = element.value[index - 1]; - return buildItem(item); - }, - ); - }), + SliverList.list( + children: _inRealms.entries.map((element) { + return ExpansionTile( + tilePadding: const EdgeInsets.symmetric(horizontal: 24), + minTileHeight: 48, + title: Text(element.value.first.realm!.name), + children: element.value.map((x) => buildItem(x)).toList(), + ); + }).toList(), + ), ], ); } diff --git a/lib/widgets/navigation/app_navigation_drawer.dart b/lib/widgets/navigation/app_navigation_drawer.dart index 46283b5..12d0ad3 100644 --- a/lib/widgets/navigation/app_navigation_drawer.dart +++ b/lib/widgets/navigation/app_navigation_drawer.dart @@ -3,10 +3,12 @@ import 'package:get/get.dart'; import 'package:solian/models/account_status.dart'; import 'package:solian/providers/account_status.dart'; import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/content/channel.dart'; import 'package:solian/router.dart'; import 'package:solian/shells/root_shell.dart'; import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/account_status_action.dart'; +import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/navigation/app_navigation.dart'; import 'package:badges/badges.dart' as badges; @@ -21,8 +23,12 @@ class AppNavigationDrawer extends StatefulWidget { class _AppNavigationDrawerState extends State { int? _selectedIndex = 0; + AccountStatus? _accountStatus; + late final AuthProvider _auth; + late final ChannelProvider _channels; + void getStatus() async { final StatusProvider provider = Get.find(); @@ -50,6 +56,8 @@ class _AppNavigationDrawerState extends State { @override void initState() { super.initState(); + _auth = Get.find(); + _channels = Get.find(); detectSelectedIndex(); getStatus(); } @@ -145,7 +153,38 @@ class _AppNavigationDrawerState extends State { ), const Divider(thickness: 0.3, height: 1).paddingOnly( top: 12, - bottom: 8, + ), + FutureBuilder( + future: _auth.getProfileWithCheck(), + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data == null) { + return const SizedBox(); + } + + final selfId = snapshot.data!.body['id']; + + return Column( + children: [ + ExpansionTile( + title: Text('chat'.tr), + tilePadding: const EdgeInsets.symmetric(horizontal: 24), + children: [ + Obx( + () => SizedBox( + height: 360, + child: ChannelListWidget( + channels: _channels.groupChannels, + selfId: selfId, + isDense: true, + useReplace: true, + ), + ), + ), + ], + ), + ], + ); + }, ), ], );