Merge pull request #63 from krawieck/profile-tab-revamp
This commit is contained in:
commit
c6a1eff7ed
|
@ -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,
|
||||
|
|
|
@ -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'),
|
||||
)
|
||||
]
|
||||
],
|
||||
|
|
|
@ -5,62 +5,66 @@ 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 colorOnTopOfAccentColor =
|
||||
textColorBasedOnBackground(theme.accentColor);
|
||||
final userDetailsSnap = useFuture(_userDetails, preserveState: false);
|
||||
|
||||
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 {
|
||||
if (!userDetailsSnap.hasData) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (userDetailsSnap.hasError) {
|
||||
return Center(
|
||||
child: Text(
|
||||
'no bio',
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
),
|
||||
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
||||
Icon(Icons.error),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text('ERROR: ${userDetailsSnap.error}'),
|
||||
)
|
||||
]),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
}
|
||||
}();
|
||||
|
||||
Widget tabs() => DefaultTabController(
|
||||
final userView = userDetailsSnap.data.user;
|
||||
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
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'),
|
||||
|
@ -68,42 +72,73 @@ class UserProfile extends HookWidget {
|
|||
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,
|
||||
],
|
||||
),
|
||||
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),
|
||||
]),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: Stack(
|
||||
/// 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) {
|
||||
final theme = Theme.of(context);
|
||||
final colorOnTopOfAccentColor =
|
||||
textColorBasedOnBackground(theme.accentColor);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
if (userViewSnap.data?.banner != null)
|
||||
CachedNetworkImage(
|
||||
imageUrl: userViewSnap.data.banner,
|
||||
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(
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
color: theme.primaryColor,
|
||||
height: 200,
|
||||
color: theme.accentColor,
|
||||
),
|
||||
Container(
|
||||
height: 200,
|
||||
|
@ -139,7 +174,7 @@ class UserProfile extends HookWidget {
|
|||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
if (userViewSnap.data?.avatar != null)
|
||||
if (userView.avatar != null)
|
||||
SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
|
@ -154,24 +189,25 @@ class UserProfile extends HookWidget {
|
|||
),
|
||||
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
|
||||
padding: userView.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),
|
||||
padding:
|
||||
EdgeInsets.only(top: userView.avatar == null ? 10 : 0),
|
||||
child: Text(
|
||||
userViewSnap.data?.preferredUsername ??
|
||||
userViewSnap.data?.name ??
|
||||
'',
|
||||
userView.preferredUsername ?? userView.name,
|
||||
style: theme.textTheme.headline6,
|
||||
),
|
||||
),
|
||||
|
@ -182,13 +218,13 @@ class UserProfile extends HookWidget {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'@${userViewSnap.data?.name ?? ''}@',
|
||||
'@${userView.name}@',
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => goToInstance(context, instanceUrl),
|
||||
onTap: () => goToInstance(context, userView.instanceUrl),
|
||||
child: Text(
|
||||
'$instanceUrl',
|
||||
'${userView.instanceUrl}',
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
)
|
||||
|
@ -211,10 +247,9 @@ class UserProfile extends HookWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text(
|
||||
'''
|
||||
${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfPosts) : '-'} Post${pluralS(userViewSnap.data?.numberOfPosts ?? 0)}''',
|
||||
style:
|
||||
TextStyle(color: colorOnTopOfAccentColor),
|
||||
'${compactNumber(userView.numberOfPosts)}'
|
||||
' Post${pluralS(userView.numberOfPosts)}',
|
||||
style: TextStyle(color: colorOnTopOfAccentColor),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -233,8 +268,8 @@ ${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfPosts) : '-'} P
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text(
|
||||
'''
|
||||
${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfComments) : '-'} Comment${pluralS(userViewSnap.data?.numberOfComments ?? 0)}''',
|
||||
'${compactNumber(userView.numberOfComments)}'
|
||||
' Comment${pluralS(userView.numberOfComments)}',
|
||||
style:
|
||||
TextStyle(color: colorOnTopOfAccentColor),
|
||||
),
|
||||
|
@ -249,8 +284,7 @@ ${userViewSnap.hasData ? compactNumber(userViewSnap.data.numberOfComments) : '-'
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(top: 15),
|
||||
child: Text(
|
||||
'''
|
||||
Joined ${userViewSnap.hasData ? timeago.format(userViewSnap.data.published) : ''}''',
|
||||
'Joined ${timeago.format(userView.published)}',
|
||||
style: theme.textTheme.bodyText1,
|
||||
),
|
||||
),
|
||||
|
@ -266,22 +300,130 @@ Joined ${userViewSnap.hasData ? timeago.format(userViewSnap.data.published) : ''
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: Text(
|
||||
userViewSnap.hasData
|
||||
? DateFormat('MMM dd, yyyy')
|
||||
.format(userViewSnap.data.published)
|
||||
: '',
|
||||
DateFormat('MMM dd, yyyy').format(userView.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),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue