add inbox page + other stuff (#164)
This commit is contained in:
parent
5789cfb01d
commit
19b2688316
|
@ -4,6 +4,14 @@
|
|||
|
||||
- Added page with saved posts/comments. It can be accessed from the profile tab under the bookmark icon
|
||||
- Added modlog page. Can be visited in the context of an instance or community from the about tab
|
||||
- Added inbox page, that can be accessed by tapping bell in the home tab
|
||||
- Added abillity to send private messages
|
||||
|
||||
### Changed
|
||||
|
||||
- Titles on some pages, have an appear affect when scrolling down
|
||||
- Long pressing comments now have a ripple effect
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -2,5 +2,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||
|
||||
import '../widgets/infinite_scroll.dart';
|
||||
|
||||
InfiniteScrollController useInfiniteScrollController() =>
|
||||
useMemoized(() => InfiniteScrollController());
|
||||
InfiniteScrollController useInfiniteScrollController() {
|
||||
final controller = useMemoized(() => InfiniteScrollController());
|
||||
|
||||
useEffect(() => controller.dispose, []);
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ class AddAccountPage extends HookWidget {
|
|||
|
||||
useEffect(() {
|
||||
LemmyApiV2(selectedInstance.value)
|
||||
.run(GetSite())
|
||||
.run(const GetSite())
|
||||
.then((site) => icon.value = site.siteView.site.icon);
|
||||
return null;
|
||||
}, [selectedInstance.value]);
|
||||
|
|
|
@ -33,7 +33,8 @@ class AddInstancePage extends HookWidget {
|
|||
return;
|
||||
}
|
||||
try {
|
||||
icon.value = (await LemmyApiV2(inst).run(GetSite())).siteView.site.icon;
|
||||
icon.value =
|
||||
(await LemmyApiV2(inst).run(const GetSite())).siteView.site.icon;
|
||||
isSite.value = true;
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
|
|
|
@ -36,7 +36,7 @@ class CommunitiesTab extends HookWidget {
|
|||
final futures = accountsStore.loggedInInstances
|
||||
.map(
|
||||
(instanceHost) => LemmyApiV2(instanceHost)
|
||||
.run(GetSite())
|
||||
.run(const GetSite())
|
||||
.then((e) => e.siteView.site),
|
||||
)
|
||||
.toList();
|
||||
|
@ -211,9 +211,7 @@ class CommunitiesTab extends HookWidget {
|
|||
url: comm.community.icon,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'!${comm.community.name}${comm.community.local ? '' : '@${comm.community.originInstanceHost}'}',
|
||||
),
|
||||
Text(comm.community.originDisplayName),
|
||||
],
|
||||
),
|
||||
trailing: _CommunitySubscribeToggle(
|
||||
|
|
|
@ -20,6 +20,7 @@ import '../widgets/bottom_modal.dart';
|
|||
import '../widgets/fullscreenable_image.dart';
|
||||
import '../widgets/info_table_popup.dart';
|
||||
import '../widgets/markdown_text.dart';
|
||||
import '../widgets/reveal_after_scroll.dart';
|
||||
import '../widgets/sortable_infinite_list.dart';
|
||||
import 'create_post.dart';
|
||||
import 'modlog_page.dart';
|
||||
|
@ -54,6 +55,7 @@ class CommunityPage extends HookWidget {
|
|||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final accountsStore = useAccountsStore();
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final fullCommunitySnap = useMemoFuture(() {
|
||||
final token = accountsStore.defaultTokenFor(instanceHost);
|
||||
|
@ -125,12 +127,7 @@ class CommunityPage extends HookWidget {
|
|||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'id': community.community.id,
|
||||
'actorId': community.community.actorId,
|
||||
'created by': '@${community.creator.name}',
|
||||
'published': community.community.published,
|
||||
});
|
||||
showInfoTablePopup(context: context, table: community.toJson());
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -143,12 +140,22 @@ class CommunityPage extends HookWidget {
|
|||
body: DefaultTabController(
|
||||
length: 3,
|
||||
child: NestedScrollView(
|
||||
controller: scrollController,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
|
||||
SliverAppBar(
|
||||
expandedHeight: community.community.icon == null ? 220 : 300,
|
||||
pinned: true,
|
||||
backgroundColor: theme.cardColor,
|
||||
title: Text('!${community.community.name}'),
|
||||
title: RevealAfterScroll(
|
||||
scrollController: scrollController,
|
||||
after: community.community.icon == null ? 110 : 190,
|
||||
fade: true,
|
||||
child: Text(
|
||||
community.community.displayName,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.share), onPressed: _share),
|
||||
IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu),
|
||||
|
@ -437,8 +444,7 @@ class _AboutTab extends StatelessWidget {
|
|||
ListTile(
|
||||
title: Text(
|
||||
mod.moderator.preferredUsername ?? '@${mod.moderator.name}'),
|
||||
onTap: () =>
|
||||
goToUser.byId(context, mod.instanceHost, mod.moderator.id),
|
||||
onTap: () => goToUser.fromUserSafe(context, mod.moderator),
|
||||
),
|
||||
]
|
||||
],
|
||||
|
|
|
@ -15,6 +15,7 @@ import '../util/extensions/spaced.dart';
|
|||
import '../util/goto.dart';
|
||||
import '../util/pictrs.dart';
|
||||
import '../util/unawaited.dart';
|
||||
import '../widgets/markdown_mode_icon.dart';
|
||||
import '../widgets/markdown_text.dart';
|
||||
import '../widgets/radio_picker.dart';
|
||||
import 'full_post.dart';
|
||||
|
@ -263,7 +264,7 @@ class CreatePostPage extends HookWidget {
|
|||
leading: const CloseButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(showFancy.value ? Icons.build : Icons.brush),
|
||||
icon: markdownModeIcon(fancy: showFancy.value),
|
||||
onPressed: () => showFancy.value = !showFancy.value,
|
||||
),
|
||||
],
|
||||
|
|
|
@ -8,9 +8,11 @@ import 'package:lemmy_api_client/v2.dart';
|
|||
import '../hooks/logged_in_action.dart';
|
||||
import '../hooks/refreshable.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/extensions/api.dart';
|
||||
import '../util/more_icon.dart';
|
||||
import '../widgets/comment_section.dart';
|
||||
import '../widgets/post.dart';
|
||||
import '../widgets/reveal_after_scroll.dart';
|
||||
import '../widgets/save_post_button.dart';
|
||||
import '../widgets/write_comment.dart';
|
||||
|
||||
|
@ -31,6 +33,8 @@ class FullPostPage extends HookWidget {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accStore = useAccountsStore();
|
||||
final scrollController = useScrollController();
|
||||
|
||||
final fullPostRefreshable =
|
||||
useRefreshable(() => LemmyApiV2(instanceHost).run(GetPost(
|
||||
id: id,
|
||||
|
@ -85,7 +89,7 @@ class FullPostPage extends HookWidget {
|
|||
comment() async {
|
||||
final newComment = await showCupertinoModalPopup<CommentView>(
|
||||
context: context,
|
||||
builder: (_) => WriteComment.toPost(post),
|
||||
builder: (_) => WriteComment.toPost(post.post),
|
||||
);
|
||||
if (newComment != null) {
|
||||
newComments.value = [...newComments.value, newComment];
|
||||
|
@ -94,6 +98,15 @@ class FullPostPage extends HookWidget {
|
|||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: false,
|
||||
title: RevealAfterScroll(
|
||||
scrollController: scrollController,
|
||||
after: 65,
|
||||
child: Text(
|
||||
post.community.originDisplayName,
|
||||
overflow: TextOverflow.fade,
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.share), onPressed: sharePost),
|
||||
SavePostButton(post),
|
||||
|
@ -108,8 +121,10 @@ class FullPostPage extends HookWidget {
|
|||
body: RefreshIndicator(
|
||||
onRefresh: refresh,
|
||||
child: ListView(
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: [
|
||||
const SizedBox(height: 15),
|
||||
PostWidget(post, fullPost: true),
|
||||
if (fullPostRefreshable.snapshot.hasData)
|
||||
CommentSection(
|
||||
|
|
|
@ -32,8 +32,8 @@ class HomeTab extends HookWidget {
|
|||
final theme = Theme.of(context);
|
||||
final instancesIcons = useMemoFuture(() async {
|
||||
final instances = accStore.instances.toList(growable: false);
|
||||
final sites = await Future.wait(instances
|
||||
.map((e) => LemmyApiV2(e).run(GetSite()).catchError((e) => null)));
|
||||
final sites = await Future.wait(instances.map(
|
||||
(e) => LemmyApiV2(e).run(const GetSite()).catchError((e) => null)));
|
||||
|
||||
return {
|
||||
for (var i = 0; i < sites.length; i++)
|
||||
|
@ -229,7 +229,8 @@ class HomeTab extends HookWidget {
|
|||
child: Text(
|
||||
title,
|
||||
style: theme.appBarTheme.textTheme.headline6,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
|
|
|
@ -1,22 +1,392 @@
|
|||
import 'dart:math' show pi;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v2.dart';
|
||||
import 'package:matrix4_transform/matrix4_transform.dart';
|
||||
|
||||
import '../hooks/delayed_loading.dart';
|
||||
import '../hooks/infinite_scroll.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/delayed_action.dart';
|
||||
import '../util/extensions/api.dart';
|
||||
import '../util/extensions/datetime.dart';
|
||||
import '../util/goto.dart';
|
||||
import '../util/more_icon.dart';
|
||||
import '../widgets/bottom_modal.dart';
|
||||
import '../widgets/comment.dart';
|
||||
import '../widgets/infinite_scroll.dart';
|
||||
import '../widgets/info_table_popup.dart';
|
||||
import '../widgets/markdown_mode_icon.dart';
|
||||
import '../widgets/markdown_text.dart';
|
||||
import '../widgets/radio_picker.dart';
|
||||
import '../widgets/sortable_infinite_list.dart';
|
||||
import '../widgets/tile_action.dart';
|
||||
import 'write_message.dart';
|
||||
|
||||
class InboxPage extends HookWidget {
|
||||
const InboxPage();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
Widget build(BuildContext context) {
|
||||
final accStore = useAccountsStore();
|
||||
final selected = useState(accStore.defaultInstanceHost);
|
||||
final theme = Theme.of(context);
|
||||
final isc = useInfiniteScrollController();
|
||||
final unreadOnly = useState(true);
|
||||
|
||||
if (accStore.hasNoAccount) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'🚧 WORK IN PROGRESS 🚧',
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
)
|
||||
body: const Center(child: Text('no accounts added')),
|
||||
);
|
||||
}
|
||||
|
||||
toggleUnreadOnly() {
|
||||
unreadOnly.value = !unreadOnly.value;
|
||||
isc.clear();
|
||||
}
|
||||
|
||||
return DefaultTabController(
|
||||
length: 3,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: RadioPicker<String>(
|
||||
onChanged: (val) {
|
||||
selected.value = val;
|
||||
isc.clear();
|
||||
},
|
||||
title: 'select instance',
|
||||
groupValue: selected.value,
|
||||
buttonBuilder: (context, displayString, onPressed) => TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
),
|
||||
onPressed: onPressed,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
displayString,
|
||||
style: theme.appBarTheme.textTheme.headline6,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.arrow_drop_down),
|
||||
],
|
||||
),
|
||||
),
|
||||
values: accStore.loggedInInstances.toList(),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(unreadOnly.value ? Icons.mail : Icons.mail_outline),
|
||||
onPressed: toggleUnreadOnly,
|
||||
tooltip: unreadOnly.value ? 'show all' : 'show only unread',
|
||||
)
|
||||
],
|
||||
bottom: const TabBar(
|
||||
tabs: [
|
||||
Tab(text: 'Replies'),
|
||||
Tab(text: 'Mentions'),
|
||||
Tab(text: 'Messages'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
body: TabBarView(
|
||||
children: [
|
||||
SortableInfiniteList<CommentView>(
|
||||
noItems: const Text('no replies'),
|
||||
controller: isc,
|
||||
defaultSort: SortType.new_,
|
||||
fetcher: (page, batchSize, sortType) =>
|
||||
LemmyApiV2(selected.value).run(GetReplies(
|
||||
auth: accStore.defaultTokenFor(selected.value).raw,
|
||||
sort: sortType,
|
||||
limit: batchSize,
|
||||
page: page,
|
||||
unreadOnly: unreadOnly.value,
|
||||
)),
|
||||
itemBuilder: (cv) => CommentWidget.fromCommentView(
|
||||
cv,
|
||||
canBeMarkedAsRead: true,
|
||||
hideOnRead: unreadOnly.value,
|
||||
),
|
||||
),
|
||||
SortableInfiniteList<UserMentionView>(
|
||||
noItems: const Text('no mentions'),
|
||||
controller: isc,
|
||||
defaultSort: SortType.new_,
|
||||
fetcher: (page, batchSize, sortType) =>
|
||||
LemmyApiV2(selected.value).run(GetUserMentions(
|
||||
auth: accStore.defaultTokenFor(selected.value).raw,
|
||||
sort: sortType,
|
||||
limit: batchSize,
|
||||
page: page,
|
||||
unreadOnly: unreadOnly.value,
|
||||
)),
|
||||
itemBuilder: (umv) => CommentWidget.fromUserMentionView(
|
||||
umv,
|
||||
hideOnRead: unreadOnly.value,
|
||||
),
|
||||
),
|
||||
InfiniteScroll<PrivateMessageView>(
|
||||
noItems: const Padding(
|
||||
padding: EdgeInsets.only(top: 60),
|
||||
child: Text('no messages'),
|
||||
),
|
||||
controller: isc,
|
||||
fetcher: (page, batchSize) => LemmyApiV2(selected.value).run(
|
||||
GetPrivateMessages(
|
||||
auth: accStore.defaultTokenFor(selected.value).raw,
|
||||
limit: batchSize,
|
||||
page: page,
|
||||
unreadOnly: unreadOnly.value,
|
||||
),
|
||||
),
|
||||
itemBuilder: (mv) => PrivateMessageTile(
|
||||
privateMessageView: mv,
|
||||
hideOnRead: unreadOnly.value,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PrivateMessageTile extends HookWidget {
|
||||
final PrivateMessageView privateMessageView;
|
||||
final bool hideOnRead;
|
||||
|
||||
const PrivateMessageTile({
|
||||
@required this.privateMessageView,
|
||||
this.hideOnRead = false,
|
||||
}) : assert(privateMessageView != null),
|
||||
assert(hideOnRead != null);
|
||||
static const double _iconSize = 16;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accStore = useAccountsStore();
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final pmv = useState(privateMessageView);
|
||||
final raw = useState(false);
|
||||
final selectable = useState(false);
|
||||
final deleted = useState(pmv.value.privateMessage.deleted);
|
||||
final deleteDelayed = useDelayedLoading(const Duration(milliseconds: 250));
|
||||
final read = useState(pmv.value.privateMessage.read);
|
||||
final readDelayed = useDelayedLoading(const Duration(milliseconds: 200));
|
||||
|
||||
final toMe = useMemoized(() =>
|
||||
pmv.value.recipient.originInstanceHost == pmv.value.instanceHost &&
|
||||
pmv.value.recipient.id ==
|
||||
accStore.defaultTokenFor(pmv.value.instanceHost)?.payload?.id);
|
||||
|
||||
final otherSide =
|
||||
useMemoized(() => toMe ? pmv.value.creator : pmv.value.recipient);
|
||||
|
||||
void showMoreMenu() {
|
||||
showBottomModal(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
pop() => Navigator.of(context).pop();
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(raw.value ? 'Show fancy' : 'Show raw'),
|
||||
leading: markdownModeIcon(fancy: !raw.value),
|
||||
onTap: () {
|
||||
raw.value = !raw.value;
|
||||
pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Make ${selectable.value ? 'un' : ''}selectable'),
|
||||
leading: Icon(
|
||||
selectable.value ? Icons.assignment : Icons.content_cut),
|
||||
onTap: () {
|
||||
selectable.value = !selectable.value;
|
||||
pop();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text('Nerd stuff'),
|
||||
leading: const Icon(Icons.info_outline),
|
||||
onTap: () {
|
||||
pop();
|
||||
showInfoTablePopup(
|
||||
context: context, table: pmv.value.toJson());
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
handleDelete() => delayedAction<PrivateMessageView>(
|
||||
context: context,
|
||||
delayedLoading: deleteDelayed,
|
||||
instanceHost: pmv.value.instanceHost,
|
||||
query: DeletePrivateMessage(
|
||||
privateMessageId: pmv.value.privateMessage.id,
|
||||
auth: accStore.defaultTokenFor(pmv.value.instanceHost)?.raw,
|
||||
deleted: !deleted.value,
|
||||
),
|
||||
onSuccess: (val) => deleted.value = val.privateMessage.deleted,
|
||||
);
|
||||
|
||||
handleRead() => delayedAction<PrivateMessageView>(
|
||||
context: context,
|
||||
delayedLoading: readDelayed,
|
||||
instanceHost: pmv.value.instanceHost,
|
||||
query: MarkPrivateMessageAsRead(
|
||||
privateMessageId: pmv.value.privateMessage.id,
|
||||
auth: accStore.defaultTokenFor(pmv.value.instanceHost)?.raw,
|
||||
read: !read.value,
|
||||
),
|
||||
// TODO: add notification for notifying parent list
|
||||
onSuccess: (val) => read.value = val.privateMessage.read,
|
||||
);
|
||||
|
||||
if (hideOnRead && read.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final body = raw.value
|
||||
? selectable.value
|
||||
? SelectableText(pmv.value.privateMessage.content)
|
||||
: Text(pmv.value.privateMessage.content)
|
||||
: MarkdownText(
|
||||
pmv.value.privateMessage.content,
|
||||
instanceHost: pmv.value.instanceHost,
|
||||
selectable: selectable.value,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
toMe ? 'from ' : 'to ',
|
||||
style: TextStyle(color: theme.textTheme.caption.color),
|
||||
),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
onTap: () => goToUser.fromUserSafe(context, otherSide),
|
||||
child: Row(
|
||||
children: [
|
||||
if (otherSide.avatar != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: otherSide.avatar,
|
||||
height: 20,
|
||||
width: 20,
|
||||
imageBuilder: (context, imageProvider) => Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: imageProvider,
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget: (_, __, ___) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
otherSide.originDisplayName,
|
||||
style: TextStyle(color: theme.accentColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (pmv.value.privateMessage.updated != null) const Text('🖊 '),
|
||||
Text(pmv.value.privateMessage.updated?.fancy ??
|
||||
pmv.value.privateMessage.published.fancy),
|
||||
const SizedBox(width: 5),
|
||||
Transform(
|
||||
transform: Matrix4Transform()
|
||||
.rotateByCenter((toMe ? -1 : 1) * pi / 2,
|
||||
const Size(_iconSize, _iconSize))
|
||||
.flipVertically(
|
||||
origin: const Offset(_iconSize / 2, _iconSize / 2))
|
||||
.matrix4,
|
||||
child: const Opacity(
|
||||
opacity: 0.8,
|
||||
child: Icon(Icons.reply, size: _iconSize),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
if (pmv.value.privateMessage.deleted)
|
||||
const Text('deleted by creator',
|
||||
style: TextStyle(fontStyle: FontStyle.italic))
|
||||
else
|
||||
body,
|
||||
Row(children: [
|
||||
const Spacer(),
|
||||
TileAction(
|
||||
icon: moreIcon,
|
||||
onPressed: showMoreMenu,
|
||||
tooltip: 'more',
|
||||
),
|
||||
if (toMe) ...[
|
||||
TileAction(
|
||||
iconColor: read.value ? theme.accentColor : null,
|
||||
icon: Icons.check,
|
||||
tooltip: 'mark as read',
|
||||
onPressed: handleRead,
|
||||
delayedLoading: readDelayed,
|
||||
),
|
||||
TileAction(
|
||||
icon: Icons.reply,
|
||||
tooltip: 'reply',
|
||||
onPressed: () {
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (_) => WriteMessagePage.send(
|
||||
instanceHost: pmv.value.instanceHost,
|
||||
recipient: otherSide,
|
||||
));
|
||||
},
|
||||
)
|
||||
] else ...[
|
||||
TileAction(
|
||||
icon: Icons.edit,
|
||||
tooltip: 'edit',
|
||||
onPressed: () async {
|
||||
final val = await showCupertinoModalPopup<PrivateMessageView>(
|
||||
context: context,
|
||||
builder: (_) => WriteMessagePage.edit(pmv.value));
|
||||
if (pmv != null) pmv.value = val;
|
||||
},
|
||||
),
|
||||
TileAction(
|
||||
delayedLoading: deleteDelayed,
|
||||
icon: deleted.value ? Icons.restore : Icons.delete,
|
||||
tooltip: deleted.value ? 'restore' : 'delete',
|
||||
onPressed: handleDelete,
|
||||
),
|
||||
]
|
||||
]),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import '../widgets/bottom_modal.dart';
|
|||
import '../widgets/fullscreenable_image.dart';
|
||||
import '../widgets/info_table_popup.dart';
|
||||
import '../widgets/markdown_text.dart';
|
||||
import '../widgets/reveal_after_scroll.dart';
|
||||
import '../widgets/sortable_infinite_list.dart';
|
||||
import 'communities_list.dart';
|
||||
import 'modlog_page.dart';
|
||||
|
@ -33,8 +34,8 @@ class InstancePage extends HookWidget {
|
|||
|
||||
InstancePage({@required this.instanceHost})
|
||||
: assert(instanceHost != null),
|
||||
siteFuture = LemmyApiV2(instanceHost).run(GetSite()),
|
||||
communitiesFuture = LemmyApiV2(instanceHost).run(ListCommunities(
|
||||
siteFuture = LemmyApiV2(instanceHost).run(const GetSite()),
|
||||
communitiesFuture = LemmyApiV2(instanceHost).run(const ListCommunities(
|
||||
type: PostListingType.local, sort: SortType.hot, limit: 6));
|
||||
|
||||
@override
|
||||
|
@ -43,6 +44,7 @@ class InstancePage extends HookWidget {
|
|||
final siteSnap = useFuture(siteFuture);
|
||||
final colorOnCard = textColorBasedOnBackground(theme.cardColor);
|
||||
final accStore = useAccountsStore();
|
||||
final scrollController = useScrollController();
|
||||
|
||||
if (!siteSnap.hasData) {
|
||||
return Scaffold(
|
||||
|
@ -85,15 +87,7 @@ class InstancePage extends HookWidget {
|
|||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'url': instanceHost,
|
||||
'creator': '@${site.siteView.creator.name}',
|
||||
'version': site.version,
|
||||
'enableDownvotes': site.siteView.site.enableDownvotes,
|
||||
'enableNsfw': site.siteView.site.enableNsfw,
|
||||
'published': site.siteView.site.published,
|
||||
'updated': site.siteView.site.updated,
|
||||
});
|
||||
showInfoTablePopup(context: context, table: site.toJson());
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -105,14 +99,20 @@ class InstancePage extends HookWidget {
|
|||
body: DefaultTabController(
|
||||
length: 3,
|
||||
child: NestedScrollView(
|
||||
controller: scrollController,
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
|
||||
SliverAppBar(
|
||||
expandedHeight: 250,
|
||||
pinned: true,
|
||||
backgroundColor: theme.cardColor,
|
||||
title: Text(
|
||||
site.siteView.site.name,
|
||||
style: TextStyle(color: colorOnCard),
|
||||
title: RevealAfterScroll(
|
||||
after: 150,
|
||||
fade: true,
|
||||
scrollController: scrollController,
|
||||
child: Text(
|
||||
site.siteView.site.name,
|
||||
style: TextStyle(color: colorOnCard),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(icon: const Icon(Icons.share), onPressed: _share),
|
||||
|
@ -323,7 +323,7 @@ class _AboutTab extends HookWidget {
|
|||
subtitle: u.user.bio != null
|
||||
? MarkdownText(u.user.bio, instanceHost: instanceHost)
|
||||
: null,
|
||||
onTap: () => goToUser.byId(context, instanceHost, u.user.id),
|
||||
onTap: () => goToUser.fromUserSafe(context, u.user),
|
||||
leading: Avatar(url: u.user.avatar),
|
||||
),
|
||||
const _Divider(),
|
||||
|
|
|
@ -71,6 +71,8 @@ class MediaViewPage extends HookWidget {
|
|||
Colors.black.withOpacity(max(0, 1.0 - (offset.value.dy.abs() / 200))),
|
||||
appBar: showButtons.value
|
||||
? AppBar(
|
||||
brightness: Brightness.dark,
|
||||
iconTheme: const IconThemeData(color: Colors.white),
|
||||
backgroundColor: Colors.black38,
|
||||
leading: const CloseButton(),
|
||||
actions: [
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v2.dart';
|
||||
|
||||
import '../comment_tree.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../widgets/comment.dart';
|
||||
import '../widgets/post.dart';
|
||||
|
@ -106,10 +105,7 @@ class _SearchResultsList extends HookWidget {
|
|||
itemBuilder: (data) {
|
||||
switch (type) {
|
||||
case SearchType.comments:
|
||||
return CommentWidget(
|
||||
CommentTree(data as CommentView),
|
||||
postCreatorId: null,
|
||||
);
|
||||
return CommentWidget.fromCommentView(data as CommentView);
|
||||
case SearchType.communities:
|
||||
return CommunitiesListItem(community: data as CommunityView);
|
||||
case SearchType.posts:
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import 'package:esys_flutter_share/esys_flutter_share.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v2.dart';
|
||||
|
||||
import '../hooks/logged_in_action.dart';
|
||||
import '../widgets/user_profile.dart';
|
||||
import 'write_message.dart';
|
||||
|
||||
/// Page showing posts, comments, and general info about a user.
|
||||
class UserPage extends HookWidget {
|
||||
|
@ -43,15 +46,12 @@ class UserPage extends HookWidget {
|
|||
appBar: AppBar(
|
||||
actions: [
|
||||
if (userDetailsSnap.hasData) ...[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.email),
|
||||
onPressed: () {}, // TODO: go to messaging page
|
||||
),
|
||||
SendMessageButton(userDetailsSnap.data.userView.user),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.share),
|
||||
onPressed: () => Share.text('Share user',
|
||||
userDetailsSnap.data.userView.user.actorId, 'text/plain'),
|
||||
)
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
|
@ -59,3 +59,24 @@ class UserPage extends HookWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SendMessageButton extends HookWidget {
|
||||
final UserSafe user;
|
||||
|
||||
const SendMessageButton(this.user);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loggedInAction = useLoggedInAction(user.instanceHost);
|
||||
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.email),
|
||||
onPressed: loggedInAction((token) => showCupertinoModalPopup(
|
||||
context: context,
|
||||
builder: (_) => WriteMessagePage.send(
|
||||
instanceHost: user.instanceHost,
|
||||
recipient: user,
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ class UsersListItem extends StatelessWidget {
|
|||
),
|
||||
)
|
||||
: null,
|
||||
onTap: () => goToUser.byId(context, user.instanceHost, user.user.id),
|
||||
onTap: () => goToUser.fromUserSafe(context, user.user),
|
||||
leading: Avatar(url: user.user.avatar),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lemmy_api_client/v2.dart';
|
||||
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/extensions/api.dart';
|
||||
import '../widgets/markdown_mode_icon.dart';
|
||||
import '../widgets/markdown_text.dart';
|
||||
|
||||
/// Page for writing and editing a private message
|
||||
class WriteMessagePage extends HookWidget {
|
||||
final UserSafe recipient;
|
||||
final String instanceHost;
|
||||
|
||||
/// if it's non null then this page is used for edit
|
||||
final PrivateMessage privateMessage;
|
||||
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||
|
||||
final bool _isEdit;
|
||||
|
||||
WriteMessagePage.send({
|
||||
@required this.recipient,
|
||||
@required this.instanceHost,
|
||||
}) : assert(recipient != null),
|
||||
assert(instanceHost != null),
|
||||
privateMessage = null,
|
||||
_isEdit = false;
|
||||
|
||||
WriteMessagePage.edit(PrivateMessageView pmv)
|
||||
: privateMessage = pmv.privateMessage,
|
||||
recipient = pmv.recipient,
|
||||
instanceHost = pmv.instanceHost,
|
||||
_isEdit = true;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accStore = useAccountsStore();
|
||||
final showFancy = useState(false);
|
||||
final bodyController =
|
||||
useTextEditingController(text: privateMessage?.content);
|
||||
final loading = useState(false);
|
||||
|
||||
final submit = _isEdit ? 'save' : 'send';
|
||||
final title = _isEdit ? 'Edit message' : 'Send message';
|
||||
|
||||
handleSubmit() async {
|
||||
if (_isEdit) {
|
||||
loading.value = true;
|
||||
try {
|
||||
final msg = await LemmyApiV2(instanceHost).run(EditPrivateMessage(
|
||||
auth: accStore.defaultTokenFor(instanceHost)?.raw,
|
||||
privateMessageId: privateMessage.id,
|
||||
content: bodyController.text,
|
||||
));
|
||||
Navigator.of(context).pop(msg);
|
||||
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text(e.toString()),
|
||||
));
|
||||
}
|
||||
loading.value = false;
|
||||
} else {
|
||||
loading.value = true;
|
||||
try {
|
||||
await LemmyApiV2(instanceHost).run(CreatePrivateMessage(
|
||||
auth: accStore.defaultTokenFor(instanceHost)?.raw,
|
||||
content: bodyController.text,
|
||||
recipientId: recipient.id,
|
||||
));
|
||||
Navigator.of(context).pop();
|
||||
// TODO: maybe send notification so that infinite list
|
||||
// containing this widget adds new message?
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
scaffoldKey.currentState.showSnackBar(SnackBar(
|
||||
content: Text(e.toString()),
|
||||
));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final body = IndexedStack(
|
||||
index: showFancy.value ? 1 : 0,
|
||||
children: [
|
||||
TextField(
|
||||
controller: bodyController,
|
||||
keyboardType: TextInputType.multiline,
|
||||
maxLines: null,
|
||||
minLines: 5,
|
||||
autofocus: true,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: MarkdownText(
|
||||
bodyController.text,
|
||||
instanceHost: instanceHost,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
key: scaffoldKey,
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
leading: const CloseButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: markdownModeIcon(fancy: showFancy.value),
|
||||
onPressed: () => showFancy.value = !showFancy.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Text('to ${recipient.displayName}'),
|
||||
const SizedBox(height: 16),
|
||||
body,
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: loading.value ? () {} : handleSubmit,
|
||||
child: loading.value
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator())
|
||||
: Text(submit),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -29,7 +29,7 @@ class AccountsStore extends ChangeNotifier {
|
|||
|
||||
// I barely understand what I did. Long story short it casts a
|
||||
// raw json into a nested ObservableMap
|
||||
nestedMapsCast<T>(T f(Map<String, dynamic> json)) => HashMap.of(
|
||||
nestedMapsCast<T>(T f(String jwt)) => HashMap.of(
|
||||
(jsonDecode(prefs.getString(SharedPrefKeys.tokens) ??
|
||||
'{"lemmy.ml":{}}') as Map<String, dynamic>)
|
||||
?.map(
|
||||
|
@ -37,8 +37,7 @@ class AccountsStore extends ChangeNotifier {
|
|||
k,
|
||||
HashMap.of(
|
||||
(e as Map<String, dynamic>)?.map(
|
||||
(k, e) => MapEntry(
|
||||
k, e == null ? null : f(e as Map<String, dynamic>)),
|
||||
(k, e) => MapEntry(k, e == null ? null : f(e as String)),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -46,7 +45,7 @@ class AccountsStore extends ChangeNotifier {
|
|||
);
|
||||
|
||||
// set saved settings or create defaults
|
||||
_tokens = nestedMapsCast((json) => Jwt(json['raw'] as String));
|
||||
_tokens = nestedMapsCast((json) => Jwt(json));
|
||||
_defaultAccount = prefs.getString(SharedPrefKeys.defaultAccount);
|
||||
_defaultAccounts = HashMap.of(Map.castFrom(
|
||||
jsonDecode(prefs.getString(SharedPrefKeys.defaultAccounts) ?? 'null')
|
||||
|
@ -155,6 +154,14 @@ class AccountsStore extends ChangeNotifier {
|
|||
return _tokens[instanceHost][_defaultAccounts[instanceHost]];
|
||||
}
|
||||
|
||||
/// returns token for user of a certain id
|
||||
Jwt tokenForId(String instanceHost, int userId) =>
|
||||
_tokens.containsKey(instanceHost)
|
||||
? _tokens[instanceHost]
|
||||
.values
|
||||
.firstWhere((val) => val.payload.id == userId, orElse: () => null)
|
||||
: null;
|
||||
|
||||
Jwt tokenFor(String instanceHost, String username) {
|
||||
if (!usernamesFor(instanceHost).contains(username)) {
|
||||
return null;
|
||||
|
@ -243,7 +250,7 @@ class AccountsStore extends ChangeNotifier {
|
|||
|
||||
if (!assumeValid) {
|
||||
try {
|
||||
await LemmyApiV2(instanceHost).run(GetSite());
|
||||
await LemmyApiV2(instanceHost).run(const GetSite());
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (_) {
|
||||
throw Exception('This instance seems to not exist');
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lemmy_api_client/v2.dart';
|
||||
|
||||
import '../hooks/delayed_loading.dart';
|
||||
|
||||
/// Executes an API action that uses [DelayedLoading], has a try-catch
|
||||
/// that displays a [SnackBar] on the Scaffold.of(context) when the action fails
|
||||
Future<void> delayedAction<T>({
|
||||
@required BuildContext context,
|
||||
@required DelayedLoading delayedLoading,
|
||||
@required String instanceHost,
|
||||
@required LemmyApiQuery<T> query,
|
||||
Function(T) onSuccess,
|
||||
Function(T) cleanup,
|
||||
}) async {
|
||||
assert(delayedLoading != null);
|
||||
assert(instanceHost != null);
|
||||
assert(query != null);
|
||||
assert(context != null);
|
||||
|
||||
T val;
|
||||
try {
|
||||
delayedLoading.start();
|
||||
val = await LemmyApiV2(instanceHost).run<T>(query);
|
||||
onSuccess?.call(val);
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
Scaffold.of(context).showSnackBar(SnackBar(content: Text(e.toString())));
|
||||
}
|
||||
cleanup?.call(val);
|
||||
delayedLoading.cancel();
|
||||
}
|
|
@ -26,7 +26,13 @@ extension GetOriginInstanceCommentView on Comment {
|
|||
|
||||
String _extract(String url) => urlHost(url);
|
||||
|
||||
extension DisplayNames on UserSafe {
|
||||
extension CommunityDisplayNames on CommunitySafe {
|
||||
String get displayName => '!$name';
|
||||
String get originDisplayName =>
|
||||
local ? displayName : '!$name@$originInstanceHost';
|
||||
}
|
||||
|
||||
extension UserDisplayNames on UserSafe {
|
||||
String get displayName {
|
||||
if (preferredUsername != null && preferredUsername.isNotEmpty) {
|
||||
return preferredUsername;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lemmy_api_client/v2.dart';
|
||||
|
||||
import '../pages/community.dart';
|
||||
import '../pages/full_post.dart';
|
||||
|
@ -57,6 +58,9 @@ abstract class goToUser {
|
|||
static void byName(
|
||||
BuildContext context, String instanceHost, String userName) =>
|
||||
throw UnimplementedError('need to create UserProfile constructor first');
|
||||
|
||||
static void fromUserSafe(BuildContext context, UserSafe userSafe) =>
|
||||
goToUser.byId(context, userSafe.instanceHost, userSafe.id);
|
||||
}
|
||||
|
||||
void goToPost(BuildContext context, String instanceHost, int postId) => goTo(
|
||||
|
|
|
@ -24,8 +24,7 @@ class BottomModal extends StatelessWidget {
|
|||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Colors.grey.withOpacity(0.5),
|
||||
width: 0.2,
|
||||
color: Colors.grey.withOpacity(0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
|
|
|
@ -10,6 +10,7 @@ import '../comment_tree.dart';
|
|||
import '../hooks/delayed_loading.dart';
|
||||
import '../hooks/logged_in_action.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import '../util/delayed_action.dart';
|
||||
import '../util/extensions/api.dart';
|
||||
import '../util/extensions/datetime.dart';
|
||||
import '../util/goto.dart';
|
||||
|
@ -18,17 +19,20 @@ import '../util/text_color.dart';
|
|||
import 'avatar.dart';
|
||||
import 'bottom_modal.dart';
|
||||
import 'info_table_popup.dart';
|
||||
import 'markdown_mode_icon.dart';
|
||||
import 'markdown_text.dart';
|
||||
import 'tile_action.dart';
|
||||
import 'write_comment.dart';
|
||||
|
||||
/// A single comment that renders its replies
|
||||
class CommentWidget extends HookWidget {
|
||||
final int indent;
|
||||
final int postCreatorId;
|
||||
final int depth;
|
||||
final CommentTree commentTree;
|
||||
final bool detached;
|
||||
|
||||
final UserMentionView userMentionView;
|
||||
final bool wasVoted;
|
||||
final bool canBeMarkedAsRead;
|
||||
final bool hideOnRead;
|
||||
|
||||
static const colors = [
|
||||
Colors.pink,
|
||||
|
@ -40,39 +44,39 @@ class CommentWidget extends HookWidget {
|
|||
|
||||
CommentWidget(
|
||||
this.commentTree, {
|
||||
this.indent = 0,
|
||||
@required this.postCreatorId,
|
||||
this.depth = 0,
|
||||
this.detached = false,
|
||||
}) : wasVoted =
|
||||
(commentTree.comment.myVote ?? VoteType.none) != VoteType.none;
|
||||
this.canBeMarkedAsRead = false,
|
||||
this.hideOnRead = false,
|
||||
}) : wasVoted =
|
||||
(commentTree.comment.myVote ?? VoteType.none) != VoteType.none,
|
||||
userMentionView = null;
|
||||
|
||||
CommentWidget.fromCommentView(
|
||||
CommentView cv, {
|
||||
bool canBeMarkedAsRead = false,
|
||||
bool hideOnRead = false,
|
||||
}) : this(
|
||||
CommentTree(cv),
|
||||
detached: true,
|
||||
canBeMarkedAsRead: canBeMarkedAsRead,
|
||||
hideOnRead: hideOnRead,
|
||||
);
|
||||
|
||||
CommentWidget.fromUserMentionView(
|
||||
this.userMentionView, {
|
||||
this.hideOnRead = false,
|
||||
}) : commentTree =
|
||||
CommentTree(CommentView.fromJson(userMentionView.toJson())),
|
||||
depth = 0,
|
||||
wasVoted = (userMentionView.myVote ?? VoteType.none) != VoteType.none,
|
||||
detached = true,
|
||||
canBeMarkedAsRead = true;
|
||||
|
||||
_showCommentInfo(BuildContext context) {
|
||||
final com = commentTree.comment;
|
||||
showInfoTablePopup(context, {
|
||||
'id': com.comment.id,
|
||||
'creatorId': com.comment.creatorId,
|
||||
'postId': com.comment.postId,
|
||||
'postName': com.post.name,
|
||||
'parentId': com.comment.parentId,
|
||||
'removed': com.comment.removed,
|
||||
'read': com.comment.read,
|
||||
'published': com.comment.published,
|
||||
'updated': com.comment.updated,
|
||||
'deleted': com.comment.deleted,
|
||||
'apId': com.comment.apId,
|
||||
'local': com.comment.local,
|
||||
'communityId': com.community.id,
|
||||
'communityActorId': com.community.actorId,
|
||||
'communityLocal': com.community.local,
|
||||
'communityName': com.community.name,
|
||||
'communityIcon': com.community.icon,
|
||||
'banned': com.creator.banned,
|
||||
'bannedFromCommunity': com.creatorBannedFromCommunity,
|
||||
'creatorActirId': com.creator.actorId,
|
||||
'userId': com.creator.id,
|
||||
'upvotes': com.counts.upvotes,
|
||||
'downvotes': com.counts.downvotes,
|
||||
'score': com.counts.score,
|
||||
showInfoTablePopup(context: context, table: {
|
||||
...com.toJson(),
|
||||
'% of upvotes':
|
||||
'${100 * (com.counts.upvotes / (com.counts.upvotes + com.counts.downvotes))}%',
|
||||
});
|
||||
|
@ -95,32 +99,33 @@ class CommentWidget extends HookWidget {
|
|||
final collapsed = useState(false);
|
||||
final myVote = useState(commentTree.comment.myVote ?? VoteType.none);
|
||||
final isDeleted = useState(commentTree.comment.comment.deleted);
|
||||
final isRead = useState(commentTree.comment.comment.read);
|
||||
|
||||
final delayedVoting = useDelayedLoading();
|
||||
final delayedDeletion = useDelayedLoading();
|
||||
final loggedInAction = useLoggedInAction(commentTree.comment.instanceHost);
|
||||
|
||||
final newReplies = useState(const <CommentTree>[]);
|
||||
|
||||
final comment = commentTree.comment;
|
||||
|
||||
handleDelete(Jwt token) async {
|
||||
final api = LemmyApiV2(token.payload.iss);
|
||||
if (hideOnRead && isRead.value) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
delayedDeletion.start();
|
||||
handleDelete(Jwt token) {
|
||||
Navigator.of(context).pop();
|
||||
try {
|
||||
final res = await api.run(DeleteComment(
|
||||
delayedAction<FullCommentView>(
|
||||
context: context,
|
||||
delayedLoading: delayedDeletion,
|
||||
instanceHost: token.payload.iss,
|
||||
query: DeleteComment(
|
||||
commentId: comment.comment.id,
|
||||
deleted: !isDeleted.value,
|
||||
auth: token.raw,
|
||||
));
|
||||
isDeleted.value = res.commentView.comment.deleted;
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
Scaffold.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Failed to delete/restore comment')));
|
||||
return;
|
||||
}
|
||||
delayedDeletion.cancel();
|
||||
),
|
||||
onSuccess: (res) => isDeleted.value = res.commentView.comment.deleted,
|
||||
);
|
||||
}
|
||||
|
||||
void _openMoreMenu(BuildContext context) {
|
||||
|
@ -162,7 +167,7 @@ class CommentWidget extends HookWidget {
|
|||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(showRaw.value ? Icons.brush : Icons.build),
|
||||
leading: markdownModeIcon(fancy: !showRaw.value),
|
||||
title: Text('Show ${showRaw.value ? 'fancy' : 'raw'} text'),
|
||||
onTap: () {
|
||||
showRaw.value = !showRaw.value;
|
||||
|
@ -188,29 +193,26 @@ class CommentWidget extends HookWidget {
|
|||
reply() async {
|
||||
final newComment = await showCupertinoModalPopup<CommentView>(
|
||||
context: context,
|
||||
builder: (_) => WriteComment.toComment(comment),
|
||||
builder: (_) => WriteComment.toComment(
|
||||
comment: comment.comment, post: comment.post),
|
||||
);
|
||||
if (newComment != null) {
|
||||
newReplies.value = [...newReplies.value, CommentTree(newComment)];
|
||||
}
|
||||
}
|
||||
|
||||
vote(VoteType vote, Jwt token) async {
|
||||
final api = LemmyApiV2(token.payload.iss);
|
||||
|
||||
delayedVoting.start();
|
||||
try {
|
||||
final res = await api.run(CreateCommentLike(
|
||||
commentId: comment.comment.id, score: vote, auth: token.raw));
|
||||
myVote.value = res.commentView.myVote ?? VoteType.none;
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
Scaffold.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('voting failed :(')));
|
||||
return;
|
||||
}
|
||||
delayedVoting.cancel();
|
||||
}
|
||||
vote(VoteType vote, Jwt token) => delayedAction<FullCommentView>(
|
||||
context: context,
|
||||
delayedLoading: delayedVoting,
|
||||
instanceHost: token.payload.iss,
|
||||
query: CreateCommentLike(
|
||||
commentId: comment.comment.id,
|
||||
score: vote,
|
||||
auth: token.raw,
|
||||
),
|
||||
onSuccess: (res) =>
|
||||
myVote.value = res.commentView.myVote ?? VoteType.none,
|
||||
);
|
||||
|
||||
final body = () {
|
||||
if (isDeleted.value) {
|
||||
|
@ -261,7 +263,7 @@ class CommentWidget extends HookWidget {
|
|||
if (selectable.value &&
|
||||
!isDeleted.value &&
|
||||
!comment.comment.removed)
|
||||
_CommentAction(
|
||||
TileAction(
|
||||
icon: Icons.content_copy,
|
||||
tooltip: 'copy',
|
||||
onPressed: () {
|
||||
|
@ -272,27 +274,32 @@ class CommentWidget extends HookWidget {
|
|||
content: Text('comment copied to clipboard'))));
|
||||
}),
|
||||
const Spacer(),
|
||||
if (canBeMarkedAsRead)
|
||||
_MarkAsRead(
|
||||
commentTree.comment,
|
||||
onChanged: (val) => isRead.value = val,
|
||||
),
|
||||
if (detached)
|
||||
_CommentAction(
|
||||
TileAction(
|
||||
icon: Icons.link,
|
||||
onPressed: () =>
|
||||
goToPost(context, comment.instanceHost, comment.post.id),
|
||||
tooltip: 'go to post',
|
||||
),
|
||||
_CommentAction(
|
||||
TileAction(
|
||||
icon: Icons.more_horiz,
|
||||
onPressed: () => _openMoreMenu(context),
|
||||
loading: delayedDeletion.loading,
|
||||
delayedLoading: delayedDeletion,
|
||||
tooltip: 'more',
|
||||
),
|
||||
_SaveComment(commentTree.comment),
|
||||
if (!isDeleted.value && !comment.comment.removed)
|
||||
_CommentAction(
|
||||
TileAction(
|
||||
icon: Icons.reply,
|
||||
onPressed: loggedInAction((_) => reply()),
|
||||
tooltip: 'reply',
|
||||
),
|
||||
_CommentAction(
|
||||
TileAction(
|
||||
icon: Icons.arrow_upward,
|
||||
iconColor: myVote.value == VoteType.up ? theme.accentColor : null,
|
||||
onPressed: loggedInAction((token) => vote(
|
||||
|
@ -301,7 +308,7 @@ class CommentWidget extends HookWidget {
|
|||
)),
|
||||
tooltip: 'upvote',
|
||||
),
|
||||
_CommentAction(
|
||||
TileAction(
|
||||
icon: Icons.arrow_downward,
|
||||
iconColor: myVote.value == VoteType.down ? Colors.red : null,
|
||||
onPressed: loggedInAction(
|
||||
|
@ -314,18 +321,19 @@ class CommentWidget extends HookWidget {
|
|||
),
|
||||
]);
|
||||
|
||||
return GestureDetector(
|
||||
onLongPress: () => collapsed.value = !collapsed.value,
|
||||
return InkWell(
|
||||
onLongPress:
|
||||
selectable.value ? null : () => collapsed.value = !collapsed.value,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
margin: EdgeInsets.only(left: indent > 1 ? (indent - 1) * 5.0 : 0),
|
||||
margin: EdgeInsets.only(left: depth > 1 ? (depth - 1) * 5.0 : 0),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
left: indent > 0
|
||||
left: depth > 0
|
||||
? BorderSide(
|
||||
color: colors[indent % colors.length], width: 5)
|
||||
color: colors[depth % colors.length], width: 5)
|
||||
: BorderSide.none,
|
||||
top: const BorderSide(width: 0.2))),
|
||||
child: Column(
|
||||
|
@ -335,8 +343,8 @@ class CommentWidget extends HookWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: InkWell(
|
||||
onTap: () => goToUser.byId(
|
||||
context, comment.instanceHost, comment.creator.id),
|
||||
onTap: () =>
|
||||
goToUser.fromUserSafe(context, comment.creator),
|
||||
child: Avatar(
|
||||
url: comment.creator.avatar,
|
||||
radius: 10,
|
||||
|
@ -345,14 +353,16 @@ class CommentWidget extends HookWidget {
|
|||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => goToUser.byId(
|
||||
context, comment.instanceHost, comment.creator.id),
|
||||
onTap: () =>
|
||||
goToUser.fromUserSafe(context, comment.creator),
|
||||
child: Text(comment.creator.originDisplayName,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).accentColor,
|
||||
color: theme.accentColor,
|
||||
)),
|
||||
),
|
||||
if (isOP) _CommentTag('OP', Theme.of(context).accentColor),
|
||||
if (isOP) _CommentTag('OP', theme.accentColor),
|
||||
if (comment.creator.admin)
|
||||
_CommentTag('ADMIN', theme.accentColor),
|
||||
if (comment.creator.banned)
|
||||
const _CommentTag('BANNED', Colors.red),
|
||||
if (comment.creatorBannedFromCommunity)
|
||||
|
@ -389,17 +399,58 @@ class CommentWidget extends HookWidget {
|
|||
),
|
||||
if (!collapsed.value)
|
||||
for (final c in newReplies.value.followedBy(commentTree.children))
|
||||
CommentWidget(
|
||||
c,
|
||||
indent: indent + 1,
|
||||
postCreatorId: postCreatorId,
|
||||
),
|
||||
CommentWidget(c, depth: depth + 1),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MarkAsRead extends HookWidget {
|
||||
final CommentView commentView;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
const _MarkAsRead(this.commentView, {this.onChanged});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final accStore = useAccountsStore();
|
||||
|
||||
final comment = commentView.comment;
|
||||
final recipient = commentView.recipient;
|
||||
final instanceHost = commentView.instanceHost;
|
||||
final post = commentView.post;
|
||||
|
||||
final isRead = useState(comment.read);
|
||||
final delayedRead = useDelayedLoading();
|
||||
|
||||
Future<void> handleMarkAsSeen() => delayedAction<FullCommentView>(
|
||||
context: context,
|
||||
delayedLoading: delayedRead,
|
||||
instanceHost: instanceHost,
|
||||
query: MarkCommentAsRead(
|
||||
commentId: comment.id,
|
||||
read: !isRead.value,
|
||||
auth: recipient != null
|
||||
? accStore.tokenFor(instanceHost, recipient.name)?.raw
|
||||
: accStore.tokenForId(instanceHost, post.creatorId)?.raw,
|
||||
),
|
||||
onSuccess: (val) {
|
||||
isRead.value = val.commentView.comment.read;
|
||||
onChanged?.call(isRead.value);
|
||||
},
|
||||
);
|
||||
|
||||
return TileAction(
|
||||
icon: Icons.check,
|
||||
delayedLoading: delayedRead,
|
||||
onPressed: handleMarkAsSeen,
|
||||
iconColor: isRead.value ? Theme.of(context).accentColor : null,
|
||||
tooltip: 'mark as ${isRead.value ? 'un' : ''}read',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SaveComment extends HookWidget {
|
||||
final CommentView comment;
|
||||
|
||||
|
@ -411,27 +462,20 @@ class _SaveComment extends HookWidget {
|
|||
final isSaved = useState(comment.saved ?? false);
|
||||
final delayed = useDelayedLoading();
|
||||
|
||||
handleSave(Jwt token) async {
|
||||
final api = LemmyApiV2(comment.instanceHost);
|
||||
handleSave(Jwt token) => delayedAction<FullCommentView>(
|
||||
context: context,
|
||||
delayedLoading: delayed,
|
||||
instanceHost: comment.instanceHost,
|
||||
query: SaveComment(
|
||||
commentId: comment.comment.id,
|
||||
save: !isSaved.value,
|
||||
auth: token.raw,
|
||||
),
|
||||
onSuccess: (res) => isSaved.value = res.commentView.saved,
|
||||
);
|
||||
|
||||
delayed.start();
|
||||
try {
|
||||
final res = await api.run(SaveComment(
|
||||
commentId: comment.comment.id,
|
||||
save: !isSaved.value,
|
||||
auth: token.raw,
|
||||
));
|
||||
isSaved.value = res.commentView.saved;
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
Scaffold.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('saving failed :(')));
|
||||
}
|
||||
delayed.cancel();
|
||||
}
|
||||
|
||||
return _CommentAction(
|
||||
loading: delayed.loading,
|
||||
return TileAction(
|
||||
delayedLoading: delayed,
|
||||
icon: isSaved.value ? Icons.bookmark : Icons.bookmark_border,
|
||||
onPressed: loggedInAction(delayed.pending ? (_) {} : handleSave),
|
||||
tooltip: '${isSaved.value ? 'unsave' : 'save'} comment',
|
||||
|
@ -463,39 +507,3 @@ class _CommentTag extends StatelessWidget {
|
|||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _CommentAction extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
final String tooltip;
|
||||
final bool loading;
|
||||
final Color iconColor;
|
||||
|
||||
const _CommentAction({
|
||||
Key key,
|
||||
this.loading = false,
|
||||
this.iconColor,
|
||||
@required this.icon,
|
||||
@required this.onPressed,
|
||||
@required this.tooltip,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => IconButton(
|
||||
constraints: BoxConstraints.tight(const Size(32, 26)),
|
||||
icon: loading
|
||||
? SizedBox.fromSize(
|
||||
size: const Size.square(22),
|
||||
child: const CircularProgressIndicator())
|
||||
: Icon(
|
||||
icon,
|
||||
color: iconColor ??
|
||||
Theme.of(context).iconTheme.color.withAlpha(190),
|
||||
),
|
||||
splashRadius: 25,
|
||||
onPressed: onPressed,
|
||||
iconSize: 22,
|
||||
tooltip: tooltip,
|
||||
padding: const EdgeInsets.all(0),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -94,14 +94,9 @@ class CommentSection extends HookWidget {
|
|||
),
|
||||
)
|
||||
else if (sorting.value == CommentSortType.chat)
|
||||
for (final com in rawComments)
|
||||
CommentWidget(
|
||||
CommentTree(com),
|
||||
postCreatorId: postCreatorId,
|
||||
)
|
||||
for (final com in rawComments) CommentWidget.fromCommentView(com)
|
||||
else
|
||||
for (final com in comments)
|
||||
CommentWidget(com, postCreatorId: postCreatorId),
|
||||
for (final com in comments) CommentWidget(com),
|
||||
const BottomSafe(kMinInteractiveDimension + kFloatingActionButtonMargin),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -73,7 +73,6 @@ class InfiniteScroll<T> extends HookWidget {
|
|||
data.value = [];
|
||||
hasMore.current = true;
|
||||
};
|
||||
return controller.dispose;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -1,21 +1,47 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
void showInfoTablePopup(BuildContext context, Map<String, dynamic> table) {
|
||||
showDialog(
|
||||
import 'bottom_modal.dart';
|
||||
|
||||
void showInfoTablePopup({
|
||||
@required BuildContext context,
|
||||
@required Map<String, dynamic> table,
|
||||
String title,
|
||||
}) {
|
||||
assert(context != null);
|
||||
assert(table != null);
|
||||
|
||||
showBottomModal(
|
||||
context: context,
|
||||
builder: (c) => SimpleDialog(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
title: title,
|
||||
builder: (context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 15,
|
||||
),
|
||||
children: [
|
||||
Table(
|
||||
children: table.entries
|
||||
.map((e) => TableRow(
|
||||
children: [Text('${e.key}:'), Text(e.value.toString())]))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
children: [
|
||||
Table(children: [
|
||||
for (final e in table.entries)
|
||||
TableRow(children: [
|
||||
Text('${e.key}:'),
|
||||
if (e.value is Map<String, dynamic>)
|
||||
GestureDetector(
|
||||
onTap: () => showInfoTablePopup(
|
||||
context: context,
|
||||
table: e.value as Map<String, dynamic>,
|
||||
title: e.key,
|
||||
),
|
||||
child: Text(
|
||||
'[tap to show]',
|
||||
style: TextStyle(color: Theme.of(context).accentColor),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(e.value.toString())
|
||||
])
|
||||
]),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
/// shows either brush icon if fancy is true, or build icon if it's false
|
||||
/// used mostly for pages where markdown editor is used
|
||||
///
|
||||
/// brush icon is rotated to look similarly to build icon
|
||||
Widget markdownModeIcon({bool fancy}) => fancy
|
||||
? const Icon(Icons.build)
|
||||
: const RotatedBox(
|
||||
quarterTurns: 1,
|
||||
child: Icon(Icons.brush),
|
||||
);
|
|
@ -76,17 +76,10 @@ class PostWidget extends HookWidget {
|
|||
leading: const Icon(Icons.info_outline),
|
||||
title: const Text('Nerd stuff'),
|
||||
onTap: () {
|
||||
showInfoTablePopup(context, {
|
||||
'id': post.post.id,
|
||||
'apId': post.post.apId,
|
||||
'upvotes': post.counts.upvotes,
|
||||
'downvotes': post.counts.downvotes,
|
||||
'score': post.counts.score,
|
||||
showInfoTablePopup(context: context, table: {
|
||||
'% of upvotes':
|
||||
'${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%',
|
||||
'local': post.post.local,
|
||||
'published': post.post.published,
|
||||
'updated': post.post.updated ?? 'never',
|
||||
...post.toJson(),
|
||||
});
|
||||
},
|
||||
),
|
||||
|
@ -124,6 +117,7 @@ class PostWidget extends HookWidget {
|
|||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: () => goToCommunity.byId(
|
||||
context, instanceHost, post.community.id),
|
||||
child: Avatar(
|
||||
|
@ -195,10 +189,9 @@ class PostWidget extends HookWidget {
|
|||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => goToUser.byId(
|
||||
..onTap = () => goToUser.fromUserSafe(
|
||||
context,
|
||||
post.instanceHost,
|
||||
post.creator.id,
|
||||
post.creator,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
|
@ -263,6 +256,7 @@ class PostWidget extends HookWidget {
|
|||
post.post.thumbnailUrl != null) ...[
|
||||
const Spacer(),
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
onTap: _openLink,
|
||||
child: Stack(
|
||||
children: [
|
||||
|
@ -322,16 +316,27 @@ class PostWidget extends HookWidget {
|
|||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(post.post.embedTitle ?? '',
|
||||
style: theme.textTheme.subtitle1
|
||||
.apply(fontWeightDelta: 2)))
|
||||
child: Text(
|
||||
post.post.embedTitle ?? '',
|
||||
style: theme.textTheme.subtitle1
|
||||
.apply(fontWeightDelta: 2),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (post.post.embedDescription != null &&
|
||||
post.post.embedDescription.isNotEmpty)
|
||||
Row(
|
||||
children: [
|
||||
Flexible(child: Text(post.post.embedDescription))
|
||||
Flexible(
|
||||
child: Text(
|
||||
post.post.embedDescription,
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -387,82 +392,85 @@ class PostWidget extends HookWidget {
|
|||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black54)],
|
||||
boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black45)],
|
||||
color: theme.cardColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
child: InkWell(
|
||||
child: GestureDetector(
|
||||
onTap: fullPost
|
||||
? null
|
||||
: () => goTo(context, (context) => FullPostPage.fromPostView(post)),
|
||||
child: Column(
|
||||
children: [
|
||||
info(),
|
||||
title(),
|
||||
if (whatType(post.post.url) != MediaType.other &&
|
||||
whatType(post.post.url) != MediaType.none)
|
||||
postImage()
|
||||
else if (post.post.url != null && post.post.url.isNotEmpty)
|
||||
linkPreview(),
|
||||
if (post.post.body != null && fullPost)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child:
|
||||
MarkdownText(post.post.body, instanceHost: instanceHost)),
|
||||
if (post.post.body != null && !fullPost)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final span = TextSpan(
|
||||
text: post.post.body,
|
||||
);
|
||||
final tp = TextPainter(
|
||||
text: span,
|
||||
maxLines: 10,
|
||||
textDirection: Directionality.of(context),
|
||||
)..layout(maxWidth: constraints.maxWidth - 20);
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Column(
|
||||
children: [
|
||||
info(),
|
||||
title(),
|
||||
if (whatType(post.post.url) != MediaType.other &&
|
||||
whatType(post.post.url) != MediaType.none)
|
||||
postImage()
|
||||
else if (post.post.url != null && post.post.url.isNotEmpty)
|
||||
linkPreview(),
|
||||
if (post.post.body != null && fullPost)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: MarkdownText(post.post.body,
|
||||
instanceHost: instanceHost)),
|
||||
if (post.post.body != null && !fullPost)
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final span = TextSpan(
|
||||
text: post.post.body,
|
||||
);
|
||||
final tp = TextPainter(
|
||||
text: span,
|
||||
maxLines: 10,
|
||||
textDirection: Directionality.of(context),
|
||||
)..layout(maxWidth: constraints.maxWidth - 20);
|
||||
|
||||
if (tp.didExceedMaxLines) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: tp.height),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: 0.8,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: MarkdownText(post.post.body,
|
||||
instanceHost: instanceHost)),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: tp.preferredLineHeight * 2.5,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
theme.cardColor.withAlpha(0),
|
||||
theme.cardColor,
|
||||
],
|
||||
if (tp.didExceedMaxLines) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: tp.height),
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: [
|
||||
ClipRect(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
heightFactor: 0.8,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: MarkdownText(post.post.body,
|
||||
instanceHost: instanceHost)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: MarkdownText(post.post.body,
|
||||
instanceHost: instanceHost));
|
||||
}
|
||||
},
|
||||
),
|
||||
actions(),
|
||||
],
|
||||
Container(
|
||||
height: tp.preferredLineHeight * 2.5,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
theme.cardColor.withAlpha(0),
|
||||
theme.cardColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: MarkdownText(post.post.body,
|
||||
instanceHost: instanceHost));
|
||||
}
|
||||
},
|
||||
),
|
||||
actions(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import 'dart:math' show max, min;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
|
||||
/// Makes the child reveal itself after given distance
|
||||
class RevealAfterScroll extends HookWidget {
|
||||
final Widget child;
|
||||
|
||||
/// distance after which [child] should appear
|
||||
final int after;
|
||||
final int transition;
|
||||
final ScrollController scrollController;
|
||||
final bool fade;
|
||||
|
||||
const RevealAfterScroll({
|
||||
@required this.scrollController,
|
||||
@required this.child,
|
||||
@required this.after,
|
||||
this.transition = 15,
|
||||
this.fade = false,
|
||||
}) : assert(scrollController != null),
|
||||
assert(child != null),
|
||||
assert(after != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
useListenable(scrollController);
|
||||
|
||||
final scroll = scrollController.position.pixels;
|
||||
|
||||
return Opacity(
|
||||
opacity:
|
||||
fade ? max(0, min(transition, scroll - after + 20)) / transition : 1,
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, max(0, after - scroll)),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ class SortableInfiniteList<T> extends HookWidget {
|
|||
final InfiniteScrollController controller;
|
||||
final Function onStyleChange;
|
||||
final Widget noItems;
|
||||
final SortType defaultSort;
|
||||
|
||||
const SortableInfiniteList({
|
||||
@required this.fetcher,
|
||||
|
@ -26,15 +27,17 @@ class SortableInfiniteList<T> extends HookWidget {
|
|||
this.controller,
|
||||
this.onStyleChange,
|
||||
this.noItems,
|
||||
this.defaultSort = SortType.active,
|
||||
}) : assert(fetcher != null),
|
||||
assert(itemBuilder != null);
|
||||
assert(itemBuilder != null),
|
||||
assert(defaultSort != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final defaultController = useInfiniteScrollController();
|
||||
final isc = controller ?? defaultController;
|
||||
|
||||
final sort = useState(SortType.active);
|
||||
final sort = useState(defaultSort);
|
||||
|
||||
void changeSorting(SortType newSort) {
|
||||
sort.value = newSort;
|
||||
|
@ -92,7 +95,6 @@ class InfiniteCommentList extends StatelessWidget {
|
|||
Widget build(BuildContext context) => SortableInfiniteList<CommentView>(
|
||||
itemBuilder: (comment) => CommentWidget(
|
||||
CommentTree(comment),
|
||||
postCreatorId: null,
|
||||
detached: true,
|
||||
),
|
||||
fetcher: fetcher,
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../hooks/delayed_loading.dart';
|
||||
|
||||
/// [IconButton], usually at the bottom of some tile, that performs an async
|
||||
/// action that uses [DelayedLoading], has reduced size to be more compact,
|
||||
/// and has built in spinner
|
||||
class TileAction extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
final String tooltip;
|
||||
final DelayedLoading delayedLoading;
|
||||
final Color iconColor;
|
||||
|
||||
const TileAction({
|
||||
Key key,
|
||||
this.delayedLoading,
|
||||
this.iconColor,
|
||||
@required this.icon,
|
||||
@required this.onPressed,
|
||||
@required this.tooltip,
|
||||
}) : assert(icon != null),
|
||||
assert(onPressed != null),
|
||||
assert(tooltip != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => IconButton(
|
||||
constraints: BoxConstraints.tight(const Size(36, 30)),
|
||||
icon: delayedLoading?.loading ?? false
|
||||
? SizedBox.fromSize(
|
||||
size: const Size.square(22),
|
||||
child: const CircularProgressIndicator())
|
||||
: Icon(
|
||||
icon,
|
||||
color: iconColor ??
|
||||
Theme.of(context).iconTheme.color.withAlpha(190),
|
||||
),
|
||||
splashRadius: 25,
|
||||
onPressed: delayedLoading?.pending ?? false ? () {} : onPressed,
|
||||
iconSize: 25,
|
||||
tooltip: tooltip,
|
||||
padding: const EdgeInsets.all(0),
|
||||
);
|
||||
}
|
|
@ -4,24 +4,22 @@ import 'package:lemmy_api_client/v2.dart';
|
|||
|
||||
import '../hooks/delayed_loading.dart';
|
||||
import '../hooks/stores.dart';
|
||||
import 'markdown_mode_icon.dart';
|
||||
import 'markdown_text.dart';
|
||||
|
||||
/// Modal for writing a comment to a given post/comment (aka reply)
|
||||
/// on submit pops the navigator stack with a [CommentView]
|
||||
/// or `null` if cancelled
|
||||
class WriteComment extends HookWidget {
|
||||
final PostView post;
|
||||
final CommentView comment;
|
||||
final Post post;
|
||||
final Comment comment;
|
||||
|
||||
final String instanceHost;
|
||||
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
|
||||
|
||||
WriteComment.toPost(this.post)
|
||||
: instanceHost = post.instanceHost,
|
||||
comment = null;
|
||||
WriteComment.toComment(this.comment)
|
||||
: instanceHost = comment.instanceHost,
|
||||
post = null;
|
||||
WriteComment.toPost(this.post) : comment = null;
|
||||
WriteComment.toComment({@required this.comment, @required this.post})
|
||||
: assert(comment != null),
|
||||
assert(post != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -32,15 +30,15 @@ class WriteComment extends HookWidget {
|
|||
|
||||
final preview = () {
|
||||
final body = MarkdownText(
|
||||
comment?.comment?.content ?? post.post.body ?? '',
|
||||
instanceHost: instanceHost,
|
||||
comment?.content ?? post.body,
|
||||
instanceHost: post.instanceHost,
|
||||
);
|
||||
|
||||
if (post != null) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
post.post.name,
|
||||
post.name,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
|
@ -53,22 +51,21 @@ class WriteComment extends HookWidget {
|
|||
}();
|
||||
|
||||
handleSubmit() async {
|
||||
final api = LemmyApiV2(instanceHost);
|
||||
final api = LemmyApiV2(post.instanceHost);
|
||||
|
||||
final token = accStore.defaultTokenFor(instanceHost);
|
||||
final token = accStore.defaultTokenFor(post.instanceHost);
|
||||
|
||||
delayed.start();
|
||||
try {
|
||||
final res = await api.run(CreateComment(
|
||||
content: controller.text,
|
||||
postId: post?.post?.id ?? comment.post.id,
|
||||
parentId: comment?.comment?.id,
|
||||
postId: post.id,
|
||||
parentId: comment?.id,
|
||||
auth: token.raw,
|
||||
));
|
||||
Navigator.of(context).pop(res.commentView);
|
||||
// ignore: avoid_catches_without_on_clauses
|
||||
} catch (e) {
|
||||
print(e);
|
||||
scaffoldKey.currentState.showSnackBar(
|
||||
const SnackBar(content: Text('Failed to post comment')));
|
||||
}
|
||||
|
@ -81,7 +78,7 @@ class WriteComment extends HookWidget {
|
|||
leading: const CloseButton(),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(showFancy.value ? Icons.build : Icons.brush),
|
||||
icon: markdownModeIcon(fancy: showFancy.value),
|
||||
onPressed: () => showFancy.value = !showFancy.value,
|
||||
),
|
||||
],
|
||||
|
@ -110,7 +107,7 @@ class WriteComment extends HookWidget {
|
|||
padding: const EdgeInsets.all(16),
|
||||
child: MarkdownText(
|
||||
controller.text,
|
||||
instanceHost: instanceHost,
|
||||
instanceHost: post.instanceHost,
|
||||
),
|
||||
)
|
||||
],
|
||||
|
|
23
pubspec.lock
23
pubspec.lock
|
@ -232,14 +232,14 @@ packages:
|
|||
name: image_picker
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.6.7+21"
|
||||
version: "0.6.7+22"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.6"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -267,7 +267,7 @@ packages:
|
|||
name: lemmy_api_client
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.11.0"
|
||||
version: "0.12.0"
|
||||
markdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -323,7 +323,7 @@ packages:
|
|||
name: package_info
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3+2"
|
||||
version: "0.4.3+4"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -463,7 +463,7 @@ packages:
|
|||
name: shared_preferences_windows
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.0.2+2"
|
||||
version: "0.0.2+3"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
|
@ -482,7 +482,7 @@ packages:
|
|||
name: sqflite
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.3.2+2"
|
||||
version: "1.3.2+3"
|
||||
sqflite_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -580,7 +580,7 @@ packages:
|
|||
name: url_launcher_web
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.5+1"
|
||||
version: "0.1.5+3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -602,13 +602,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.0-nullsafety.3"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.7.4"
|
||||
version: "1.7.4+1"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -44,8 +44,7 @@ dependencies:
|
|||
# utils
|
||||
timeago: ^2.0.27
|
||||
fuzzy: <1.0.0
|
||||
lemmy_api_client: ^0.11.0
|
||||
|
||||
lemmy_api_client: ^0.12.0
|
||||
matrix4_transform: ^1.1.7
|
||||
|
||||
flutter:
|
||||
|
|
Loading…
Reference in New Issue