lemmur-app-android/lib/pages/community.dart

524 lines
17 KiB
Dart
Raw Normal View History

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
2021-04-05 20:14:39 +02:00
import 'package:lemmy_api_client/v3.dart';
2020-09-09 22:04:50 +02:00
import 'package:url_launcher/url_launcher.dart' as ul;
2020-09-17 00:24:49 +02:00
import '../hooks/delayed_loading.dart';
import '../hooks/logged_in_action.dart';
import '../hooks/memo_future.dart';
2020-09-17 00:24:49 +02:00
import '../hooks/stores.dart';
2021-03-01 14:21:45 +01:00
import '../l10n/l10n.dart';
2021-01-02 01:13:13 +01:00
import '../util/extensions/api.dart';
2021-02-09 15:12:13 +01:00
import '../util/extensions/spaced.dart';
import '../util/goto.dart';
import '../util/intl.dart';
import '../util/more_icon.dart';
2021-03-18 19:24:29 +01:00
import '../util/share.dart';
2021-02-18 09:19:00 +01:00
import '../widgets/avatar.dart';
2020-09-09 22:04:50 +02:00
import '../widgets/bottom_modal.dart';
import '../widgets/fullscreenable_image.dart';
2020-10-26 18:47:49 +01:00
import '../widgets/info_table_popup.dart';
import '../widgets/markdown_text.dart';
2021-02-24 20:52:18 +01:00
import '../widgets/reveal_after_scroll.dart';
2020-09-29 10:53:40 +02:00
import '../widgets/sortable_infinite_list.dart';
import 'create_post.dart';
2021-02-21 16:29:12 +01:00
import 'modlog_page.dart';
2020-09-30 19:05:00 +02:00
/// Displays posts, comments, and general info about the given community
class CommunityPage extends HookWidget {
final CommunityView? _community;
final String instanceHost;
final String? communityName;
final int? communityId;
2021-01-03 18:21:56 +01:00
const CommunityPage.fromName({
required String this.communityName,
required this.instanceHost,
}) : communityId = null,
2020-09-07 21:42:04 +02:00
_community = null;
2021-01-03 18:21:56 +01:00
const CommunityPage.fromId({
required int this.communityId,
required this.instanceHost,
}) : communityName = null,
_community = null;
CommunityPage.fromCommunityView(CommunityView this._community)
: instanceHost = _community.instanceHost,
2021-01-24 20:01:55 +01:00
communityId = _community.community.id,
communityName = _community.community.name;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
2020-09-16 23:15:42 +02:00
final accountsStore = useAccountsStore();
2021-02-24 20:52:18 +01:00
final scrollController = useScrollController();
2020-09-16 23:15:42 +02:00
2020-09-16 23:22:04 +02:00
final fullCommunitySnap = useMemoFuture(() {
final token = accountsStore.defaultTokenFor(instanceHost);
if (communityId != null) {
2021-04-05 20:14:39 +02:00
return LemmyApiV3(instanceHost).run(GetCommunity(
2021-01-24 20:01:55 +01:00
id: communityId,
auth: token?.raw,
));
} else {
2021-04-05 20:14:39 +02:00
return LemmyApiV3(instanceHost).run(GetCommunity(
2021-01-24 20:01:55 +01:00
name: communityName,
auth: token?.raw,
));
}
});
final community = () {
if (fullCommunitySnap.hasData) {
return fullCommunitySnap.data!.communityView;
} else if (_community != null) {
return _community;
} else {
return null;
}
}();
2020-09-09 22:04:50 +02:00
// FALLBACK
if (community == null) {
return Scaffold(
2021-02-09 15:12:13 +01:00
appBar: AppBar(),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (fullCommunitySnap.hasError) ...[
2021-01-03 19:43:39 +01:00
const Icon(Icons.error),
Padding(
padding: const EdgeInsets.all(8),
child: Text('ERROR: ${fullCommunitySnap.error}'),
)
] else
2021-01-03 19:43:39 +01:00
const CircularProgressIndicator(semanticsLabel: 'loading')
],
),
),
);
}
2020-09-09 22:04:50 +02:00
// FUNCTIONS
2021-03-20 15:50:49 +01:00
void _share() => share(community.community.actorId, context: context);
2020-09-09 22:04:50 +02:00
2020-09-10 15:18:24 +02:00
void _openMoreMenu() {
2021-02-09 15:12:13 +01:00
showBottomModal(
2020-09-10 15:18:24 +02:00
context: context,
2021-02-09 15:12:13 +01:00
builder: (context) => Column(
children: [
ListTile(
leading: const Icon(Icons.open_in_browser),
title: const Text('Open in browser'),
onTap: () async => await ul.canLaunch(community.community.actorId)
? ul.launch(community.community.actorId)
2021-03-10 08:34:30 +01:00
: ScaffoldMessenger.of(context).showSnackBar(
2021-02-09 15:12:13 +01:00
const SnackBar(content: Text("can't open in browser"))),
),
ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('Nerd stuff'),
onTap: () {
2021-02-24 20:52:18 +01:00
showInfoTablePopup(context: context, table: community.toJson());
2021-02-09 15:12:13 +01:00
},
),
],
2020-09-09 22:04:50 +02:00
),
);
}
return Scaffold(
floatingActionButton: CreatePostFab(community: community),
body: DefaultTabController(
length: 3,
child: NestedScrollView(
2021-02-24 20:52:18 +01:00
controller: scrollController,
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
SliverAppBar(
2021-02-09 15:12:13 +01:00
expandedHeight: community.community.icon == null ? 220 : 300,
pinned: true,
backgroundColor: theme.cardColor,
2021-02-24 20:52:18 +01:00
title: RevealAfterScroll(
scrollController: scrollController,
after: community.community.icon == null ? 110 : 190,
fade: true,
child: Text(
community.community.displayName,
overflow: TextOverflow.fade,
softWrap: false,
),
),
actions: [
2021-01-03 19:43:39 +01:00
IconButton(icon: const Icon(Icons.share), onPressed: _share),
IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu),
],
flexibleSpace: FlexibleSpaceBar(
2021-01-31 15:07:36 +01:00
background: _CommunityOverview(
community: community,
instanceHost: instanceHost,
onlineUsers: fullCommunitySnap.data?.online,
),
),
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
],
),
),
),
),
],
body: TabBarView(
children: [
InfinitePostList(
2020-09-29 10:53:40 +02:00
fetcher: (page, batchSize, sort) =>
2021-04-05 20:14:39 +02:00
LemmyApiV3(community.instanceHost).run(GetPosts(
2021-01-24 20:01:55 +01:00
type: PostListingType.community,
sort: sort,
communityId: community.community.id,
page: page,
limit: batchSize,
2021-04-05 20:14:39 +02:00
savedOnly: false,
2021-01-24 20:01:55 +01:00
)),
2020-09-29 10:53:40 +02:00
),
InfiniteCommentList(
fetcher: (page, batchSize, sortType) =>
2021-04-05 20:14:39 +02:00
LemmyApiV3(community.instanceHost).run(GetComments(
2021-01-24 20:01:55 +01:00
communityId: community.community.id,
auth: accountsStore
.defaultTokenFor(community.instanceHost)
?.raw,
type: CommentListingType.community,
sort: sortType,
limit: batchSize,
page: page,
2021-04-05 20:14:39 +02:00
savedOnly: false,
2021-01-24 20:01:55 +01:00
))),
2020-09-06 00:03:52 +02:00
_AboutTab(
community: community,
moderators: fullCommunitySnap.data?.moderators,
2021-01-31 15:07:36 +01:00
onlineUsers: fullCommunitySnap.data?.online,
),
],
),
),
),
);
}
}
class _CommunityOverview extends StatelessWidget {
final CommunityView community;
final String instanceHost;
final int? onlineUsers;
2021-01-31 15:07:36 +01:00
const _CommunityOverview({
required this.community,
required this.instanceHost,
required this.onlineUsers,
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final shadow = BoxShadow(color: theme.canvasColor, blurRadius: 5);
2021-01-24 20:01:55 +01:00
final icon = community.community.icon != null
? Stack(
alignment: Alignment.center,
children: [
Container(
width: 90,
height: 90,
decoration: BoxDecoration(
2021-02-09 15:12:13 +01:00
shape: BoxShape.circle,
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.7),
blurRadius: 3,
),
],
),
),
2021-02-18 09:19:00 +01:00
FullscreenableImage(
url: community.community.icon!,
2021-02-18 09:19:00 +01:00
child: Avatar(
2021-01-24 20:01:55 +01:00
url: community.community.icon,
2021-02-18 09:19:00 +01:00
radius: 83 / 2,
),
),
],
)
: null;
return Stack(children: [
2021-01-24 20:01:55 +01:00
if (community.community.banner != null)
2020-09-11 20:43:00 +02:00
FullscreenableImage(
url: community.community.banner!,
child: CachedNetworkImage(
imageUrl: community.community.banner!,
2021-01-03 19:43:39 +01:00
errorWidget: (_, __, ___) => const SizedBox.shrink(),
),
2020-09-11 20:43:00 +02:00
),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 45),
child: Column(children: [
if (icon != null) icon,
// NAME
Center(
2021-01-03 18:03:59 +01:00
child: Padding(
padding: const EdgeInsets.only(top: 10),
child: RichText(
overflow: TextOverflow.ellipsis, // TODO: fix overflowing
text: TextSpan(
style:
theme.textTheme.subtitle1?.copyWith(shadows: [shadow]),
2021-01-03 18:03:59 +01:00
children: [
2021-01-03 19:43:39 +01:00
const TextSpan(
2021-01-03 18:03:59 +01:00
text: '!',
style: TextStyle(fontWeight: FontWeight.w200)),
TextSpan(
2021-01-24 20:01:55 +01:00
text: community.community.name,
2021-01-03 19:43:39 +01:00
style: const TextStyle(fontWeight: FontWeight.w600)),
const TextSpan(
2021-01-03 18:03:59 +01:00
text: '@',
style: TextStyle(fontWeight: FontWeight.w200)),
TextSpan(
text: community.community.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, community.community.originInstanceHost),
2021-01-03 18:03:59 +01:00
),
],
),
),
),
2021-01-03 18:03:59 +01:00
),
// TITLE/MOTTO
Center(
2021-02-09 15:12:13 +01:00
child: Padding(
padding: const EdgeInsets.only(top: 8, left: 20, right: 20),
child: Text(
community.community.title,
textAlign: TextAlign.center,
style:
TextStyle(fontWeight: FontWeight.w300, shadows: [shadow]),
),
),
2021-02-09 15:12:13 +01:00
),
Padding(
padding: const EdgeInsets.only(top: 20),
child: Stack(
children: [
// INFO ICONS
Padding(
padding: const EdgeInsets.only(top: 5),
child: Row(
children: [
2021-01-03 19:43:39 +01:00
const Spacer(),
const Padding(
padding: EdgeInsets.only(right: 3),
child: Icon(Icons.people, size: 20),
),
2021-01-24 20:01:55 +01:00
Text(compactNumber(community.counts.subscribers)),
2021-02-09 15:12:13 +01:00
const Spacer(flex: 4),
2021-01-03 19:43:39 +01:00
const Padding(
padding: EdgeInsets.only(right: 3),
child: Icon(Icons.record_voice_over, size: 20),
),
2021-01-31 15:07:36 +01:00
Text(onlineUsers == null
? 'xx'
: compactNumber(onlineUsers!)),
2021-01-03 19:43:39 +01:00
const Spacer(),
],
),
),
_FollowButton(community),
],
),
),
]),
),
),
]);
}
}
2020-09-06 00:03:52 +02:00
class _AboutTab extends StatelessWidget {
final CommunityView community;
final List<CommunityModeratorView>? moderators;
final int? onlineUsers;
2020-09-06 00:03:52 +02:00
const _AboutTab({
Key? key,
required this.community,
required this.moderators,
required this.onlineUsers,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ListView(
2021-01-03 19:43:39 +01:00
padding: const EdgeInsets.only(top: 20),
children: [
2021-01-24 20:01:55 +01:00
if (community.community.description != null) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: MarkdownText(community.community.description!,
instanceHost: community.instanceHost),
),
2021-01-03 19:43:39 +01:00
const _Divider(),
],
SizedBox(
2021-02-09 15:12:13 +01:00
height: 32,
child: ListView(
scrollDirection: Axis.horizontal,
2021-02-09 15:12:13 +01:00
padding: const EdgeInsets.symmetric(horizontal: 15),
children: [
2021-02-09 15:12:13 +01:00
Chip(
label: Text(L10n.of(context)!
2021-03-03 13:16:05 +01:00
.number_of_users_online(onlineUsers ?? 0))),
Chip(
label: Text(L10n.of(context)!
2021-03-03 13:16:05 +01:00
.number_of_subscribers(community.counts.subscribers))),
2021-02-09 15:12:13 +01:00
Chip(
label: Text(
'${community.counts.posts} post${pluralS(community.counts.posts)}')),
Chip(
label: Text(
'${community.counts.comments} comment${pluralS(community.counts.comments)}')),
].spaced(8),
),
),
2021-01-03 19:43:39 +01:00
const _Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
2021-02-09 15:12:13 +01:00
child: OutlinedButton(
2021-02-16 22:39:46 +01:00
onPressed: () => goTo(
context,
(context) => ModlogPage.forCommunity(
instanceHost: community.instanceHost,
communityId: community.community.id,
communityName: community.community.name,
),
),
child: Text(L10n.of(context)!.modlog),
),
),
2021-01-03 19:43:39 +01:00
const _Divider(),
if (moderators != null && moderators!.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Text('Mods:', style: theme.textTheme.subtitle2),
),
for (final mod in moderators!)
2021-01-24 20:01:55 +01:00
// TODO: add user picture, maybe make it into reusable component
ListTile(
2021-01-24 20:01:55 +01:00
title: Text(
mod.moderator.preferredUsername ?? '@${mod.moderator.name}'),
2021-04-05 20:14:39 +02:00
onTap: () => goToUser.fromPersonSafe(context, mod.moderator),
),
]
],
);
}
}
class _Divider extends StatelessWidget {
2021-01-03 19:43:39 +01:00
const _Divider();
@override
2021-01-03 19:43:39 +01:00
Widget build(BuildContext context) => const Padding(
padding: EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Divider(),
);
}
class _FollowButton extends HookWidget {
final CommunityView community;
2021-01-03 18:21:56 +01:00
const _FollowButton(this.community);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
2020-09-16 01:23:57 +02:00
final isSubbed = useState(community.subscribed);
2021-02-09 15:12:13 +01:00
final delayed = useDelayedLoading(Duration.zero);
final loggedInAction = useLoggedInAction(community.instanceHost);
subscribe(Jwt token) async {
2020-09-16 23:29:14 +02:00
delayed.start();
try {
2021-04-05 20:14:39 +02:00
await LemmyApiV3(community.instanceHost).run(FollowCommunity(
2021-01-24 20:01:55 +01:00
communityId: community.community.id,
follow: !isSubbed.value,
2021-01-24 20:01:55 +01:00
auth: token.raw));
isSubbed.value = !isSubbed.value;
// ignore: avoid_catches_without_on_clauses
} catch (e) {
2021-03-10 08:34:30 +01:00
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Row(
children: [
2021-01-03 19:43:39 +01:00
const Icon(Icons.warning),
const SizedBox(width: 10),
Text("couldn't ${isSubbed.value ? 'un' : ''}sub :<"),
],
),
));
}
2020-09-16 23:29:14 +02:00
delayed.cancel();
}
2021-02-09 15:12:13 +01:00
return ElevatedButtonTheme(
data: ElevatedButtonThemeData(
style: theme.elevatedButtonTheme.style?.copyWith(
2021-02-09 15:12:13 +01:00
shape: MaterialStateProperty.all(const StadiumBorder()),
textStyle: MaterialStateProperty.all(theme.textTheme.subtitle1),
),
),
child: Center(
child: SizedBox(
height: 27,
width: 160,
child: delayed.loading
? const ElevatedButton(
onPressed: null,
child: SizedBox(
height: 15,
width: 15,
child: CircularProgressIndicator(),
),
)
: ElevatedButton.icon(
onPressed:
loggedInAction(delayed.pending ? (_) {} : subscribe),
icon: isSubbed.value
? const Icon(Icons.remove, size: 18)
: const Icon(Icons.add, size: 18),
2021-03-03 13:16:05 +01:00
label: Text(isSubbed.value
? L10n.of(context)!.unsubscribe
: L10n.of(context)!.subscribe),
),
2021-02-09 15:12:13 +01:00
),
),
);
}
}