From b10163575f2d6e346b3358181d8448360052c9e1 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Wed, 15 Apr 2020 18:06:20 +0200 Subject: [PATCH] Start to build groups page --- lib/helpers/groups_helper.dart | 63 +++++++++++ lib/models/advanced_group_info.dart | 43 +++++++ lib/models/group.dart | 5 + lib/ui/routes/home_route.dart | 31 ++++- .../screens/group_access_denied_screen.dart | 106 ++++++++++++++++++ lib/ui/screens/group_screen.dart | 80 +++++++++++++ lib/ui/screens/groups_list_screen.dart | 4 +- lib/ui/widgets/group_membership_widget.dart | 26 +++-- lib/ui/widgets/navbar_widget.dart | 1 + 9 files changed, 347 insertions(+), 12 deletions(-) create mode 100644 lib/models/advanced_group_info.dart create mode 100644 lib/ui/screens/group_access_denied_screen.dart create mode 100644 lib/ui/screens/group_screen.dart diff --git a/lib/helpers/groups_helper.dart b/lib/helpers/groups_helper.dart index 26b9706..3852a82 100644 --- a/lib/helpers/groups_helper.dart +++ b/lib/helpers/groups_helper.dart @@ -1,4 +1,5 @@ import 'package:comunic/lists/groups_list.dart'; +import 'package:comunic/models/advanced_group_info.dart'; import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/group.dart'; import 'package:comunic/utils/api_utils.dart'; @@ -35,6 +36,17 @@ const _APIGroupsPostsCreationLevelsMap = { final _groupsListCache = GroupsList(); +/// Callback for getting advanced user information +enum GetAdvancedInfoStatus { SUCCESS, ACCESS_DENIED } + +class GetAdvancedInfoResult { + final GetAdvancedInfoStatus status; + final AdvancedGroupInfo info; + + GetAdvancedInfoResult(this.status, this.info) : assert(status != null); +} + +/// Groups helper class GroupsHelper { /// Download a list of groups information from the server Future _downloadList(Set groups) async { @@ -92,6 +104,15 @@ class GroupsHelper { return list; } + /// Get information about a single group + /// + /// Throws in case of failure + Future getSingle(int groupID, {bool force = false}) async { + return (await getListOrThrow(Set()..add(groupID), force: force)) + .values + .first; + } + /// Get the list of groups of a user Future> getListUser() async => (await APIRequest(uri: "groups/get_my_list", needLogin: true).exec()) @@ -127,6 +148,27 @@ class GroupsHelper { "accept": accept ? "true" : "false", }); + /// Get advanced information about the user + Future getAdvancedInfo(int groupID) async { + // Get advanced information about the user + final result = + await (APIRequest(uri: "groups/get_advanced_info", needLogin: true) + ..addInt("id", groupID)) + .exec(); + + switch (result.code) { + case 401: + return GetAdvancedInfoResult(GetAdvancedInfoStatus.ACCESS_DENIED, null); + + case 200: + return GetAdvancedInfoResult(GetAdvancedInfoStatus.SUCCESS, + _getAdvancedGroupInfoFromAPI(result.getObject())); + + default: + throw Exception("Could not get advanced group information!"); + } + } + /// Turn an API entry into a group object Group _getGroupFromAPI(Map map) { return Group( @@ -142,4 +184,25 @@ class GroupsHelper { virtualDirectory: map["virtual_directory"], following: map["following"]); } + + /// Get advanced group information + AdvancedGroupInfo _getAdvancedGroupInfoFromAPI(Map map) => + AdvancedGroupInfo( + id: map["id"], + name: map["name"], + iconURL: map["icon_url"], + numberMembers: map["number_members"], + membershipLevel: _APIGroupsMembershipLevelsMap[map["membership"]], + visibilityLevel: _APIGroupsVisibilityLevelsMap[map["visibility"]], + registrationLevel: + _APIGroupsRegistrationLevelsMap[map["registration_level"]], + postCreationLevel: _APIGroupsPostsCreationLevelsMap[map["posts_level"]], + virtualDirectory: map["virtual_directory"], + following: map["following"], + timeCreate: map["time_create"], + description: map["description"], + url: map["url"], + numberLikes: map["number_likes"], + isLiking: map["is_liking"], + ); } diff --git a/lib/models/advanced_group_info.dart b/lib/models/advanced_group_info.dart new file mode 100644 index 0000000..2db177d --- /dev/null +++ b/lib/models/advanced_group_info.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'group.dart'; + +/// Advanced group information +/// +/// @author Pierre Hubert + +class AdvancedGroupInfo extends Group { + final int timeCreate; + final String description; + final String url; + final int numberLikes; + final bool isLiking; + + AdvancedGroupInfo({ + @required int id, + @required String name, + @required String iconURL, + @required int numberMembers, + @required GroupMembershipLevel membershipLevel, + @required GroupVisibilityLevel visibilityLevel, + @required GroupRegistrationLevel registrationLevel, + @required GroupPostCreationLevel postCreationLevel, + @required String virtualDirectory, + @required bool following, + @required this.timeCreate, + @required this.description, + @required this.url, + @required this.numberLikes, + @required this.isLiking, + }) : super( + id: id, + name: name, + iconURL: iconURL, + numberMembers: numberMembers, + membershipLevel: membershipLevel, + visibilityLevel: visibilityLevel, + registrationLevel: registrationLevel, + postCreationLevel: postCreationLevel, + virtualDirectory: virtualDirectory, + following: following); +} diff --git a/lib/models/group.dart b/lib/models/group.dart index a74a301..b3dfabc 100644 --- a/lib/models/group.dart +++ b/lib/models/group.dart @@ -53,4 +53,9 @@ class Group { assert(following != null); get displayName => this.name; + + bool get getIsAtLeastMember => + membershipLevel == GroupMembershipLevel.ADMINISTRATOR || + membershipLevel == GroupMembershipLevel.MODERATOR || + membershipLevel == GroupMembershipLevel.MEMBER; } diff --git a/lib/ui/routes/home_route.dart b/lib/ui/routes/home_route.dart index dc37917..49c9ae6 100644 --- a/lib/ui/routes/home_route.dart +++ b/lib/ui/routes/home_route.dart @@ -2,6 +2,7 @@ import 'package:comunic/helpers/account_helper.dart'; import 'package:comunic/ui/routes/app_settings_route.dart'; import 'package:comunic/ui/screens/conversations_list_screen.dart'; import 'package:comunic/ui/screens/friends_list_screen.dart'; +import 'package:comunic/ui/screens/group_screen.dart'; import 'package:comunic/ui/screens/groups_list_screen.dart'; import 'package:comunic/ui/screens/newest_posts.dart'; import 'package:comunic/ui/screens/notifications_screen.dart'; @@ -21,6 +22,10 @@ import 'login_route.dart'; class HomeRoute extends StatefulWidget { @override State createState() => _HomeRouteState(); + + /// Get current instance of Home controller + static HomeController of(BuildContext context) => + context.findAncestorStateOfType(); } class CurrPage { @@ -28,9 +33,24 @@ class CurrPage { final Map args; const CurrPage(this.action, {this.args}) : assert(action != null); + + @override + String toString() => + "CurrPage {\n\taction: " + + this.action.toString() + + "\n\targs: " + + this.args.toString() + + "\n}"; } -class _HomeRouteState extends State { +/// Public interface of home controller +abstract class HomeController extends State { + /// Open a specific group page specified by its [groupID] + void openGroup(int groupID); +} + +/// Private implementation of HomeController +class _HomeRouteState extends HomeController { CurrPage get _currTab => history.last; List history = List(); @@ -106,6 +126,9 @@ class _HomeRouteState extends State { case BarCallbackActions.OPEN_GROUPS: return GroupsListScreen(); + case BarCallbackActions.OPEN_GROUP_PAGE: + return GroupPageScreen(groupID: _currTab.args["groupID"]); + default: throw "Invalid tab : " + _currTab.toString(); } @@ -156,4 +179,10 @@ class _HomeRouteState extends State { return LoginRoute(); })); } + + @override + void openGroup(int groupID) { + _pushPage(CurrPage(BarCallbackActions.OPEN_GROUP_PAGE, + args: {"groupID": groupID})); + } } diff --git a/lib/ui/screens/group_access_denied_screen.dart b/lib/ui/screens/group_access_denied_screen.dart new file mode 100644 index 0000000..c0c1df0 --- /dev/null +++ b/lib/ui/screens/group_access_denied_screen.dart @@ -0,0 +1,106 @@ +import 'package:comunic/helpers/groups_helper.dart'; +import 'package:comunic/models/group.dart'; +import 'package:comunic/ui/widgets/group_icon_widget.dart'; +import 'package:comunic/ui/widgets/group_membership_widget.dart'; +import 'package:comunic/ui/widgets/safe_state.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; +import 'package:flutter/material.dart'; + +/// Group access denied screen +/// +/// @author Pierre Hubert + +class GroupAccessDeniedScreen extends StatefulWidget { + final int groupID; + final Function() onMembershipAcquired; + + const GroupAccessDeniedScreen({ + Key key, + @required this.groupID, + @required this.onMembershipAcquired, + }) : assert(groupID != null), + assert(onMembershipAcquired != null), + super(key: key); + + @override + _GroupAccessDeniedScreenState createState() => + _GroupAccessDeniedScreenState(); +} + +class _GroupAccessDeniedScreenState extends SafeState { + Group _group; + + bool error = false; + + @override + void initState() { + _refresh(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (error) + return buildErrorCard(tr("Could not get basic group information!"), + actions: [ + MaterialButton( + child: Text(tr("Try again").toUpperCase()), + onPressed: () => this._refresh(), + ) + ]); + + if (_group == null) return buildCenteredProgressBar(); + + return Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Spacer( + flex: 5, + ), + GroupIcon(group: _group), + Spacer(), + Text( + _group.displayName, + style: TextStyle(fontSize: 20), + ), + Spacer(), + Text(tr("A registration is required to access this group page.")), + Spacer(), + GroupMembershipWidget( + group: _group, + onUpdated: () => this._refresh(), + onError: () => this._refresh(), + ), + Spacer( + flex: 5, + ) + ], + ), + ), + ); + } + + /// Get basic information about the groups + Future _refresh() async { + try { + setState(() { + error = false; + _group = null; + }); + + // Get information about a single group + final group = await GroupsHelper().getSingle(widget.groupID, force: true); + + if (group.getIsAtLeastMember) widget.onMembershipAcquired(); + + setState(() => _group = group); + } catch (e) { + print(e); + setState(() => error = true); + } + } +} diff --git a/lib/ui/screens/group_screen.dart b/lib/ui/screens/group_screen.dart new file mode 100644 index 0000000..74dee54 --- /dev/null +++ b/lib/ui/screens/group_screen.dart @@ -0,0 +1,80 @@ +import 'package:comunic/helpers/groups_helper.dart'; +import 'package:comunic/ui/screens/group_access_denied_screen.dart'; +import 'package:comunic/ui/widgets/safe_state.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; +import 'package:flutter/material.dart'; + +/// Group screen +/// +/// @author Pierre Hubert + +class GroupPageScreen extends StatefulWidget { + final int groupID; + + const GroupPageScreen({Key key, @required this.groupID}) + : assert(groupID != null), + super(key: key); + + @override + _GroupPageScreenState createState() => _GroupPageScreenState(); +} + +class _GroupPageScreenState extends SafeState { + int get groupID => widget.groupID; + + GetAdvancedInfoResult _getGroupResult; + var error = false; + + @override + void initState() { + _refreshPage(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + if (error) + return buildErrorCard(tr("Could not load information about the group!"), + actions: [ + MaterialButton( + onPressed: () => _refreshPage(), + child: Text(tr("Try again").toUpperCase()), + ) + ]); + + // If we are still loading the page + if (_getGroupResult == null) return buildCenteredProgressBar(); + + // Check if access to the group was denied + if (_getGroupResult.status == GetAdvancedInfoStatus.ACCESS_DENIED) + return GroupAccessDeniedScreen( + groupID: groupID, + onMembershipAcquired: () => _refreshPage(), + ); + + // Create another screen for the group page + } + + /// Refresh the page + Future _refreshPage() async { + try { + setState(() { + error = false; + _getGroupResult = null; + }); + + // Get information about the group + final result = await GroupsHelper().getAdvancedInfo(groupID); + + setState(() { + _getGroupResult = result; + }); + } catch (e) { + print(e); + setState(() { + error = true; + }); + } + } +} diff --git a/lib/ui/screens/groups_list_screen.dart b/lib/ui/screens/groups_list_screen.dart index 08291fa..3391017 100644 --- a/lib/ui/screens/groups_list_screen.dart +++ b/lib/ui/screens/groups_list_screen.dart @@ -1,6 +1,7 @@ import 'package:comunic/helpers/groups_helper.dart'; import 'package:comunic/lists/groups_list.dart'; import 'package:comunic/models/group.dart'; +import 'package:comunic/ui/routes/home_route.dart'; import 'package:comunic/ui/widgets/group_icon_widget.dart'; import 'package:comunic/ui/widgets/group_membership_widget.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; @@ -40,7 +41,7 @@ class _GroupsListScreenState extends SafeState { hide: !_error, actions: [ MaterialButton( - child: Text(tr("Try again".toUpperCase())), + child: Text(tr("Try again").toUpperCase()), onPressed: () => _refreshList(), ), ], @@ -66,6 +67,7 @@ class _GroupsListScreenState extends SafeState { trailing: IconButton( icon: Icon(Icons.delete), onPressed: () => _deleteGroup(g)), + onTap: () => HomeRoute.of(context).openGroup(g.id), )) .toList(), ), diff --git a/lib/ui/widgets/group_membership_widget.dart b/lib/ui/widgets/group_membership_widget.dart index 0f69569..16c5824 100644 --- a/lib/ui/widgets/group_membership_widget.dart +++ b/lib/ui/widgets/group_membership_widget.dart @@ -13,11 +13,11 @@ import 'package:flutter/material.dart'; class GroupMembershipWidget extends StatefulWidget { final Group group; final Function() onUpdated; + final Function() onError; - const GroupMembershipWidget({ - @required this.group, - this.onUpdated, - }) : assert(group != null); + const GroupMembershipWidget( + {@required this.group, this.onUpdated, this.onError}) + : assert(group != null); @override _GroupMembershipWidgetState createState() => _GroupMembershipWidgetState(); @@ -50,6 +50,9 @@ class _GroupMembershipWidgetState extends SafeState { case GroupMembershipLevel.VISITOR: return _buildVisitorState(); + + default: + throw Exception("Unkonwn grou pmembership level state: $_level"); } } @@ -84,9 +87,10 @@ class _GroupMembershipWidgetState extends SafeState { message: tr("Do you really want to reject this invitation?"))) return; - if (!await GroupsHelper().respondInvitation(_id, accept)) + if (!await GroupsHelper().respondInvitation(_id, accept)) { showSimpleSnack(context, tr("Could not respond to your invitation!")); - else { + if (this.widget.onError != null) this.widget.onError(); + } else { // Refresh state group.membershipLevel = accept ? GroupMembershipLevel.MEMBER : GroupMembershipLevel.VISITOR; @@ -114,9 +118,10 @@ class _GroupMembershipWidgetState extends SafeState { /// Cancel group membership request void _cancelRequest() async { - if (!await GroupsHelper().cancelRequest(_id)) + if (!await GroupsHelper().cancelRequest(_id)) { showSimpleSnack(context, tr("Could not cancel your membership request!")); - else { + if (this.widget.onError != null) this.widget.onError(); + } else { // Refresh state group.membershipLevel = GroupMembershipLevel.VISITOR; this.setState(() {}); @@ -138,9 +143,10 @@ class _GroupMembershipWidgetState extends SafeState { /// Create new membership request void _requestMembership() async { - if (!await GroupsHelper().sendRequest(_id)) + if (!await GroupsHelper().sendRequest(_id)) { showSimpleSnack(context, tr("Could not send your membership request!")); - else { + if (this.widget.onError != null) this.widget.onError(); + } else { // Refresh state group.membershipLevel = GroupMembershipLevel.PENDING; this.setState(() {}); diff --git a/lib/ui/widgets/navbar_widget.dart b/lib/ui/widgets/navbar_widget.dart index 59437a1..1a85c24 100644 --- a/lib/ui/widgets/navbar_widget.dart +++ b/lib/ui/widgets/navbar_widget.dart @@ -16,6 +16,7 @@ enum BarCallbackActions { OPEN_FRIENDS, OPEN_MY_PAGE, OPEN_GROUPS, + OPEN_GROUP_PAGE, OPEN_APP_SETTINGS, NONE, ACTION_LOGOUT