1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2024-11-22 12:59:21 +00:00

Add user profile route

This commit is contained in:
Pierre HUBERT 2021-04-26 09:33:54 +02:00
parent 5ab21bd63e
commit f92846cb76
8 changed files with 278 additions and 39 deletions

View File

@ -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<ForezMemberProfileRoute> {
final double _appBarHeight = 256.0;
final _key = GlobalKey<AsyncScreenWidgetState>();
AdvancedUserInfo _user;
PresenceSet _presence;
Future<void> _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: <Widget>[
_buildAppBar(),
_buildList(),
],
),
);
Widget _buildAppBar() => SliverAppBar(
expandedHeight: _appBarHeight,
pinned: true,
flexibleSpace: FlexibleSpaceBar(
title: Text(_user.fullName),
background: Stack(
fit: StackFit.expand,
children: <Widget>[
_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>[Color(0x60000000), Color(0x00000000)],
),
),
),
],
),
),
);
Widget _buildList() => SliverList(
delegate: SliverChildListDelegate(<Widget>[
// 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(),
]));
}

View File

@ -1,4 +1,5 @@
import 'package:comunic/forez/helpers/forez_group_helper.dart'; 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/forez/ui/screens/forez_directory_screen.dart';
import 'package:comunic/helpers/events_helper.dart'; import 'package:comunic/helpers/events_helper.dart';
import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/conversation.dart';
@ -72,7 +73,13 @@ class _MainRouteState extends MainController {
void openGroup(int groupID, {int conversationID}) => _unsupportedFeature(); void openGroup(int groupID, {int conversationID}) => _unsupportedFeature();
@override @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 @override
void openFriendsList() => _unsupportedFeature(); void openFriendsList() => _unsupportedFeature();

View File

@ -6,6 +6,7 @@ import 'package:comunic/lists/group_members_list.dart';
import 'package:comunic/lists/users_list.dart'; import 'package:comunic/lists/users_list.dart';
import 'package:comunic/models/group_membership.dart'; import 'package:comunic/models/group_membership.dart';
import 'package:comunic/models/user.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/account_image_widget.dart';
import 'package:comunic/ui/widgets/async_screen_widget.dart'; import 'package:comunic/ui/widgets/async_screen_widget.dart';
import 'package:comunic/utils/account_utils.dart'; import 'package:comunic/utils/account_utils.dart';
@ -123,9 +124,8 @@ class _ForezDirectoryScreenState extends State<ForezDirectoryScreen> {
if (user != null) _openUserProfile(user); if (user != null) _openUserProfile(user);
} }
void _openUserProfile(User user) { void _openUserProfile(User user) =>
print("Open user profile ${user.fullName}"); MainController.of(context).openUserPage(user.id);
}
} }
class _ForezMemberTile extends StatelessWidget { class _ForezMemberTile extends StatelessWidget {

View File

@ -1,4 +1,6 @@
import 'package:comunic/helpers/groups_helper.dart'; 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/api_request.dart';
import 'package:comunic/models/group.dart'; import 'package:comunic/models/group.dart';
@ -14,4 +16,16 @@ class ForezGroupsHelper {
.map(GroupsHelper.getGroupFromAPI) .map(GroupsHelper.getGroupFromAPI)
.toList(); .toList();
} }
/// Get advanced information about a Forez member
///
/// This methods throws an exception in case of failure
static Future<AdvancedUserInfo> 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);
}
} }

View File

@ -151,36 +151,11 @@ class UsersHelper {
throw new GetUserAdvancedUserError(cause); throw new GetUserAdvancedUserError(cause);
} }
final data = response.getObject(); return apiToAdvancedUserInfo(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"],
);
} }
/// Parse the list of custom emojies /// Parse the list of custom emojies
CustomEmojiesList _parseCustomEmojies(List<dynamic> list) { static CustomEmojiesList _parseCustomEmojies(List<dynamic> list) {
final l = list.cast<Map<String, dynamic>>(); final l = list.cast<Map<String, dynamic>>();
return CustomEmojiesList() return CustomEmojiesList()
@ -193,4 +168,30 @@ class UsersHelper {
)) ))
.toList()); .toList());
} }
static AdvancedUserInfo apiToAdvancedUserInfo(Map<String, dynamic> 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"],
);
} }

View File

@ -60,6 +60,10 @@ class AdvancedUserInfo extends User implements LikeElement {
bool get hasPersonalWebsite => personalWebsite.isNotEmpty; bool get hasPersonalWebsite => personalWebsite.isNotEmpty;
bool get hasEmailAddress => emailAddress != null && emailAddress.isNotEmpty;
bool get hasLocation => location != null && location.isNotEmpty;
@override @override
LikesType get likeType => LikesType.USER; LikesType get likeType => LikesType.USER;
} }

View File

@ -28,12 +28,24 @@ class AsyncScreenWidget extends StatefulWidget {
/// Specify whether old data can be kept or not while updating this widget /// Specify whether old data can be kept or not while updating this widget
final bool showOldDataWhileUpdating; 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({ const AsyncScreenWidget({
Key key, Key key,
@required this.onReload, @required this.onReload,
@required this.onBuild, @required this.onBuild,
@required this.errorMessage, @required this.errorMessage,
this.showOldDataWhileUpdating = false, this.showOldDataWhileUpdating = false,
this.loadingWidget,
this.errorWidget,
}) : assert(onReload != null), }) : assert(onReload != null),
assert(onBuild != null), assert(onBuild != null),
assert(errorMessage != null), assert(errorMessage != null),
@ -59,16 +71,17 @@ class AsyncScreenWidgetState extends SafeState<AsyncScreenWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// In case of error // In case of error
if (error) if (error)
return buildErrorCard(widget.errorMessage, actions: [ return widget.errorWidget ??
MaterialButton( buildErrorCard(widget.errorMessage, actions: [
textColor: Colors.white, MaterialButton(
onPressed: () => refresh(), textColor: Colors.white,
child: Text(tr("Try again").toUpperCase()), onPressed: () => refresh(),
) child: Text(tr("Try again").toUpperCase()),
]); )
]);
// Show loading states // Show loading states
if (!ready) return buildCenteredProgressBar(); if (!ready) return widget.loadingWidget ?? buildCenteredProgressBar();
// The widget is ready, show it // The widget is ready, show it
return RefreshIndicator( return RefreshIndicator(

View File

@ -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}));
},
);
}
}