Merge pull request #261 from krawieck/feature/blocking-v2
This commit is contained in:
commit
a38574c314
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "flutter",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": [
|
||||||
|
"pub",
|
||||||
|
"run",
|
||||||
|
"build_runner",
|
||||||
|
"build",
|
||||||
|
"--delete-conflicting-outputs"
|
||||||
|
],
|
||||||
|
"problemMatcher": ["$dart-build_runner"],
|
||||||
|
"group": "build",
|
||||||
|
"label": "flutter: build_runner build"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "flutter",
|
||||||
|
"command": "flutter",
|
||||||
|
"args": [
|
||||||
|
"pub",
|
||||||
|
"run",
|
||||||
|
"build_runner",
|
||||||
|
"watch",
|
||||||
|
"--delete-conflicting-outputs"
|
||||||
|
],
|
||||||
|
"problemMatcher": ["$dart-build_runner"],
|
||||||
|
"group": "build",
|
||||||
|
"label": "flutter: flutter pub run build_runner watch"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -4,6 +4,11 @@
|
||||||
|
|
||||||
- Logging: local logs about some actions/errors. Can be accessed from **settings > about lemmur > logs**
|
- Logging: local logs about some actions/errors. Can be accessed from **settings > about lemmur > logs**
|
||||||
- Android theme-aware splash screen (thanks to [@mimi89999](https://github.com/mimi89999))
|
- Android theme-aware splash screen (thanks to [@mimi89999](https://github.com/mimi89999))
|
||||||
|
- Blocking of users and communities (from post and from comment)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed a bug where post would go out of sync with full version of the post
|
||||||
|
|
||||||
## v0.6.0 - 2021-09-06
|
## v0.6.0 - 2021-09-06
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,7 @@ linter:
|
||||||
- use_setters_to_change_properties
|
- use_setters_to_change_properties
|
||||||
- use_to_and_as_if_applicable
|
- use_to_and_as_if_applicable
|
||||||
- void_checks
|
- void_checks
|
||||||
|
- use_named_constants
|
||||||
|
|
||||||
analyzer:
|
analyzer:
|
||||||
exclude:
|
exclude:
|
||||||
|
|
|
@ -39,6 +39,15 @@ extension on CommentSortType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension SortCommentTreeList on List<CommentTree> {
|
||||||
|
void sortBy(CommentSortType sortType) {
|
||||||
|
sort(sortType.sortFunction);
|
||||||
|
for (final el in this) {
|
||||||
|
el._sort(sortType.sortFunction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class CommentTree {
|
class CommentTree {
|
||||||
CommentView comment;
|
CommentView comment;
|
||||||
List<CommentTree> children = [];
|
List<CommentTree> children = [];
|
||||||
|
@ -71,14 +80,4 @@ class CommentTree {
|
||||||
el._sort(compare);
|
el._sort(compare);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sorts in-place a list of CommentTrees according to a given sortType
|
|
||||||
static List<CommentTree> sortList(
|
|
||||||
CommentSortType sortType, List<CommentTree> comms) {
|
|
||||||
comms.sort(sortType.sortFunction);
|
|
||||||
for (final el in comms) {
|
|
||||||
el._sort(sortType.sortFunction);
|
|
||||||
}
|
|
||||||
return comms;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../pages/settings.dart';
|
import '../pages/settings/settings.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
import 'stores.dart';
|
import 'stores.dart';
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ import '../l10n/l10n.dart';
|
||||||
import '../util/extensions/api.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/extensions/spaced.dart';
|
import '../util/extensions/spaced.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
|
import '../util/icons.dart';
|
||||||
import '../util/intl.dart';
|
import '../util/intl.dart';
|
||||||
import '../util/more_icon.dart';
|
|
||||||
import '../util/share.dart';
|
import '../util/share.dart';
|
||||||
import '../widgets/avatar.dart';
|
import '../widgets/avatar.dart';
|
||||||
import '../widgets/bottom_modal.dart';
|
import '../widgets/bottom_modal.dart';
|
||||||
|
|
|
@ -12,12 +12,11 @@ import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../util/extensions/api.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/extensions/spaced.dart';
|
import '../util/extensions/spaced.dart';
|
||||||
import '../util/goto.dart';
|
|
||||||
import '../util/pictrs.dart';
|
import '../util/pictrs.dart';
|
||||||
import '../widgets/editor.dart';
|
import '../widgets/editor.dart';
|
||||||
import '../widgets/markdown_mode_icon.dart';
|
import '../widgets/markdown_mode_icon.dart';
|
||||||
import '../widgets/radio_picker.dart';
|
import '../widgets/radio_picker.dart';
|
||||||
import 'full_post.dart';
|
import 'full_post/full_post.dart';
|
||||||
|
|
||||||
/// Fab that triggers the [CreatePost] modal
|
/// Fab that triggers the [CreatePost] modal
|
||||||
/// After creation it will navigate to the newly created post
|
/// After creation it will navigate to the newly created post
|
||||||
|
@ -39,10 +38,8 @@ class CreatePostFab extends HookWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (postView != null) {
|
if (postView != null) {
|
||||||
await goTo(
|
await Navigator.of(context)
|
||||||
context,
|
.push(FullPostPage.fromPostViewRoute(postView));
|
||||||
(_) => FullPostPage.fromPostView(postView),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
|
|
|
@ -1,156 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
|
||||||
|
|
||||||
import '../hooks/logged_in_action.dart';
|
|
||||||
import '../hooks/refreshable.dart';
|
|
||||||
import '../hooks/stores.dart';
|
|
||||||
import '../util/extensions/api.dart';
|
|
||||||
import '../util/more_icon.dart';
|
|
||||||
import '../util/share.dart';
|
|
||||||
import '../widgets/comment_section.dart';
|
|
||||||
import '../widgets/post.dart';
|
|
||||||
import '../widgets/reveal_after_scroll.dart';
|
|
||||||
import '../widgets/save_post_button.dart';
|
|
||||||
import '../widgets/write_comment.dart';
|
|
||||||
|
|
||||||
/// Displays a post with its comment section
|
|
||||||
class FullPostPage extends HookWidget {
|
|
||||||
final int id;
|
|
||||||
final String instanceHost;
|
|
||||||
final PostView? post;
|
|
||||||
|
|
||||||
const FullPostPage({required this.id, required this.instanceHost})
|
|
||||||
: post = null;
|
|
||||||
FullPostPage.fromPostView(PostView this.post)
|
|
||||||
: id = post.post.id,
|
|
||||||
instanceHost = post.instanceHost;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final accStore = useAccountsStore();
|
|
||||||
final scrollController = useScrollController();
|
|
||||||
|
|
||||||
final fullPostRefreshable =
|
|
||||||
useRefreshable(() => LemmyApiV3(instanceHost).run(GetPost(
|
|
||||||
id: id,
|
|
||||||
auth: accStore.defaultUserDataFor(instanceHost)?.jwt.raw,
|
|
||||||
)));
|
|
||||||
final loggedInAction = useLoggedInAction(instanceHost);
|
|
||||||
final newComments = useState(const <CommentView>[]);
|
|
||||||
|
|
||||||
// FALLBACK VIEW
|
|
||||||
if (!fullPostRefreshable.snapshot.hasData && this.post == null) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(),
|
|
||||||
body: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
if (fullPostRefreshable.snapshot.hasError)
|
|
||||||
Text(fullPostRefreshable.snapshot.error.toString())
|
|
||||||
else
|
|
||||||
const CircularProgressIndicator(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// VARIABLES
|
|
||||||
|
|
||||||
final post = fullPostRefreshable.snapshot.hasData
|
|
||||||
? fullPostRefreshable.snapshot.data!.postView
|
|
||||||
: this.post!;
|
|
||||||
|
|
||||||
final fullPost = fullPostRefreshable.snapshot.data;
|
|
||||||
|
|
||||||
// FUNCTIONS
|
|
||||||
|
|
||||||
refresh() async {
|
|
||||||
await HapticFeedback.mediumImpact();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fullPostRefreshable.refresh();
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Text(e.toString()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sharePost() => share(post.post.apId, context: context);
|
|
||||||
|
|
||||||
comment() async {
|
|
||||||
final newComment = await Navigator.of(context).push(
|
|
||||||
WriteComment.toPostRoute(post.post),
|
|
||||||
);
|
|
||||||
if (newComment != null) {
|
|
||||||
newComments.value = [...newComments.value, newComment];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
centerTitle: false,
|
|
||||||
title: RevealAfterScroll(
|
|
||||||
scrollController: scrollController,
|
|
||||||
after: 65,
|
|
||||||
child: Text(
|
|
||||||
post.community.originPreferredName,
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
IconButton(icon: const Icon(Icons.share), onPressed: sharePost),
|
|
||||||
SavePostButton(post),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(moreIcon),
|
|
||||||
onPressed: () => PostWidget.showMoreMenu(
|
|
||||||
context: context,
|
|
||||||
post: post,
|
|
||||||
fullPost: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
floatingActionButton: post.post.locked
|
|
||||||
? null
|
|
||||||
: FloatingActionButton(
|
|
||||||
onPressed: loggedInAction((_) => comment()),
|
|
||||||
child: const Icon(Icons.comment),
|
|
||||||
),
|
|
||||||
body: RefreshIndicator(
|
|
||||||
onRefresh: refresh,
|
|
||||||
child: ListView(
|
|
||||||
controller: scrollController,
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 15),
|
|
||||||
PostWidget(post, fullPost: true),
|
|
||||||
if (fullPost != null)
|
|
||||||
CommentSection(
|
|
||||||
newComments.value.followedBy(fullPost.comments).toList(),
|
|
||||||
postCreatorId: fullPost.postView.creator.id)
|
|
||||||
else if (fullPostRefreshable.snapshot.hasError)
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 10, vertical: 30),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.error),
|
|
||||||
Text('Error: ${fullPostRefreshable.snapshot.error}')
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.only(top: 40),
|
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,135 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../comment_tree.dart';
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../widgets/bottom_modal.dart';
|
||||||
|
import '../../widgets/bottom_safe.dart';
|
||||||
|
import '../../widgets/comment/comment.dart';
|
||||||
|
import 'full_post.dart';
|
||||||
|
import 'full_post_store.dart';
|
||||||
|
|
||||||
|
class _SortSelection {
|
||||||
|
final IconData icon;
|
||||||
|
final String term;
|
||||||
|
|
||||||
|
const _SortSelection(this.icon, this.term);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Manages comments section, sorts them
|
||||||
|
class CommentSection extends StatelessWidget {
|
||||||
|
static const sortPairs = {
|
||||||
|
CommentSortType.hot: _SortSelection(Icons.whatshot, L10nStrings.hot),
|
||||||
|
CommentSortType.new_: _SortSelection(Icons.new_releases, L10nStrings.new_),
|
||||||
|
CommentSortType.old: _SortSelection(Icons.calendar_today, L10nStrings.old),
|
||||||
|
CommentSortType.top: _SortSelection(Icons.trending_up, L10nStrings.top),
|
||||||
|
CommentSortType.chat: _SortSelection(Icons.chat, L10nStrings.chat),
|
||||||
|
};
|
||||||
|
|
||||||
|
const CommentSection({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ObserverBuilder<FullPostStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
final fullPostView = store.fullPostView;
|
||||||
|
|
||||||
|
// error & spinner handling
|
||||||
|
if (fullPostView == null) {
|
||||||
|
if (store.fullPostState.errorTerm != null) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 30),
|
||||||
|
child: FailedToLoad(
|
||||||
|
message: 'Comments failed to load',
|
||||||
|
refresh: () => store.refresh(context
|
||||||
|
.read<AccountsStore>()
|
||||||
|
.defaultUserDataFor(store.instanceHost)
|
||||||
|
?.jwt)),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 40),
|
||||||
|
child: Center(child: CircularProgressIndicator.adaptive()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () {
|
||||||
|
showBottomModal(
|
||||||
|
title: 'sort by',
|
||||||
|
context: context,
|
||||||
|
builder: (context) => Column(
|
||||||
|
children: [
|
||||||
|
for (final e in sortPairs.entries)
|
||||||
|
ListTile(
|
||||||
|
leading: Icon(e.value.icon),
|
||||||
|
title: Text((e.value.term).tr(context)),
|
||||||
|
trailing: store.sorting == e.key
|
||||||
|
? const Icon(Icons.check)
|
||||||
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
store.updateSorting(e.key);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text((sortPairs[store.sorting]!.term).tr(context)),
|
||||||
|
const Icon(Icons.arrow_drop_down),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// sorting menu goes here
|
||||||
|
if (fullPostView.comments.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 50),
|
||||||
|
child: Text(
|
||||||
|
'no comments yet',
|
||||||
|
style: TextStyle(fontStyle: FontStyle.italic),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else ...[
|
||||||
|
for (final com in store.newComments)
|
||||||
|
CommentWidget.fromCommentView(
|
||||||
|
com,
|
||||||
|
detached: false,
|
||||||
|
key: ValueKey(com),
|
||||||
|
),
|
||||||
|
if (store.sorting == CommentSortType.chat)
|
||||||
|
for (final com in fullPostView.comments)
|
||||||
|
CommentWidget.fromCommentView(
|
||||||
|
com,
|
||||||
|
detached: false,
|
||||||
|
key: ValueKey(com),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
for (final com in store.sortedCommentTree!)
|
||||||
|
CommentWidget(
|
||||||
|
com,
|
||||||
|
key: ValueKey(com),
|
||||||
|
),
|
||||||
|
const BottomSafe.fab(),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import '../../util/async_store_listener.dart';
|
||||||
|
import '../../util/extensions/api.dart';
|
||||||
|
import '../../util/icons.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../util/share.dart';
|
||||||
|
import '../../widgets/post/post.dart';
|
||||||
|
import '../../widgets/post/post_more_menu.dart';
|
||||||
|
import '../../widgets/post/post_store.dart';
|
||||||
|
import '../../widgets/post/save_post_button.dart';
|
||||||
|
import '../../widgets/reveal_after_scroll.dart';
|
||||||
|
import '../../widgets/write_comment.dart';
|
||||||
|
import 'comment_section.dart';
|
||||||
|
import 'full_post_store.dart';
|
||||||
|
|
||||||
|
/// Displays a post with its comment section
|
||||||
|
class FullPostPage extends HookWidget {
|
||||||
|
const FullPostPage._();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
|
||||||
|
final loggedInAction =
|
||||||
|
useLoggedInAction(context.read<FullPostStore>().instanceHost);
|
||||||
|
|
||||||
|
return AsyncStoreListener(
|
||||||
|
asyncStore: context.read<FullPostStore>().fullPostState,
|
||||||
|
child: AsyncStoreListener<BlockedCommunity>(
|
||||||
|
asyncStore: context.read<FullPostStore>().communityBlockingState,
|
||||||
|
successMessageBuilder: (context, data) {
|
||||||
|
final name = data.communityView.community.originPreferredName;
|
||||||
|
return '${data.blocked ? 'Blocked' : 'Unblocked'} $name';
|
||||||
|
},
|
||||||
|
child: ObserverBuilder<FullPostStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
Future<void> refresh() async {
|
||||||
|
unawaited(HapticFeedback.mediumImpact());
|
||||||
|
await store.refresh(context
|
||||||
|
.read<AccountsStore>()
|
||||||
|
.defaultUserDataFor(store.instanceHost)
|
||||||
|
?.jwt);
|
||||||
|
}
|
||||||
|
|
||||||
|
final postStore = store.postStore;
|
||||||
|
|
||||||
|
if (postStore == null) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(),
|
||||||
|
body: Center(
|
||||||
|
child: (store.fullPostState.isLoading)
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: FailedToLoad(
|
||||||
|
message: 'Post failed to load', refresh: refresh),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final post = postStore.postView;
|
||||||
|
|
||||||
|
// VARIABLES
|
||||||
|
|
||||||
|
sharePost() => share(post.post.apId, context: context);
|
||||||
|
|
||||||
|
comment() async {
|
||||||
|
final newComment = await Navigator.of(context).push(
|
||||||
|
WriteComment.toPostRoute(post.post),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newComment != null) {
|
||||||
|
store.addComment(newComment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
centerTitle: false,
|
||||||
|
title: RevealAfterScroll(
|
||||||
|
scrollController: scrollController,
|
||||||
|
after: 65,
|
||||||
|
child: Text(
|
||||||
|
post.community.originPreferredName,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(icon: Icon(shareIcon), onPressed: sharePost),
|
||||||
|
Provider.value(
|
||||||
|
value: postStore,
|
||||||
|
child: const SavePostButton(),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(moreIcon),
|
||||||
|
onPressed: () => PostMoreMenuButton.show(
|
||||||
|
context: context,
|
||||||
|
postStore: postStore,
|
||||||
|
fullPostStore: store,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: post.post.locked
|
||||||
|
? null
|
||||||
|
: FloatingActionButton(
|
||||||
|
onPressed: loggedInAction((_) => comment()),
|
||||||
|
child: const Icon(Icons.comment),
|
||||||
|
),
|
||||||
|
body: RefreshIndicator(
|
||||||
|
onRefresh: refresh,
|
||||||
|
child: ListView(
|
||||||
|
controller: scrollController,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
PostTile.fromPostStore(postStore),
|
||||||
|
const CommentSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Jwt? _tryGetJwt(BuildContext context, String instanceHost) {
|
||||||
|
return context.read<AccountsStore>().defaultUserDataFor(instanceHost)?.jwt;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Route route(int id, String instanceHost) => MaterialPageRoute(
|
||||||
|
builder: (context) => Provider(
|
||||||
|
create: (context) =>
|
||||||
|
FullPostStore(instanceHost: instanceHost, postId: id)
|
||||||
|
..refresh(_tryGetJwt(context, instanceHost)),
|
||||||
|
child: const FullPostPage._(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
static Route fromPostViewRoute(PostView postView) => MaterialPageRoute(
|
||||||
|
builder: (context) => Provider(
|
||||||
|
create: (context) => FullPostStore.fromPostView(postView)
|
||||||
|
..refresh(_tryGetJwt(context, postView.instanceHost)),
|
||||||
|
child: const FullPostPage._(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
static Route fromPostStoreRoute(PostStore postStore) => MaterialPageRoute(
|
||||||
|
builder: (context) => Provider(
|
||||||
|
create: (context) => FullPostStore.fromPostStore(postStore)
|
||||||
|
..refresh(_tryGetJwt(context, postStore.postView.instanceHost)),
|
||||||
|
child: const FullPostPage._(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FailedToLoad extends StatelessWidget {
|
||||||
|
final String message;
|
||||||
|
final VoidCallback refresh;
|
||||||
|
|
||||||
|
const FailedToLoad({required this.refresh, required this.message});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(message),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: refresh,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('try again'),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../comment_tree.dart';
|
||||||
|
import '../../util/async_store.dart';
|
||||||
|
import '../../widgets/post/post_store.dart';
|
||||||
|
|
||||||
|
part 'full_post_store.g.dart';
|
||||||
|
|
||||||
|
class FullPostStore = _FullPostStore with _$FullPostStore;
|
||||||
|
|
||||||
|
abstract class _FullPostStore with Store {
|
||||||
|
final int postId;
|
||||||
|
final String instanceHost;
|
||||||
|
|
||||||
|
_FullPostStore({
|
||||||
|
this.postStore,
|
||||||
|
required this.postId,
|
||||||
|
required this.instanceHost,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ignore: unused_element
|
||||||
|
_FullPostStore.fromPostView(PostView postView)
|
||||||
|
: postId = postView.post.id,
|
||||||
|
instanceHost = postView.instanceHost,
|
||||||
|
postStore = PostStore(postView);
|
||||||
|
|
||||||
|
// ignore: unused_element
|
||||||
|
_FullPostStore.fromPostStore(PostStore this.postStore)
|
||||||
|
: postId = postStore.postView.post.id,
|
||||||
|
instanceHost = postStore.postView.instanceHost;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
FullPostView? fullPostView;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
ObservableList<CommentView> newComments = ObservableList<CommentView>();
|
||||||
|
|
||||||
|
@observable
|
||||||
|
CommentSortType sorting = CommentSortType.hot;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
PostStore? postStore;
|
||||||
|
|
||||||
|
final fullPostState = AsyncStore<FullPostView>();
|
||||||
|
final communityBlockingState = AsyncStore<BlockedCommunity>();
|
||||||
|
|
||||||
|
@action
|
||||||
|
// ignore: use_setters_to_change_properties
|
||||||
|
void updateSorting(CommentSortType sort) {
|
||||||
|
sorting = sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
List<CommentTree>? get commentTree {
|
||||||
|
if (fullPostView == null) return null;
|
||||||
|
return CommentTree.fromList(fullPostView!.comments);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
List<CommentTree>? get sortedCommentTree {
|
||||||
|
return commentTree?..sortBy(sorting);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
PostView? get postView => postStore?.postView;
|
||||||
|
|
||||||
|
@computed
|
||||||
|
Iterable<CommentView>? get comments =>
|
||||||
|
fullPostView?.comments.followedBy(newComments);
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> refresh([Jwt? token]) async {
|
||||||
|
final result = await fullPostState.runLemmy(
|
||||||
|
instanceHost, GetPost(id: postId, auth: token?.raw));
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
postStore ??= PostStore(result.postView);
|
||||||
|
|
||||||
|
fullPostView = result;
|
||||||
|
postStore!.updatePostView(result.postView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> blockCommunity(Jwt token) async {
|
||||||
|
final result = await communityBlockingState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
BlockCommunity(
|
||||||
|
communityId: fullPostView!.communityView.community.id,
|
||||||
|
block: !fullPostView!.communityView.blocked,
|
||||||
|
auth: token.raw));
|
||||||
|
if (result != null) {
|
||||||
|
fullPostView =
|
||||||
|
fullPostView!.copyWith(communityView: result.communityView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
void addComment(CommentView commentView) =>
|
||||||
|
newComments.insert(0, commentView);
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'full_post_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
||||||
|
|
||||||
|
mixin _$FullPostStore on _FullPostStore, Store {
|
||||||
|
Computed<List<CommentTree>?>? _$commentTreeComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CommentTree>? get commentTree => (_$commentTreeComputed ??=
|
||||||
|
Computed<List<CommentTree>?>(() => super.commentTree,
|
||||||
|
name: '_FullPostStore.commentTree'))
|
||||||
|
.value;
|
||||||
|
Computed<List<CommentTree>?>? _$sortedCommentTreeComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CommentTree>? get sortedCommentTree => (_$sortedCommentTreeComputed ??=
|
||||||
|
Computed<List<CommentTree>?>(() => super.sortedCommentTree,
|
||||||
|
name: '_FullPostStore.sortedCommentTree'))
|
||||||
|
.value;
|
||||||
|
Computed<PostView?>? _$postViewComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PostView? get postView =>
|
||||||
|
(_$postViewComputed ??= Computed<PostView?>(() => super.postView,
|
||||||
|
name: '_FullPostStore.postView'))
|
||||||
|
.value;
|
||||||
|
Computed<Iterable<CommentView>?>? _$commentsComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<CommentView>? get comments => (_$commentsComputed ??=
|
||||||
|
Computed<Iterable<CommentView>?>(() => super.comments,
|
||||||
|
name: '_FullPostStore.comments'))
|
||||||
|
.value;
|
||||||
|
|
||||||
|
final _$fullPostViewAtom = Atom(name: '_FullPostStore.fullPostView');
|
||||||
|
|
||||||
|
@override
|
||||||
|
FullPostView? get fullPostView {
|
||||||
|
_$fullPostViewAtom.reportRead();
|
||||||
|
return super.fullPostView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set fullPostView(FullPostView? value) {
|
||||||
|
_$fullPostViewAtom.reportWrite(value, super.fullPostView, () {
|
||||||
|
super.fullPostView = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$newCommentsAtom = Atom(name: '_FullPostStore.newComments');
|
||||||
|
|
||||||
|
@override
|
||||||
|
ObservableList<CommentView> get newComments {
|
||||||
|
_$newCommentsAtom.reportRead();
|
||||||
|
return super.newComments;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set newComments(ObservableList<CommentView> value) {
|
||||||
|
_$newCommentsAtom.reportWrite(value, super.newComments, () {
|
||||||
|
super.newComments = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$sortingAtom = Atom(name: '_FullPostStore.sorting');
|
||||||
|
|
||||||
|
@override
|
||||||
|
CommentSortType get sorting {
|
||||||
|
_$sortingAtom.reportRead();
|
||||||
|
return super.sorting;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set sorting(CommentSortType value) {
|
||||||
|
_$sortingAtom.reportWrite(value, super.sorting, () {
|
||||||
|
super.sorting = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$postStoreAtom = Atom(name: '_FullPostStore.postStore');
|
||||||
|
|
||||||
|
@override
|
||||||
|
PostStore? get postStore {
|
||||||
|
_$postStoreAtom.reportRead();
|
||||||
|
return super.postStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set postStore(PostStore? value) {
|
||||||
|
_$postStoreAtom.reportWrite(value, super.postStore, () {
|
||||||
|
super.postStore = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$refreshAsyncAction = AsyncAction('_FullPostStore.refresh');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> refresh([Jwt? token]) {
|
||||||
|
return _$refreshAsyncAction.run(() => super.refresh(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$blockCommunityAsyncAction =
|
||||||
|
AsyncAction('_FullPostStore.blockCommunity');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> blockCommunity(Jwt token) {
|
||||||
|
return _$blockCommunityAsyncAction.run(() => super.blockCommunity(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$_FullPostStoreActionController =
|
||||||
|
ActionController(name: '_FullPostStore');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateSorting(CommentSortType sort) {
|
||||||
|
final _$actionInfo = _$_FullPostStoreActionController.startAction(
|
||||||
|
name: '_FullPostStore.updateSorting');
|
||||||
|
try {
|
||||||
|
return super.updateSorting(sort);
|
||||||
|
} finally {
|
||||||
|
_$_FullPostStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void addComment(CommentView commentView) {
|
||||||
|
final _$actionInfo = _$_FullPostStoreActionController.startAction(
|
||||||
|
name: '_FullPostStore.addComment');
|
||||||
|
try {
|
||||||
|
return super.addComment(commentView);
|
||||||
|
} finally {
|
||||||
|
_$_FullPostStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
fullPostView: ${fullPostView},
|
||||||
|
newComments: ${newComments},
|
||||||
|
sorting: ${sorting},
|
||||||
|
postStore: ${postStore},
|
||||||
|
commentTree: ${commentTree},
|
||||||
|
sortedCommentTree: ${sortedCommentTree},
|
||||||
|
postView: ${postView},
|
||||||
|
comments: ${comments}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,8 +13,8 @@ import '../widgets/bottom_modal.dart';
|
||||||
import '../widgets/cached_network_image.dart';
|
import '../widgets/cached_network_image.dart';
|
||||||
import '../widgets/infinite_scroll.dart';
|
import '../widgets/infinite_scroll.dart';
|
||||||
import '../widgets/sortable_infinite_list.dart';
|
import '../widgets/sortable_infinite_list.dart';
|
||||||
import 'add_account.dart';
|
|
||||||
import 'inbox.dart';
|
import 'inbox.dart';
|
||||||
|
import 'settings/add_account.dart';
|
||||||
|
|
||||||
/// First thing users sees when opening the app
|
/// First thing users sees when opening the app
|
||||||
/// Shows list of posts from all or just specific instances
|
/// Shows list of posts from all or just specific instances
|
||||||
|
|
|
@ -13,7 +13,7 @@ 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';
|
||||||
import '../util/more_icon.dart';
|
import '../util/icons.dart';
|
||||||
import '../widgets/bottom_modal.dart';
|
import '../widgets/bottom_modal.dart';
|
||||||
import '../widgets/cached_network_image.dart';
|
import '../widgets/cached_network_image.dart';
|
||||||
import '../widgets/comment/comment.dart';
|
import '../widgets/comment/comment.dart';
|
||||||
|
|
|
@ -8,7 +8,7 @@ import '../l10n/l10n.dart';
|
||||||
import '../util/extensions/api.dart';
|
import '../util/extensions/api.dart';
|
||||||
import '../util/extensions/spaced.dart';
|
import '../util/extensions/spaced.dart';
|
||||||
import '../util/goto.dart';
|
import '../util/goto.dart';
|
||||||
import '../util/more_icon.dart';
|
import '../util/icons.dart';
|
||||||
import '../util/share.dart';
|
import '../util/share.dart';
|
||||||
import '../util/text_color.dart';
|
import '../util/text_color.dart';
|
||||||
import '../widgets/avatar.dart';
|
import '../widgets/avatar.dart';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import '../hooks/delayed_loading.dart';
|
||||||
import '../hooks/image_picker.dart';
|
import '../hooks/image_picker.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../util/more_icon.dart';
|
import '../util/icons.dart';
|
||||||
import '../util/pictrs.dart';
|
import '../util/pictrs.dart';
|
||||||
import '../widgets/bottom_modal.dart';
|
import '../widgets/bottom_modal.dart';
|
||||||
import '../widgets/bottom_safe.dart';
|
import '../widgets/bottom_safe.dart';
|
||||||
|
|
|
@ -6,7 +6,7 @@ import '../util/goto.dart';
|
||||||
import '../widgets/radio_picker.dart';
|
import '../widgets/radio_picker.dart';
|
||||||
import '../widgets/user_profile.dart';
|
import '../widgets/user_profile.dart';
|
||||||
import 'saved_page.dart';
|
import 'saved_page.dart';
|
||||||
import 'settings.dart';
|
import 'settings/settings.dart';
|
||||||
|
|
||||||
/// Profile page for a logged in user. The difference between this and
|
/// Profile page for a logged in user. The difference between this and
|
||||||
/// UserPage is that here you have access to settings
|
/// UserPage is that here you have access to settings
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:lemmy_api_client/v3.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../l10n/l10n.dart';
|
||||||
import '../widgets/comment/comment.dart';
|
import '../widgets/comment/comment.dart';
|
||||||
import '../widgets/post.dart';
|
import '../widgets/post/post.dart';
|
||||||
import '../widgets/sortable_infinite_list.dart';
|
import '../widgets/sortable_infinite_list.dart';
|
||||||
import 'communities_list.dart';
|
import 'communities_list.dart';
|
||||||
import 'users_list.dart';
|
import 'users_list.dart';
|
||||||
|
@ -108,7 +108,7 @@ class _SearchResultsList extends HookWidget {
|
||||||
case SearchType.posts:
|
case SearchType.posts:
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 20),
|
padding: const EdgeInsets.only(bottom: 20),
|
||||||
child: PostWidget(data as PostView),
|
child: PostTile.fromPostView(data as PostView),
|
||||||
);
|
);
|
||||||
case SearchType.users:
|
case SearchType.users:
|
||||||
return UsersListItem(user: data as PersonViewSafe);
|
return UsersListItem(user: data as PersonViewSafe);
|
||||||
|
|
|
@ -4,13 +4,13 @@ import 'package:lemmy_api_client/v3.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
import '../../hooks/delayed_loading.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
import '../stores/config_store.dart';
|
import '../../stores/config_store.dart';
|
||||||
import '../widgets/cached_network_image.dart';
|
import '../../widgets/cached_network_image.dart';
|
||||||
import '../widgets/fullscreenable_image.dart';
|
import '../../widgets/fullscreenable_image.dart';
|
||||||
import '../widgets/radio_picker.dart';
|
import '../../widgets/radio_picker.dart';
|
||||||
import 'add_instance.dart';
|
import 'add_instance.dart';
|
||||||
|
|
||||||
/// A modal where an account can be added for a given instance
|
/// A modal where an account can be added for a given instance
|
|
@ -2,11 +2,11 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../hooks/debounce.dart';
|
import '../../hooks/debounce.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../../hooks/stores.dart';
|
||||||
import '../util/cleanup_url.dart';
|
import '../../util/cleanup_url.dart';
|
||||||
import '../widgets/cached_network_image.dart';
|
import '../../widgets/cached_network_image.dart';
|
||||||
import '../widgets/fullscreenable_image.dart';
|
import '../../widgets/fullscreenable_image.dart';
|
||||||
|
|
||||||
/// A page that let's user add a new instance. Pops a url of the added instance
|
/// A page that let's user add a new instance. Pops a url of the added instance
|
||||||
class AddInstancePage extends HookWidget {
|
class AddInstancePage extends HookWidget {
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../../util/async_store_listener.dart';
|
||||||
|
import '../../../util/extensions/api.dart';
|
||||||
|
import '../../../util/goto.dart';
|
||||||
|
import '../../../util/observer_consumers.dart';
|
||||||
|
import '../../../widgets/avatar.dart';
|
||||||
|
import 'community_block_store.dart';
|
||||||
|
import 'user_block_store.dart';
|
||||||
|
|
||||||
|
class BlockPersonTile extends StatelessWidget {
|
||||||
|
const BlockPersonTile({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AsyncStoreListener(
|
||||||
|
asyncStore: context.read<UserBlockStore>().unblockingState,
|
||||||
|
child: ObserverBuilder<UserBlockStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Avatar(url: store.person.avatar),
|
||||||
|
title: Text(store.person.originPreferredName),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: store.unblockingState.isLoading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: const Icon(Icons.cancel),
|
||||||
|
tooltip: 'unblock',
|
||||||
|
onPressed: store.unblock,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
goToUser.byId(
|
||||||
|
context, store.person.instanceHost, store.person.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BlockCommunityTile extends HookWidget {
|
||||||
|
const BlockCommunityTile({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AsyncStoreListener(
|
||||||
|
asyncStore: context.read<CommunityBlockStore>().unblockingState,
|
||||||
|
child: ObserverBuilder<CommunityBlockStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
return ListTile(
|
||||||
|
leading: Avatar(url: store.community.icon),
|
||||||
|
title: Text(store.community.originPreferredName),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: store.unblockingState.isLoading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: const Icon(Icons.cancel),
|
||||||
|
tooltip: 'unblock',
|
||||||
|
onPressed: store.unblock,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
goToCommunity.byId(
|
||||||
|
context, store.community.instanceHost, store.community.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../../hooks/stores.dart';
|
||||||
|
import '../../../l10n/l10n_from_string.dart';
|
||||||
|
import '../../../stores/accounts_store.dart';
|
||||||
|
import '../../../util/async_store_listener.dart';
|
||||||
|
import '../../../util/observer_consumers.dart';
|
||||||
|
import 'block_tile.dart';
|
||||||
|
import 'blocks_store.dart';
|
||||||
|
|
||||||
|
class BlocksPage extends HookWidget {
|
||||||
|
const BlocksPage._();
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final accStore = useAccountsStore();
|
||||||
|
|
||||||
|
return DefaultTabController(
|
||||||
|
length: accStore.loggedInInstances.length,
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Blocks'),
|
||||||
|
bottom: TabBar(
|
||||||
|
isScrollable: true,
|
||||||
|
tabs: [
|
||||||
|
for (final instance in accStore.loggedInInstances)
|
||||||
|
Tab(
|
||||||
|
child: Text(
|
||||||
|
'${accStore.defaultUsernameFor(instance)!}@$instance'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: TabBarView(
|
||||||
|
children: [
|
||||||
|
for (final instance in accStore.loggedInInstances)
|
||||||
|
_UserBlocksWrapper(
|
||||||
|
instanceHost: instance,
|
||||||
|
username: accStore.defaultUsernameFor(instance)!,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Route route() =>
|
||||||
|
MaterialPageRoute(builder: (context) => const BlocksPage._());
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserBlocksWrapper extends StatelessWidget {
|
||||||
|
final String instanceHost;
|
||||||
|
final String username;
|
||||||
|
|
||||||
|
const _UserBlocksWrapper(
|
||||||
|
{required this.instanceHost, required this.username});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Provider<BlocksStore>(
|
||||||
|
create: (context) => BlocksStore(
|
||||||
|
instanceHost: instanceHost,
|
||||||
|
token: context
|
||||||
|
.read<AccountsStore>()
|
||||||
|
.userDataFor(instanceHost, username)!
|
||||||
|
.jwt,
|
||||||
|
)..refresh(),
|
||||||
|
child: const _UserBlocks(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UserBlocks extends StatelessWidget {
|
||||||
|
const _UserBlocks();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AsyncStoreListener(
|
||||||
|
asyncStore: context.read<BlocksStore>().blocksState,
|
||||||
|
child: ObserverBuilder<BlocksStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: store.refresh,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
if (!store.isUsable) ...[
|
||||||
|
if (store.blocksState.isLoading)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 64),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (store.blocksState.errorTerm != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 64),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
store.blocksState.errorTerm!.tr(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
] else ...[
|
||||||
|
for (final user in store.blockedUsers!)
|
||||||
|
Provider(
|
||||||
|
create: (context) => user,
|
||||||
|
key: ValueKey(user),
|
||||||
|
child: const BlockPersonTile(),
|
||||||
|
),
|
||||||
|
if (store.blockedUsers!.isEmpty)
|
||||||
|
const ListTile(
|
||||||
|
title: Center(
|
||||||
|
child: Text('No users blocked'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// TODO: add user search & block
|
||||||
|
// ListTile(
|
||||||
|
// leading: const Padding(
|
||||||
|
// padding: EdgeInsets.only(left: 16, right: 10),
|
||||||
|
// child: Icon(Icons.add),
|
||||||
|
// ),
|
||||||
|
// onTap: () {},
|
||||||
|
// title: const Text('Block User'),
|
||||||
|
// ),
|
||||||
|
const Divider(),
|
||||||
|
for (final community in store.blockedCommunities!)
|
||||||
|
Provider(
|
||||||
|
create: (context) => community,
|
||||||
|
key: ValueKey(community),
|
||||||
|
child: const BlockCommunityTile(),
|
||||||
|
),
|
||||||
|
if (store.blockedCommunities!.isEmpty)
|
||||||
|
const ListTile(
|
||||||
|
title: Center(
|
||||||
|
child: Text('No communities blocked'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// TODO: add community search & block
|
||||||
|
// const ListTile(
|
||||||
|
// leading: Padding(
|
||||||
|
// padding: EdgeInsets.only(left: 16, right: 10),
|
||||||
|
// child: Icon(Icons.add),
|
||||||
|
// ),
|
||||||
|
// onTap: () {},
|
||||||
|
// title: Text('Block Community'),
|
||||||
|
// ),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../../util/async_store.dart';
|
||||||
|
import 'community_block_store.dart';
|
||||||
|
import 'user_block_store.dart';
|
||||||
|
|
||||||
|
part 'blocks_store.g.dart';
|
||||||
|
|
||||||
|
class BlocksStore = _BlocksStore with _$BlocksStore;
|
||||||
|
|
||||||
|
abstract class _BlocksStore with Store {
|
||||||
|
final String instanceHost;
|
||||||
|
final Jwt token;
|
||||||
|
|
||||||
|
_BlocksStore({required this.instanceHost, required this.token});
|
||||||
|
|
||||||
|
@observable
|
||||||
|
List<UserBlockStore>? _blockedUsers;
|
||||||
|
|
||||||
|
@observable
|
||||||
|
List<CommunityBlockStore>? _blockedCommunities;
|
||||||
|
|
||||||
|
final blocksState = AsyncStore<FullSiteView>();
|
||||||
|
|
||||||
|
@computed
|
||||||
|
Iterable<UserBlockStore>? get blockedUsers =>
|
||||||
|
_blockedUsers?.where((u) => u.blocked);
|
||||||
|
|
||||||
|
@computed
|
||||||
|
Iterable<CommunityBlockStore>? get blockedCommunities =>
|
||||||
|
_blockedCommunities?.where((c) => c.blocked);
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get isUsable => blockedUsers != null && blockedCommunities != null;
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> refresh() async {
|
||||||
|
final result =
|
||||||
|
await blocksState.runLemmy(instanceHost, GetSite(auth: token.raw));
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
_blockedUsers = result.myUser!.personBlocks
|
||||||
|
.map((e) => UserBlockStore(
|
||||||
|
instanceHost: instanceHost, token: token, person: e.target))
|
||||||
|
.toList();
|
||||||
|
_blockedCommunities = result.myUser!.communityBlocks
|
||||||
|
.map((e) => CommunityBlockStore(
|
||||||
|
instanceHost: instanceHost, token: token, community: e.community))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'blocks_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
||||||
|
|
||||||
|
mixin _$BlocksStore on _BlocksStore, Store {
|
||||||
|
Computed<Iterable<UserBlockStore>?>? _$blockedUsersComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<UserBlockStore>? get blockedUsers => (_$blockedUsersComputed ??=
|
||||||
|
Computed<Iterable<UserBlockStore>?>(() => super.blockedUsers,
|
||||||
|
name: '_BlocksStore.blockedUsers'))
|
||||||
|
.value;
|
||||||
|
Computed<Iterable<CommunityBlockStore>?>? _$blockedCommunitiesComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<CommunityBlockStore>? get blockedCommunities =>
|
||||||
|
(_$blockedCommunitiesComputed ??=
|
||||||
|
Computed<Iterable<CommunityBlockStore>?>(
|
||||||
|
() => super.blockedCommunities,
|
||||||
|
name: '_BlocksStore.blockedCommunities'))
|
||||||
|
.value;
|
||||||
|
Computed<bool>? _$isUsableComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get isUsable => (_$isUsableComputed ??=
|
||||||
|
Computed<bool>(() => super.isUsable, name: '_BlocksStore.isUsable'))
|
||||||
|
.value;
|
||||||
|
|
||||||
|
final _$_blockedUsersAtom = Atom(name: '_BlocksStore._blockedUsers');
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<UserBlockStore>? get _blockedUsers {
|
||||||
|
_$_blockedUsersAtom.reportRead();
|
||||||
|
return super._blockedUsers;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set _blockedUsers(List<UserBlockStore>? value) {
|
||||||
|
_$_blockedUsersAtom.reportWrite(value, super._blockedUsers, () {
|
||||||
|
super._blockedUsers = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$_blockedCommunitiesAtom =
|
||||||
|
Atom(name: '_BlocksStore._blockedCommunities');
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<CommunityBlockStore>? get _blockedCommunities {
|
||||||
|
_$_blockedCommunitiesAtom.reportRead();
|
||||||
|
return super._blockedCommunities;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set _blockedCommunities(List<CommunityBlockStore>? value) {
|
||||||
|
_$_blockedCommunitiesAtom.reportWrite(value, super._blockedCommunities, () {
|
||||||
|
super._blockedCommunities = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$refreshAsyncAction = AsyncAction('_BlocksStore.refresh');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> refresh() {
|
||||||
|
return _$refreshAsyncAction.run(() => super.refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
blockedUsers: ${blockedUsers},
|
||||||
|
blockedCommunities: ${blockedCommunities},
|
||||||
|
isUsable: ${isUsable}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../../util/async_store.dart';
|
||||||
|
|
||||||
|
part 'community_block_store.g.dart';
|
||||||
|
|
||||||
|
class CommunityBlockStore = _CommunityBlockStore with _$CommunityBlockStore;
|
||||||
|
|
||||||
|
abstract class _CommunityBlockStore with Store {
|
||||||
|
final String instanceHost;
|
||||||
|
final Jwt token;
|
||||||
|
final CommunitySafe community;
|
||||||
|
|
||||||
|
_CommunityBlockStore({
|
||||||
|
required this.instanceHost,
|
||||||
|
required this.token,
|
||||||
|
required this.community,
|
||||||
|
});
|
||||||
|
|
||||||
|
final unblockingState = AsyncStore<BlockedCommunity>();
|
||||||
|
|
||||||
|
@observable
|
||||||
|
bool blocked = true;
|
||||||
|
|
||||||
|
Future<void> unblock() async {
|
||||||
|
final result = await unblockingState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
BlockCommunity(
|
||||||
|
communityId: community.id,
|
||||||
|
block: false,
|
||||||
|
auth: token.raw,
|
||||||
|
));
|
||||||
|
if (result != null) {
|
||||||
|
blocked = result.blocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'community_block_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
||||||
|
|
||||||
|
mixin _$CommunityBlockStore on _CommunityBlockStore, Store {
|
||||||
|
final _$blockedAtom = Atom(name: '_CommunityBlockStore.blocked');
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get blocked {
|
||||||
|
_$blockedAtom.reportRead();
|
||||||
|
return super.blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set blocked(bool value) {
|
||||||
|
_$blockedAtom.reportWrite(value, super.blocked, () {
|
||||||
|
super.blocked = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
blocked: ${blocked}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../../util/async_store.dart';
|
||||||
|
|
||||||
|
part 'user_block_store.g.dart';
|
||||||
|
|
||||||
|
class UserBlockStore = _UserBlockStore with _$UserBlockStore;
|
||||||
|
|
||||||
|
abstract class _UserBlockStore with Store {
|
||||||
|
final String instanceHost;
|
||||||
|
final Jwt token;
|
||||||
|
final PersonSafe person;
|
||||||
|
|
||||||
|
_UserBlockStore({
|
||||||
|
required this.instanceHost,
|
||||||
|
required this.token,
|
||||||
|
required this.person,
|
||||||
|
});
|
||||||
|
|
||||||
|
final unblockingState = AsyncStore<BlockedPerson>();
|
||||||
|
|
||||||
|
@observable
|
||||||
|
bool blocked = true;
|
||||||
|
|
||||||
|
Future<void> unblock() async {
|
||||||
|
final result = await unblockingState.runLemmy(
|
||||||
|
instanceHost,
|
||||||
|
BlockPerson(
|
||||||
|
personId: person.id,
|
||||||
|
block: false,
|
||||||
|
auth: token.raw,
|
||||||
|
));
|
||||||
|
if (result != null) {
|
||||||
|
blocked = result.blocked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'user_block_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
||||||
|
|
||||||
|
mixin _$UserBlockStore on _UserBlockStore, Store {
|
||||||
|
final _$blockedAtom = Atom(name: '_UserBlockStore.blocked');
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get blocked {
|
||||||
|
_$blockedAtom.reportRead();
|
||||||
|
return super.blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set blocked(bool value) {
|
||||||
|
_$blockedAtom.reportWrite(value, super.blocked, () {
|
||||||
|
super.blocked = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
blocked: ${blocked}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,52 +4,65 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
import 'package:flutter_speed_dial/flutter_speed_dial.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../hooks/stores.dart';
|
import '../../hooks/stores.dart';
|
||||||
import '../l10n/l10n.dart';
|
import '../../l10n/l10n.dart';
|
||||||
import '../util/goto.dart';
|
import '../../util/goto.dart';
|
||||||
import '../widgets/about_tile.dart';
|
import '../../widgets/about_tile.dart';
|
||||||
import '../widgets/bottom_modal.dart';
|
import '../../widgets/bottom_modal.dart';
|
||||||
import '../widgets/radio_picker.dart';
|
import '../../widgets/radio_picker.dart';
|
||||||
|
import '../manage_account.dart';
|
||||||
import 'add_account.dart';
|
import 'add_account.dart';
|
||||||
import 'add_instance.dart';
|
import 'add_instance.dart';
|
||||||
import 'manage_account.dart';
|
import 'blocks/blocks.dart';
|
||||||
|
|
||||||
/// Page with a list of different settings sections
|
/// Page with a list of different settings sections
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends HookWidget {
|
||||||
const SettingsPage();
|
const SettingsPage();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) => Scaffold(
|
Widget build(BuildContext context) {
|
||||||
appBar: AppBar(
|
final hasAnyUsers = useAccountsStoreSelect((store) => !store.hasNoAccount);
|
||||||
title: Text(L10n.of(context)!.settings),
|
|
||||||
),
|
return Scaffold(
|
||||||
body: ListView(
|
appBar: AppBar(
|
||||||
children: [
|
title: Text(L10n.of(context)!.settings),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.settings),
|
||||||
|
title: const Text('General'),
|
||||||
|
onTap: () {
|
||||||
|
goTo(context, (_) => const GeneralConfigPage());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.person),
|
||||||
|
title: const Text('Accounts'),
|
||||||
|
onTap: () {
|
||||||
|
goTo(context, (_) => AccountsConfigPage());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (hasAnyUsers)
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.settings),
|
leading: const Icon(Icons.block),
|
||||||
title: const Text('General'),
|
title: const Text('Blocks'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
goTo(context, (_) => const GeneralConfigPage());
|
Navigator.of(context).push(BlocksPage.route());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.person),
|
leading: const Icon(Icons.color_lens),
|
||||||
title: const Text('Accounts'),
|
title: const Text('Appearance'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
goTo(context, (_) => AccountsConfigPage());
|
goTo(context, (_) => const AppearanceConfigPage());
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
const AboutTile()
|
||||||
leading: const Icon(Icons.color_lens),
|
],
|
||||||
title: const Text('Appearance'),
|
),
|
||||||
onTap: () {
|
);
|
||||||
goTo(context, (_) => const AppearanceConfigPage());
|
}
|
||||||
},
|
|
||||||
),
|
|
||||||
const AboutTile()
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Settings for theme color, AMOLED switch
|
/// Settings for theme color, AMOLED switch
|
|
@ -5,21 +5,24 @@ import '../l10n/l10n_from_string.dart';
|
||||||
import 'async_store.dart';
|
import 'async_store.dart';
|
||||||
import 'observer_consumers.dart';
|
import 'observer_consumers.dart';
|
||||||
|
|
||||||
class AsyncStoreListener extends StatelessWidget {
|
class AsyncStoreListener<T> extends StatelessWidget {
|
||||||
final AsyncStore asyncStore;
|
final AsyncStore<T> asyncStore;
|
||||||
final String? successMessage;
|
final String Function(
|
||||||
|
BuildContext context,
|
||||||
|
T data,
|
||||||
|
)? successMessageBuilder;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
const AsyncStoreListener({
|
const AsyncStoreListener({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.asyncStore,
|
required this.asyncStore,
|
||||||
this.successMessage,
|
this.successMessageBuilder,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ObserverListener<AsyncStore>(
|
return ObserverListener<AsyncStore<T>>(
|
||||||
store: asyncStore,
|
store: asyncStore,
|
||||||
listener: (context, store) {
|
listener: (context, store) {
|
||||||
final errorTerm = store.errorTerm;
|
final errorTerm = store.errorTerm;
|
||||||
|
@ -29,10 +32,12 @@ class AsyncStoreListener extends StatelessWidget {
|
||||||
..hideCurrentSnackBar()
|
..hideCurrentSnackBar()
|
||||||
..showSnackBar(SnackBar(content: Text(errorTerm.tr(context))));
|
..showSnackBar(SnackBar(content: Text(errorTerm.tr(context))));
|
||||||
} else if (store.asyncState is AsyncStateData &&
|
} else if (store.asyncState is AsyncStateData &&
|
||||||
successMessage != null) {
|
(successMessageBuilder != null)) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
..hideCurrentSnackBar()
|
..hideCurrentSnackBar()
|
||||||
..showSnackBar(SnackBar(content: Text(successMessage!)));
|
..showSnackBar(SnackBar(
|
||||||
|
content: Text(successMessageBuilder!(
|
||||||
|
context, (store.asyncState as AsyncStateData).data))));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: child,
|
child: child,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
|
||||||
import '../pages/community.dart';
|
import '../pages/community.dart';
|
||||||
import '../pages/full_post.dart';
|
import '../pages/full_post/full_post.dart';
|
||||||
import '../pages/instance.dart';
|
import '../pages/instance.dart';
|
||||||
import '../pages/media_view.dart';
|
import '../pages/media_view.dart';
|
||||||
import '../pages/user.dart';
|
import '../pages/user.dart';
|
||||||
|
@ -62,8 +62,8 @@ abstract class goToUser {
|
||||||
goToUser.byId(context, personSafe.instanceHost, personSafe.id);
|
goToUser.byId(context, personSafe.instanceHost, personSafe.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
void goToPost(BuildContext context, String instanceHost, int postId) => goTo(
|
void goToPost(BuildContext context, String instanceHost, int postId) =>
|
||||||
context, (context) => FullPostPage(instanceHost: instanceHost, id: postId));
|
Navigator.of(context).push(FullPostPage.route(postId, instanceHost));
|
||||||
|
|
||||||
void goToMedia(BuildContext context, String url) => Navigator.push(
|
void goToMedia(BuildContext context, String url) => Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
final _isApple = Platform.isIOS || Platform.isMacOS;
|
||||||
|
|
||||||
|
final moreIcon = _isApple ? Icons.more_horiz : Icons.more_vert;
|
||||||
|
|
||||||
|
final shareIcon = _isApple ? Icons.ios_share : Icons.share;
|
|
@ -1,6 +0,0 @@
|
||||||
import 'dart:io' show Platform;
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
final moreIcon =
|
|
||||||
(Platform.isIOS || Platform.isMacOS) ? Icons.more_horiz : Icons.more_vert;
|
|
|
@ -90,13 +90,20 @@ class CommentWidget extends StatelessWidget {
|
||||||
detached: detached,
|
detached: detached,
|
||||||
hideOnRead: hideOnRead,
|
hideOnRead: hideOnRead,
|
||||||
),
|
),
|
||||||
builder: (context, child) => AsyncStoreListener(
|
builder: (context, child) => AsyncStoreListener<BlockedPerson>(
|
||||||
asyncStore: context.read<CommentStore>().votingState,
|
asyncStore: context.read<CommentStore>().blockingState,
|
||||||
|
successMessageBuilder: (context, state) {
|
||||||
|
final name = state.personView.person.preferredName;
|
||||||
|
return state.blocked ? '$name blocked' : '$name unblocked';
|
||||||
|
},
|
||||||
child: AsyncStoreListener(
|
child: AsyncStoreListener(
|
||||||
asyncStore: context.read<CommentStore>().deletingState,
|
asyncStore: context.read<CommentStore>().votingState,
|
||||||
child: AsyncStoreListener(
|
child: AsyncStoreListener(
|
||||||
asyncStore: context.read<CommentStore>().savingState,
|
asyncStore: context.read<CommentStore>().deletingState,
|
||||||
child: const _CommentWidget(),
|
child: AsyncStoreListener(
|
||||||
|
asyncStore: context.read<CommentStore>().savingState,
|
||||||
|
child: const _CommentWidget(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -128,18 +128,33 @@ class _CommentMoreMenuPopup extends HookWidget {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (store.isMine)
|
if (store.isMine) ...[
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.edit),
|
leading: const Icon(Icons.edit),
|
||||||
title: const Text('Edit'),
|
title: const Text('Edit'),
|
||||||
onTap: handleEdit,
|
onTap: handleEdit,
|
||||||
),
|
),
|
||||||
if (store.isMine)
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: Icon(comment.deleted ? Icons.restore : Icons.delete),
|
leading: Icon(comment.deleted ? Icons.restore : Icons.delete),
|
||||||
title: Text(comment.deleted ? 'Restore' : 'Delete'),
|
title: Text(comment.deleted ? 'Restore' : 'Delete'),
|
||||||
onTap: loggedInAction(handleDelete),
|
onTap: loggedInAction(handleDelete),
|
||||||
),
|
),
|
||||||
|
] else
|
||||||
|
ListTile(
|
||||||
|
leading: store.blockingState.isLoading
|
||||||
|
? const SizedBox(
|
||||||
|
height: 20,
|
||||||
|
width: 20,
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.block),
|
||||||
|
title: Text(
|
||||||
|
'${store.comment.creatorBlocked ? 'Unblock' : 'Block'} ${store.comment.creator.preferredName}'),
|
||||||
|
onTap: loggedInAction((token) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
store.block(token);
|
||||||
|
}),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.info_outline),
|
leading: const Icon(Icons.info_outline),
|
||||||
title: const Text('Nerd stuff'),
|
title: const Text('Nerd stuff'),
|
||||||
|
|
|
@ -35,6 +35,7 @@ abstract class _CommentStore with Store {
|
||||||
final savingState = AsyncStore<FullCommentView>();
|
final savingState = AsyncStore<FullCommentView>();
|
||||||
final markPersonMentionAsReadState = AsyncStore<PersonMentionView>();
|
final markPersonMentionAsReadState = AsyncStore<PersonMentionView>();
|
||||||
final markAsReadState = AsyncStore<FullCommentView>();
|
final markAsReadState = AsyncStore<FullCommentView>();
|
||||||
|
final blockingState = AsyncStore<BlockedPerson>();
|
||||||
|
|
||||||
@computed
|
@computed
|
||||||
bool get isMine =>
|
bool get isMine =>
|
||||||
|
@ -103,6 +104,21 @@ abstract class _CommentStore with Store {
|
||||||
if (result != null) comment = result.commentView;
|
if (result != null) comment = result.commentView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> block(Jwt token) async {
|
||||||
|
final result = await blockingState.runLemmy(
|
||||||
|
comment.instanceHost,
|
||||||
|
BlockPerson(
|
||||||
|
personId: comment.creator.id,
|
||||||
|
block: !comment.creatorBlocked,
|
||||||
|
auth: token.raw,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (result != null) {
|
||||||
|
comment = comment.copyWith(creatorBlocked: result.blocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
Future<void> markAsRead(Jwt token) async {
|
Future<void> markAsRead(Jwt token) async {
|
||||||
if (userMentionId != null) {
|
if (userMentionId != null) {
|
||||||
|
|
|
@ -102,6 +102,13 @@ mixin _$CommentStore on _CommentStore, Store {
|
||||||
return _$saveAsyncAction.run(() => super.save(token));
|
return _$saveAsyncAction.run(() => super.save(token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _$blockAsyncAction = AsyncAction('_CommentStore.block');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> block(Jwt token) {
|
||||||
|
return _$blockAsyncAction.run(() => super.block(token));
|
||||||
|
}
|
||||||
|
|
||||||
final _$markAsReadAsyncAction = AsyncAction('_CommentStore.markAsRead');
|
final _$markAsReadAsyncAction = AsyncAction('_CommentStore.markAsRead');
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -1,111 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
|
||||||
|
|
||||||
import '../comment_tree.dart';
|
|
||||||
import '../l10n/l10n.dart';
|
|
||||||
import 'bottom_modal.dart';
|
|
||||||
import 'bottom_safe.dart';
|
|
||||||
import 'comment/comment.dart';
|
|
||||||
|
|
||||||
/// Manages comments section, sorts them
|
|
||||||
class CommentSection extends HookWidget {
|
|
||||||
final List<CommentView> rawComments;
|
|
||||||
final List<CommentTree> comments;
|
|
||||||
final int postCreatorId;
|
|
||||||
final CommentSortType sortType;
|
|
||||||
|
|
||||||
static const sortPairs = {
|
|
||||||
CommentSortType.hot: [Icons.whatshot, L10nStrings.hot],
|
|
||||||
CommentSortType.new_: [Icons.new_releases, L10nStrings.new_],
|
|
||||||
CommentSortType.old: [Icons.calendar_today, L10nStrings.old],
|
|
||||||
CommentSortType.top: [Icons.trending_up, L10nStrings.top],
|
|
||||||
CommentSortType.chat: [Icons.chat, L10nStrings.chat],
|
|
||||||
};
|
|
||||||
|
|
||||||
CommentSection(
|
|
||||||
List<CommentView> rawComments, {
|
|
||||||
required this.postCreatorId,
|
|
||||||
this.sortType = CommentSortType.hot,
|
|
||||||
}) : comments =
|
|
||||||
CommentTree.sortList(sortType, CommentTree.fromList(rawComments)),
|
|
||||||
rawComments = rawComments
|
|
||||||
..sort((b, a) => a.comment.published.compareTo(b.comment.published));
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final sorting = useState(sortType);
|
|
||||||
|
|
||||||
void sortComments(CommentSortType sort) {
|
|
||||||
if (sort != sorting.value && sort != CommentSortType.chat) {
|
|
||||||
CommentTree.sortList(sort, comments);
|
|
||||||
}
|
|
||||||
|
|
||||||
sorting.value = sort;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
OutlinedButton(
|
|
||||||
onPressed: () {
|
|
||||||
showBottomModal(
|
|
||||||
title: 'sort by',
|
|
||||||
context: context,
|
|
||||||
builder: (context) => Column(
|
|
||||||
children: [
|
|
||||||
for (final e in sortPairs.entries)
|
|
||||||
ListTile(
|
|
||||||
leading: Icon(e.value[0] as IconData),
|
|
||||||
title: Text((e.value[1] as String).tr(context)),
|
|
||||||
trailing: sorting.value == e.key
|
|
||||||
? const Icon(Icons.check)
|
|
||||||
: null,
|
|
||||||
onTap: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
sortComments(e.key);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Text((sortPairs[sorting.value]![1] as String).tr(context)),
|
|
||||||
const Icon(Icons.arrow_drop_down),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// sorting menu goes here
|
|
||||||
if (comments.isEmpty)
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 50),
|
|
||||||
child: Text(
|
|
||||||
'no comments yet',
|
|
||||||
style: TextStyle(fontStyle: FontStyle.italic),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (sorting.value == CommentSortType.chat)
|
|
||||||
for (final com in rawComments)
|
|
||||||
CommentWidget.fromCommentView(
|
|
||||||
com,
|
|
||||||
detached: false,
|
|
||||||
key: ValueKey(com),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
for (final com in comments)
|
|
||||||
CommentWidget(
|
|
||||||
com,
|
|
||||||
key: ValueKey(com),
|
|
||||||
),
|
|
||||||
const BottomSafe.fab(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,595 +0,0 @@
|
||||||
import 'package:flutter/gestures.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
import 'package:url_launcher/url_launcher.dart' as ul;
|
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
|
||||||
import '../hooks/logged_in_action.dart';
|
|
||||||
import '../hooks/stores.dart';
|
|
||||||
import '../l10n/l10n.dart';
|
|
||||||
import '../pages/create_post.dart';
|
|
||||||
import '../pages/full_post.dart';
|
|
||||||
import '../stores/accounts_store.dart';
|
|
||||||
import '../url_launcher.dart';
|
|
||||||
import '../util/cleanup_url.dart';
|
|
||||||
import '../util/extensions/api.dart';
|
|
||||||
import '../util/extensions/datetime.dart';
|
|
||||||
import '../util/goto.dart';
|
|
||||||
import '../util/more_icon.dart';
|
|
||||||
import '../util/share.dart';
|
|
||||||
import 'avatar.dart';
|
|
||||||
import 'bottom_modal.dart';
|
|
||||||
import 'cached_network_image.dart';
|
|
||||||
import 'fullscreenable_image.dart';
|
|
||||||
import 'info_table_popup.dart';
|
|
||||||
import 'markdown_text.dart';
|
|
||||||
import 'save_post_button.dart';
|
|
||||||
|
|
||||||
enum MediaType {
|
|
||||||
image,
|
|
||||||
gallery,
|
|
||||||
video,
|
|
||||||
other,
|
|
||||||
none,
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaType whatType(String? url) {
|
|
||||||
if (url == null || url.isEmpty) return MediaType.none;
|
|
||||||
|
|
||||||
// TODO: make detection more nuanced
|
|
||||||
if (url.endsWith('.jpg') ||
|
|
||||||
url.endsWith('.jpeg') ||
|
|
||||||
url.endsWith('.png') ||
|
|
||||||
url.endsWith('.gif') ||
|
|
||||||
url.endsWith('.webp') ||
|
|
||||||
url.endsWith('.bmp') ||
|
|
||||||
url.endsWith('.wbpm')) {
|
|
||||||
return MediaType.image;
|
|
||||||
}
|
|
||||||
return MediaType.other;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A post overview card
|
|
||||||
class PostWidget extends HookWidget {
|
|
||||||
final PostView post;
|
|
||||||
final String instanceHost;
|
|
||||||
final bool fullPost;
|
|
||||||
|
|
||||||
PostWidget(this.post, {this.fullPost = false})
|
|
||||||
: instanceHost = post.instanceHost;
|
|
||||||
|
|
||||||
// == ACTIONS ==
|
|
||||||
|
|
||||||
static void showMoreMenu({
|
|
||||||
required BuildContext context,
|
|
||||||
required PostView post,
|
|
||||||
bool fullPost = false,
|
|
||||||
}) {
|
|
||||||
final isMine = context
|
|
||||||
.read<AccountsStore>()
|
|
||||||
.defaultUserDataFor(post.instanceHost)
|
|
||||||
?.userId ==
|
|
||||||
post.creator.id;
|
|
||||||
|
|
||||||
showBottomModal(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => Column(
|
|
||||||
children: [
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.open_in_browser),
|
|
||||||
title: const Text('Open in browser'),
|
|
||||||
onTap: () async => await ul.canLaunch(post.post.apId)
|
|
||||||
? ul.launch(post.post.apId)
|
|
||||||
: ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text("can't open in browser"))),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.info_outline),
|
|
||||||
title: const Text('Nerd stuff'),
|
|
||||||
onTap: () {
|
|
||||||
showInfoTablePopup(context: context, table: {
|
|
||||||
'% of upvotes':
|
|
||||||
'${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%',
|
|
||||||
...post.toJson(),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (isMine)
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.edit),
|
|
||||||
title: const Text('Edit'),
|
|
||||||
onTap: () async {
|
|
||||||
final postView = await Navigator.of(context).push(
|
|
||||||
CreatePostPage.editRoute(post.post),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (postView != null) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
if (fullPost) {
|
|
||||||
await goToReplace(
|
|
||||||
context,
|
|
||||||
(_) => FullPostPage.fromPostView(postView),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await goTo(
|
|
||||||
context,
|
|
||||||
(_) => FullPostPage.fromPostView(postView),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// == UI ==
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
void _openLink(String url) =>
|
|
||||||
linkLauncher(context: context, url: url, instanceHost: instanceHost);
|
|
||||||
|
|
||||||
final urlDomain = () {
|
|
||||||
if (whatType(post.post.url) == MediaType.none) return null;
|
|
||||||
|
|
||||||
return urlHost(post.post.url!);
|
|
||||||
}();
|
|
||||||
|
|
||||||
/// assemble info section
|
|
||||||
Widget info() => Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (post.community.icon != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 10),
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
onTap: () => goToCommunity.byId(
|
|
||||||
context, instanceHost, post.community.id),
|
|
||||||
child: Avatar(
|
|
||||||
url: post.community.icon,
|
|
||||||
noBlank: true,
|
|
||||||
radius: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
RichText(
|
|
||||||
overflow:
|
|
||||||
TextOverflow.ellipsis, // TODO: fix overflowing
|
|
||||||
text: TextSpan(
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 15,
|
|
||||||
color: theme.textTheme.bodyText1?.color),
|
|
||||||
children: [
|
|
||||||
const TextSpan(
|
|
||||||
text: '!',
|
|
||||||
style:
|
|
||||||
TextStyle(fontWeight: FontWeight.w300)),
|
|
||||||
TextSpan(
|
|
||||||
text: post.community.name,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToCommunity.byId(
|
|
||||||
context,
|
|
||||||
instanceHost,
|
|
||||||
post.community.id)),
|
|
||||||
const TextSpan(
|
|
||||||
text: '@',
|
|
||||||
style:
|
|
||||||
TextStyle(fontWeight: FontWeight.w300)),
|
|
||||||
TextSpan(
|
|
||||||
text: post.post.originInstanceHost,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToInstance(context,
|
|
||||||
post.post.originInstanceHost)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
RichText(
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
text: TextSpan(
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: theme.textTheme.bodyText1?.color),
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: L10n.of(context)!.by,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w300),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: ' ${post.creator.originPreferredName}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600),
|
|
||||||
recognizer: TapGestureRecognizer()
|
|
||||||
..onTap = () => goToUser.fromPersonSafe(
|
|
||||||
context,
|
|
||||||
post.creator,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text:
|
|
||||||
' · ${post.post.published.fancyShort}'),
|
|
||||||
if (post.post.locked)
|
|
||||||
const TextSpan(text: ' · 🔒'),
|
|
||||||
if (post.post.stickied)
|
|
||||||
const TextSpan(text: ' · 📌'),
|
|
||||||
if (post.post.nsfw) const TextSpan(text: ' · '),
|
|
||||||
if (post.post.nsfw)
|
|
||||||
TextSpan(
|
|
||||||
text: L10n.of(context)!.nsfw,
|
|
||||||
style:
|
|
||||||
const TextStyle(color: Colors.red)),
|
|
||||||
if (urlDomain != null)
|
|
||||||
TextSpan(text: ' · $urlDomain'),
|
|
||||||
if (post.post.removed)
|
|
||||||
const TextSpan(text: ' · REMOVED'),
|
|
||||||
if (post.post.deleted)
|
|
||||||
const TextSpan(text: ' · DELETED'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
if (!fullPost)
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () =>
|
|
||||||
showMoreMenu(context: context, post: post),
|
|
||||||
icon: Icon(moreIcon),
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
visualDensity: VisualDensity.compact,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
/// assemble title section
|
|
||||||
Widget title() => Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 100,
|
|
||||||
child: Text(
|
|
||||||
post.post.name,
|
|
||||||
textAlign: TextAlign.left,
|
|
||||||
softWrap: true,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18, fontWeight: FontWeight.w600),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (whatType(post.post.url) == MediaType.other &&
|
|
||||||
post.post.thumbnailUrl != null) ...[
|
|
||||||
const Spacer(),
|
|
||||||
InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
onTap: () => _openLink(post.post.url!),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: post.post.thumbnailUrl!,
|
|
||||||
width: 70,
|
|
||||||
height: 70,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error) =>
|
|
||||||
Text(error.toString()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Positioned(
|
|
||||||
top: 8,
|
|
||||||
right: 8,
|
|
||||||
child: Icon(
|
|
||||||
Icons.launch,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
/// assemble link preview
|
|
||||||
Widget linkPreview() {
|
|
||||||
assert(post.post.url != null);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => _openLink(post.post.url!),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: Theme.of(context).iconTheme.color!.withAlpha(170)),
|
|
||||||
borderRadius: BorderRadius.circular(5)),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Spacer(),
|
|
||||||
Text('$urlDomain ',
|
|
||||||
style: theme.textTheme.caption
|
|
||||||
?.apply(fontStyle: FontStyle.italic)),
|
|
||||||
const Icon(Icons.launch, size: 12),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
post.post.embedTitle ?? '',
|
|
||||||
style: theme.textTheme.subtitle1
|
|
||||||
?.apply(fontWeightDelta: 2),
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (post.post.embedDescription != null &&
|
|
||||||
post.post.embedDescription!.isNotEmpty)
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
post.post.embedDescription!,
|
|
||||||
maxLines: 4,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// assemble image
|
|
||||||
Widget postImage() {
|
|
||||||
assert(post.post.url != null);
|
|
||||||
|
|
||||||
return FullscreenableImage(
|
|
||||||
url: post.post.url!,
|
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: post.post.url!,
|
|
||||||
errorBuilder: (_, ___) => const Icon(Icons.warning),
|
|
||||||
loadingBuilder: (context, progress) =>
|
|
||||||
CircularProgressIndicator(value: progress?.progress),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// assemble actions section
|
|
||||||
Widget actions() => Padding(
|
|
||||||
padding: const EdgeInsets.fromLTRB(10, 5, 10, 5),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.comment),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Expanded(
|
|
||||||
flex: 999,
|
|
||||||
child: Text(
|
|
||||||
L10n.of(context)!.number_of_comments(post.counts.comments),
|
|
||||||
overflow: TextOverflow.fade,
|
|
||||||
softWrap: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
if (!fullPost)
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.share),
|
|
||||||
onPressed: () => share(post.post.apId, context: context),
|
|
||||||
),
|
|
||||||
if (!fullPost) SavePostButton(post),
|
|
||||||
_Voting(post),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black45)],
|
|
||||||
color: theme.cardColor,
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
|
||||||
),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: fullPost
|
|
||||||
? null
|
|
||||||
: () => goTo(context, (context) => FullPostPage.fromPostView(post)),
|
|
||||||
child: Material(
|
|
||||||
type: MaterialType.transparency,
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
info(),
|
|
||||||
title(),
|
|
||||||
if (whatType(post.post.url) != MediaType.other &&
|
|
||||||
whatType(post.post.url) != MediaType.none)
|
|
||||||
postImage()
|
|
||||||
else if (post.post.url != null && post.post.url!.isNotEmpty)
|
|
||||||
linkPreview(),
|
|
||||||
if (post.post.body != null && fullPost)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: MarkdownText(
|
|
||||||
post.post.body!,
|
|
||||||
instanceHost: instanceHost,
|
|
||||||
selectable: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (post.post.body != null && !fullPost)
|
|
||||||
LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final span = TextSpan(
|
|
||||||
text: post.post.body,
|
|
||||||
);
|
|
||||||
final tp = TextPainter(
|
|
||||||
text: span,
|
|
||||||
maxLines: 10,
|
|
||||||
textDirection: Directionality.of(context),
|
|
||||||
)..layout(maxWidth: constraints.maxWidth - 20);
|
|
||||||
|
|
||||||
if (tp.didExceedMaxLines) {
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(maxHeight: tp.height),
|
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
children: [
|
|
||||||
ClipRect(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topCenter,
|
|
||||||
heightFactor: 0.8,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: MarkdownText(post.post.body!,
|
|
||||||
instanceHost: instanceHost)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: tp.preferredLineHeight * 2.5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
theme.cardColor.withAlpha(0),
|
|
||||||
theme.cardColor,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(10),
|
|
||||||
child: MarkdownText(post.post.body!,
|
|
||||||
instanceHost: instanceHost));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
actions(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Voting extends HookWidget {
|
|
||||||
final PostView post;
|
|
||||||
|
|
||||||
final bool wasVoted;
|
|
||||||
|
|
||||||
_Voting(this.post)
|
|
||||||
: wasVoted = (post.myVote ?? VoteType.none) != VoteType.none;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final myVote = useState(post.myVote ?? VoteType.none);
|
|
||||||
final loading = useDelayedLoading();
|
|
||||||
final showScores =
|
|
||||||
useConfigStoreSelect((configStore) => configStore.showScores);
|
|
||||||
final loggedInAction = useLoggedInAction(post.instanceHost);
|
|
||||||
|
|
||||||
vote(VoteType vote, Jwt token) async {
|
|
||||||
final api = LemmyApiV3(post.instanceHost);
|
|
||||||
|
|
||||||
loading.start();
|
|
||||||
try {
|
|
||||||
final res = await api.run(
|
|
||||||
CreatePostLike(postId: post.post.id, score: vote, auth: token.raw));
|
|
||||||
myVote.value = res.myVote ?? VoteType.none;
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(const SnackBar(content: Text('voting failed :(')));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loading.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.arrow_upward,
|
|
||||||
color: myVote.value == VoteType.up
|
|
||||||
? theme.colorScheme.secondary
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
onPressed: loggedInAction(
|
|
||||||
(token) => vote(
|
|
||||||
myVote.value == VoteType.up ? VoteType.none : VoteType.up,
|
|
||||||
token,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (loading.loading)
|
|
||||||
const SizedBox(
|
|
||||||
width: 20, height: 20, child: CircularProgressIndicator())
|
|
||||||
else if (showScores)
|
|
||||||
Text(NumberFormat.compact()
|
|
||||||
.format(post.counts.score + (wasVoted ? 0 : myVote.value.value))),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.arrow_downward,
|
|
||||||
color: myVote.value == VoteType.down ? Colors.red : null,
|
|
||||||
),
|
|
||||||
onPressed: loggedInAction(
|
|
||||||
(token) => vote(
|
|
||||||
myVote.value == VoteType.down ? VoteType.none : VoteType.down,
|
|
||||||
token,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,92 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../pages/full_post/full_post.dart';
|
||||||
|
import '../../util/async_store_listener.dart';
|
||||||
|
import '../../util/extensions/api.dart';
|
||||||
|
import 'post_actions.dart';
|
||||||
|
import 'post_body.dart';
|
||||||
|
import 'post_info_section.dart';
|
||||||
|
import 'post_link_preview.dart';
|
||||||
|
import 'post_media.dart';
|
||||||
|
import 'post_status.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
import 'post_title.dart';
|
||||||
|
|
||||||
|
class PostTile extends StatelessWidget {
|
||||||
|
final PostStore postStore;
|
||||||
|
final IsFullPost fullPost;
|
||||||
|
|
||||||
|
const PostTile.fromPostStore(this.postStore, {this.fullPost = true});
|
||||||
|
PostTile.fromPostView(PostView post, {this.fullPost = false})
|
||||||
|
: postStore = PostStore(post);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MultiProvider(
|
||||||
|
providers: [
|
||||||
|
Provider.value(value: postStore),
|
||||||
|
Provider.value(value: fullPost),
|
||||||
|
],
|
||||||
|
builder: (context, child) {
|
||||||
|
return AsyncStoreListener(
|
||||||
|
asyncStore: context.read<PostStore>().savingState,
|
||||||
|
child: AsyncStoreListener(
|
||||||
|
asyncStore: context.read<PostStore>().votingState,
|
||||||
|
child: AsyncStoreListener<BlockedPerson>(
|
||||||
|
asyncStore: context.read<PostStore>().userBlockingState,
|
||||||
|
successMessageBuilder: (context, state) {
|
||||||
|
final name = state.personView.person.preferredName;
|
||||||
|
return state.blocked ? '$name blocked' : '$name unblocked';
|
||||||
|
},
|
||||||
|
child: const _Post(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A post overview card
|
||||||
|
class _Post extends StatelessWidget {
|
||||||
|
const _Post();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isFullPost = context.read<IsFullPost>();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
boxShadow: const [BoxShadow(blurRadius: 10, color: Colors.black45)],
|
||||||
|
color: theme.cardColor,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
|
),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: isFullPost
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final postStore = context.read<PostStore>();
|
||||||
|
Navigator.of(context)
|
||||||
|
.push(FullPostPage.fromPostStoreRoute(postStore));
|
||||||
|
},
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Column(
|
||||||
|
children: const [
|
||||||
|
PostInfoSection(),
|
||||||
|
PostTitle(),
|
||||||
|
PostMedia(),
|
||||||
|
PostLinkPreview(),
|
||||||
|
PostBody(),
|
||||||
|
PostActions(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../util/icons.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../../util/share.dart';
|
||||||
|
import 'post_status.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
import 'post_voting.dart';
|
||||||
|
import 'save_post_button.dart';
|
||||||
|
|
||||||
|
class PostActions extends HookWidget {
|
||||||
|
const PostActions();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final fullPost = context.read<IsFullPost>();
|
||||||
|
|
||||||
|
// assemble actions section
|
||||||
|
return ObserverBuilder<PostStore>(builder: (context, store) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.comment),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
L10n.of(context)!
|
||||||
|
.number_of_comments(store.postView.counts.comments),
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
softWrap: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!fullPost)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(shareIcon),
|
||||||
|
onPressed: () =>
|
||||||
|
share(store.postView.post.apId, context: context),
|
||||||
|
),
|
||||||
|
if (!fullPost) const SavePostButton(),
|
||||||
|
const PostVoting(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../markdown_text.dart';
|
||||||
|
import 'post_status.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
|
||||||
|
class PostBody extends StatelessWidget {
|
||||||
|
const PostBody();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final fullPost = context.read<IsFullPost>();
|
||||||
|
|
||||||
|
return ObserverBuilder<PostStore>(builder: (context, store) {
|
||||||
|
final body = store.postView.post.body;
|
||||||
|
if (body == null) return const SizedBox();
|
||||||
|
|
||||||
|
if (fullPost) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: MarkdownText(
|
||||||
|
body,
|
||||||
|
instanceHost: store.postView.instanceHost,
|
||||||
|
selectable: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final span = TextSpan(
|
||||||
|
text: body,
|
||||||
|
);
|
||||||
|
final tp = TextPainter(
|
||||||
|
text: span,
|
||||||
|
maxLines: 10,
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
)..layout(maxWidth: constraints.maxWidth - 20);
|
||||||
|
|
||||||
|
if (tp.didExceedMaxLines) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxHeight: tp.height),
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
children: [
|
||||||
|
ClipRect(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
heightFactor: 0.8,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: MarkdownText(body,
|
||||||
|
instanceHost: store.postView.instanceHost),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
height: tp.preferredLineHeight * 2.5,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
theme.cardColor.withAlpha(0),
|
||||||
|
theme.cardColor,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: MarkdownText(body,
|
||||||
|
instanceHost: store.postView.instanceHost));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../l10n/l10n.dart';
|
||||||
|
import '../../util/extensions/api.dart';
|
||||||
|
import '../../util/extensions/datetime.dart';
|
||||||
|
import '../../util/goto.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../avatar.dart';
|
||||||
|
import 'post_more_menu.dart';
|
||||||
|
import 'post_status.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
|
||||||
|
class PostInfoSection extends StatelessWidget {
|
||||||
|
const PostInfoSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ObserverBuilder<PostStore>(builder: (context, store) {
|
||||||
|
final fullPost = context.read<IsFullPost>();
|
||||||
|
final post = store.postView;
|
||||||
|
final instanceHost = store.postView.instanceHost;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (post.community.icon != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
onTap: () => goToCommunity.byId(
|
||||||
|
context, instanceHost, post.community.id),
|
||||||
|
child: Avatar(
|
||||||
|
url: post.community.icon,
|
||||||
|
noBlank: true,
|
||||||
|
radius: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
RichText(
|
||||||
|
overflow: TextOverflow.ellipsis, // TODO: fix overflowing
|
||||||
|
text: TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
color: theme.textTheme.bodyText1?.color),
|
||||||
|
children: [
|
||||||
|
const TextSpan(
|
||||||
|
text: '!',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w300)),
|
||||||
|
TextSpan(
|
||||||
|
text: post.community.name,
|
||||||
|
style:
|
||||||
|
const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToCommunity.byId(
|
||||||
|
context, instanceHost, post.community.id)),
|
||||||
|
const TextSpan(
|
||||||
|
text: '@',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w300)),
|
||||||
|
TextSpan(
|
||||||
|
text: post.post.originInstanceHost,
|
||||||
|
style:
|
||||||
|
const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToInstance(
|
||||||
|
context, post.post.originInstanceHost)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
RichText(
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
text: TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: theme.textTheme.bodyText1?.color),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: L10n.of(context)!.by,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w300),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ${post.creator.originPreferredName}',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () => goToUser.fromPersonSafe(
|
||||||
|
context,
|
||||||
|
post.creator,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: ' · ${post.post.published.fancyShort}'),
|
||||||
|
if (post.post.locked) const TextSpan(text: ' · 🔒'),
|
||||||
|
if (post.post.stickied) const TextSpan(text: ' · 📌'),
|
||||||
|
if (post.post.nsfw) const TextSpan(text: ' · '),
|
||||||
|
if (post.post.nsfw)
|
||||||
|
TextSpan(
|
||||||
|
text: L10n.of(context)!.nsfw,
|
||||||
|
style: const TextStyle(color: Colors.red)),
|
||||||
|
if (store.urlDomain != null)
|
||||||
|
TextSpan(text: ' · ${store.urlDomain}'),
|
||||||
|
if (post.post.removed)
|
||||||
|
const TextSpan(text: ' · REMOVED'),
|
||||||
|
if (post.post.deleted)
|
||||||
|
const TextSpan(text: ' · DELETED'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (!fullPost) const PostMoreMenuButton(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../url_launcher.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
|
||||||
|
class PostLinkPreview extends StatelessWidget {
|
||||||
|
const PostLinkPreview();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ObserverBuilder<PostStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
final url = store.postView.post.url;
|
||||||
|
|
||||||
|
if (store.hasMedia || url == null || url.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => linkLauncher(
|
||||||
|
context: context,
|
||||||
|
url: url,
|
||||||
|
instanceHost: store.postView.instanceHost,
|
||||||
|
),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).iconTheme.color!.withAlpha(170)),
|
||||||
|
borderRadius: BorderRadius.circular(5)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
Text('${store.urlDomain} ',
|
||||||
|
style: theme.textTheme.caption
|
||||||
|
?.apply(fontStyle: FontStyle.italic)),
|
||||||
|
const Icon(Icons.launch, size: 12),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
store.postView.post.embedTitle ?? '',
|
||||||
|
style:
|
||||||
|
theme.textTheme.subtitle1?.apply(fontWeightDelta: 2),
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (store.postView.post.embedDescription?.isNotEmpty ??
|
||||||
|
false)
|
||||||
|
Text(
|
||||||
|
store.postView.post.embedDescription!,
|
||||||
|
maxLines: 4,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../cached_network_image.dart';
|
||||||
|
import '../fullscreenable_image.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
|
||||||
|
/// assembles image
|
||||||
|
class PostMedia extends StatelessWidget {
|
||||||
|
const PostMedia();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ObserverBuilder<PostStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
final post = store.postView.post;
|
||||||
|
if (!store.hasMedia) return const SizedBox();
|
||||||
|
|
||||||
|
final url = post.url!; // hasMedia returns false if url is null
|
||||||
|
|
||||||
|
return FullscreenableImage(
|
||||||
|
url: url,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: url,
|
||||||
|
errorBuilder: (_, ___) => const Icon(Icons.warning),
|
||||||
|
loadingBuilder: (context, progress) =>
|
||||||
|
CircularProgressIndicator.adaptive(value: progress?.progress),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,139 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart' as ul;
|
||||||
|
|
||||||
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../pages/create_post.dart';
|
||||||
|
import '../../pages/full_post/full_post_store.dart';
|
||||||
|
import '../../stores/accounts_store.dart';
|
||||||
|
import '../../util/icons.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../bottom_modal.dart';
|
||||||
|
import '../info_table_popup.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
|
||||||
|
class PostMoreMenuButton extends StatelessWidget {
|
||||||
|
const PostMoreMenuButton();
|
||||||
|
|
||||||
|
static void show({
|
||||||
|
required BuildContext context,
|
||||||
|
required PostStore postStore,
|
||||||
|
required FullPostStore? fullPostStore,
|
||||||
|
}) {
|
||||||
|
// TODO: add blocking!
|
||||||
|
showBottomModal(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
PostMoreMenu(postStore: postStore, fullPostStore: fullPostStore),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () => show(
|
||||||
|
context: context,
|
||||||
|
postStore: context.read<PostStore>(),
|
||||||
|
fullPostStore: null,
|
||||||
|
),
|
||||||
|
icon: Icon(moreIcon),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PostMoreMenu extends HookWidget {
|
||||||
|
final PostStore postStore;
|
||||||
|
final FullPostStore? fullPostStore;
|
||||||
|
const PostMoreMenu({
|
||||||
|
required this.postStore,
|
||||||
|
required this.fullPostStore,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final loggedInAction = useLoggedInAction(postStore.postView.instanceHost);
|
||||||
|
|
||||||
|
final isMine = context
|
||||||
|
.read<AccountsStore>()
|
||||||
|
.defaultUserDataFor(postStore.postView.instanceHost)
|
||||||
|
?.userId ==
|
||||||
|
postStore.postView.creator.id;
|
||||||
|
|
||||||
|
return ObserverBuilder<PostStore>(
|
||||||
|
store: postStore,
|
||||||
|
builder: (context, store) {
|
||||||
|
final post = store.postView;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.open_in_browser),
|
||||||
|
title: const Text('Open in browser'),
|
||||||
|
onTap: () async => await ul.canLaunch(post.post.apId)
|
||||||
|
? ul.launch(post.post.apId)
|
||||||
|
: ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("can't open in browser"))),
|
||||||
|
),
|
||||||
|
if (isMine)
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.edit),
|
||||||
|
title: const Text('Edit'),
|
||||||
|
onTap: () async {
|
||||||
|
final postView = await Navigator.of(context).push(
|
||||||
|
CreatePostPage.editRoute(post.post),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (postView != null) {
|
||||||
|
store.updatePostView(postView);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
ListTile(
|
||||||
|
leading: store.userBlockingState.isLoading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: const Icon(Icons.block),
|
||||||
|
title:
|
||||||
|
Text('${post.creatorBlocked ? 'Unblock' : 'Block'} user'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
loggedInAction(store.blockUser)();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (fullPostStore?.fullPostView != null)
|
||||||
|
ObserverBuilder<FullPostStore>(
|
||||||
|
store: fullPostStore,
|
||||||
|
builder: (context, store) {
|
||||||
|
return ListTile(
|
||||||
|
leading: store.communityBlockingState.isLoading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: const Icon(Icons.block),
|
||||||
|
title: Text(
|
||||||
|
'${store.fullPostView!.communityView.blocked ? 'Unblock' : 'Block'} community'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
loggedInAction(store.blockCommunity)();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(Icons.info_outline),
|
||||||
|
title: const Text('Nerd stuff'),
|
||||||
|
onTap: () {
|
||||||
|
showInfoTablePopup(context: context, table: {
|
||||||
|
'% of upvotes':
|
||||||
|
'${(100 * (post.counts.upvotes / (post.counts.upvotes + post.counts.downvotes))).toInt()}%',
|
||||||
|
...post.toJson(),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
typedef IsFullPost = bool;
|
|
@ -0,0 +1,99 @@
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:mobx/mobx.dart';
|
||||||
|
|
||||||
|
import '../../util/async_store.dart';
|
||||||
|
import '../../util/cleanup_url.dart';
|
||||||
|
|
||||||
|
part 'post_store.g.dart';
|
||||||
|
|
||||||
|
class PostStore = _PostStore with _$PostStore;
|
||||||
|
|
||||||
|
abstract class _PostStore with Store {
|
||||||
|
_PostStore(this.postView);
|
||||||
|
|
||||||
|
final votingState = AsyncStore<PostView>();
|
||||||
|
final savingState = AsyncStore<PostView>();
|
||||||
|
final userBlockingState = AsyncStore<BlockedPerson>();
|
||||||
|
|
||||||
|
@observable
|
||||||
|
PostView postView;
|
||||||
|
|
||||||
|
@computed
|
||||||
|
String? get urlDomain =>
|
||||||
|
postView.post.url != null ? urlHost(postView.post.url!) : null;
|
||||||
|
|
||||||
|
@computed
|
||||||
|
bool get hasMedia {
|
||||||
|
final url = postView.post.url;
|
||||||
|
if (url == null || url.isEmpty) return false;
|
||||||
|
|
||||||
|
// TODO: detect video
|
||||||
|
// this is how they do it in lemmy-ui:
|
||||||
|
// https://github.com/LemmyNet/lemmy-ui/blob/018c10c9193082237b5f8036e215a40325591acb/src/shared/utils.ts#L291
|
||||||
|
|
||||||
|
if (!url.startsWith('https://') && !url.startsWith('http://')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.endsWith('.jpg') ||
|
||||||
|
url.endsWith('.jpeg') ||
|
||||||
|
url.endsWith('.png') ||
|
||||||
|
url.endsWith('.gif') ||
|
||||||
|
url.endsWith('.webp') ||
|
||||||
|
url.endsWith('.bmp') ||
|
||||||
|
url.endsWith('.svg') ||
|
||||||
|
url.endsWith('.wbpm');
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> save(Jwt token) async {
|
||||||
|
final result = await savingState.runLemmy(
|
||||||
|
postView.instanceHost,
|
||||||
|
SavePost(
|
||||||
|
postId: postView.post.id, save: !postView.saved, auth: token.raw));
|
||||||
|
|
||||||
|
if (result != null) postView = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> blockUser(Jwt token) async {
|
||||||
|
final result = await userBlockingState.runLemmy(
|
||||||
|
postView.post.instanceHost,
|
||||||
|
BlockPerson(
|
||||||
|
personId: postView.creator.id,
|
||||||
|
block: !postView.creatorBlocked,
|
||||||
|
auth: token.raw,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
postView = postView.copyWith(creatorBlocked: result.blocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
// ignore: use_setters_to_change_properties
|
||||||
|
void updatePostView(PostView postView) {
|
||||||
|
this.postView = postView;
|
||||||
|
}
|
||||||
|
|
||||||
|
// VOTING
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> _vote(Jwt token, VoteType voteType) async {
|
||||||
|
final result = await votingState.runLemmy(
|
||||||
|
postView.instanceHost,
|
||||||
|
CreatePostLike(
|
||||||
|
postId: postView.post.id, score: voteType, auth: token.raw),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) postView = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> upVote(Jwt token) => _vote(
|
||||||
|
token, postView.myVote == VoteType.up ? VoteType.none : VoteType.up);
|
||||||
|
|
||||||
|
@action
|
||||||
|
Future<void> downVote(Jwt token) => _vote(
|
||||||
|
token, postView.myVote == VoteType.down ? VoteType.none : VoteType.down);
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'post_store.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// StoreGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
|
||||||
|
|
||||||
|
mixin _$PostStore on _PostStore, Store {
|
||||||
|
Computed<String?>? _$urlDomainComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get urlDomain =>
|
||||||
|
(_$urlDomainComputed ??= Computed<String?>(() => super.urlDomain,
|
||||||
|
name: '_PostStore.urlDomain'))
|
||||||
|
.value;
|
||||||
|
Computed<bool>? _$hasMediaComputed;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get hasMedia => (_$hasMediaComputed ??=
|
||||||
|
Computed<bool>(() => super.hasMedia, name: '_PostStore.hasMedia'))
|
||||||
|
.value;
|
||||||
|
|
||||||
|
final _$postViewAtom = Atom(name: '_PostStore.postView');
|
||||||
|
|
||||||
|
@override
|
||||||
|
PostView get postView {
|
||||||
|
_$postViewAtom.reportRead();
|
||||||
|
return super.postView;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
set postView(PostView value) {
|
||||||
|
_$postViewAtom.reportWrite(value, super.postView, () {
|
||||||
|
super.postView = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$saveAsyncAction = AsyncAction('_PostStore.save');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> save(Jwt token) {
|
||||||
|
return _$saveAsyncAction.run(() => super.save(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$blockUserAsyncAction = AsyncAction('_PostStore.blockUser');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> blockUser(Jwt token) {
|
||||||
|
return _$blockUserAsyncAction.run(() => super.blockUser(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$_voteAsyncAction = AsyncAction('_PostStore._vote');
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> _vote(Jwt token, VoteType voteType) {
|
||||||
|
return _$_voteAsyncAction.run(() => super._vote(token, voteType));
|
||||||
|
}
|
||||||
|
|
||||||
|
final _$_PostStoreActionController = ActionController(name: '_PostStore');
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updatePostView(PostView postView) {
|
||||||
|
final _$actionInfo = _$_PostStoreActionController.startAction(
|
||||||
|
name: '_PostStore.updatePostView');
|
||||||
|
try {
|
||||||
|
return super.updatePostView(postView);
|
||||||
|
} finally {
|
||||||
|
_$_PostStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> upVote(Jwt token) {
|
||||||
|
final _$actionInfo =
|
||||||
|
_$_PostStoreActionController.startAction(name: '_PostStore.upVote');
|
||||||
|
try {
|
||||||
|
return super.upVote(token);
|
||||||
|
} finally {
|
||||||
|
_$_PostStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> downVote(Jwt token) {
|
||||||
|
final _$actionInfo =
|
||||||
|
_$_PostStoreActionController.startAction(name: '_PostStore.downVote');
|
||||||
|
try {
|
||||||
|
return super.downVote(token);
|
||||||
|
} finally {
|
||||||
|
_$_PostStoreActionController.endAction(_$actionInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return '''
|
||||||
|
postView: ${postView},
|
||||||
|
urlDomain: ${urlDomain},
|
||||||
|
hasMedia: ${hasMedia}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../url_launcher.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import '../cached_network_image.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
|
||||||
|
class PostTitle extends StatelessWidget {
|
||||||
|
const PostTitle();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ObserverBuilder<PostStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
final post = store.postView.post;
|
||||||
|
final thumbnailUrl = post.thumbnailUrl;
|
||||||
|
final url = post.url;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(10).copyWith(top: 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
post.name,
|
||||||
|
textAlign: TextAlign.left,
|
||||||
|
softWrap: true,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!store.hasMedia && thumbnailUrl != null && url != null) ...[
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
onTap: () => linkLauncher(
|
||||||
|
context: context,
|
||||||
|
url: url,
|
||||||
|
instanceHost: store.postView.instanceHost),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: thumbnailUrl,
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error) =>
|
||||||
|
Text(error.toString()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Positioned(
|
||||||
|
top: 8,
|
||||||
|
right: 8,
|
||||||
|
child: Icon(
|
||||||
|
Icons.launch,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:lemmy_api_client/v3.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../hooks/stores.dart';
|
||||||
|
import '../../util/intl.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
|
||||||
|
class PostVoting extends HookWidget {
|
||||||
|
const PostVoting();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final showScores =
|
||||||
|
useConfigStoreSelect((configStore) => configStore.showScores);
|
||||||
|
final loggedInAction = useLoggedInAction(context
|
||||||
|
.select<PostStore, String>((store) => store.postView.instanceHost));
|
||||||
|
|
||||||
|
return ObserverBuilder<PostStore>(builder: (context, store) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.arrow_upward,
|
||||||
|
color: store.postView.myVote == VoteType.up
|
||||||
|
? theme.colorScheme.secondary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onPressed: loggedInAction(store.upVote),
|
||||||
|
),
|
||||||
|
if (store.votingState.isLoading)
|
||||||
|
const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator.adaptive(),
|
||||||
|
)
|
||||||
|
else if (showScores)
|
||||||
|
Text(compactNumber(store.postView.counts.score)),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.arrow_downward,
|
||||||
|
color: store.postView.myVote == VoteType.down ? Colors.red : null,
|
||||||
|
),
|
||||||
|
onPressed: loggedInAction(store.downVote),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../../hooks/logged_in_action.dart';
|
||||||
|
import '../../util/observer_consumers.dart';
|
||||||
|
import 'post_store.dart';
|
||||||
|
|
||||||
|
class SavePostButton extends HookWidget {
|
||||||
|
const SavePostButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final loggedInAction = useLoggedInAction(context
|
||||||
|
.select<PostStore, String>((store) => store.postView.instanceHost));
|
||||||
|
|
||||||
|
return ObserverBuilder<PostStore>(
|
||||||
|
builder: (context, store) {
|
||||||
|
final savedIcon =
|
||||||
|
store.postView.saved ? Icons.bookmark : Icons.bookmark_border;
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
tooltip: 'Save post',
|
||||||
|
icon: store.savingState.isLoading
|
||||||
|
? const CircularProgressIndicator.adaptive()
|
||||||
|
: Icon(savedIcon),
|
||||||
|
onPressed: loggedInAction(store.save),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:lemmy_api_client/v3.dart';
|
|
||||||
|
|
||||||
import '../hooks/delayed_loading.dart';
|
|
||||||
import '../hooks/logged_in_action.dart';
|
|
||||||
|
|
||||||
// TODO: sync this button between post and fullpost. the same with voting
|
|
||||||
|
|
||||||
class SavePostButton extends HookWidget {
|
|
||||||
final PostView post;
|
|
||||||
|
|
||||||
const SavePostButton(this.post);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final isSaved = useState(post.saved);
|
|
||||||
final savedIcon = isSaved.value ? Icons.bookmark : Icons.bookmark_border;
|
|
||||||
final loading = useDelayedLoading();
|
|
||||||
final loggedInAction = useLoggedInAction(post.instanceHost);
|
|
||||||
|
|
||||||
savePost(Jwt token) async {
|
|
||||||
final api = LemmyApiV3(post.instanceHost);
|
|
||||||
|
|
||||||
loading.start();
|
|
||||||
try {
|
|
||||||
final res = await api.run(SavePost(
|
|
||||||
postId: post.post.id, save: !isSaved.value, auth: token.raw));
|
|
||||||
isSaved.value = res.saved;
|
|
||||||
} catch (e) {
|
|
||||||
ScaffoldMessenger.of(context)
|
|
||||||
.showSnackBar(const SnackBar(content: Text('saving failed :(')));
|
|
||||||
}
|
|
||||||
loading.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loading.loading) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 5),
|
|
||||||
child: SizedBox(
|
|
||||||
width: 30,
|
|
||||||
height: 30,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
backgroundColor: Theme.of(context).iconTheme.color,
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return IconButton(
|
|
||||||
tooltip: 'Save post',
|
|
||||||
icon: Icon(savedIcon),
|
|
||||||
onPressed: loggedInAction(loading.pending ? (_) {} : savePost),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,7 +7,7 @@ import '../hooks/infinite_scroll.dart';
|
||||||
import '../hooks/stores.dart';
|
import '../hooks/stores.dart';
|
||||||
import 'comment/comment.dart';
|
import 'comment/comment.dart';
|
||||||
import 'infinite_scroll.dart';
|
import 'infinite_scroll.dart';
|
||||||
import 'post.dart';
|
import 'post/post.dart';
|
||||||
import 'post_list_options.dart';
|
import 'post_list_options.dart';
|
||||||
|
|
||||||
typedef FetcherWithSorting<T> = Future<List<T>> Function(
|
typedef FetcherWithSorting<T> = Future<List<T>> Function(
|
||||||
|
@ -74,7 +74,7 @@ class InfinitePostList extends SortableInfiniteList<PostView> {
|
||||||
}) : super(
|
}) : super(
|
||||||
itemBuilder: (post) => Column(
|
itemBuilder: (post) => Column(
|
||||||
children: [
|
children: [
|
||||||
PostWidget(post),
|
PostTile.fromPostView(post),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
version: 0.6.0+17
|
version: 0.6.0+17
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.12.0 <3.0.0"
|
sdk: ">=2.14.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
# widgets
|
# widgets
|
||||||
|
|
Loading…
Reference in New Issue