lemmur-app-android/lib/widgets/user_profile.dart

418 lines
14 KiB
Dart
Raw Permalink Normal View History

2020-08-31 12:05:45 +02:00
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:intl/intl.dart';
2021-04-05 20:14:39 +02:00
import 'package:lemmy_api_client/v3.dart';
2020-08-31 12:05:45 +02:00
2021-01-30 18:38:29 +01:00
import '../hooks/memo_future.dart';
2020-09-30 23:53:20 +02:00
import '../hooks/stores.dart';
2021-03-01 14:21:45 +01:00
import '../l10n/l10n.dart';
import '../pages/instance/instance.dart';
2021-01-30 18:38:29 +01:00
import '../pages/manage_account.dart';
2021-01-02 01:13:13 +01:00
import '../util/extensions/api.dart';
import '../util/goto.dart';
import '../util/text_color.dart';
2021-02-18 09:19:00 +01:00
import 'avatar.dart';
2021-10-21 14:40:28 +02:00
import 'cached_network_image.dart';
2020-09-30 21:47:14 +02:00
import 'fullscreenable_image.dart';
2020-09-30 23:43:21 +02:00
import 'markdown_text.dart';
import 'sortable_infinite_list.dart';
2020-08-31 15:43:09 +02:00
2020-09-30 19:05:00 +02:00
/// Shared widget of UserPage and ProfileTab
2020-08-31 12:05:45 +02:00
class UserProfile extends HookWidget {
final String instanceHost;
final int userId;
2020-08-31 12:05:45 +02:00
final FullPersonView? _fullUserView;
2020-08-31 12:05:45 +02:00
const UserProfile({required this.userId, required this.instanceHost})
: _fullUserView = null;
UserProfile.fromFullPersonView(FullPersonView this._fullUserView)
: userId = _fullUserView.personView.person.id,
instanceHost = _fullUserView.instanceHost;
2020-09-08 21:08:37 +02:00
2020-08-31 12:05:45 +02:00
@override
Widget build(BuildContext context) {
2020-09-16 23:22:04 +02:00
final theme = Theme.of(context);
final accountsStore = useAccountsStore();
final userDetailsSnap = useMemoFuture(() async {
if (_fullUserView != null) return _fullUserView;
2021-04-05 20:14:39 +02:00
return await LemmyApiV3(instanceHost).run(GetPersonDetails(
personId: userId,
savedOnly: false,
sort: SortType.active,
2021-04-11 18:27:22 +02:00
auth: accountsStore.defaultUserDataFor(instanceHost)?.jwt.raw,
));
2021-03-18 15:58:48 +01:00
}, [userId, instanceHost]);
2020-08-31 12:05:45 +02:00
2020-09-30 23:43:21 +02:00
if (!userDetailsSnap.hasData) {
return const Center(child: CircularProgressIndicator.adaptive());
2020-10-02 20:07:13 +02:00
} else if (userDetailsSnap.hasError) {
return Center(
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
2021-01-03 19:43:39 +01:00
const Icon(Icons.error),
2020-10-02 20:07:13 +02:00
Padding(
padding: const EdgeInsets.all(8),
child: Text('ERROR: ${userDetailsSnap.error}'),
)
]),
);
2020-09-30 21:47:14 +02:00
}
final fullPersonView = userDetailsSnap.data!;
final userView = fullPersonView.personView;
2020-09-30 21:47:14 +02:00
return DefaultTabController(
length: 3,
child: NestedScrollView(
headerSliverBuilder: (_, __) => [
SliverAppBar(
pinned: true,
2021-02-09 15:12:13 +01:00
expandedHeight: 300,
2020-09-30 21:47:14 +02:00
toolbarHeight: 0,
forceElevated: true,
backgroundColor: theme.cardColor,
flexibleSpace:
FlexibleSpaceBar(background: _UserOverview(userView)),
2021-02-09 15:12:13 +01:00
bottom: PreferredSize(
preferredSize: const TabBar(tabs: []).preferredSize,
child: Material(
color: theme.cardColor,
2021-03-01 14:21:45 +01:00
child: TabBar(
2021-02-09 15:12:13 +01:00
tabs: [
Tab(text: L10n.of(context).posts),
Tab(text: L10n.of(context).comments),
2021-03-01 14:21:45 +01:00
const Tab(text: 'About'),
2021-02-09 15:12:13 +01:00
],
),
),
2020-09-30 21:47:14 +02:00
),
2020-08-31 21:04:17 +02:00
),
2020-09-30 21:47:14 +02:00
],
body: TabBarView(children: [
2020-09-30 23:43:21 +02:00
// TODO: first batch is already fetched on render
// TODO: comment and post come from the same endpoint, could be shared
InfinitePostList(
2021-04-05 20:14:39 +02:00
fetcher: (page, batchSize, sort) => LemmyApiV3(instanceHost)
.run(GetPersonDetails(
personId: userView.person.id,
2020-09-30 23:43:21 +02:00
savedOnly: false,
sort: SortType.active,
page: page,
limit: batchSize,
2021-04-11 18:27:22 +02:00
auth: accountsStore.defaultUserDataFor(instanceHost)?.jwt.raw,
2021-01-24 20:01:55 +01:00
))
2020-09-30 23:43:21 +02:00
.then((val) => val.posts),
),
InfiniteCommentList(
2021-04-05 20:14:39 +02:00
fetcher: (page, batchSize, sort) => LemmyApiV3(instanceHost)
.run(GetPersonDetails(
personId: userView.person.id,
2020-09-30 23:43:21 +02:00
savedOnly: false,
sort: SortType.active,
page: page,
limit: batchSize,
2021-04-11 18:27:22 +02:00
auth: accountsStore.defaultUserDataFor(instanceHost)?.jwt.raw,
2021-01-24 20:01:55 +01:00
))
2020-09-30 23:43:21 +02:00
.then((val) => val.comments),
),
_AboutTab(fullPersonView),
2020-09-30 21:47:14 +02:00
]),
),
);
}
}
2020-09-30 23:43:21 +02:00
/// Content in the sliver flexible space
2020-10-02 20:07:13 +02:00
/// Renders general info about the given user.
/// Such as his nickname, no. of posts, no. of posts,
/// banner, avatar etc.
2020-09-30 21:47:14 +02:00
class _UserOverview extends HookWidget {
2021-04-05 20:14:39 +02:00
final PersonViewSafe userView;
2020-09-30 21:47:14 +02:00
const _UserOverview(this.userView);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorOnTopOfAccentColor =
textColorBasedOnBackground(theme.colorScheme.secondary);
2020-08-31 21:04:17 +02:00
2020-09-30 21:47:14 +02:00
return Stack(
fit: StackFit.expand,
2020-09-30 21:47:14 +02:00
children: [
2021-04-05 20:14:39 +02:00
if (userView.person.banner != null)
Align(
alignment: Alignment.topCenter,
child: FullscreenableImage(
url: userView.person.banner!,
child: CachedNetworkImage(
fit: BoxFit.cover,
height: MediaQuery.of(context).padding.top + 100,
width: double.infinity,
imageUrl: userView.person.banner!,
errorBuilder: (_, ___) => const SizedBox.shrink(),
),
2020-08-31 21:04:17 +02:00
),
2020-09-30 21:47:14 +02:00
)
else
ColoredBox(color: theme.colorScheme.secondary),
const IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: FractionalOffset.topCenter,
end: FractionalOffset.bottomCenter,
colors: [
Colors.black26,
Colors.transparent,
],
),
),
),
2020-09-30 21:47:14 +02:00
),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 60),
child: DecoratedBox(
decoration: BoxDecoration(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(40),
2020-08-31 12:05:45 +02:00
),
color: theme.cardColor,
2020-08-31 12:05:45 +02:00
),
),
),
2020-09-30 21:47:14 +02:00
),
SafeArea(
child: Column(
children: [
2021-04-05 20:14:39 +02:00
if (userView.person.avatar != null)
Container(
2020-09-30 21:47:14 +02:00
width: 80,
height: 80,
decoration: BoxDecoration(
boxShadow: const [
BoxShadow(blurRadius: 6, color: Colors.black54)
],
borderRadius: const BorderRadius.all(Radius.circular(15)),
border: Border.all(color: Colors.white, width: 3),
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: FullscreenableImage(
url: userView.person.avatar!,
child: CachedNetworkImage(
imageUrl: userView.person.avatar!,
errorBuilder: (_, ___) => const SizedBox.shrink(),
2020-08-31 21:04:17 +02:00
),
2020-08-31 12:05:45 +02:00
),
),
2020-09-30 21:47:14 +02:00
),
2021-04-05 20:14:39 +02:00
if (userView.person.avatar != null)
2021-02-09 15:12:13 +01:00
const SizedBox(height: 8)
else
const SizedBox(height: 80),
Text(
'${userView.person.preferredName}${userView.person.isCakeDay ? ' 🍰' : ''}',
2021-02-09 15:12:13 +01:00
style: theme.textTheme.headline6,
2020-09-30 21:47:14 +02:00
),
2021-02-09 15:12:13 +01:00
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
2021-04-05 20:14:39 +02:00
'@${userView.person.name}@',
2021-02-09 15:12:13 +01:00
style: theme.textTheme.caption,
),
InkWell(
onTap: () => Navigator.of(context).push(
InstancePage.route(
userView.person.originInstanceHost,
),
),
2021-02-09 15:12:13 +01:00
child: Text(
2021-04-05 20:14:39 +02:00
userView.person.originInstanceHost,
2020-09-30 21:47:14 +02:00
style: theme.textTheme.caption,
),
2021-02-09 15:12:13 +01:00
)
],
2020-09-30 21:47:14 +02:00
),
2021-02-09 15:12:13 +01:00
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Chip(
label: Row(
children: [
Icon(
Icons.article,
size: 15,
color: colorOnTopOfAccentColor,
),
const SizedBox(width: 4),
Text(
L10n.of(context)
2021-03-03 13:16:05 +01:00
.number_of_posts(userView.counts.postCount),
2021-02-09 15:12:13 +01:00
style: TextStyle(color: colorOnTopOfAccentColor),
),
],
2020-09-30 21:47:14 +02:00
),
2021-02-09 15:12:13 +01:00
),
const SizedBox(width: 16),
Chip(
label: Row(
children: [
Icon(
Icons.comment,
size: 15,
color: colorOnTopOfAccentColor,
2020-09-01 21:59:00 +02:00
),
2021-02-09 15:12:13 +01:00
const SizedBox(width: 4),
Text(
L10n.of(context)
2021-03-03 13:16:05 +01:00
.number_of_comments(userView.counts.commentCount),
2021-02-09 15:12:13 +01:00
style: TextStyle(color: colorOnTopOfAccentColor),
),
],
2020-09-30 21:47:14 +02:00
),
2021-02-09 15:12:13 +01:00
),
],
2020-09-30 21:47:14 +02:00
),
2021-02-09 15:12:13 +01:00
const SizedBox(height: 15),
Text(
'Joined ${userView.person.published.timeago(context)}',
2021-02-09 15:12:13 +01:00
style: theme.textTheme.bodyText1,
2020-09-30 21:47:14 +02:00
),
2021-02-09 15:12:13 +01:00
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.cake,
size: 13,
),
const SizedBox(width: 4),
Text(
DateFormat.yMMMMd(
Localizations.localeOf(context).toLanguageTag(),
).format(userView.person.published),
style: theme.textTheme.bodyText1,
2021-02-09 15:12:13 +01:00
),
],
2020-09-30 21:47:14 +02:00
),
2021-02-09 15:12:13 +01:00
const SizedBox(height: 8),
2020-09-30 21:47:14 +02:00
],
2020-08-31 12:05:45 +02:00
),
2020-09-30 21:47:14 +02:00
),
],
2020-08-31 12:05:45 +02:00
);
}
2020-08-31 21:04:17 +02:00
}
2020-09-30 21:47:14 +02:00
class _AboutTab extends HookWidget {
2021-04-05 20:14:39 +02:00
final FullPersonView userDetails;
2020-09-30 21:47:14 +02:00
2020-09-30 23:43:21 +02:00
const _AboutTab(this.userDetails);
2020-09-30 21:47:14 +02:00
@override
Widget build(BuildContext context) {
2020-09-30 23:43:21 +02:00
final theme = Theme.of(context);
2021-04-05 20:14:39 +02:00
final instanceHost = userDetails.personView.person.instanceHost;
2020-09-30 23:43:21 +02:00
2020-09-30 23:53:20 +02:00
final accStore = useAccountsStore();
2021-08-26 00:27:24 +02:00
final token = accStore
.userDataFor(instanceHost, userDetails.personView.person.name)
?.jwt;
2020-09-30 23:53:20 +02:00
2021-08-26 00:27:24 +02:00
final isOwnedAccount = token != null;
final followsSnap = useMemoFuture<List<CommunityFollowerView>>(() async {
if (token == null) return const [];
return LemmyApiV3(instanceHost)
.run(GetSite(auth: token.raw))
.then((value) => value.myUser!.follows);
}, [token]);
2020-09-30 23:53:20 +02:00
2020-09-30 23:43:21 +02:00
const wallPadding = EdgeInsets.symmetric(horizontal: 15);
final divider = Padding(
padding: EdgeInsets.symmetric(
2021-08-26 00:27:24 +02:00
horizontal: wallPadding.horizontal / 2,
vertical: 10,
),
2021-01-03 19:43:39 +01:00
child: const Divider(),
2020-09-30 23:43:21 +02:00
);
2020-09-30 21:47:14 +02:00
communityTile(String name, String? icon, int id) => ListTile(
2020-10-02 20:07:13 +02:00
dense: true,
onTap: () => goToCommunity.byId(context, instanceHost, id),
2020-10-02 20:07:13 +02:00
title: Text('!$name'),
2021-02-18 09:19:00 +01:00
leading: Avatar(
url: icon,
radius: 20,
),
2020-10-02 20:07:13 +02:00
);
2020-09-30 23:43:21 +02:00
return ListView(
2021-01-03 19:43:39 +01:00
padding: const EdgeInsets.symmetric(vertical: 20),
2020-09-30 23:43:21 +02:00
children: [
2020-09-30 23:53:20 +02:00
if (isOwnedAccount)
2020-10-02 20:07:13 +02:00
ListTile(
title: Row(
mainAxisAlignment: MainAxisAlignment.center,
2021-01-03 19:43:39 +01:00
children: const [
2020-10-02 20:07:13 +02:00
Icon(Icons.edit),
SizedBox(width: 10),
Text('edit profile'),
],
2020-09-30 23:53:20 +02:00
),
2021-01-30 18:38:29 +01:00
onTap: () => goTo(
context,
(_) => ManageAccountPage(
instanceHost: userDetails.instanceHost,
2021-04-05 20:14:39 +02:00
username: userDetails.personView.person.name),
2021-01-30 18:38:29 +01:00
),
2020-09-30 23:53:20 +02:00
),
2021-04-05 20:14:39 +02:00
if (userDetails.personView.person.bio != null) ...[
2020-09-30 23:43:21 +02:00
Padding(
padding: wallPadding,
child: MarkdownText(userDetails.personView.person.bio!,
instanceHost: instanceHost)),
2020-09-30 23:43:21 +02:00
divider,
],
if (userDetails.moderates.isNotEmpty) ...[
2020-10-02 20:07:13 +02:00
ListTile(
title: Center(
child: Text(
'Moderates:',
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
2020-10-02 20:07:13 +02:00
),
2020-09-30 23:43:21 +02:00
),
2020-10-02 20:07:13 +02:00
),
for (final comm
in userDetails.moderates
2021-01-24 20:01:55 +01:00
..sort((a, b) => a.community.name.compareTo(b.community.name)))
2020-10-02 20:07:13 +02:00
communityTile(
2021-01-24 20:01:55 +01:00
comm.community.name, comm.community.icon, comm.community.id),
2021-02-22 19:47:38 +01:00
divider,
2020-09-30 23:43:21 +02:00
],
2021-08-26 00:27:24 +02:00
if (followsSnap.hasData && followsSnap.data!.isNotEmpty) ...[
2021-02-22 19:47:38 +01:00
ListTile(
title: Center(
child: Text(
'Subscribed:',
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
2021-02-22 19:47:38 +01:00
),
2020-10-02 20:07:13 +02:00
),
),
for (final comm
2021-08-26 00:27:24 +02:00
in followsSnap.data!
2021-01-24 20:01:55 +01:00
..sort((a, b) => a.community.name.compareTo(b.community.name)))
2020-10-02 20:07:13 +02:00
communityTile(
2021-01-24 20:01:55 +01:00
comm.community.name, comm.community.icon, comm.community.id)
2021-02-22 19:47:38 +01:00
]
2020-09-30 23:43:21 +02:00
],
);
2020-09-30 21:47:14 +02:00
}
}