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

480 lines
17 KiB
Dart
Raw Normal View History

2020-08-27 23:27:27 +02:00
import 'package:cached_network_image/cached_network_image.dart';
import 'package:esys_flutter_share/esys_flutter_share.dart';
import 'package:flutter/cupertino.dart';
2020-08-27 23:27:27 +02:00
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
2020-09-17 15:25:47 +02:00
import 'package:flutter_hooks/flutter_hooks.dart';
2020-08-29 21:01:01 +02:00
import 'package:intl/intl.dart';
2021-01-24 20:01:55 +01:00
import 'package:lemmy_api_client/v2.dart';
2020-08-27 23:27:27 +02:00
import 'package:timeago/timeago.dart' as timeago;
2020-09-09 21:23:48 +02:00
import 'package:url_launcher/url_launcher.dart' as ul;
2020-08-27 23:27:27 +02:00
2020-09-17 16:46:40 +02:00
import '../hooks/delayed_loading.dart';
import '../hooks/logged_in_action.dart';
import '../pages/full_post.dart';
2020-09-02 23:05:34 +02:00
import '../url_launcher.dart';
2021-01-02 01:13:13 +01:00
import '../util/extensions/api.dart';
import '../util/goto.dart';
import '../util/more_icon.dart';
2020-09-09 21:23:48 +02:00
import 'bottom_modal.dart';
import 'fullscreenable_image.dart';
2020-10-26 18:47:49 +01:00
import 'info_table_popup.dart';
import 'markdown_text.dart';
2020-09-17 22:50:18 +02:00
import 'save_post_button.dart';
2020-08-29 21:01:01 +02:00
enum MediaType {
image,
gallery,
video,
other,
none,
2020-08-29 21:01:01 +02:00
}
MediaType whatType(String url) {
if (url == null || url.isEmpty) return MediaType.none;
2020-08-29 21:01:01 +02:00
2020-08-30 19:29:12 +02:00
// TODO: make detection more nuanced
2020-09-11 19:21:59 +02:00
if (url.endsWith('.jpg') ||
url.endsWith('.jpeg') ||
url.endsWith('.png') ||
url.endsWith('.gif') ||
url.endsWith('.webp') ||
url.endsWith('.bmp') ||
url.endsWith('.wbpm')) {
2020-08-29 21:01:01 +02:00
return MediaType.image;
}
return MediaType.other;
}
2020-09-30 19:05:00 +02:00
/// A post overview card
2021-01-24 20:01:55 +01:00
class PostWidget extends HookWidget {
2020-08-27 23:27:27 +02:00
final PostView post;
final String instanceHost;
final bool fullPost;
2020-08-27 23:27:27 +02:00
2021-01-24 20:01:55 +01:00
PostWidget(this.post, {this.fullPost = false})
: instanceHost = post.instanceHost;
2020-08-27 23:27:27 +02:00
// == ACTIONS ==
2020-09-09 21:23:48 +02:00
static void showMoreMenu(BuildContext context, PostView post) {
showModalBottomSheet(
backgroundColor: Colors.transparent,
context: context,
builder: (context) => BottomModal(
child: Column(
children: [
ListTile(
2021-01-03 19:43:39 +01:00
leading: const Icon(Icons.open_in_browser),
title: const Text('Open in browser'),
2021-01-24 20:01:55 +01:00
onTap: () async => await ul.canLaunch(post.post.apId)
? ul.launch(post.post.apId)
2020-09-09 21:23:48 +02:00
: Scaffold.of(context).showSnackBar(
2021-01-03 19:43:39 +01:00
const SnackBar(content: Text("can't open in browser"))),
2020-09-09 21:23:48 +02:00
),
ListTile(
2021-01-03 19:43:39 +01:00
leading: const Icon(Icons.info_outline),
title: const Text('Nerd stuff'),
2020-09-09 21:23:48 +02:00
onTap: () {
2020-10-26 18:47:49 +01:00
showInfoTablePopup(context, {
2021-01-24 20:01:55 +01:00
'id': post.post.id,
'apId': post.post.apId,
'upvotes': post.counts.upvotes,
'downvotes': post.counts.downvotes,
'score': post.counts.score,
'% of upvotes':
2021-01-24 20:01:55 +01:00
'''${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%''',
'local': post.post.local,
'published': post.post.published,
'updated': post.post.updated ?? 'never',
2020-10-26 18:47:49 +01:00
});
2020-09-09 21:23:48 +02:00
},
),
],
),
),
);
}
// == UI ==
2020-08-27 23:27:27 +02:00
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
void _openLink() => linkLauncher(
2021-01-24 20:01:55 +01:00
context: context, url: post.post.url, instanceHost: instanceHost);
2020-09-09 20:52:31 +02:00
final urlDomain = () {
2021-01-24 20:01:55 +01:00
if (whatType(post.post.url) == MediaType.none) return null;
2020-09-09 20:52:31 +02:00
2021-01-24 20:01:55 +01:00
final url = post.post.url.split('/')[2]; // TODO: change to Url(str).host
2020-09-09 20:52:31 +02:00
if (url.startsWith('www.')) return url.substring(4);
return url;
}();
/// assemble info section
Widget info() => Column(children: [
Padding(
padding: const EdgeInsets.all(10),
child: Row(children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
2021-01-24 20:01:55 +01:00
if (post.community.icon != null)
Padding(
padding: const EdgeInsets.only(right: 10),
child: InkWell(
onTap: () => goToCommunity.byId(
2021-01-24 20:01:55 +01:00
context, instanceHost, post.community.id),
child: SizedBox(
height: 40,
width: 40,
child: CachedNetworkImage(
imageBuilder: (context, imageProvider) => Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(
fit: BoxFit.cover,
image: imageProvider,
),
),
2020-08-27 23:27:27 +02:00
),
2021-01-24 20:01:55 +01:00
imageUrl: post.community.icon,
errorWidget: (context, url, error) =>
Text(error.toString()),
2020-08-27 23:27:27 +02:00
),
),
),
),
],
),
Column(
2021-01-03 18:13:25 +01:00
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
RichText(
overflow: TextOverflow.ellipsis, // TODO: fix overflowing
text: TextSpan(
style: TextStyle(
fontSize: 15,
color: theme.textTheme.bodyText1.color),
children: [
2021-01-03 19:43:39 +01:00
const TextSpan(
text: '!',
style: TextStyle(fontWeight: FontWeight.w300)),
TextSpan(
2021-01-24 20:01:55 +01:00
text: post.community.name,
2021-01-03 19:43:39 +01:00
style:
const TextStyle(fontWeight: FontWeight.w600),
recognizer: TapGestureRecognizer()
..onTap = () => goToCommunity.byId(
2021-01-24 20:01:55 +01:00
context, instanceHost, post.community.id)),
2021-01-03 19:43:39 +01:00
const TextSpan(
text: '@',
style: TextStyle(fontWeight: FontWeight.w300)),
TextSpan(
2021-01-02 01:13:13 +01:00
text: post.originInstanceHost,
2021-01-03 19:43:39 +01:00
style:
const TextStyle(fontWeight: FontWeight.w600),
recognizer: TapGestureRecognizer()
2021-01-02 01:13:13 +01:00
..onTap = () => goToInstance(
context, post.originInstanceHost)),
],
),
)
]),
Row(children: [
RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: TextStyle(
fontSize: 13,
color: theme.textTheme.bodyText1.color),
children: [
2021-01-03 19:43:39 +01:00
const TextSpan(
text: 'by',
style: TextStyle(fontWeight: FontWeight.w300)),
TextSpan(
text:
2021-01-24 20:01:55 +01:00
''' ${post.creator.preferredUsername ?? post.creator.name}''',
2021-01-03 19:43:39 +01:00
style:
const TextStyle(fontWeight: FontWeight.w600),
recognizer: TapGestureRecognizer()
..onTap = () => goToUser.byId(
2021-01-24 20:01:55 +01:00
context,
post.instanceHost,
post.creator.id,
),
),
TextSpan(
text:
2021-01-24 20:01:55 +01:00
''' · ${timeago.format(post.post.published, locale: 'en_short')}'''),
if (post.post.locked) const TextSpan(text: ' · 🔒'),
if (post.post.stickied)
const TextSpan(text: ' · 📌'),
if (post.post.nsfw) const TextSpan(text: ' · '),
if (post.post.nsfw)
2021-01-03 19:43:39 +01:00
const TextSpan(
text: 'NSFW',
style: TextStyle(color: Colors.red)),
2020-09-09 20:52:31 +02:00
if (urlDomain != null)
TextSpan(text: ' · $urlDomain'),
2021-01-24 20:01:55 +01:00
if (post.post.removed)
2021-01-03 19:43:39 +01:00
const TextSpan(text: ' · REMOVED'),
2021-01-24 20:01:55 +01:00
if (post.post.deleted)
2021-01-03 19:43:39 +01:00
const TextSpan(text: ' · DELETED'),
],
))
]),
],
),
2021-01-03 19:43:39 +01:00
const Spacer(),
if (!fullPost)
Column(
children: [
IconButton(
2020-09-09 21:23:48 +02:00
onPressed: () => showMoreMenu(context, post),
icon: Icon(moreIcon),
2021-01-03 19:43:39 +01:00
padding: const EdgeInsets.all(0),
visualDensity: VisualDensity.compact,
)
],
)
]),
),
]);
/// assemble title section
Widget title() => Padding(
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10),
child: Row(
children: [
2020-09-09 20:52:31 +02:00
Expanded(
flex: 100,
child: Text(
2021-01-24 20:01:55 +01:00
post.post.name,
textAlign: TextAlign.left,
softWrap: true,
2021-01-03 19:43:39 +01:00
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600),
),
),
2021-01-24 20:01:55 +01:00
if (whatType(post.post.url) == MediaType.other &&
post.post.thumbnailUrl != null) ...[
2021-01-03 19:43:39 +01:00
const Spacer(),
InkWell(
onTap: _openLink,
child: Stack(children: [
ClipRRect(
2021-01-03 19:43:39 +01:00
borderRadius: BorderRadius.circular(20),
child: CachedNetworkImage(
2021-01-24 20:01:55 +01:00
imageUrl: post.post.thumbnailUrl,
2021-01-03 19:43:39 +01:00
width: 70,
height: 70,
fit: BoxFit.cover,
errorWidget: (context, url, error) =>
Text(error.toString()),
),
),
const Positioned(
top: 8,
right: 8,
child: Icon(
Icons.launch,
size: 20,
),
)
]),
)
2020-09-09 20:52:31 +02:00
]
],
),
);
2020-08-27 23:27:27 +02:00
/// assemble link preview
Widget linkPreview() {
2021-01-24 20:01:55 +01:00
assert(post.post.url != null);
2020-08-27 23:27:27 +02:00
return Padding(
padding: const EdgeInsets.all(10),
child: InkWell(
onTap: _openLink,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Theme.of(context).iconTheme.color.withAlpha(170)),
borderRadius: BorderRadius.circular(5)),
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
children: [
Row(children: [
2021-01-03 19:43:39 +01:00
const Spacer(),
2020-09-09 20:52:31 +02:00
Text('$urlDomain ',
style: theme.textTheme.caption
.apply(fontStyle: FontStyle.italic)),
2021-01-03 19:43:39 +01:00
const Icon(Icons.launch, size: 12),
]),
Row(children: [
Flexible(
2021-01-24 20:01:55 +01:00
child: Text(post.post.embedTitle ?? '',
style: theme.textTheme.subtitle1
.apply(fontWeightDelta: 2)))
]),
2021-01-24 20:01:55 +01:00
if (post.post.embedDescription != null &&
post.post.embedDescription.isNotEmpty)
Row(children: [
2021-01-24 20:01:55 +01:00
Flexible(child: Text(post.post.embedDescription))
]),
],
),
2020-08-27 23:27:27 +02:00
),
),
),
);
}
2020-08-27 23:27:27 +02:00
/// assemble image
Widget postImage() {
2021-01-24 20:01:55 +01:00
assert(post.post.url != null);
return FullscreenableImage(
2021-01-24 20:01:55 +01:00
url: post.post.url,
child: CachedNetworkImage(
2021-01-24 20:01:55 +01:00
imageUrl: post.post.url,
2021-01-03 19:43:39 +01:00
errorWidget: (_, __, ___) => const Icon(Icons.warning),
progressIndicatorBuilder: (context, url, progress) =>
CircularProgressIndicator(value: progress.progress),
),
);
}
2020-08-29 21:01:01 +02:00
/// assemble actions section
Widget actions() => Padding(
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
child: Row(
children: [
2021-01-03 19:43:39 +01:00
const Icon(Icons.comment),
Expanded(
flex: 999,
child: Text(
2021-01-24 20:01:55 +01:00
' ${NumberFormat.compact().format(post.counts.comments)}'
' comment${post.counts.comments == 1 ? '' : 's'}',
overflow: TextOverflow.fade,
softWrap: false,
),
),
2021-01-03 19:43:39 +01:00
const Spacer(),
if (!fullPost)
IconButton(
2021-01-03 19:43:39 +01:00
icon: const Icon(Icons.share),
2021-01-24 20:01:55 +01:00
onPressed: () => Share.text(
'Share post url',
post.post.apId,
'text/plain')), // TODO: find a way to mark it as url
2020-09-17 22:50:18 +02:00
if (!fullPost) SavePostButton(post),
2020-09-17 16:46:40 +02:00
_Voting(post),
],
),
);
return Container(
decoration: BoxDecoration(
2021-01-03 19:43:39 +01:00
boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black54)],
color: theme.cardColor,
2021-01-03 19:43:39 +01:00
borderRadius: const BorderRadius.all(Radius.circular(20)),
),
child: InkWell(
onTap: fullPost
? null
: () => goTo(context, (context) => FullPostPage.fromPostView(post)),
child: Column(
children: [
info(),
title(),
2021-01-24 20:01:55 +01:00
if (whatType(post.post.url) != MediaType.other &&
whatType(post.post.url) != MediaType.none)
postImage()
2021-01-24 20:01:55 +01:00
else if (post.post.url != null && post.post.url.isNotEmpty)
linkPreview(),
2021-01-24 20:01:55 +01:00
if (post.post.body != null)
2020-09-30 19:37:56 +02:00
// TODO: trim content
Padding(
padding: const EdgeInsets.all(10),
2021-01-24 20:01:55 +01:00
child:
MarkdownText(post.post.body, instanceHost: instanceHost)),
actions(),
],
),
2020-08-27 23:27:27 +02:00
),
);
}
}
2020-09-17 16:46:40 +02:00
class _Voting extends HookWidget {
final PostView post;
final bool wasVoted;
_Voting(this.post)
: wasVoted = (post.myVote ?? VoteType.none) != VoteType.none;
2020-09-17 16:46:40 +02:00
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
2020-09-18 22:07:48 +02:00
final myVote = useState(post.myVote ?? VoteType.none);
2021-01-03 19:43:39 +01:00
final loading = useDelayedLoading();
final loggedInAction = useLoggedInAction(post.instanceHost);
2020-09-17 16:46:40 +02:00
vote(VoteType vote, Jwt token) async {
2021-01-24 20:01:55 +01:00
final api = LemmyApiV2(post.instanceHost);
2020-09-17 16:46:40 +02:00
loading.start();
try {
2021-01-24 20:01:55 +01:00
final res = await api.run(
CreatePostLike(postId: post.post.id, score: vote, auth: token.raw));
2020-09-17 16:46:40 +02:00
myVote.value = res.myVote;
2020-09-18 16:16:39 +02:00
// ignore: avoid_catches_without_on_clauses
2020-09-17 16:46:40 +02:00
} catch (e) {
Scaffold.of(context)
2021-01-03 19:43:39 +01:00
.showSnackBar(const SnackBar(content: Text('voting failed :(')));
2020-09-17 16:46:40 +02:00
return;
}
loading.cancel();
}
return Row(
children: [
IconButton(
icon: Icon(
Icons.arrow_upward,
2020-09-18 22:07:48 +02:00
color: myVote.value == VoteType.up ? theme.accentColor : null,
),
onPressed: loggedInAction(
2020-09-18 22:07:48 +02:00
(token) => vote(
myVote.value == VoteType.up ? VoteType.none : VoteType.up,
token,
),
)),
2020-09-17 16:46:40 +02:00
if (loading.loading)
2021-01-03 19:43:39 +01:00
const SizedBox(
width: 20, height: 20, child: CircularProgressIndicator())
2020-09-17 16:46:40 +02:00
else
Text(NumberFormat.compact()
2021-01-24 20:01:55 +01:00
.format(post.counts.score + (wasVoted ? 0 : myVote.value.value))),
2020-09-17 16:46:40 +02:00
IconButton(
icon: Icon(
Icons.arrow_downward,
2020-09-18 22:07:48 +02:00
color: myVote.value == VoteType.down ? Colors.red : null,
),
onPressed: loggedInAction(
(token) => vote(
2020-09-18 22:07:48 +02:00
myVote.value == VoteType.down ? VoteType.none : VoteType.down,
token,
),
)),
2020-09-17 16:46:40 +02:00
],
);
}
}