add inbox page + other stuff (#164)

This commit is contained in:
Filip Krawczyk 2021-02-24 20:52:18 +01:00 committed by GitHub
parent 5789cfb01d
commit 19b2688316
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1099 additions and 344 deletions

View File

@ -4,6 +4,14 @@
- Added page with saved posts/comments. It can be accessed from the profile tab under the bookmark icon - 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 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 ### Fixed

View File

@ -2,5 +2,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import '../widgets/infinite_scroll.dart'; import '../widgets/infinite_scroll.dart';
InfiniteScrollController useInfiniteScrollController() => InfiniteScrollController useInfiniteScrollController() {
useMemoized(() => InfiniteScrollController()); final controller = useMemoized(() => InfiniteScrollController());
useEffect(() => controller.dispose, []);
return controller;
}

View File

@ -32,7 +32,7 @@ class AddAccountPage extends HookWidget {
useEffect(() { useEffect(() {
LemmyApiV2(selectedInstance.value) LemmyApiV2(selectedInstance.value)
.run(GetSite()) .run(const GetSite())
.then((site) => icon.value = site.siteView.site.icon); .then((site) => icon.value = site.siteView.site.icon);
return null; return null;
}, [selectedInstance.value]); }, [selectedInstance.value]);

View File

@ -33,7 +33,8 @@ class AddInstancePage extends HookWidget {
return; return;
} }
try { 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; isSite.value = true;
// ignore: avoid_catches_without_on_clauses // ignore: avoid_catches_without_on_clauses
} catch (e) { } catch (e) {

View File

@ -36,7 +36,7 @@ class CommunitiesTab extends HookWidget {
final futures = accountsStore.loggedInInstances final futures = accountsStore.loggedInInstances
.map( .map(
(instanceHost) => LemmyApiV2(instanceHost) (instanceHost) => LemmyApiV2(instanceHost)
.run(GetSite()) .run(const GetSite())
.then((e) => e.siteView.site), .then((e) => e.siteView.site),
) )
.toList(); .toList();
@ -211,9 +211,7 @@ class CommunitiesTab extends HookWidget {
url: comm.community.icon, url: comm.community.icon,
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(comm.community.originDisplayName),
'!${comm.community.name}${comm.community.local ? '' : '@${comm.community.originInstanceHost}'}',
),
], ],
), ),
trailing: _CommunitySubscribeToggle( trailing: _CommunitySubscribeToggle(

View File

@ -20,6 +20,7 @@ import '../widgets/bottom_modal.dart';
import '../widgets/fullscreenable_image.dart'; import '../widgets/fullscreenable_image.dart';
import '../widgets/info_table_popup.dart'; import '../widgets/info_table_popup.dart';
import '../widgets/markdown_text.dart'; import '../widgets/markdown_text.dart';
import '../widgets/reveal_after_scroll.dart';
import '../widgets/sortable_infinite_list.dart'; import '../widgets/sortable_infinite_list.dart';
import 'create_post.dart'; import 'create_post.dart';
import 'modlog_page.dart'; import 'modlog_page.dart';
@ -54,6 +55,7 @@ class CommunityPage extends HookWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final accountsStore = useAccountsStore(); final accountsStore = useAccountsStore();
final scrollController = useScrollController();
final fullCommunitySnap = useMemoFuture(() { final fullCommunitySnap = useMemoFuture(() {
final token = accountsStore.defaultTokenFor(instanceHost); final token = accountsStore.defaultTokenFor(instanceHost);
@ -125,12 +127,7 @@ class CommunityPage extends HookWidget {
leading: const Icon(Icons.info_outline), leading: const Icon(Icons.info_outline),
title: const Text('Nerd stuff'), title: const Text('Nerd stuff'),
onTap: () { onTap: () {
showInfoTablePopup(context, { showInfoTablePopup(context: context, table: community.toJson());
'id': community.community.id,
'actorId': community.community.actorId,
'created by': '@${community.creator.name}',
'published': community.community.published,
});
}, },
), ),
], ],
@ -143,12 +140,22 @@ class CommunityPage extends HookWidget {
body: DefaultTabController( body: DefaultTabController(
length: 3, length: 3,
child: NestedScrollView( child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[ headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
SliverAppBar( SliverAppBar(
expandedHeight: community.community.icon == null ? 220 : 300, expandedHeight: community.community.icon == null ? 220 : 300,
pinned: true, pinned: true,
backgroundColor: theme.cardColor, 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: [ actions: [
IconButton(icon: const Icon(Icons.share), onPressed: _share), IconButton(icon: const Icon(Icons.share), onPressed: _share),
IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu), IconButton(icon: Icon(moreIcon), onPressed: _openMoreMenu),
@ -437,8 +444,7 @@ class _AboutTab extends StatelessWidget {
ListTile( ListTile(
title: Text( title: Text(
mod.moderator.preferredUsername ?? '@${mod.moderator.name}'), mod.moderator.preferredUsername ?? '@${mod.moderator.name}'),
onTap: () => onTap: () => goToUser.fromUserSafe(context, mod.moderator),
goToUser.byId(context, mod.instanceHost, mod.moderator.id),
), ),
] ]
], ],

View File

@ -15,6 +15,7 @@ import '../util/extensions/spaced.dart';
import '../util/goto.dart'; import '../util/goto.dart';
import '../util/pictrs.dart'; import '../util/pictrs.dart';
import '../util/unawaited.dart'; import '../util/unawaited.dart';
import '../widgets/markdown_mode_icon.dart';
import '../widgets/markdown_text.dart'; import '../widgets/markdown_text.dart';
import '../widgets/radio_picker.dart'; import '../widgets/radio_picker.dart';
import 'full_post.dart'; import 'full_post.dart';
@ -263,7 +264,7 @@ class CreatePostPage extends HookWidget {
leading: const CloseButton(), leading: const CloseButton(),
actions: [ actions: [
IconButton( IconButton(
icon: Icon(showFancy.value ? Icons.build : Icons.brush), icon: markdownModeIcon(fancy: showFancy.value),
onPressed: () => showFancy.value = !showFancy.value, onPressed: () => showFancy.value = !showFancy.value,
), ),
], ],

View File

@ -8,9 +8,11 @@ import 'package:lemmy_api_client/v2.dart';
import '../hooks/logged_in_action.dart'; import '../hooks/logged_in_action.dart';
import '../hooks/refreshable.dart'; import '../hooks/refreshable.dart';
import '../hooks/stores.dart'; import '../hooks/stores.dart';
import '../util/extensions/api.dart';
import '../util/more_icon.dart'; import '../util/more_icon.dart';
import '../widgets/comment_section.dart'; import '../widgets/comment_section.dart';
import '../widgets/post.dart'; import '../widgets/post.dart';
import '../widgets/reveal_after_scroll.dart';
import '../widgets/save_post_button.dart'; import '../widgets/save_post_button.dart';
import '../widgets/write_comment.dart'; import '../widgets/write_comment.dart';
@ -31,6 +33,8 @@ class FullPostPage extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accStore = useAccountsStore(); final accStore = useAccountsStore();
final scrollController = useScrollController();
final fullPostRefreshable = final fullPostRefreshable =
useRefreshable(() => LemmyApiV2(instanceHost).run(GetPost( useRefreshable(() => LemmyApiV2(instanceHost).run(GetPost(
id: id, id: id,
@ -85,7 +89,7 @@ class FullPostPage extends HookWidget {
comment() async { comment() async {
final newComment = await showCupertinoModalPopup<CommentView>( final newComment = await showCupertinoModalPopup<CommentView>(
context: context, context: context,
builder: (_) => WriteComment.toPost(post), builder: (_) => WriteComment.toPost(post.post),
); );
if (newComment != null) { if (newComment != null) {
newComments.value = [...newComments.value, newComment]; newComments.value = [...newComments.value, newComment];
@ -94,6 +98,15 @@ class FullPostPage extends HookWidget {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
centerTitle: false,
title: RevealAfterScroll(
scrollController: scrollController,
after: 65,
child: Text(
post.community.originDisplayName,
overflow: TextOverflow.fade,
),
),
actions: [ actions: [
IconButton(icon: const Icon(Icons.share), onPressed: sharePost), IconButton(icon: const Icon(Icons.share), onPressed: sharePost),
SavePostButton(post), SavePostButton(post),
@ -108,8 +121,10 @@ class FullPostPage extends HookWidget {
body: RefreshIndicator( body: RefreshIndicator(
onRefresh: refresh, onRefresh: refresh,
child: ListView( child: ListView(
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
children: [ children: [
const SizedBox(height: 15),
PostWidget(post, fullPost: true), PostWidget(post, fullPost: true),
if (fullPostRefreshable.snapshot.hasData) if (fullPostRefreshable.snapshot.hasData)
CommentSection( CommentSection(

View File

@ -32,8 +32,8 @@ class HomeTab extends HookWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final instancesIcons = useMemoFuture(() async { final instancesIcons = useMemoFuture(() async {
final instances = accStore.instances.toList(growable: false); final instances = accStore.instances.toList(growable: false);
final sites = await Future.wait(instances final sites = await Future.wait(instances.map(
.map((e) => LemmyApiV2(e).run(GetSite()).catchError((e) => null))); (e) => LemmyApiV2(e).run(const GetSite()).catchError((e) => null)));
return { return {
for (var i = 0; i < sites.length; i++) for (var i = 0; i < sites.length; i++)
@ -229,7 +229,8 @@ class HomeTab extends HookWidget {
child: Text( child: Text(
title, title,
style: theme.appBarTheme.textTheme.headline6, style: theme.appBarTheme.textTheme.headline6,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.fade,
softWrap: false,
), ),
), ),
const Icon(Icons.arrow_drop_down), const Icon(Icons.arrow_drop_down),

View File

@ -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/material.dart';
import 'package:flutter_hooks/flutter_hooks.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 { class InboxPage extends HookWidget {
const InboxPage(); const InboxPage();
@override @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(), appBar: AppBar(),
body: Center( body: const Center(child: Text('no accounts added')),
child: Column( );
mainAxisAlignment: MainAxisAlignment.center, }
children: [
Text( toggleUnreadOnly() {
'🚧 WORK IN PROGRESS 🚧', unreadOnly.value = !unreadOnly.value;
style: Theme.of(context).textTheme.headline5, 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(),
],
),
);
}
} }

View File

@ -17,6 +17,7 @@ import '../widgets/bottom_modal.dart';
import '../widgets/fullscreenable_image.dart'; import '../widgets/fullscreenable_image.dart';
import '../widgets/info_table_popup.dart'; import '../widgets/info_table_popup.dart';
import '../widgets/markdown_text.dart'; import '../widgets/markdown_text.dart';
import '../widgets/reveal_after_scroll.dart';
import '../widgets/sortable_infinite_list.dart'; import '../widgets/sortable_infinite_list.dart';
import 'communities_list.dart'; import 'communities_list.dart';
import 'modlog_page.dart'; import 'modlog_page.dart';
@ -33,8 +34,8 @@ class InstancePage extends HookWidget {
InstancePage({@required this.instanceHost}) InstancePage({@required this.instanceHost})
: assert(instanceHost != null), : assert(instanceHost != null),
siteFuture = LemmyApiV2(instanceHost).run(GetSite()), siteFuture = LemmyApiV2(instanceHost).run(const GetSite()),
communitiesFuture = LemmyApiV2(instanceHost).run(ListCommunities( communitiesFuture = LemmyApiV2(instanceHost).run(const ListCommunities(
type: PostListingType.local, sort: SortType.hot, limit: 6)); type: PostListingType.local, sort: SortType.hot, limit: 6));
@override @override
@ -43,6 +44,7 @@ class InstancePage extends HookWidget {
final siteSnap = useFuture(siteFuture); final siteSnap = useFuture(siteFuture);
final colorOnCard = textColorBasedOnBackground(theme.cardColor); final colorOnCard = textColorBasedOnBackground(theme.cardColor);
final accStore = useAccountsStore(); final accStore = useAccountsStore();
final scrollController = useScrollController();
if (!siteSnap.hasData) { if (!siteSnap.hasData) {
return Scaffold( return Scaffold(
@ -85,15 +87,7 @@ class InstancePage extends HookWidget {
leading: const Icon(Icons.info_outline), leading: const Icon(Icons.info_outline),
title: const Text('Nerd stuff'), title: const Text('Nerd stuff'),
onTap: () { onTap: () {
showInfoTablePopup(context, { showInfoTablePopup(context: context, table: site.toJson());
'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,
});
}, },
), ),
], ],
@ -105,14 +99,20 @@ class InstancePage extends HookWidget {
body: DefaultTabController( body: DefaultTabController(
length: 3, length: 3,
child: NestedScrollView( child: NestedScrollView(
controller: scrollController,
headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[ headerSliverBuilder: (context, innerBoxIsScrolled) => <Widget>[
SliverAppBar( SliverAppBar(
expandedHeight: 250, expandedHeight: 250,
pinned: true, pinned: true,
backgroundColor: theme.cardColor, backgroundColor: theme.cardColor,
title: Text( title: RevealAfterScroll(
site.siteView.site.name, after: 150,
style: TextStyle(color: colorOnCard), fade: true,
scrollController: scrollController,
child: Text(
site.siteView.site.name,
style: TextStyle(color: colorOnCard),
),
), ),
actions: [ actions: [
IconButton(icon: const Icon(Icons.share), onPressed: _share), IconButton(icon: const Icon(Icons.share), onPressed: _share),
@ -323,7 +323,7 @@ class _AboutTab extends HookWidget {
subtitle: u.user.bio != null subtitle: u.user.bio != null
? MarkdownText(u.user.bio, instanceHost: instanceHost) ? MarkdownText(u.user.bio, instanceHost: instanceHost)
: null, : null,
onTap: () => goToUser.byId(context, instanceHost, u.user.id), onTap: () => goToUser.fromUserSafe(context, u.user),
leading: Avatar(url: u.user.avatar), leading: Avatar(url: u.user.avatar),
), ),
const _Divider(), const _Divider(),

View File

@ -71,6 +71,8 @@ class MediaViewPage extends HookWidget {
Colors.black.withOpacity(max(0, 1.0 - (offset.value.dy.abs() / 200))), Colors.black.withOpacity(max(0, 1.0 - (offset.value.dy.abs() / 200))),
appBar: showButtons.value appBar: showButtons.value
? AppBar( ? AppBar(
brightness: Brightness.dark,
iconTheme: const IconThemeData(color: Colors.white),
backgroundColor: Colors.black38, backgroundColor: Colors.black38,
leading: const CloseButton(), leading: const CloseButton(),
actions: [ actions: [

View File

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v2.dart'; import 'package:lemmy_api_client/v2.dart';
import '../comment_tree.dart';
import '../hooks/stores.dart'; import '../hooks/stores.dart';
import '../widgets/comment.dart'; import '../widgets/comment.dart';
import '../widgets/post.dart'; import '../widgets/post.dart';
@ -106,10 +105,7 @@ class _SearchResultsList extends HookWidget {
itemBuilder: (data) { itemBuilder: (data) {
switch (type) { switch (type) {
case SearchType.comments: case SearchType.comments:
return CommentWidget( return CommentWidget.fromCommentView(data as CommentView);
CommentTree(data as CommentView),
postCreatorId: null,
);
case SearchType.communities: case SearchType.communities:
return CommunitiesListItem(community: data as CommunityView); return CommunitiesListItem(community: data as CommunityView);
case SearchType.posts: case SearchType.posts:

View File

@ -1,9 +1,12 @@
import 'package:esys_flutter_share/esys_flutter_share.dart'; import 'package:esys_flutter_share/esys_flutter_share.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/v2.dart'; import 'package:lemmy_api_client/v2.dart';
import '../hooks/logged_in_action.dart';
import '../widgets/user_profile.dart'; import '../widgets/user_profile.dart';
import 'write_message.dart';
/// Page showing posts, comments, and general info about a user. /// Page showing posts, comments, and general info about a user.
class UserPage extends HookWidget { class UserPage extends HookWidget {
@ -43,15 +46,12 @@ class UserPage extends HookWidget {
appBar: AppBar( appBar: AppBar(
actions: [ actions: [
if (userDetailsSnap.hasData) ...[ if (userDetailsSnap.hasData) ...[
IconButton( SendMessageButton(userDetailsSnap.data.userView.user),
icon: const Icon(Icons.email),
onPressed: () {}, // TODO: go to messaging page
),
IconButton( IconButton(
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
onPressed: () => Share.text('Share user', onPressed: () => Share.text('Share user',
userDetailsSnap.data.userView.user.actorId, 'text/plain'), 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,
))),
);
}
}

View File

@ -52,7 +52,7 @@ class UsersListItem extends StatelessWidget {
), ),
) )
: null, : null,
onTap: () => goToUser.byId(context, user.instanceHost, user.user.id), onTap: () => goToUser.fromUserSafe(context, user.user),
leading: Avatar(url: user.user.avatar), leading: Avatar(url: user.user.avatar),
); );
} }

View File

@ -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),
),
),
],
),
),
);
}
}

View File

@ -29,7 +29,7 @@ class AccountsStore extends ChangeNotifier {
// I barely understand what I did. Long story short it casts a // I barely understand what I did. Long story short it casts a
// raw json into a nested ObservableMap // 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) ?? (jsonDecode(prefs.getString(SharedPrefKeys.tokens) ??
'{"lemmy.ml":{}}') as Map<String, dynamic>) '{"lemmy.ml":{}}') as Map<String, dynamic>)
?.map( ?.map(
@ -37,8 +37,7 @@ class AccountsStore extends ChangeNotifier {
k, k,
HashMap.of( HashMap.of(
(e as Map<String, dynamic>)?.map( (e as Map<String, dynamic>)?.map(
(k, e) => MapEntry( (k, e) => MapEntry(k, e == null ? null : f(e as String)),
k, e == null ? null : f(e as Map<String, dynamic>)),
), ),
), ),
), ),
@ -46,7 +45,7 @@ class AccountsStore extends ChangeNotifier {
); );
// set saved settings or create defaults // set saved settings or create defaults
_tokens = nestedMapsCast((json) => Jwt(json['raw'] as String)); _tokens = nestedMapsCast((json) => Jwt(json));
_defaultAccount = prefs.getString(SharedPrefKeys.defaultAccount); _defaultAccount = prefs.getString(SharedPrefKeys.defaultAccount);
_defaultAccounts = HashMap.of(Map.castFrom( _defaultAccounts = HashMap.of(Map.castFrom(
jsonDecode(prefs.getString(SharedPrefKeys.defaultAccounts) ?? 'null') jsonDecode(prefs.getString(SharedPrefKeys.defaultAccounts) ?? 'null')
@ -155,6 +154,14 @@ class AccountsStore extends ChangeNotifier {
return _tokens[instanceHost][_defaultAccounts[instanceHost]]; 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) { Jwt tokenFor(String instanceHost, String username) {
if (!usernamesFor(instanceHost).contains(username)) { if (!usernamesFor(instanceHost).contains(username)) {
return null; return null;
@ -243,7 +250,7 @@ class AccountsStore extends ChangeNotifier {
if (!assumeValid) { if (!assumeValid) {
try { try {
await LemmyApiV2(instanceHost).run(GetSite()); await LemmyApiV2(instanceHost).run(const GetSite());
// ignore: avoid_catches_without_on_clauses // ignore: avoid_catches_without_on_clauses
} catch (_) { } catch (_) {
throw Exception('This instance seems to not exist'); throw Exception('This instance seems to not exist');

View File

@ -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();
}

View File

@ -26,7 +26,13 @@ extension GetOriginInstanceCommentView on Comment {
String _extract(String url) => urlHost(url); 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 { String get displayName {
if (preferredUsername != null && preferredUsername.isNotEmpty) { if (preferredUsername != null && preferredUsername.isNotEmpty) {
return preferredUsername; return preferredUsername;

View File

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lemmy_api_client/v2.dart';
import '../pages/community.dart'; import '../pages/community.dart';
import '../pages/full_post.dart'; import '../pages/full_post.dart';
@ -57,6 +58,9 @@ abstract class goToUser {
static void byName( static void byName(
BuildContext context, String instanceHost, String userName) => BuildContext context, String instanceHost, String userName) =>
throw UnimplementedError('need to create UserProfile constructor first'); 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( void goToPost(BuildContext context, String instanceHost, int postId) => goTo(

View File

@ -24,8 +24,7 @@ class BottomModal extends StatelessWidget {
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: Colors.grey.withOpacity(0.5), color: Colors.grey.withOpacity(0.3),
width: 0.2,
), ),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),

View File

@ -10,6 +10,7 @@ import '../comment_tree.dart';
import '../hooks/delayed_loading.dart'; import '../hooks/delayed_loading.dart';
import '../hooks/logged_in_action.dart'; import '../hooks/logged_in_action.dart';
import '../hooks/stores.dart'; import '../hooks/stores.dart';
import '../util/delayed_action.dart';
import '../util/extensions/api.dart'; import '../util/extensions/api.dart';
import '../util/extensions/datetime.dart'; import '../util/extensions/datetime.dart';
import '../util/goto.dart'; import '../util/goto.dart';
@ -18,17 +19,20 @@ import '../util/text_color.dart';
import 'avatar.dart'; import 'avatar.dart';
import 'bottom_modal.dart'; import 'bottom_modal.dart';
import 'info_table_popup.dart'; import 'info_table_popup.dart';
import 'markdown_mode_icon.dart';
import 'markdown_text.dart'; import 'markdown_text.dart';
import 'tile_action.dart';
import 'write_comment.dart'; import 'write_comment.dart';
/// A single comment that renders its replies /// A single comment that renders its replies
class CommentWidget extends HookWidget { class CommentWidget extends HookWidget {
final int indent; final int depth;
final int postCreatorId;
final CommentTree commentTree; final CommentTree commentTree;
final bool detached; final bool detached;
final UserMentionView userMentionView;
final bool wasVoted; final bool wasVoted;
final bool canBeMarkedAsRead;
final bool hideOnRead;
static const colors = [ static const colors = [
Colors.pink, Colors.pink,
@ -40,39 +44,39 @@ class CommentWidget extends HookWidget {
CommentWidget( CommentWidget(
this.commentTree, { this.commentTree, {
this.indent = 0, this.depth = 0,
@required this.postCreatorId,
this.detached = false, this.detached = false,
}) : wasVoted = this.canBeMarkedAsRead = false,
(commentTree.comment.myVote ?? VoteType.none) != VoteType.none; 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) { _showCommentInfo(BuildContext context) {
final com = commentTree.comment; final com = commentTree.comment;
showInfoTablePopup(context, { showInfoTablePopup(context: context, table: {
'id': com.comment.id, ...com.toJson(),
'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,
'% of upvotes': '% of upvotes':
'${100 * (com.counts.upvotes / (com.counts.upvotes + com.counts.downvotes))}%', '${100 * (com.counts.upvotes / (com.counts.upvotes + com.counts.downvotes))}%',
}); });
@ -95,32 +99,33 @@ class CommentWidget extends HookWidget {
final collapsed = useState(false); final collapsed = useState(false);
final myVote = useState(commentTree.comment.myVote ?? VoteType.none); final myVote = useState(commentTree.comment.myVote ?? VoteType.none);
final isDeleted = useState(commentTree.comment.comment.deleted); final isDeleted = useState(commentTree.comment.comment.deleted);
final isRead = useState(commentTree.comment.comment.read);
final delayedVoting = useDelayedLoading(); final delayedVoting = useDelayedLoading();
final delayedDeletion = useDelayedLoading(); final delayedDeletion = useDelayedLoading();
final loggedInAction = useLoggedInAction(commentTree.comment.instanceHost); final loggedInAction = useLoggedInAction(commentTree.comment.instanceHost);
final newReplies = useState(const <CommentTree>[]); final newReplies = useState(const <CommentTree>[]);
final comment = commentTree.comment; final comment = commentTree.comment;
handleDelete(Jwt token) async { if (hideOnRead && isRead.value) {
final api = LemmyApiV2(token.payload.iss); return const SizedBox.shrink();
}
delayedDeletion.start(); handleDelete(Jwt token) {
Navigator.of(context).pop(); Navigator.of(context).pop();
try { delayedAction<FullCommentView>(
final res = await api.run(DeleteComment( context: context,
delayedLoading: delayedDeletion,
instanceHost: token.payload.iss,
query: DeleteComment(
commentId: comment.comment.id, commentId: comment.comment.id,
deleted: !isDeleted.value, deleted: !isDeleted.value,
auth: token.raw, auth: token.raw,
)); ),
isDeleted.value = res.commentView.comment.deleted; onSuccess: (res) => 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();
} }
void _openMoreMenu(BuildContext context) { void _openMoreMenu(BuildContext context) {
@ -162,7 +167,7 @@ class CommentWidget extends HookWidget {
}, },
), ),
ListTile( ListTile(
leading: Icon(showRaw.value ? Icons.brush : Icons.build), leading: markdownModeIcon(fancy: !showRaw.value),
title: Text('Show ${showRaw.value ? 'fancy' : 'raw'} text'), title: Text('Show ${showRaw.value ? 'fancy' : 'raw'} text'),
onTap: () { onTap: () {
showRaw.value = !showRaw.value; showRaw.value = !showRaw.value;
@ -188,29 +193,26 @@ class CommentWidget extends HookWidget {
reply() async { reply() async {
final newComment = await showCupertinoModalPopup<CommentView>( final newComment = await showCupertinoModalPopup<CommentView>(
context: context, context: context,
builder: (_) => WriteComment.toComment(comment), builder: (_) => WriteComment.toComment(
comment: comment.comment, post: comment.post),
); );
if (newComment != null) { if (newComment != null) {
newReplies.value = [...newReplies.value, CommentTree(newComment)]; newReplies.value = [...newReplies.value, CommentTree(newComment)];
} }
} }
vote(VoteType vote, Jwt token) async { vote(VoteType vote, Jwt token) => delayedAction<FullCommentView>(
final api = LemmyApiV2(token.payload.iss); context: context,
delayedLoading: delayedVoting,
delayedVoting.start(); instanceHost: token.payload.iss,
try { query: CreateCommentLike(
final res = await api.run(CreateCommentLike( commentId: comment.comment.id,
commentId: comment.comment.id, score: vote, auth: token.raw)); score: vote,
myVote.value = res.commentView.myVote ?? VoteType.none; auth: token.raw,
// ignore: avoid_catches_without_on_clauses ),
} catch (e) { onSuccess: (res) =>
Scaffold.of(context) myVote.value = res.commentView.myVote ?? VoteType.none,
.showSnackBar(const SnackBar(content: Text('voting failed :('))); );
return;
}
delayedVoting.cancel();
}
final body = () { final body = () {
if (isDeleted.value) { if (isDeleted.value) {
@ -261,7 +263,7 @@ class CommentWidget extends HookWidget {
if (selectable.value && if (selectable.value &&
!isDeleted.value && !isDeleted.value &&
!comment.comment.removed) !comment.comment.removed)
_CommentAction( TileAction(
icon: Icons.content_copy, icon: Icons.content_copy,
tooltip: 'copy', tooltip: 'copy',
onPressed: () { onPressed: () {
@ -272,27 +274,32 @@ class CommentWidget extends HookWidget {
content: Text('comment copied to clipboard')))); content: Text('comment copied to clipboard'))));
}), }),
const Spacer(), const Spacer(),
if (canBeMarkedAsRead)
_MarkAsRead(
commentTree.comment,
onChanged: (val) => isRead.value = val,
),
if (detached) if (detached)
_CommentAction( TileAction(
icon: Icons.link, icon: Icons.link,
onPressed: () => onPressed: () =>
goToPost(context, comment.instanceHost, comment.post.id), goToPost(context, comment.instanceHost, comment.post.id),
tooltip: 'go to post', tooltip: 'go to post',
), ),
_CommentAction( TileAction(
icon: Icons.more_horiz, icon: Icons.more_horiz,
onPressed: () => _openMoreMenu(context), onPressed: () => _openMoreMenu(context),
loading: delayedDeletion.loading, delayedLoading: delayedDeletion,
tooltip: 'more', tooltip: 'more',
), ),
_SaveComment(commentTree.comment), _SaveComment(commentTree.comment),
if (!isDeleted.value && !comment.comment.removed) if (!isDeleted.value && !comment.comment.removed)
_CommentAction( TileAction(
icon: Icons.reply, icon: Icons.reply,
onPressed: loggedInAction((_) => reply()), onPressed: loggedInAction((_) => reply()),
tooltip: 'reply', tooltip: 'reply',
), ),
_CommentAction( TileAction(
icon: Icons.arrow_upward, icon: Icons.arrow_upward,
iconColor: myVote.value == VoteType.up ? theme.accentColor : null, iconColor: myVote.value == VoteType.up ? theme.accentColor : null,
onPressed: loggedInAction((token) => vote( onPressed: loggedInAction((token) => vote(
@ -301,7 +308,7 @@ class CommentWidget extends HookWidget {
)), )),
tooltip: 'upvote', tooltip: 'upvote',
), ),
_CommentAction( TileAction(
icon: Icons.arrow_downward, icon: Icons.arrow_downward,
iconColor: myVote.value == VoteType.down ? Colors.red : null, iconColor: myVote.value == VoteType.down ? Colors.red : null,
onPressed: loggedInAction( onPressed: loggedInAction(
@ -314,18 +321,19 @@ class CommentWidget extends HookWidget {
), ),
]); ]);
return GestureDetector( return InkWell(
onLongPress: () => collapsed.value = !collapsed.value, onLongPress:
selectable.value ? null : () => collapsed.value = !collapsed.value,
child: Column( child: Column(
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(10), 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( decoration: BoxDecoration(
border: Border( border: Border(
left: indent > 0 left: depth > 0
? BorderSide( ? BorderSide(
color: colors[indent % colors.length], width: 5) color: colors[depth % colors.length], width: 5)
: BorderSide.none, : BorderSide.none,
top: const BorderSide(width: 0.2))), top: const BorderSide(width: 0.2))),
child: Column( child: Column(
@ -335,8 +343,8 @@ class CommentWidget extends HookWidget {
Padding( Padding(
padding: const EdgeInsets.only(right: 5), padding: const EdgeInsets.only(right: 5),
child: InkWell( child: InkWell(
onTap: () => goToUser.byId( onTap: () =>
context, comment.instanceHost, comment.creator.id), goToUser.fromUserSafe(context, comment.creator),
child: Avatar( child: Avatar(
url: comment.creator.avatar, url: comment.creator.avatar,
radius: 10, radius: 10,
@ -345,14 +353,16 @@ class CommentWidget extends HookWidget {
), ),
), ),
InkWell( InkWell(
onTap: () => goToUser.byId( onTap: () =>
context, comment.instanceHost, comment.creator.id), goToUser.fromUserSafe(context, comment.creator),
child: Text(comment.creator.originDisplayName, child: Text(comment.creator.originDisplayName,
style: TextStyle( 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) if (comment.creator.banned)
const _CommentTag('BANNED', Colors.red), const _CommentTag('BANNED', Colors.red),
if (comment.creatorBannedFromCommunity) if (comment.creatorBannedFromCommunity)
@ -389,17 +399,58 @@ class CommentWidget extends HookWidget {
), ),
if (!collapsed.value) if (!collapsed.value)
for (final c in newReplies.value.followedBy(commentTree.children)) for (final c in newReplies.value.followedBy(commentTree.children))
CommentWidget( CommentWidget(c, depth: depth + 1),
c,
indent: indent + 1,
postCreatorId: postCreatorId,
),
], ],
), ),
); );
} }
} }
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 { class _SaveComment extends HookWidget {
final CommentView comment; final CommentView comment;
@ -411,27 +462,20 @@ class _SaveComment extends HookWidget {
final isSaved = useState(comment.saved ?? false); final isSaved = useState(comment.saved ?? false);
final delayed = useDelayedLoading(); final delayed = useDelayedLoading();
handleSave(Jwt token) async { handleSave(Jwt token) => delayedAction<FullCommentView>(
final api = LemmyApiV2(comment.instanceHost); 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(); return TileAction(
try { delayedLoading: delayed,
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,
icon: isSaved.value ? Icons.bookmark : Icons.bookmark_border, icon: isSaved.value ? Icons.bookmark : Icons.bookmark_border,
onPressed: loggedInAction(delayed.pending ? (_) {} : handleSave), onPressed: loggedInAction(delayed.pending ? (_) {} : handleSave),
tooltip: '${isSaved.value ? 'unsave' : 'save'} comment', 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),
);
}

View File

@ -94,14 +94,9 @@ class CommentSection extends HookWidget {
), ),
) )
else if (sorting.value == CommentSortType.chat) else if (sorting.value == CommentSortType.chat)
for (final com in rawComments) for (final com in rawComments) CommentWidget.fromCommentView(com)
CommentWidget(
CommentTree(com),
postCreatorId: postCreatorId,
)
else else
for (final com in comments) for (final com in comments) CommentWidget(com),
CommentWidget(com, postCreatorId: postCreatorId),
const BottomSafe(kMinInteractiveDimension + kFloatingActionButtonMargin), const BottomSafe(kMinInteractiveDimension + kFloatingActionButtonMargin),
]); ]);
} }

View File

@ -73,7 +73,6 @@ class InfiniteScroll<T> extends HookWidget {
data.value = []; data.value = [];
hasMore.current = true; hasMore.current = true;
}; };
return controller.dispose;
} }
return null; return null;

View File

@ -1,21 +1,47 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void showInfoTablePopup(BuildContext context, Map<String, dynamic> table) { import 'bottom_modal.dart';
showDialog(
void showInfoTablePopup({
@required BuildContext context,
@required Map<String, dynamic> table,
String title,
}) {
assert(context != null);
assert(table != null);
showBottomModal(
context: context, context: context,
builder: (c) => SimpleDialog( title: title,
contentPadding: const EdgeInsets.symmetric( builder: (context) => Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20, horizontal: 20,
vertical: 15, vertical: 15,
), ),
children: [ child: Column(
Table( children: [
children: table.entries Table(children: [
.map((e) => TableRow( for (final e in table.entries)
children: [Text('${e.key}:'), Text(e.value.toString())])) TableRow(children: [
.toList(), 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())
])
]),
],
),
), ),
); );
} }

View File

@ -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),
);

View File

@ -76,17 +76,10 @@ class PostWidget extends HookWidget {
leading: const Icon(Icons.info_outline), leading: const Icon(Icons.info_outline),
title: const Text('Nerd stuff'), title: const Text('Nerd stuff'),
onTap: () { onTap: () {
showInfoTablePopup(context, { showInfoTablePopup(context: context, table: {
'id': post.post.id,
'apId': post.post.apId,
'upvotes': post.counts.upvotes,
'downvotes': post.counts.downvotes,
'score': post.counts.score,
'% of upvotes': '% of upvotes':
'${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%', '${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%',
'local': post.post.local, ...post.toJson(),
'published': post.post.published,
'updated': post.post.updated ?? 'never',
}); });
}, },
), ),
@ -124,6 +117,7 @@ class PostWidget extends HookWidget {
Padding( Padding(
padding: const EdgeInsets.only(right: 10), padding: const EdgeInsets.only(right: 10),
child: InkWell( child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () => goToCommunity.byId( onTap: () => goToCommunity.byId(
context, instanceHost, post.community.id), context, instanceHost, post.community.id),
child: Avatar( child: Avatar(
@ -195,10 +189,9 @@ class PostWidget extends HookWidget {
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w600), fontWeight: FontWeight.w600),
recognizer: TapGestureRecognizer() recognizer: TapGestureRecognizer()
..onTap = () => goToUser.byId( ..onTap = () => goToUser.fromUserSafe(
context, context,
post.instanceHost, post.creator,
post.creator.id,
), ),
), ),
TextSpan( TextSpan(
@ -263,6 +256,7 @@ class PostWidget extends HookWidget {
post.post.thumbnailUrl != null) ...[ post.post.thumbnailUrl != null) ...[
const Spacer(), const Spacer(),
InkWell( InkWell(
borderRadius: BorderRadius.circular(20),
onTap: _openLink, onTap: _openLink,
child: Stack( child: Stack(
children: [ children: [
@ -322,16 +316,27 @@ class PostWidget extends HookWidget {
Row( Row(
children: [ children: [
Flexible( Flexible(
child: Text(post.post.embedTitle ?? '', child: Text(
style: theme.textTheme.subtitle1 post.post.embedTitle ?? '',
.apply(fontWeightDelta: 2))) style: theme.textTheme.subtitle1
.apply(fontWeightDelta: 2),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
)
], ],
), ),
if (post.post.embedDescription != null && if (post.post.embedDescription != null &&
post.post.embedDescription.isNotEmpty) post.post.embedDescription.isNotEmpty)
Row( Row(
children: [ 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( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black54)], boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black45)],
color: theme.cardColor, color: theme.cardColor,
borderRadius: const BorderRadius.all(Radius.circular(20)), borderRadius: const BorderRadius.all(Radius.circular(20)),
), ),
child: InkWell( child: GestureDetector(
onTap: fullPost onTap: fullPost
? null ? null
: () => goTo(context, (context) => FullPostPage.fromPostView(post)), : () => goTo(context, (context) => FullPostPage.fromPostView(post)),
child: Column( child: Material(
children: [ type: MaterialType.transparency,
info(), child: Column(
title(), children: [
if (whatType(post.post.url) != MediaType.other && info(),
whatType(post.post.url) != MediaType.none) title(),
postImage() if (whatType(post.post.url) != MediaType.other &&
else if (post.post.url != null && post.post.url.isNotEmpty) whatType(post.post.url) != MediaType.none)
linkPreview(), postImage()
if (post.post.body != null && fullPost) else if (post.post.url != null && post.post.url.isNotEmpty)
Padding( linkPreview(),
padding: const EdgeInsets.all(10), if (post.post.body != null && fullPost)
child: Padding(
MarkdownText(post.post.body, instanceHost: instanceHost)), padding: const EdgeInsets.all(10),
if (post.post.body != null && !fullPost) child: MarkdownText(post.post.body,
LayoutBuilder( instanceHost: instanceHost)),
builder: (context, constraints) { if (post.post.body != null && !fullPost)
final span = TextSpan( LayoutBuilder(
text: post.post.body, builder: (context, constraints) {
); final span = TextSpan(
final tp = TextPainter( text: post.post.body,
text: span, );
maxLines: 10, final tp = TextPainter(
textDirection: Directionality.of(context), text: span,
)..layout(maxWidth: constraints.maxWidth - 20); maxLines: 10,
textDirection: Directionality.of(context),
)..layout(maxWidth: constraints.maxWidth - 20);
if (tp.didExceedMaxLines) { if (tp.didExceedMaxLines) {
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxHeight: tp.height), constraints: BoxConstraints(maxHeight: tp.height),
child: Stack( child: Stack(
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
children: [ children: [
ClipRect( ClipRect(
child: Align( child: Align(
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
heightFactor: 0.8, heightFactor: 0.8,
child: Padding( child: Padding(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: MarkdownText(post.post.body, child: MarkdownText(post.post.body,
instanceHost: instanceHost)), 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,
],
), ),
), ),
), Container(
], height: tp.preferredLineHeight * 2.5,
), decoration: BoxDecoration(
); gradient: LinearGradient(
} else { begin: Alignment.topCenter,
return Padding( end: Alignment.bottomCenter,
padding: const EdgeInsets.all(10), colors: [
child: MarkdownText(post.post.body, theme.cardColor.withAlpha(0),
instanceHost: instanceHost)); theme.cardColor,
} ],
}, ),
), ),
actions(), ),
], ],
),
);
} else {
return Padding(
padding: const EdgeInsets.all(10),
child: MarkdownText(post.post.body,
instanceHost: instanceHost));
}
},
),
actions(),
],
),
), ),
), ),
); );

View File

@ -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,
),
);
}
}

View File

@ -19,6 +19,7 @@ class SortableInfiniteList<T> extends HookWidget {
final InfiniteScrollController controller; final InfiniteScrollController controller;
final Function onStyleChange; final Function onStyleChange;
final Widget noItems; final Widget noItems;
final SortType defaultSort;
const SortableInfiniteList({ const SortableInfiniteList({
@required this.fetcher, @required this.fetcher,
@ -26,15 +27,17 @@ class SortableInfiniteList<T> extends HookWidget {
this.controller, this.controller,
this.onStyleChange, this.onStyleChange,
this.noItems, this.noItems,
this.defaultSort = SortType.active,
}) : assert(fetcher != null), }) : assert(fetcher != null),
assert(itemBuilder != null); assert(itemBuilder != null),
assert(defaultSort != null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final defaultController = useInfiniteScrollController(); final defaultController = useInfiniteScrollController();
final isc = controller ?? defaultController; final isc = controller ?? defaultController;
final sort = useState(SortType.active); final sort = useState(defaultSort);
void changeSorting(SortType newSort) { void changeSorting(SortType newSort) {
sort.value = newSort; sort.value = newSort;
@ -92,7 +95,6 @@ class InfiniteCommentList extends StatelessWidget {
Widget build(BuildContext context) => SortableInfiniteList<CommentView>( Widget build(BuildContext context) => SortableInfiniteList<CommentView>(
itemBuilder: (comment) => CommentWidget( itemBuilder: (comment) => CommentWidget(
CommentTree(comment), CommentTree(comment),
postCreatorId: null,
detached: true, detached: true,
), ),
fetcher: fetcher, fetcher: fetcher,

View File

@ -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),
);
}

View File

@ -4,24 +4,22 @@ import 'package:lemmy_api_client/v2.dart';
import '../hooks/delayed_loading.dart'; import '../hooks/delayed_loading.dart';
import '../hooks/stores.dart'; import '../hooks/stores.dart';
import 'markdown_mode_icon.dart';
import 'markdown_text.dart'; import 'markdown_text.dart';
/// Modal for writing a comment to a given post/comment (aka reply) /// Modal for writing a comment to a given post/comment (aka reply)
/// on submit pops the navigator stack with a [CommentView] /// on submit pops the navigator stack with a [CommentView]
/// or `null` if cancelled /// or `null` if cancelled
class WriteComment extends HookWidget { class WriteComment extends HookWidget {
final PostView post; final Post post;
final CommentView comment; final Comment comment;
final String instanceHost;
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey(); final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
WriteComment.toPost(this.post) WriteComment.toPost(this.post) : comment = null;
: instanceHost = post.instanceHost, WriteComment.toComment({@required this.comment, @required this.post})
comment = null; : assert(comment != null),
WriteComment.toComment(this.comment) assert(post != null);
: instanceHost = comment.instanceHost,
post = null;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -32,15 +30,15 @@ class WriteComment extends HookWidget {
final preview = () { final preview = () {
final body = MarkdownText( final body = MarkdownText(
comment?.comment?.content ?? post.post.body ?? '', comment?.content ?? post.body,
instanceHost: instanceHost, instanceHost: post.instanceHost,
); );
if (post != null) { if (post != null) {
return Column( return Column(
children: [ children: [
Text( Text(
post.post.name, post.name,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@ -53,22 +51,21 @@ class WriteComment extends HookWidget {
}(); }();
handleSubmit() async { handleSubmit() async {
final api = LemmyApiV2(instanceHost); final api = LemmyApiV2(post.instanceHost);
final token = accStore.defaultTokenFor(instanceHost); final token = accStore.defaultTokenFor(post.instanceHost);
delayed.start(); delayed.start();
try { try {
final res = await api.run(CreateComment( final res = await api.run(CreateComment(
content: controller.text, content: controller.text,
postId: post?.post?.id ?? comment.post.id, postId: post.id,
parentId: comment?.comment?.id, parentId: comment?.id,
auth: token.raw, auth: token.raw,
)); ));
Navigator.of(context).pop(res.commentView); Navigator.of(context).pop(res.commentView);
// ignore: avoid_catches_without_on_clauses // ignore: avoid_catches_without_on_clauses
} catch (e) { } catch (e) {
print(e);
scaffoldKey.currentState.showSnackBar( scaffoldKey.currentState.showSnackBar(
const SnackBar(content: Text('Failed to post comment'))); const SnackBar(content: Text('Failed to post comment')));
} }
@ -81,7 +78,7 @@ class WriteComment extends HookWidget {
leading: const CloseButton(), leading: const CloseButton(),
actions: [ actions: [
IconButton( IconButton(
icon: Icon(showFancy.value ? Icons.build : Icons.brush), icon: markdownModeIcon(fancy: showFancy.value),
onPressed: () => showFancy.value = !showFancy.value, onPressed: () => showFancy.value = !showFancy.value,
), ),
], ],
@ -110,7 +107,7 @@ class WriteComment extends HookWidget {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: MarkdownText( child: MarkdownText(
controller.text, controller.text,
instanceHost: instanceHost, instanceHost: post.instanceHost,
), ),
) )
], ],

View File

@ -232,14 +232,14 @@ packages:
name: image_picker name: image_picker
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.6.7+21" version: "0.6.7+22"
image_picker_platform_interface: image_picker_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: image_picker_platform_interface name: image_picker_platform_interface
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.1" version: "1.1.6"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@ -267,7 +267,7 @@ packages:
name: lemmy_api_client name: lemmy_api_client
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.11.0" version: "0.12.0"
markdown: markdown:
dependency: "direct main" dependency: "direct main"
description: description:
@ -323,7 +323,7 @@ packages:
name: package_info name: package_info
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3+2" version: "0.4.3+4"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -463,7 +463,7 @@ packages:
name: shared_preferences_windows name: shared_preferences_windows
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.0.2+2" version: "0.0.2+3"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -482,7 +482,7 @@ packages:
name: sqflite name: sqflite
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.2+2" version: "1.3.2+3"
sqflite_common: sqflite_common:
dependency: transitive dependency: transitive
description: description:
@ -580,7 +580,7 @@ packages:
name: url_launcher_web name: url_launcher_web
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.1.5+1" version: "0.1.5+3"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
@ -602,13 +602,20 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.1.0-nullsafety.3" 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: win32:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.7.4" version: "1.7.4+1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View File

@ -44,8 +44,7 @@ dependencies:
# utils # utils
timeago: ^2.0.27 timeago: ^2.0.27
fuzzy: <1.0.0 fuzzy: <1.0.0
lemmy_api_client: ^0.11.0 lemmy_api_client: ^0.12.0
matrix4_transform: ^1.1.7 matrix4_transform: ^1.1.7
flutter: flutter: