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 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
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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]);
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
|
@ -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,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(),
|
||||||
|
|
|
@ -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: [
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
|
))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// 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');
|
||||||
|
|
|
@ -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);
|
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;
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
|
|
|
@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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())
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 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,
|
||||||
|
|
|
@ -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/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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|
23
pubspec.lock
23
pubspec.lock
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue