From 3a5a395f790cfa49ded3d970964c28d3368d25da Mon Sep 17 00:00:00 2001 From: Pierre HUBERT Date: Fri, 5 Jul 2019 11:40:43 +0200 Subject: [PATCH] Can create posts --- lib/enums/post_target.dart | 8 + lib/helpers/posts_helper.dart | 39 ++++ lib/helpers/users_helper.dart | 1 + lib/models/advanced_user_info.dart | 3 + lib/models/new_post.dart | 33 ++++ lib/ui/routes/user_page_route.dart | 21 ++ lib/ui/widgets/post_create_form_widget.dart | 205 ++++++++++++++++++++ 7 files changed, 310 insertions(+) create mode 100644 lib/enums/post_target.dart create mode 100644 lib/models/new_post.dart create mode 100644 lib/ui/widgets/post_create_form_widget.dart diff --git a/lib/enums/post_target.dart b/lib/enums/post_target.dart new file mode 100644 index 0000000..ddcbc79 --- /dev/null +++ b/lib/enums/post_target.dart @@ -0,0 +1,8 @@ +/// Posts targets types enum +/// +/// @author Pierre HUBERT + +enum PostTarget { + USER_PAGE, + GROUP_PAGE +} \ No newline at end of file diff --git a/lib/helpers/posts_helper.dart b/lib/helpers/posts_helper.dart index 4682555..fc3ec9d 100644 --- a/lib/helpers/posts_helper.dart +++ b/lib/helpers/posts_helper.dart @@ -1,4 +1,5 @@ import 'package:comunic/enums/post_kind.dart'; +import 'package:comunic/enums/post_target.dart'; import 'package:comunic/enums/post_visibility_level.dart'; import 'package:comunic/enums/user_access_levels.dart'; import 'package:comunic/helpers/comments_helper.dart'; @@ -6,6 +7,7 @@ import 'package:comunic/helpers/survey_helper.dart'; import 'package:comunic/lists/comments_list.dart'; import 'package:comunic/lists/posts_list.dart'; import 'package:comunic/models/api_request.dart'; +import 'package:comunic/models/new_post.dart'; import 'package:comunic/models/post.dart'; /// Posts helper @@ -37,6 +39,11 @@ const _APIUserAccessMap = { "full": UserAccessLevels.FULL }; +const _APIPostsTargetKindsMap = { + PostTarget.USER_PAGE: "user", + PostTarget.GROUP_PAGE: "group" +}; + class PostsHelper { /// Get the list of latest posts. Return the list of posts or null in case of /// failure @@ -77,6 +84,38 @@ class PostsHelper { } } + /// Create a new post + /// + /// This function crash in case of error + Future createPost(NewPost post) async { + APIRequest request = + APIRequest(uri: "posts/create", needLogin: true, args: { + "kind-page": _APIPostsTargetKindsMap[post.target], + "kind-id": post.targetID.toString(), + "visibility": _APIPostsVisibilityLevelMap.map( + (s, v) => MapEntry(v, s))[post.visibility], + "kind": _APIPostsKindsMap.map((s, k) => MapEntry(k, s))[post.kind], + "content": post.content + }); + + switch (post.kind) { + case PostKind.TEXT: + break; + + case PostKind.IMAGE: + request.addFile("image", post.image); + break; + + default: + throw Exception("Unsupported post type :" + post.kind.toString()); + break; + } + + final response = await request.execWithFiles(); + + if (!response.isOK) throw Exception("Could not create the post !"); + } + /// Update a post content Future updateContent(int id, String newContent) async { return (await APIRequest( diff --git a/lib/helpers/users_helper.dart b/lib/helpers/users_helper.dart index ad2eca7..2054db5 100644 --- a/lib/helpers/users_helper.dart +++ b/lib/helpers/users_helper.dart @@ -155,6 +155,7 @@ class UsersHelper { data["virtualDirectory"] == "" ? null : data["virtualDirectory"], accountImageURL: data["accountImage"], publicNote: data["publicNote"], + canPostTexts: data["can_post_texts"], ); } } diff --git a/lib/models/advanced_user_info.dart b/lib/models/advanced_user_info.dart index ac42946..dbfb55e 100644 --- a/lib/models/advanced_user_info.dart +++ b/lib/models/advanced_user_info.dart @@ -8,6 +8,7 @@ import 'package:meta/meta.dart'; class AdvancedUserInfo extends User { final String publicNote; + final bool canPostTexts; const AdvancedUserInfo({ @required int id, @@ -17,7 +18,9 @@ class AdvancedUserInfo extends User { @required String virtualDirectory, @required String accountImageURL, @required this.publicNote, + @required this.canPostTexts, }) : assert(publicNote != null), + assert(canPostTexts != null), super( id: id, firstName: firstName, diff --git a/lib/models/new_post.dart b/lib/models/new_post.dart new file mode 100644 index 0000000..0a1843e --- /dev/null +++ b/lib/models/new_post.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import 'package:comunic/enums/post_kind.dart'; +import 'package:comunic/enums/post_target.dart'; +import 'package:comunic/enums/post_visibility_level.dart'; +import 'package:meta/meta.dart'; + +/// New post information +/// +/// @author Pierre HUBERT + +class NewPost { + final PostTarget target; + final int targetID; + final PostVisibilityLevel visibility; + final String content; + final File image; + final PostKind kind; + + NewPost({ + @required this.target, + @required this.targetID, + @required this.visibility, + @required this.content, + @required this.kind, + @required this.image, + }) : assert(target != null), + assert(targetID != null), + assert(visibility != null), + assert(content != null), + assert(kind != PostKind.TEXT || content.length > 3), + assert(kind != PostKind.IMAGE || image != null); +} diff --git a/lib/ui/routes/user_page_route.dart b/lib/ui/routes/user_page_route.dart index e456f65..d4d2f74 100644 --- a/lib/ui/routes/user_page_route.dart +++ b/lib/ui/routes/user_page_route.dart @@ -1,9 +1,11 @@ +import 'package:comunic/enums/post_target.dart'; import 'package:comunic/helpers/posts_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/models/advanced_user_info.dart'; import 'package:comunic/ui/routes/other_friends_lists_route.dart'; import 'package:comunic/ui/routes/user_access_denied_route.dart'; import 'package:comunic/ui/widgets/network_image_widget.dart'; +import 'package:comunic/ui/widgets/post_create_form_widget.dart'; import 'package:comunic/ui/widgets/posts_list_widget.dart'; import 'package:comunic/utils/conversations_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; @@ -38,6 +40,8 @@ class _UserPageRouteState extends State { final double _appBarHeight = 256.0; _PageStatus _status = _PageStatus.LOADING; AdvancedUserInfo _userInfo; + GlobalKey _refreshIndicatorKey = + GlobalKey(); _setStatus(_PageStatus s) => setState(() => _status = s); @@ -80,8 +84,10 @@ class _UserPageRouteState extends State { return Scaffold( body: RefreshIndicator( + key: _refreshIndicatorKey, child: CustomScrollView( slivers: [_buildHeader(), _buildBody()], + physics: AlwaysScrollableScrollPhysics(), ), onRefresh: _getUserInfo, ), @@ -175,6 +181,16 @@ class _UserPageRouteState extends State { return SliverList( delegate: SliverChildListDelegate( [ + // Posts create form + _userInfo.canPostTexts + ? PostCreateFormWidget( + postTarget: PostTarget.USER_PAGE, + targetID: _userInfo.id, + onCreated: _postCreated, + ) + : Container(), + + // Posts list PostsListWidget( getPostsList: () => _postsHelper.getUserPosts(widget.userID), showPostsTarget: false, @@ -199,4 +215,9 @@ class _UserPageRouteState extends State { break; } } + + /// Method called once a post has been created + void _postCreated() { + _refreshIndicatorKey.currentState.show(); + } } diff --git a/lib/ui/widgets/post_create_form_widget.dart b/lib/ui/widgets/post_create_form_widget.dart new file mode 100644 index 0000000..08b0a5e --- /dev/null +++ b/lib/ui/widgets/post_create_form_widget.dart @@ -0,0 +1,205 @@ +import 'dart:io'; + +import 'package:comunic/enums/post_kind.dart'; +import 'package:comunic/enums/post_target.dart'; +import 'package:comunic/enums/post_visibility_level.dart'; +import 'package:comunic/helpers/posts_helper.dart'; +import 'package:comunic/models/new_post.dart'; +import 'package:comunic/utils/files_utils.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:comunic/utils/post_utils.dart'; +import 'package:comunic/utils/ui_utils.dart'; +import 'package:flutter/material.dart'; + +/// Widget that allows to create posts +/// +/// @author Pierre HUBERT + +const _ActiveButtonsColor = Colors.blue; +const _ActiveButtonsTextColor = Colors.white; +const _InactiveButtonsColor = Colors.grey; +const _InactiveButtonsTextColor = Colors.black; + +class PostCreateFormWidget extends StatefulWidget { + final PostTarget postTarget; + final int targetID; + final void Function() onCreated; + + const PostCreateFormWidget({ + Key key, + @required this.postTarget, + @required this.targetID, + @required this.onCreated, + }) : assert(postTarget != null), + assert(targetID != null), + super(key: key); + + @override + _PostCreateFormWidgetState createState() => _PostCreateFormWidgetState(); +} + +class _PostCreateFormWidgetState extends State { + // Helpers + final PostsHelper _postHelper = PostsHelper(); + + // Class members + bool _isCreating = false; + final TextEditingController _postTextController = TextEditingController(); + PostVisibilityLevel _postVisibilityLevel; + File _postImage; + + bool get hasImage => _postImage != null; + + bool get canSubmitForm => + !_isCreating && _postTextController.text.length > 5 || hasImage; + + PostKind get postKind { + if (hasImage) + return PostKind.IMAGE; + else + return PostKind.TEXT; + } + + @override + void initState() { + super.initState(); + + _postVisibilityLevel = widget.postTarget == PostTarget.GROUP_PAGE + ? PostVisibilityLevel.GROUP_MEMBERS + : PostVisibilityLevel.FRIENDS; + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Post text content + TextField( + controller: _postTextController, + minLines: 3, + maxLines: 10, + decoration: InputDecoration(hintText: tr("Create a new post...")), + onChanged: (s) => setState(() {}), + ), + + // Post options + Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Include image button + _PostOptionWidget( + icon: Icons.image, + selected: hasImage, + onTap: _pickImageForPost, + ), + + // Post visibility level + _PostOptionWidget( + icon: PostVisibilityLevelsMapIcons[_postVisibilityLevel], + selected: false, + customColor: Colors.black, + onTap: _changeVisibilityLevel, + ), + + // Submit post button + _isCreating + ? Container() + : FlatButton( + child: Text(tr("Send").toUpperCase()), + onPressed: canSubmitForm ? _submitForm : null, + color: _ActiveButtonsColor, + textColor: _ActiveButtonsTextColor, + disabledColor: _InactiveButtonsColor, + disabledTextColor: _InactiveButtonsTextColor, + ), + ], + ), + ) + ], + ); + } + + /// Change post visibility level + Future _changeVisibilityLevel() async { + final newLevel = await showPostVisibilityPicker( + context: context, + initialLevel: _postVisibilityLevel, + isGroup: widget.postTarget == PostTarget.GROUP_PAGE, + ); + + setState(() => _postVisibilityLevel = newLevel); + } + + /// Pick an image for the new post + Future _pickImageForPost() async { + final image = await pickImage(context); + + if (image == null) return; + + setState(() { + this._postImage = image; + }); + } + + /// Submit new post + Future _submitForm() async { + if (!canSubmitForm) + showSimpleSnack(context, tr("Form can not be submitted at this point!")); + + setState(() => _isCreating = true); + + try { + await _postHelper.createPost(NewPost( + target: widget.postTarget, + targetID: widget.targetID, + visibility: _postVisibilityLevel, + content: _postTextController.text, + kind: postKind, + image: _postImage, + )); + setState(() => _isCreating = false); + + showSimpleSnack(context, tr("The post has been successfully created!")); + + widget.onCreated(); + } catch (e) { + setState(() => _isCreating = false); + print("Error while creating post : " + e.toString()); + showSimpleSnack(context, tr("Could not create post !")); + } + } +} + +/// Widget for a single post option +class _PostOptionWidget extends StatelessWidget { + final IconData icon; + final bool selected; + final Color customColor; + final void Function() onTap; + + const _PostOptionWidget( + {Key key, + @required this.icon, + @required this.selected, + @required this.onTap, + this.customColor}) + : assert(icon != null), + assert(selected != null), + assert(onTap != null), + super(key: key); + + bool get hasCustomColor => customColor != null; + + @override + Widget build(BuildContext context) { + return IconButton( + icon: Icon(icon), + onPressed: onTap, + color: hasCustomColor + ? customColor + : selected ? _ActiveButtonsColor : _InactiveButtonsColor, + ); + } +}