From cd08e658408e3ea0aea2cc2f0c527e02883bf833 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Wed, 29 May 2024 23:22:24 +0800 Subject: [PATCH] :sparkles: Realm channels --- lib/providers/content/channel.dart | 16 ++++ lib/router.dart | 2 +- lib/screens/channel/channel_organize.dart | 39 +++++++--- lib/screens/contact.dart | 64 +--------------- lib/screens/realms/realm_view.dart | 91 ++++++++++++++++++++++- lib/translations.dart | 6 ++ lib/widgets/channel/channel_list.dart | 76 +++++++++++++++++++ 7 files changed, 216 insertions(+), 78 deletions(-) create mode 100644 lib/widgets/channel/channel_list.dart diff --git a/lib/providers/content/channel.dart b/lib/providers/content/channel.dart index b927607..c5b84ee 100644 --- a/lib/providers/content/channel.dart +++ b/lib/providers/content/channel.dart @@ -21,6 +21,22 @@ class ChannelProvider extends GetxController { return resp; } + Future listChannel({String scope = 'global'}) async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) throw Exception('unauthorized'); + + final client = GetConnect(maxAuthRetries: 3); + client.httpClient.baseUrl = ServiceFinder.services['messaging']; + client.httpClient.addAuthenticator(auth.requestAuthenticator); + + final resp = await client.get('/api/channels/$scope'); + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); + } + + return resp; + } + Future listAvailableChannel({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 f161b6f..04d43bc 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -110,7 +110,7 @@ abstract class AppRouter { final arguments = state.extra as ChannelOrganizeArguments?; return ChannelOrganizeScreen( edit: arguments?.edit, - realm: state.uri.queryParameters['realm'], + realm: arguments?.realm, ); }, ), diff --git a/lib/screens/channel/channel_organize.dart b/lib/screens/channel/channel_organize.dart index 81796e1..de00bf8 100644 --- a/lib/screens/channel/channel_organize.dart +++ b/lib/screens/channel/channel_organize.dart @@ -3,6 +3,7 @@ import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'package:solian/exts.dart'; import 'package:solian/models/channel.dart'; +import 'package:solian/models/realm.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/channel.dart'; import 'package:solian/router.dart'; @@ -12,13 +13,14 @@ import 'package:uuid/uuid.dart'; class ChannelOrganizeArguments { final Channel? edit; + final Realm? realm; - ChannelOrganizeArguments({this.edit}); + ChannelOrganizeArguments({this.edit, this.realm}); } class ChannelOrganizeScreen extends StatefulWidget { final Channel? edit; - final String? realm; + final Realm? realm; const ChannelOrganizeScreen({super.key, this.edit, this.realm}); @@ -49,7 +51,7 @@ class _ChannelOrganizeScreenState extends State { client.httpClient.baseUrl = ServiceFinder.services['messaging']; client.httpClient.addAuthenticator(auth.requestAuthenticator); - final scope = (widget.realm?.isNotEmpty ?? false) ? widget.realm : 'global'; + final scope = widget.realm != null ? widget.realm!.alias : 'global'; final payload = { 'alias': _aliasController.value.text.toLowerCase(), 'name': _nameController.value.text, @@ -60,9 +62,9 @@ class _ChannelOrganizeScreenState extends State { Response? resp; try { if (widget.edit != null) { - resp = await provider.updateChannel(scope!, widget.edit!.id, payload); + resp = await provider.updateChannel(scope, widget.edit!.id, payload); } else { - resp = await provider.createChannel(scope!, payload); + resp = await provider.createChannel(scope, payload); } } catch (e) { context.showErrorDialog(e); @@ -99,6 +101,13 @@ class _ChannelOrganizeScreenState extends State { @override Widget build(BuildContext context) { + final notifyBannerActions = [ + TextButton( + onPressed: cancelAction, + child: Text('cancel'.tr), + ), + ]; + return Material( color: Theme.of(context).colorScheme.surface, child: Scaffold( @@ -126,13 +135,19 @@ class _ChannelOrganizeScreenState extends State { 'channelEditingNotify' .trParams({'channel': '#${widget.edit!.alias}'}), ), - actions: [ - TextButton( - onPressed: cancelAction, - child: Text('cancel'.tr), - ), - ], - ), + actions: notifyBannerActions, + ).paddingOnly(bottom: 6), + if (widget.realm != null) + MaterialBanner( + leading: const Icon(Icons.group), + leadingPadding: const EdgeInsets.only(left: 10, right: 20), + dividerColor: Colors.transparent, + content: Text( + 'channelInRealmNotify' + .trParams({'realm': '#${widget.realm!.alias}'}), + ), + actions: notifyBannerActions, + ).paddingOnly(bottom: 6), Row( children: [ Expanded( diff --git a/lib/screens/contact.dart b/lib/screens/contact.dart index 985b837..008354a 100644 --- a/lib/screens/contact.dart +++ b/lib/screens/contact.dart @@ -8,8 +8,8 @@ import 'package:solian/providers/content/channel.dart'; import 'package:solian/router.dart'; import 'package:solian/screens/account/notification.dart'; import 'package:solian/theme.dart'; -import 'package:solian/widgets/account/account_avatar.dart'; import 'package:solian/widgets/account/signin_required_overlay.dart'; +import 'package:solian/widgets/channel/channel_list.dart'; class ContactScreen extends StatefulWidget { const ContactScreen({super.key}); @@ -137,13 +137,7 @@ class _ContactScreenState extends State { SliverToBoxAdapter( child: const LinearProgressIndicator().animate().scaleX(), ), - SliverList.builder( - itemCount: _channels.length, - itemBuilder: (context, index) { - final element = _channels[index]; - return buildItem(element); - }, - ), + ChannelListWidget(channels: _channels, selfId: _accountId ?? 0), ], ), ); @@ -151,58 +145,4 @@ class _ContactScreenState extends State { ), ); } - - Widget buildItem(Channel element) { - if (element.type == 1) { - final otherside = element.members! - .where((e) => e.account.externalId != _accountId) - .first; - - return ListTile( - leading: AccountAvatar( - content: otherside.account.avatar, - bgColor: Colors.indigo, - feColor: Colors.white, - ), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), - title: Text(otherside.account.nick), - subtitle: Text( - 'channelDirectDescription' - .trParams({'username': '@${otherside.account.name}'}), - ), - onTap: () { - AppRouter.instance.pushNamed( - 'channelChat', - pathParameters: {'alias': element.alias}, - queryParameters: { - if (element.realmId != null) 'realm': element.realm!.alias, - }, - ); - }, - ); - } - - 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, - }, - ); - }, - ); - } } diff --git a/lib/screens/realms/realm_view.dart b/lib/screens/realms/realm_view.dart index f84e513..f59a14c 100644 --- a/lib/screens/realms/realm_view.dart +++ b/lib/screens/realms/realm_view.dart @@ -2,14 +2,19 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:solian/exts.dart'; +import 'package:solian/models/channel.dart'; import 'package:solian/models/pagination.dart'; import 'package:solian/models/post.dart'; import 'package:solian/models/realm.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/providers/content/channel.dart'; import 'package:solian/providers/content/post.dart'; import 'package:solian/providers/content/realm.dart'; import 'package:solian/router.dart'; +import 'package:solian/screens/channel/channel_organize.dart'; import 'package:solian/screens/posts/post_publish.dart'; import 'package:solian/theme.dart'; +import 'package:solian/widgets/channel/channel_list.dart'; import 'package:solian/widgets/posts/post_list.dart'; class RealmViewScreen extends StatefulWidget { @@ -26,6 +31,7 @@ class _RealmViewScreenState extends State { String? _overrideAlias; Realm? _realm; + final List _channels = List.empty(growable: true); getRealm({String? overrideAlias}) async { final RealmProvider provider = Get.find(); @@ -46,11 +52,29 @@ class _RealmViewScreenState extends State { setState(() => _isBusy = false); } + getChannels() async { + setState(() => _isBusy = true); + + final ChannelProvider provider = Get.find(); + final resp = await provider.listChannel(scope: _realm!.alias); + + setState(() { + _channels.clear(); + _channels.addAll( + resp.body.map((e) => Channel.fromJson(e)).toList().cast(), + ); + }); + + setState(() => _isBusy = false); + } + @override void initState() { super.initState(); - getRealm(); + getRealm().then((_) { + getChannels(); + }); } @override @@ -113,7 +137,11 @@ class _RealmViewScreenState extends State { return TabBarView( children: [ RealmPostListWidget(realm: _realm!), - Icon(Icons.directions_transit), + RealmChannelListWidget( + realm: _realm!, + channels: _channels, + onRefresh: () => getChannels(), + ), ], ); }, @@ -174,7 +202,7 @@ class _RealmPostListWidgetState extends State { SliverToBoxAdapter( child: ListTile( leading: const Icon(Icons.post_add), - contentPadding: const EdgeInsets.symmetric(horizontal: 24), + contentPadding: const EdgeInsets.only(left: 24, right: 8), tileColor: Theme.of(context).colorScheme.surfaceContainer, title: Text('postNew'.tr), subtitle: Text( @@ -199,3 +227,60 @@ class _RealmPostListWidgetState extends State { ); } } + +class RealmChannelListWidget extends StatelessWidget { + final Realm realm; + final List channels; + final Future Function() onRefresh; + + const RealmChannelListWidget({ + super.key, + required this.realm, + required this.channels, + required this.onRefresh, + }); + + @override + Widget build(BuildContext context) { + final AuthProvider auth = Get.find(); + + return FutureBuilder( + future: auth.getProfile(), + builder: (context, snapshot) { + return RefreshIndicator( + onRefresh: onRefresh, + child: CustomScrollView( + slivers: [ + SliverToBoxAdapter( + child: ListTile( + leading: const Icon(Icons.add_box), + contentPadding: const EdgeInsets.only(left: 32, right: 8), + tileColor: Theme.of(context).colorScheme.surfaceContainer, + title: Text('channelNew'.tr), + subtitle: Text( + 'channelNewInRealmHint' + .trParams({'realm': '#${realm.alias}'}), + ), + onTap: () { + AppRouter.instance + .pushNamed( + 'channelOrganizing', + extra: ChannelOrganizeArguments(realm: realm), + ) + .then((value) { + if (value != null) onRefresh(); + }); + }, + ), + ), + ChannelListWidget( + channels: channels, + selfId: snapshot.data?.body['id'] ?? 0, + ) + ], + ), + ); + }, + ); + } +} diff --git a/lib/translations.dart b/lib/translations.dart index 835f592..5db7692 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -108,10 +108,13 @@ class SolianMessages extends Translations { 'realmPublic': 'Public Realm', 'realmCommunity': 'Community Realm', 'realmDetail': 'Realm detail', + 'channelNew': 'Create a new channel', + 'channelNewInRealmHint': 'Create channel in realm @realm', 'channelOrganizing': 'Organize a channel', 'channelOrganizeCommon': 'Create regular channel', 'channelOrganizeDirect': 'Create DM', 'channelOrganizeDirectHint': 'Choose friend to create DM', + 'channelInRealmNotify': 'You\'re creating channel in realm @realm', 'channelEditingNotify': 'You\'re editing channel @channel', 'channelAlias': 'Alias (Identifier)', 'channelName': 'Name', @@ -233,10 +236,13 @@ class SolianMessages extends Translations { 'realmPublic': '公开领域', 'realmCommunity': '社区领域', 'realmDetail': '领域详情', + 'channelNew': '创建新频道', + 'channelNewInRealmHint': '在领域 @realm 里创建新频道', 'channelOrganizing': '组织频道', 'channelOrganizeCommon': '创建普通频道', 'channelOrganizeDirect': '创建私信频道', 'channelOrganizeDirectHint': '选择好友来创建私信', + 'channelInRealmNotify': '你正在领域 @realm 中创建频道', 'channelEditingNotify': '你正在编辑频道 @channel', 'channelAlias': '别称(标识符)', 'channelName': '显示名称', diff --git a/lib/widgets/channel/channel_list.dart b/lib/widgets/channel/channel_list.dart new file mode 100644 index 0000000..3a8e9de --- /dev/null +++ b/lib/widgets/channel/channel_list.dart @@ -0,0 +1,76 @@ +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/router.dart'; +import 'package:solian/widgets/account/account_avatar.dart'; + +class ChannelListWidget extends StatelessWidget { + final List channels; + final int selfId; + + const ChannelListWidget( + {super.key, required this.channels, required this.selfId}); + + @override + Widget build(BuildContext context) { + return SliverList.builder( + itemCount: channels.length, + itemBuilder: (context, index) { + final element = channels[index]; + + if (element.type == 1) { + final otherside = element.members! + .where((e) => e.account.externalId != selfId) + .first; + + return ListTile( + leading: AccountAvatar( + content: otherside.account.avatar, + bgColor: Colors.indigo, + feColor: Colors.white, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + title: Text(otherside.account.nick), + subtitle: Text( + 'channelDirectDescription' + .trParams({'username': '@${otherside.account.name}'}), + ), + onTap: () { + AppRouter.instance.pushNamed( + 'channelChat', + pathParameters: {'alias': element.alias}, + queryParameters: { + if (element.realmId != null) 'realm': element.realm!.alias, + }, + ); + }, + ); + } + + 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, + }, + ); + }, + ); + }, + ); + } +}