421 lines
14 KiB
Dart
421 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:lemmy_api_client/v3.dart';
|
|
|
|
import '../hooks/memo_future.dart';
|
|
import '../hooks/stores.dart';
|
|
import '../l10n/l10n.dart';
|
|
import '../pages/manage_account.dart';
|
|
import '../util/extensions/api.dart';
|
|
import '../util/extensions/cake_day.dart';
|
|
import '../util/extensions/datetime.dart';
|
|
import '../util/goto.dart';
|
|
import '../util/text_color.dart';
|
|
import 'avatar.dart';
|
|
import 'cached_network_image.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 String instanceHost;
|
|
final int userId;
|
|
|
|
final FullPersonView? _fullUserView;
|
|
|
|
const UserProfile({required this.userId, required this.instanceHost})
|
|
: _fullUserView = null;
|
|
|
|
UserProfile.fromFullPersonView(FullPersonView this._fullUserView)
|
|
: userId = _fullUserView.personView.person.id,
|
|
instanceHost = _fullUserView.instanceHost;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final accountsStore = useAccountsStore();
|
|
final userDetailsSnap = useMemoFuture(() async {
|
|
if (_fullUserView != null) return _fullUserView;
|
|
|
|
return await LemmyApiV3(instanceHost).run(GetPersonDetails(
|
|
personId: userId,
|
|
savedOnly: false,
|
|
sort: SortType.active,
|
|
auth: accountsStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
|
));
|
|
}, [userId, instanceHost]);
|
|
|
|
if (!userDetailsSnap.hasData) {
|
|
return const Center(child: CircularProgressIndicator.adaptive());
|
|
} else if (userDetailsSnap.hasError) {
|
|
return Center(
|
|
child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
|
|
const Icon(Icons.error),
|
|
Padding(
|
|
padding: const EdgeInsets.all(8),
|
|
child: Text('ERROR: ${userDetailsSnap.error}'),
|
|
)
|
|
]),
|
|
);
|
|
}
|
|
final fullPersonView = userDetailsSnap.data!;
|
|
final userView = fullPersonView.personView;
|
|
|
|
return DefaultTabController(
|
|
length: 3,
|
|
child: NestedScrollView(
|
|
headerSliverBuilder: (_, __) => [
|
|
SliverAppBar(
|
|
pinned: true,
|
|
expandedHeight: 300,
|
|
toolbarHeight: 0,
|
|
forceElevated: true,
|
|
backgroundColor: theme.cardColor,
|
|
flexibleSpace:
|
|
FlexibleSpaceBar(background: _UserOverview(userView)),
|
|
bottom: PreferredSize(
|
|
preferredSize: const TabBar(tabs: []).preferredSize,
|
|
child: Material(
|
|
color: theme.cardColor,
|
|
child: TabBar(
|
|
tabs: [
|
|
Tab(text: L10n.of(context)!.posts),
|
|
Tab(text: L10n.of(context)!.comments),
|
|
const 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) => LemmyApiV3(instanceHost)
|
|
.run(GetPersonDetails(
|
|
personId: userView.person.id,
|
|
savedOnly: false,
|
|
sort: SortType.active,
|
|
page: page,
|
|
limit: batchSize,
|
|
auth: accountsStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
|
))
|
|
.then((val) => val.posts),
|
|
),
|
|
InfiniteCommentList(
|
|
fetcher: (page, batchSize, sort) => LemmyApiV3(instanceHost)
|
|
.run(GetPersonDetails(
|
|
personId: userView.person.id,
|
|
savedOnly: false,
|
|
sort: SortType.active,
|
|
page: page,
|
|
limit: batchSize,
|
|
auth: accountsStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
|
))
|
|
.then((val) => val.comments),
|
|
),
|
|
_AboutTab(fullPersonView),
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// 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 PersonViewSafe userView;
|
|
|
|
const _UserOverview(this.userView);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final colorOnTopOfAccentColor =
|
|
textColorBasedOnBackground(theme.colorScheme.secondary);
|
|
|
|
return Stack(
|
|
children: [
|
|
if (userView.person.banner != null)
|
|
// TODO: for some reason doesnt react to presses
|
|
FullscreenableImage(
|
|
url: userView.person.banner!,
|
|
child: CachedNetworkImage(
|
|
imageUrl: userView.person.banner!,
|
|
errorBuilder: (_, ___) => const SizedBox.shrink(),
|
|
),
|
|
)
|
|
else
|
|
Container(
|
|
width: double.infinity,
|
|
height: 200,
|
|
color: theme.colorScheme.secondary,
|
|
),
|
|
Container(
|
|
height: 200,
|
|
decoration: const 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: const BorderRadius.only(
|
|
topRight: Radius.circular(40),
|
|
topLeft: Radius.circular(40),
|
|
),
|
|
color: theme.cardColor,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SafeArea(
|
|
child: Column(
|
|
children: [
|
|
if (userView.person.avatar != null)
|
|
SizedBox(
|
|
width: 80,
|
|
height: 80,
|
|
child: Container(
|
|
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(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (userView.person.avatar != null)
|
|
const SizedBox(height: 8)
|
|
else
|
|
const SizedBox(height: 80),
|
|
Text(
|
|
'${userView.person.preferredName}${userView.person.isCakeDay ? ' 🍰' : ''}',
|
|
style: theme.textTheme.headline6,
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
'@${userView.person.name}@',
|
|
style: theme.textTheme.caption,
|
|
),
|
|
InkWell(
|
|
onTap: () => goToInstance(
|
|
context, userView.person.originInstanceHost),
|
|
child: Text(
|
|
userView.person.originInstanceHost,
|
|
style: theme.textTheme.caption,
|
|
),
|
|
)
|
|
],
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Chip(
|
|
label: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.article,
|
|
size: 15,
|
|
color: colorOnTopOfAccentColor,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
L10n.of(context)!
|
|
.number_of_posts(userView.counts.postCount),
|
|
style: TextStyle(color: colorOnTopOfAccentColor),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Chip(
|
|
label: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.comment,
|
|
size: 15,
|
|
color: colorOnTopOfAccentColor,
|
|
),
|
|
const SizedBox(width: 4),
|
|
Text(
|
|
L10n.of(context)!
|
|
.number_of_comments(userView.counts.commentCount),
|
|
style: TextStyle(color: colorOnTopOfAccentColor),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 15),
|
|
Text(
|
|
'Joined ${userView.person.published.fancy}',
|
|
style: theme.textTheme.bodyText1,
|
|
),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
const Icon(
|
|
Icons.cake,
|
|
size: 13,
|
|
),
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 4),
|
|
child: Text(
|
|
DateFormat('MMM dd, yyyy')
|
|
.format(userView.person.published),
|
|
style: theme.textTheme.bodyText1,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AboutTab extends HookWidget {
|
|
final FullPersonView userDetails;
|
|
|
|
const _AboutTab(this.userDetails);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final instanceHost = userDetails.personView.person.instanceHost;
|
|
|
|
final accStore = useAccountsStore();
|
|
final token = accStore
|
|
.userDataFor(instanceHost, userDetails.personView.person.name)
|
|
?.jwt;
|
|
|
|
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]);
|
|
|
|
const wallPadding = EdgeInsets.symmetric(horizontal: 15);
|
|
|
|
final divider = Padding(
|
|
padding: EdgeInsets.symmetric(
|
|
horizontal: wallPadding.horizontal / 2,
|
|
vertical: 10,
|
|
),
|
|
child: const Divider(),
|
|
);
|
|
|
|
communityTile(String name, String? icon, int id) => ListTile(
|
|
dense: true,
|
|
onTap: () => goToCommunity.byId(context, instanceHost, id),
|
|
title: Text('!$name'),
|
|
leading: Avatar(
|
|
url: icon,
|
|
radius: 20,
|
|
),
|
|
);
|
|
|
|
return ListView(
|
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
|
children: [
|
|
if (isOwnedAccount)
|
|
ListTile(
|
|
title: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: const [
|
|
Icon(Icons.edit),
|
|
SizedBox(width: 10),
|
|
Text('edit profile'),
|
|
],
|
|
),
|
|
onTap: () => goTo(
|
|
context,
|
|
(_) => ManageAccountPage(
|
|
instanceHost: userDetails.instanceHost,
|
|
username: userDetails.personView.person.name),
|
|
),
|
|
),
|
|
if (userDetails.personView.person.bio != null) ...[
|
|
Padding(
|
|
padding: wallPadding,
|
|
child: MarkdownText(userDetails.personView.person.bio!,
|
|
instanceHost: instanceHost)),
|
|
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.community.name.compareTo(b.community.name)))
|
|
communityTile(
|
|
comm.community.name, comm.community.icon, comm.community.id),
|
|
divider,
|
|
],
|
|
if (followsSnap.hasData && followsSnap.data!.isNotEmpty) ...[
|
|
ListTile(
|
|
title: Center(
|
|
child: Text(
|
|
'Subscribed:',
|
|
style: theme.textTheme.headline6?.copyWith(fontSize: 18),
|
|
),
|
|
),
|
|
),
|
|
for (final comm
|
|
in followsSnap.data!
|
|
..sort((a, b) => a.community.name.compareTo(b.community.name)))
|
|
communityTile(
|
|
comm.community.name, comm.community.icon, comm.community.id)
|
|
]
|
|
],
|
|
);
|
|
}
|
|
}
|