Merge pull request #59 from krawieck/post-creation

This commit is contained in:
Filip Krawczyk 2020-09-29 20:51:45 +02:00 committed by GitHub
commit d17317dc5d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 287 additions and 13 deletions

View File

@ -6,17 +6,21 @@ import '../pages/settings.dart';
import '../util/goto.dart';
import 'stores.dart';
/// If user has an account for the given instance the passed wrapper will call
/// the passed action with a Jwt token. Otherwise the action is ignored and a
/// Snackbar is rendered. If [any] is set to true, this check is performed for
/// all instances and if any of them have an account, the wrapped action will be
/// called with a null token.
Function(
Function(Jwt token) action, [
String message,
]) useLoggedInAction(
String instanceUrl,
) {
]) useLoggedInAction(String instanceUrl, {bool any = false}) {
final context = useContext();
final store = useAccountsStore();
return (Function(Jwt token) action, [message]) {
if (store.isAnonymousFor(instanceUrl)) {
if (any && store.hasNoAccount ||
!any && store.isAnonymousFor(instanceUrl)) {
return () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(message ?? 'you have to be logged in to do that'),

View File

@ -3,4 +3,4 @@ import 'package:flutter_hooks/flutter_hooks.dart';
AsyncSnapshot<T> useMemoFuture<T>(Future<T> Function() valueBuilder,
[List<Object> keys = const <dynamic>[]]) =>
useFuture(useMemoized<Future<T>>(valueBuilder, keys));
useFuture(useMemoized<Future<T>>(valueBuilder, keys), preserveState: false);

View File

@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import 'hooks/stores.dart';
import 'pages/communities_tab.dart';
import 'pages/create_post.dart';
import 'pages/profile_tab.dart';
import 'stores/accounts_store.dart';
import 'stores/config_store.dart';
@ -109,10 +110,7 @@ class MyHomePage extends HookWidget {
index: currentTab.value,
children: pages,
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {}, // TODO: create post
),
floatingActionButton: CreatePostFab(),
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: BottomAppBar(
shape: CircularNotchedRectangle(),

232
lib/pages/create_post.dart Normal file
View File

@ -0,0 +1,232 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lemmy_api_client/lemmy_api_client.dart';
import '../hooks/delayed_loading.dart';
import '../hooks/logged_in_action.dart';
import '../hooks/memo_future.dart';
import '../hooks/stores.dart';
import '../util/extensions/api.dart';
import '../util/goto.dart';
import '../util/spaced.dart';
import '../widgets/markdown_text.dart';
import 'full_post.dart';
class CreatePostFab extends HookWidget {
@override
Widget build(BuildContext context) {
final loggedInAction = useLoggedInAction(null, any: true);
return FloatingActionButton(
child: Icon(Icons.add),
onPressed: loggedInAction((_) => showCupertinoModalPopup(
context: context, builder: (_) => CreatePost())),
);
}
}
class CreatePost extends HookWidget {
final CommunityView community;
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey();
CreatePost() : community = null;
CreatePost.toCommunity(this.community);
@override
Widget build(BuildContext context) {
final urlController = useTextEditingController();
final titleController = useTextEditingController();
final bodyController = useTextEditingController();
final accStore = useAccountsStore();
final selectedInstance =
useState(community?.instanceUrl ?? accStore.loggedInInstances.first);
final selectedCommunity = useState(community);
final showFancy = useState(false);
final nsfw = useState(false);
final delayed = useDelayedLoading();
final allCommunitiesSnap = useMemoFuture(
() => LemmyApi(selectedInstance.value)
.v1
.listCommunities(
sort: SortType.hot,
limit: 9999,
auth: accStore.defaultTokenFor(selectedInstance.value).raw,
)
.then(
(value) {
value.sort((a, b) => a.name.compareTo(b.name));
return value;
},
),
[selectedInstance.value],
);
// TODO: use drop down from AddAccountPage
final instanceDropdown = InputDecorator(
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
border: OutlineInputBorder()),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedInstance.value,
onChanged: (val) => selectedInstance.value = val,
items: accStore.loggedInInstances
.map((instance) => DropdownMenuItem(
value: instance,
child: Text(instance),
))
.toList(),
),
),
);
// TODO: use lazy autocomplete
final communitiesDropdown = InputDecorator(
decoration: const InputDecoration(
contentPadding: EdgeInsets.symmetric(vertical: 1, horizontal: 20),
border: OutlineInputBorder()),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedCommunity.value?.name,
hint: Text('Community'),
onChanged: (val) => selectedCommunity.value =
allCommunitiesSnap.data.firstWhere((e) => e.name == val),
items: allCommunitiesSnap.hasData
? allCommunitiesSnap.data
.map((e) => DropdownMenuItem(
value: e.name,
child: Text(e.name),
))
.toList()
: [
DropdownMenuItem(
value: '',
child: CircularProgressIndicator(),
)
],
),
),
);
final url = TextField(
controller: urlController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'URL',
suffixIcon: Icon(Icons.link)),
);
final title = TextField(
controller: titleController,
minLines: 1,
maxLines: 2,
decoration:
InputDecoration(border: OutlineInputBorder(), labelText: 'Title'),
);
final body = IndexedStack(
index: showFancy.value ? 1 : 0,
children: [
TextField(
controller: bodyController,
expands: true,
maxLines: null,
textAlignVertical: TextAlignVertical.top,
decoration:
InputDecoration(border: OutlineInputBorder(), labelText: 'Body'),
),
Padding(
padding: const EdgeInsets.all(16),
child: MarkdownText(
bodyController.text,
instanceUrl: selectedInstance.value,
),
)
],
);
handleSubmit() async {
if (selectedCommunity.value == null || titleController.text.isEmpty) {
scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text('Choosing a community and a title is required'),
));
return;
}
final api = LemmyApi(selectedInstance.value).v1;
final token = accStore.defaultTokenFor(selectedInstance.value);
delayed.start();
try {
final res = await api.createPost(
url: urlController.text.isEmpty ? null : urlController.text,
body: bodyController.text.isEmpty ? null : bodyController.text,
nsfw: nsfw.value,
name: titleController.text,
communityId: selectedCommunity.value.id,
auth: token.raw);
goToReplace(context, (_) => FullPostPage.fromPostView(res));
return;
// ignore: avoid_catches_without_on_clauses
} catch (e) {
scaffoldKey.currentState
.showSnackBar(SnackBar(content: Text('Failed to post')));
}
delayed.cancel();
}
return Scaffold(
key: scaffoldKey,
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.close),
onPressed: Navigator.of(context).pop,
),
actions: [
IconButton(
icon: Icon(showFancy.value ? Icons.build : Icons.brush),
onPressed: () => showFancy.value = !showFancy.value,
),
],
),
body: SafeArea(
child: Column(
children: spaced(6, [
instanceDropdown,
communitiesDropdown,
url,
title,
Expanded(child: body),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GestureDetector(
onTap: () => nsfw.value = !nsfw.value,
child: Row(
children: [
Checkbox(
value: nsfw.value,
onChanged: (val) => nsfw.value = val,
),
Text('NSFW')
],
),
),
FlatButton(
onPressed: delayed.pending ? () {} : handleSubmit,
child: delayed.loading
? CircularProgressIndicator()
: Text('post'),
)
],
),
]),
),
),
);
}
}

View File

@ -193,7 +193,7 @@ abstract class _AccountsStore with Store {
}
bool isAnonymousFor(String instanceUrl) => Computed(() {
if (!users.containsKey(instanceUrl)) {
if (!instances.contains(instanceUrl)) {
return true;
}
@ -201,7 +201,14 @@ abstract class _AccountsStore with Store {
}).value;
@computed
bool get hasNoAccount => users.values.every((e) => e.isEmpty);
bool get hasNoAccount => loggedInInstances.isEmpty;
@computed
Iterable<String> get instances => users.keys;
@computed
Iterable<String> get loggedInInstances =>
instances.where((e) => !isAnonymousFor(e));
/// adds a new account
/// if it's the first account ever the account is

View File

@ -30,6 +30,20 @@ mixin _$AccountsStore on _AccountsStore, Store {
(_$hasNoAccountComputed ??= Computed<bool>(() => super.hasNoAccount,
name: '_AccountsStore.hasNoAccount'))
.value;
Computed<Iterable<String>> _$instancesComputed;
@override
Iterable<String> get instances =>
(_$instancesComputed ??= Computed<Iterable<String>>(() => super.instances,
name: '_AccountsStore.instances'))
.value;
Computed<Iterable<String>> _$loggedInInstancesComputed;
@override
Iterable<String> get loggedInInstances => (_$loggedInInstancesComputed ??=
Computed<Iterable<String>>(() => super.loggedInInstances,
name: '_AccountsStore.loggedInInstances'))
.value;
final _$usersAtom = Atom(name: '_AccountsStore.users');
@ -173,7 +187,9 @@ users: ${users},
tokens: ${tokens},
defaultUser: ${defaultUser},
defaultToken: ${defaultToken},
hasNoAccount: ${hasNoAccount}
hasNoAccount: ${hasNoAccount},
instances: ${instances},
loggedInInstances: ${loggedInInstances}
''';
}
}

View File

@ -14,6 +14,14 @@ Future<dynamic> goTo(
builder: builder,
));
Future<dynamic> goToReplace(
BuildContext context,
Widget Function(BuildContext context) builder,
) =>
Navigator.of(context).pushReplacement(CupertinoPageRoute(
builder: builder,
));
void goToInstance(BuildContext context, String instanceUrl) =>
goTo(context, (context) => InstancePage(instanceUrl: instanceUrl));

9
lib/util/spaced.dart Normal file
View File

@ -0,0 +1,9 @@
import 'package:flutter/cupertino.dart';
List<Widget> spaced(double gap, Iterable<Widget> children) => children
.expand((item) sync* {
yield SizedBox(width: gap, height: gap);
yield item;
})
.skip(1)
.toList();

View File

@ -62,7 +62,7 @@ class WriteComment extends HookWidget {
final res = await api.createComment(
content: controller.text,
postId: post?.id ?? comment.postId,
parentId: comment?.parentId,
parentId: comment?.id,
auth: token.raw);
Navigator.of(context).pop(res);
// ignore: avoid_catches_without_on_clauses