Merge pull request #59 from krawieck/post-creation
This commit is contained in:
commit
d17317dc5d
|
@ -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'),
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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'),
|
||||
)
|
||||
],
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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();
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue