Merge pull request #63 from krawieck/profile-tab-revamp

This commit is contained in:
Filip Krawczyk 2020-10-02 23:39:19 +02:00 committed by GitHub
commit c6a1eff7ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 375 additions and 236 deletions

View File

@ -43,6 +43,8 @@ class UserProfileTab extends HookWidget {
return Scaffold(
extendBodyBehindAppBar: true,
// TODO: this is not visible in light mode when the sliver app bar
// in UserProfile is folded
appBar: AppBar(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,

View File

@ -9,34 +9,29 @@ import '../widgets/user_profile.dart';
class UserPage extends HookWidget {
final int userId;
final String instanceUrl;
final Future<UserView> _userView;
final Future<UserDetails> _userDetails;
UserPage({@required this.userId, @required this.instanceUrl})
: assert(userId != null),
assert(instanceUrl != null),
_userView = LemmyApi(instanceUrl)
.v1
.getUserDetails(
userId: userId, savedOnly: true, sort: SortType.active)
.then((res) => res.user);
_userDetails = LemmyApi(instanceUrl).v1.getUserDetails(
userId: userId, savedOnly: true, sort: SortType.active);
UserPage.fromName({@required this.instanceUrl, @required String username})
: assert(instanceUrl != null),
assert(username != null),
userId = null,
_userView = LemmyApi(instanceUrl)
.v1
.getUserDetails(
username: username, savedOnly: true, sort: SortType.active)
.then((res) => res.user);
_userDetails = LemmyApi(instanceUrl).v1.getUserDetails(
username: username, savedOnly: true, sort: SortType.active);
@override
Widget build(BuildContext context) {
final userViewSnap = useFuture(_userView);
final userDetailsSnap = useFuture(_userDetails);
final body = () {
if (userViewSnap.hasData) {
return UserProfile.fromUserView(userViewSnap.data);
} else if (userViewSnap.hasError) {
if (userDetailsSnap.hasData) {
return UserProfile.fromUserDetails(userDetailsSnap.data);
} else if (userDetailsSnap.hasError) {
return Center(child: Text('Could not find that user.'));
} else {
return Center(child: CircularProgressIndicator());
@ -49,15 +44,15 @@ class UserPage extends HookWidget {
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
actions: [
if (userViewSnap.hasData) ...[
if (userDetailsSnap.hasData) ...[
IconButton(
icon: Icon(Icons.email),
onPressed: () {}, // TODO: go to messaging page
),
IconButton(
icon: Icon(Icons.share),
onPressed: () => Share.text(
'Share user', userViewSnap.data.actorId, 'text/plain'),
onPressed: () => Share.text('Share user',
userDetailsSnap.data.user.actorId, 'text/plain'),
)
]
],

View File

@ -5,28 +5,117 @@ import 'package:intl/intl.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import 'package:timeago/timeago.dart' as timeago;
import '../hooks/stores.dart';
import '../util/extensions/api.dart';
import '../util/goto.dart';
import '../util/intl.dart';
import '../util/text_color.dart';
import 'badge.dart';
import 'fullscreenable_image.dart';
import 'markdown_text.dart';
import 'sortable_infinite_list.dart';
/// Shared widget of UserPage and ProfileTab
class UserProfile extends HookWidget {
final Future<UserView> _userView;
final Future<UserDetails> _userDetails;
final String instanceUrl;
// TODO: add `.fromUser` constructor
UserProfile({@required int userId, @required this.instanceUrl})
: _userView = LemmyApi(instanceUrl)
.v1
.getUserDetails(
userId: userId, savedOnly: true, sort: SortType.active)
.then((res) => res.user);
: _userDetails = LemmyApi(instanceUrl).v1.getUserDetails(
userId: userId, savedOnly: false, sort: SortType.active);
UserProfile.fromUserView(UserView userView)
: _userView = Future.value(userView),
instanceUrl = userView.instanceUrl;
UserProfile.fromUserDetails(UserDetails userDetails)
: _userDetails = Future.value(userDetails),
instanceUrl = userDetails.user.instanceUrl;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final userDetailsSnap = useFuture(_userDetails, preserveState: false);
if (!userDetailsSnap.hasData) {
return const Center(child: CircularProgressIndicator());
} else if (userDetailsSnap.hasError) {
return Center(
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
Icon(Icons.error),
Padding(
padding: const EdgeInsets.all(8),
child: Text('ERROR: ${userDetailsSnap.error}'),
)
]),
);
}
final userView = userDetailsSnap.data.user;
return DefaultTabController(
length: 3,
child: NestedScrollView(
headerSliverBuilder: (_, __) => [
SliverAppBar(
pinned: true,
expandedHeight: 265,
toolbarHeight: 0,
forceElevated: true,
elevation: 0,
backgroundColor: theme.cardColor,
brightness: theme.brightness,
iconTheme: theme.iconTheme,
flexibleSpace:
FlexibleSpaceBar(background: _UserOverview(userView)),
bottom: TabBar(
labelColor: theme.textTheme.bodyText1.color,
tabs: [
Tab(text: 'Posts'),
Tab(text: 'Comments'),
Tab(text: 'About'),
],
),
),
],
body: TabBarView(children: [
// TODO: first batch is already fetched on render
// TODO: comment and post come from the same endpoint, could be shared
InfinitePostList(
fetcher: (page, batchSize, sort) => LemmyApi(instanceUrl)
.v1
.getUserDetails(
userId: userView.id,
savedOnly: false,
sort: SortType.active,
page: page,
limit: batchSize,
)
.then((val) => val.posts),
),
InfiniteCommentList(
fetcher: (page, batchSize, sort) => LemmyApi(instanceUrl)
.v1
.getUserDetails(
userId: userView.id,
savedOnly: false,
sort: SortType.active,
page: page,
limit: batchSize,
)
.then((val) => val.comments),
),
_AboutTab(userDetailsSnap.data),
]),
),
);
}
}
/// Content in the sliver flexible space
/// Renders general info about the given user.
/// Such as his nickname, no. of posts, no. of posts,
/// banner, avatar etc.
class _UserOverview extends HookWidget {
final UserView userView;
const _UserOverview(this.userView);
@override
Widget build(BuildContext context) {
@ -34,185 +123,153 @@ class UserProfile extends HookWidget {
final colorOnTopOfAccentColor =
textColorBasedOnBackground(theme.accentColor);
final userViewSnap = useFuture(_userView, preserveState: false);
final bio = () {
if (userViewSnap.hasData) {
if (userViewSnap.data.bio != null) {
return Padding(
padding: const EdgeInsets.all(10),
child: Text(userViewSnap.data.bio),
);
} else {
return Center(
child: Text(
'no bio',
style: const TextStyle(fontStyle: FontStyle.italic),
),
);
}
} else {
return Center(child: CircularProgressIndicator());
}
}();
Widget tabs() => DefaultTabController(
length: 3,
child: Column(
children: [
TabBar(
labelColor: theme.textTheme.bodyText1.color,
tabs: [
Tab(text: 'Posts'),
Tab(text: 'Comments'),
Tab(text: 'About'),
],
),
Expanded(
child: TabBarView(
children: [
Center(
child: Text(
'Posts',
style: const TextStyle(fontSize: 36),
),
),
Center(
child: Text(
'Comments',
style: const TextStyle(fontSize: 36),
),
),
bio,
],
),
)
],
),
);
return Center(
child: Stack(
children: [
if (userViewSnap.data?.banner != null)
CachedNetworkImage(
imageUrl: userViewSnap.data.banner,
return Stack(
children: [
if (userView.banner != null)
// TODO: for some reason doesnt react to presses
FullscreenableImage(
url: userView.banner,
child: CachedNetworkImage(
imageUrl: userView.banner,
errorWidget: (_, __, ___) => SizedBox.shrink(),
)
else
Container(
),
)
else
Container(
width: double.infinity,
height: 200,
color: theme.accentColor,
),
Container(
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: [
Colors.black26,
Colors.transparent,
],
),
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: SizedBox(
width: double.infinity,
height: double.infinity,
color: theme.primaryColor,
),
Container(
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: [
Colors.black26,
Colors.transparent,
],
),
),
),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: SizedBox(
width: double.infinity,
height: double.infinity,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(40),
topLeft: Radius.circular(40),
),
color: theme.scaffoldBackgroundColor,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
topRight: Radius.circular(40),
topLeft: Radius.circular(40),
),
color: theme.scaffoldBackgroundColor,
),
),
),
),
SafeArea(
child: Column(
children: [
if (userViewSnap.data?.avatar != null)
SizedBox(
width: 80,
height: 80,
child: Container(
// clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
boxShadow: [
BoxShadow(blurRadius: 6, color: Colors.black54)
],
borderRadius: BorderRadius.all(Radius.circular(15)),
border: Border.all(color: Colors.white, width: 3),
),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
SafeArea(
child: Column(
children: [
if (userView.avatar != null)
SizedBox(
width: 80,
height: 80,
child: Container(
// clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
boxShadow: [
BoxShadow(blurRadius: 6, color: Colors.black54)
],
borderRadius: BorderRadius.all(Radius.circular(15)),
border: Border.all(color: Colors.white, width: 3),
),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(12)),
child: FullscreenableImage(
url: userView.avatar,
child: CachedNetworkImage(
imageUrl: userViewSnap.data.avatar,
imageUrl: userView.avatar,
errorWidget: (_, __, ___) => SizedBox.shrink(),
),
),
),
),
Padding(
padding: userViewSnap.data?.avatar != null
? const EdgeInsets.only(top: 8.0)
: const EdgeInsets.only(top: 70),
child: Padding(
padding: EdgeInsets.only(
top: userViewSnap.data?.avatar == null ? 10 : 0),
child: Text(
userViewSnap.data?.preferredUsername ??
userViewSnap.data?.name ??
'',
style: theme.textTheme.headline6,
),
),
Padding(
padding: userView.avatar != null
? const EdgeInsets.only(top: 8.0)
: const EdgeInsets.only(top: 70),
child: Padding(
padding:
EdgeInsets.only(top: userView.avatar == null ? 10 : 0),
child: Text(
userView.preferredUsername ?? userView.name,
style: theme.textTheme.headline6,
),
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'@${userViewSnap.data?.name ?? ''}@',
),
Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'@${userView.name}@',
style: theme.textTheme.caption,
),
InkWell(
onTap: () => goToInstance(context, userView.instanceUrl),
child: Text(
'${userView.instanceUrl}',
style: theme.textTheme.caption,
),
InkWell(
onTap: () => goToInstance(context, instanceUrl),
child: Text(
'$instanceUrl',
style: theme.textTheme.caption,
),
)
],
),
)
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Badge(
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Badge(
child: Row(
children: [
Icon(
Icons.comment, // TODO: should be article icon
size: 15,
color: colorOnTopOfAccentColor,
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
'${compactNumber(userView.numberOfPosts)}'
' Post${pluralS(userView.numberOfPosts)}',
style: TextStyle(color: colorOnTopOfAccentColor),
),
),
],
),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Badge(
child: Row(
children: [
Icon(
Icons.comment, // TODO: should be article icon
Icons.comment,
size: 15,
color: colorOnTopOfAccentColor,
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
'''
${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfPosts) : '-'} Post${pluralS(userViewSnap.data?.numberOfPosts ?? 0)}''',
'${compactNumber(userView.numberOfComments)}'
' Comment${pluralS(userView.numberOfComments)}',
style:
TextStyle(color: colorOnTopOfAccentColor),
),
@ -220,68 +277,153 @@ ${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfPosts) : '-'} P
],
),
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Badge(
child: Row(
children: [
Icon(
Icons.comment,
size: 15,
color: colorOnTopOfAccentColor,
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
'''
${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfComments) : '-'} Comment${pluralS(userViewSnap.data?.numberOfComments ?? 0)}''',
style:
TextStyle(color: colorOnTopOfAccentColor),
),
),
],
),
),
),
],
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Text(
'''
Joined ${userViewSnap.hasData ? timeago.format(userViewSnap.data.published) : ''}''',
style: theme.textTheme.bodyText1,
),
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Text(
'Joined ${timeago.format(userView.published)}',
style: theme.textTheme.bodyText1,
),
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.cake,
size: 13,
),
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.cake,
size: 13,
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
DateFormat('MMM dd, yyyy').format(userView.published),
style: theme.textTheme.bodyText1,
),
Padding(
padding: const EdgeInsets.only(left: 4.0),
child: Text(
userViewSnap.hasData
? DateFormat('MMM dd, yyyy')
.format(userViewSnap.data.published)
: '',
style: theme.textTheme.bodyText1,
),
),
],
),
),
],
),
Expanded(child: tabs())
],
),
),
// Expanded(child: tabs())
],
),
],
),
),
],
);
}
}
class _AboutTab extends HookWidget {
final UserDetails userDetails;
const _AboutTab(this.userDetails);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final instanceUrl = userDetails.user.instanceUrl;
final accStore = useAccountsStore();
final isOwnedAccount = accStore.loggedInInstances.contains(instanceUrl) &&
accStore.tokens[instanceUrl].containsKey(userDetails.user.name);
const wallPadding = EdgeInsets.symmetric(horizontal: 15);
final divider = Padding(
padding: EdgeInsets.symmetric(
horizontal: wallPadding.horizontal / 2, vertical: 10),
child: Divider(),
);
communityTile(String name, String icon, int id) => ListTile(
dense: true,
onTap: () => goToCommunity.byId(context, instanceUrl, id),
title: Text('!$name'),
leading: icon != null
? CachedNetworkImage(
height: 40,
width: 40,
imageUrl: icon,
errorWidget: (_, __, ___) => SizedBox(width: 40, height: 40),
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.cover,
image: imageProvider,
),
),
))
: SizedBox(width: 40),
);
return ListView(
padding: EdgeInsets.symmetric(vertical: 20),
children: [
if (isOwnedAccount)
ListTile(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.edit),
SizedBox(width: 10),
Text('edit profile'),
],
),
onTap: () {}, // TODO: go to account editing
),
if (userDetails.user.bio != null) ...[
Padding(
padding: wallPadding,
child:
MarkdownText(userDetails.user.bio, instanceUrl: instanceUrl)),
divider,
],
if (userDetails.moderates.isNotEmpty) ...[
ListTile(
title: Center(
child: Text(
'Moderates:',
style: theme.textTheme.headline6.copyWith(fontSize: 18),
),
),
),
for (final comm
in userDetails.moderates
..sort((a, b) => a.communityName.compareTo(b.communityName)))
communityTile(
comm.communityName, comm.communityIcon, comm.communityId),
divider
],
ListTile(
title: Center(
child: Text(
'Subscribed:',
style: theme.textTheme.headline6.copyWith(fontSize: 18),
),
),
),
if (userDetails.follows.isNotEmpty)
for (final comm
in userDetails.follows
..sort((a, b) => a.communityName.compareTo(b.communityName)))
communityTile(
comm.communityName, comm.communityIcon, comm.communityId)
else
Padding(
padding: const EdgeInsets.only(top: 8),
child: Center(
child: Text(
'this user does not subscribe to any community',
style: TextStyle(fontStyle: FontStyle.italic),
),
),
)
],
);
}
}