From f92846cb76ccd2591703a0a264da2dae374016b8 Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Mon, 26 Apr 2021 09:33:54 +0200 Subject: [PATCH] Add user profile route --- .../ui/routes/forez_member_profile_route.dart | 175 ++++++++++++++++++ lib/forez/ui/routes/forez_route.dart | 9 +- .../ui/screens/forez_directory_screen.dart | 6 +- lib/helpers/forez_groups_helper.dart | 14 ++ lib/helpers/users_helper.dart | 55 +++--- lib/models/advanced_user_info.dart | 4 + lib/ui/widgets/async_screen_widget.dart | 29 ++- lib/ui/widgets/copy_icon.dart | 25 +++ 8 files changed, 278 insertions(+), 39 deletions(-) create mode 100644 lib/forez/ui/routes/forez_member_profile_route.dart create mode 100644 lib/ui/widgets/copy_icon.dart diff --git a/lib/forez/ui/routes/forez_member_profile_route.dart b/lib/forez/ui/routes/forez_member_profile_route.dart new file mode 100644 index 0000000..581ad55 --- /dev/null +++ b/lib/forez/ui/routes/forez_member_profile_route.dart @@ -0,0 +1,175 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:comunic/forez/helpers/forez_group_helper.dart'; +import 'package:comunic/helpers/forez_groups_helper.dart'; +import 'package:comunic/helpers/forez_presence_helper.dart'; +import 'package:comunic/lists/forez_presences_set.dart'; +import 'package:comunic/models/advanced_user_info.dart'; +import 'package:comunic/models/displayed_content.dart'; +import 'package:comunic/ui/widgets/async_screen_widget.dart'; +import 'package:comunic/ui/widgets/copy_icon.dart'; +import 'package:comunic/ui/widgets/forez_presence_calendar_widget.dart'; +import 'package:comunic/ui/widgets/text_widget.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +/// Show information about a Forez member +/// +/// @author Pierre Hubert + +class ForezMemberProfileRoute extends StatefulWidget { + final int userID; + + const ForezMemberProfileRoute({ + Key key, + @required this.userID, + }) : assert(userID != null), + super(key: key); + + @override + _ForezMemberProfileRouteState createState() => + _ForezMemberProfileRouteState(); +} + +class _ForezMemberProfileRouteState extends State { + final double _appBarHeight = 256.0; + + final _key = GlobalKey(); + + AdvancedUserInfo _user; + PresenceSet _presence; + + Future _load() async { + _user = await ForezGroupsHelper.getMemberInfo(forezGroup.id, widget.userID); + _presence = + await ForezPresenceHelper.getForUser(forezGroup.id, widget.userID); + } + + @override + Widget build(BuildContext context) => AsyncScreenWidget( + onReload: _load, + onBuild: _buildProfile, + loadingWidget: _buildLoading(), + errorWidget: _buildError(), + errorMessage: tr( + "Failed to load user information, maybe it is not a Forez member yet?")); + + Widget _buildLoading() => Scaffold( + appBar: AppBar( + title: Text(tr("Loading...")), + ), + body: buildCenteredProgressBar(), + ); + + Widget _buildError() => Scaffold( + appBar: AppBar( + title: Text(tr("Error")), + ), + body: buildErrorCard( + tr("Failed to load user information, maybe it is not a Forez member yet?"), + actions: [ + MaterialButton( + onPressed: () => _key.currentState.refresh(), + child: Text(tr("Try again")), + textColor: Colors.white, + ) + ]), + ); + + Widget _buildProfile() => Scaffold( + body: CustomScrollView( + slivers: [ + _buildAppBar(), + _buildList(), + ], + ), + ); + + Widget _buildAppBar() => SliverAppBar( + expandedHeight: _appBarHeight, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + title: Text(_user.fullName), + background: Stack( + fit: StackFit.expand, + children: [ + _user.accountImageURL == null + ? Container() + : CachedNetworkImage( + fit: BoxFit.cover, + imageUrl: _user.accountImageURL, + height: _appBarHeight, + ), + // This gradient ensures that the toolbar icons are distinct + // against the background image. + const DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment(0.0, -1.0), + end: Alignment(0.0, -0.4), + colors: [Color(0x60000000), Color(0x00000000)], + ), + ), + ), + ], + ), + ), + ); + + Widget _buildList() => SliverList( + delegate: SliverChildListDelegate([ + // Public note + !_user.hasPublicNote + ? Container() + : ListTile( + leading: Icon(Icons.note), + title: TextWidget(content: DisplayedString(_user.publicNote)), + subtitle: Text(tr("Note")), + ), + + // Email address + !_user.hasEmailAddress + ? Container() + : ListTile( + leading: Icon(Icons.email), + title: Text(_user.emailAddress), + subtitle: Text(tr("Email address")), + trailing: CopyIcon(_user.emailAddress), + ), + + // Location + !_user.hasLocation + ? Container() + : ListTile( + leading: Icon(Icons.location_on), + title: Text(_user.location), + subtitle: Text(tr("Location")), + trailing: CopyIcon(_user.location), + ), + + // Website + !_user.hasPersonalWebsite + ? Container() + : ListTile( + leading: Icon(Icons.link), + title: Text(_user.personalWebsite), + subtitle: Text(tr("Website")), + trailing: IconButton( + icon: Icon(Icons.open_in_new), + onPressed: () => launch(_user.personalWebsite), + ), + ), + + Divider(), + ListTile( + leading: Icon(Icons.calendar_today), + title: Text(tr("Presence in Forez")), + subtitle: Text(_presence.containsDate(DateTime.now()) + ? tr("Present today") + : tr("Absent")), + ), + PresenceCalendarWidget(presenceSet: _presence), + Divider(), + ])); +} diff --git a/lib/forez/ui/routes/forez_route.dart b/lib/forez/ui/routes/forez_route.dart index 817eb2f..af48ea6 100644 --- a/lib/forez/ui/routes/forez_route.dart +++ b/lib/forez/ui/routes/forez_route.dart @@ -1,4 +1,5 @@ import 'package:comunic/forez/helpers/forez_group_helper.dart'; +import 'package:comunic/forez/ui/routes/forez_member_profile_route.dart'; import 'package:comunic/forez/ui/screens/forez_directory_screen.dart'; import 'package:comunic/helpers/events_helper.dart'; import 'package:comunic/models/conversation.dart'; @@ -72,7 +73,13 @@ class _MainRouteState extends MainController { void openGroup(int groupID, {int conversationID}) => _unsupportedFeature(); @override - void openUserPage(int userID) => _unsupportedFeature(); + void openUserPage(int userID) => pushPage(PageInfo( + child: ForezMemberProfileRoute(userID: userID), + hideNavBar: true, + canShowAsDialog: true, + type: PageType.USER_PAGE, + id: userID, + )); @override void openFriendsList() => _unsupportedFeature(); diff --git a/lib/forez/ui/screens/forez_directory_screen.dart b/lib/forez/ui/screens/forez_directory_screen.dart index e6d5ea2..88a3f4a 100644 --- a/lib/forez/ui/screens/forez_directory_screen.dart +++ b/lib/forez/ui/screens/forez_directory_screen.dart @@ -6,6 +6,7 @@ import 'package:comunic/lists/group_members_list.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/group_membership.dart'; import 'package:comunic/models/user.dart'; +import 'package:comunic/ui/routes/main_route/main_route.dart'; import 'package:comunic/ui/widgets/account_image_widget.dart'; import 'package:comunic/ui/widgets/async_screen_widget.dart'; import 'package:comunic/utils/account_utils.dart'; @@ -123,9 +124,8 @@ class _ForezDirectoryScreenState extends State { if (user != null) _openUserProfile(user); } - void _openUserProfile(User user) { - print("Open user profile ${user.fullName}"); - } + void _openUserProfile(User user) => + MainController.of(context).openUserPage(user.id); } class _ForezMemberTile extends StatelessWidget { diff --git a/lib/helpers/forez_groups_helper.dart b/lib/helpers/forez_groups_helper.dart index fd1ce6e..aa8c14d 100644 --- a/lib/helpers/forez_groups_helper.dart +++ b/lib/helpers/forez_groups_helper.dart @@ -1,4 +1,6 @@ import 'package:comunic/helpers/groups_helper.dart'; +import 'package:comunic/helpers/users_helper.dart'; +import 'package:comunic/models/advanced_user_info.dart'; import 'package:comunic/models/api_request.dart'; import 'package:comunic/models/group.dart'; @@ -14,4 +16,16 @@ class ForezGroupsHelper { .map(GroupsHelper.getGroupFromAPI) .toList(); } + + /// Get advanced information about a Forez member + /// + /// This methods throws an exception in case of failure + static Future getMemberInfo(int groupID, int userID) async { + final response = await APIRequest.withLogin("forez/get_member_info") + .addInt("group", groupID) + .addInt("user", userID) + .execWithThrowGetObject(); + + return UsersHelper.apiToAdvancedUserInfo(response); + } } diff --git a/lib/helpers/users_helper.dart b/lib/helpers/users_helper.dart index b5bb96e..157cb13 100644 --- a/lib/helpers/users_helper.dart +++ b/lib/helpers/users_helper.dart @@ -151,36 +151,11 @@ class UsersHelper { throw new GetUserAdvancedUserError(cause); } - final data = response.getObject(); - - return AdvancedUserInfo( - id: data["userID"], - firstName: data["firstName"], - lastName: data["lastName"], - pageVisibility: data["publicPage"] == "false" - ? UserPageVisibility.PRIVATE - : (data["openPage"] == "false" - ? UserPageVisibility.PRIVATE - : UserPageVisibility.OPEN), - virtualDirectory: - data["virtualDirectory"] == "" ? null : data["virtualDirectory"], - accountImageURL: data["accountImage"], - emailAddress: data["email_address"], - customEmojies: _parseCustomEmojies(data["customEmojis"]), - publicNote: data["publicNote"], - canPostTexts: data["can_post_texts"], - isFriendsListPublic: data["friend_list_public"], - numberFriends: data["number_friends"], - accountCreationTime: data["account_creation_time"], - personalWebsite: data["personnalWebsite"], - location: data["location"], - likes: data["pageLikes"], - userLike: data["user_like_page"], - ); + return apiToAdvancedUserInfo(response.getObject()); } /// Parse the list of custom emojies - CustomEmojiesList _parseCustomEmojies(List list) { + static CustomEmojiesList _parseCustomEmojies(List list) { final l = list.cast>(); return CustomEmojiesList() @@ -193,4 +168,30 @@ class UsersHelper { )) .toList()); } + + static AdvancedUserInfo apiToAdvancedUserInfo(Map data) => + AdvancedUserInfo( + id: data["userID"], + firstName: data["firstName"], + lastName: data["lastName"], + pageVisibility: data["publicPage"] == "false" + ? UserPageVisibility.PRIVATE + : (data["openPage"] == "false" + ? UserPageVisibility.PRIVATE + : UserPageVisibility.OPEN), + virtualDirectory: + data["virtualDirectory"] == "" ? null : data["virtualDirectory"], + accountImageURL: data["accountImage"], + emailAddress: data["email_address"], + customEmojies: _parseCustomEmojies(data["customEmojis"]), + publicNote: data["publicNote"], + canPostTexts: data["can_post_texts"], + isFriendsListPublic: data["friend_list_public"], + numberFriends: data["number_friends"], + accountCreationTime: data["account_creation_time"], + personalWebsite: data["personnalWebsite"], + location: data["location"], + likes: data["pageLikes"], + userLike: data["user_like_page"], + ); } diff --git a/lib/models/advanced_user_info.dart b/lib/models/advanced_user_info.dart index 1f00ddd..f951eed 100644 --- a/lib/models/advanced_user_info.dart +++ b/lib/models/advanced_user_info.dart @@ -60,6 +60,10 @@ class AdvancedUserInfo extends User implements LikeElement { bool get hasPersonalWebsite => personalWebsite.isNotEmpty; + bool get hasEmailAddress => emailAddress != null && emailAddress.isNotEmpty; + + bool get hasLocation => location != null && location.isNotEmpty; + @override LikesType get likeType => LikesType.USER; } diff --git a/lib/ui/widgets/async_screen_widget.dart b/lib/ui/widgets/async_screen_widget.dart index 90eaa0b..d32672f 100644 --- a/lib/ui/widgets/async_screen_widget.dart +++ b/lib/ui/widgets/async_screen_widget.dart @@ -28,12 +28,24 @@ class AsyncScreenWidget extends StatefulWidget { /// Specify whether old data can be kept or not while updating this widget final bool showOldDataWhileUpdating; + /// Widget to use while we are refreshing + /// + /// This widget is optional + final Widget loadingWidget; + + /// Widget to use in case of error + /// + /// This widget is optional + final Widget errorWidget; + const AsyncScreenWidget({ Key key, @required this.onReload, @required this.onBuild, @required this.errorMessage, this.showOldDataWhileUpdating = false, + this.loadingWidget, + this.errorWidget, }) : assert(onReload != null), assert(onBuild != null), assert(errorMessage != null), @@ -59,16 +71,17 @@ class AsyncScreenWidgetState extends SafeState { Widget build(BuildContext context) { // In case of error if (error) - return buildErrorCard(widget.errorMessage, actions: [ - MaterialButton( - textColor: Colors.white, - onPressed: () => refresh(), - child: Text(tr("Try again").toUpperCase()), - ) - ]); + return widget.errorWidget ?? + buildErrorCard(widget.errorMessage, actions: [ + MaterialButton( + textColor: Colors.white, + onPressed: () => refresh(), + child: Text(tr("Try again").toUpperCase()), + ) + ]); // Show loading states - if (!ready) return buildCenteredProgressBar(); + if (!ready) return widget.loadingWidget ?? buildCenteredProgressBar(); // The widget is ready, show it return RefreshIndicator( diff --git a/lib/ui/widgets/copy_icon.dart b/lib/ui/widgets/copy_icon.dart new file mode 100644 index 0000000..8501732 --- /dev/null +++ b/lib/ui/widgets/copy_icon.dart @@ -0,0 +1,25 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; +import 'package:flutter/material.dart'; + +/// Icon used to copy content in clipboard +/// +/// @author Pierre Hubert + +class CopyIcon extends StatelessWidget { + final String value; + + const CopyIcon(this.value) : assert(value != null); + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon(Icons.content_copy), + onPressed: () { + FlutterClipboard.copy(value); + snack(context, tr("'%c%' was copied to clipboard", args: {"c": value})); + }, + ); + } +}