From ffe80277dbd2bce6e1a64ada969b92a59f72ec7a Mon Sep 17 00:00:00 2001 From: olorin99 <36951539+olorin99@users.noreply.github.com> Date: Wed, 3 Jan 2024 13:52:10 +1000 Subject: [PATCH] Further Microblog/Entry Support (#4) * Added posts sub feeds for magazines and users. * Added ability to edit posts. * Added ability to edit post comments. * Added ability to delete posts and post comments. * Added ability to edit/delete entries and entry comments. * Edit/Delete buttons are now greyed out on feed page. Editing populates the text field with the current body of the entry/post. * Edit/Delete buttons are greyed out if logged in user is not op. --- lib/src/api/comments.dart | 36 +++++ lib/src/api/entries.dart | 41 +++++ lib/src/api/post_comments.dart | 36 +++++ lib/src/api/posts.dart | 36 +++++ lib/src/screens/entries/entry_comment.dart | 23 +++ lib/src/screens/entries/entry_item.dart | 9 ++ lib/src/screens/entries/entry_page.dart | 26 +++ lib/src/screens/explore/magazine_screen.dart | 161 ++++++++++++------- lib/src/screens/explore/user_screen.dart | 161 ++++++++++++------- lib/src/screens/posts/post_comment.dart | 23 +++ lib/src/screens/posts/post_item.dart | 9 ++ lib/src/screens/posts/post_page.dart | 24 +++ lib/src/screens/posts/posts_list.dart | 1 + lib/src/utils/utils.dart | 4 + lib/src/widgets/action_bar.dart | 76 ++++++++- 15 files changed, 543 insertions(+), 123 deletions(-) diff --git a/lib/src/api/comments.dart b/lib/src/api/comments.dart index 8e5fd5d..8f1e7af 100644 --- a/lib/src/api/comments.dart +++ b/lib/src/api/comments.dart @@ -175,3 +175,39 @@ Future postComment( return Comment.fromJson(jsonDecode(response.body) as Map); } + +Future editComment( + http.Client client, + String instanceHost, + int commentId, + String body, + String lang, + bool? isAdult + ) async { + final response = await client.put(Uri.https( + instanceHost, + '/api/comments/$commentId' + ), + body: jsonEncode({ + 'body': body, + 'lang': lang, + 'isAdult': isAdult ?? false + })); + + httpErrorHandler(response, message: "Failed to edit comment"); + + return Comment.fromJson(jsonDecode(response.body) as Map); +} + +Future deleteComment( + http.Client client, + String instanceHost, + int commentId, + ) async { + final response = await client.delete(Uri.https( + instanceHost, + '/api/comments/$commentId' + )); + + httpErrorHandler(response, message: "Failed to delete comment"); +} \ No newline at end of file diff --git a/lib/src/api/entries.dart b/lib/src/api/entries.dart index 86da43a..4f1a018 100644 --- a/lib/src/api/entries.dart +++ b/lib/src/api/entries.dart @@ -154,3 +154,44 @@ Future putFavorite( return EntryItem.fromJson(jsonDecode(response.body) as Map); } + +Future editEntry( + http.Client client, + String instanceHost, + int entryID, + String title, + bool isOc, + String body, + String lang, + bool isAdult + ) async { + final response = await client.put(Uri.https( + instanceHost, + '/api/entry/$entryID' + ), + body: jsonEncode({ + 'title': title, + 'tags': [], + 'isOc': isOc, + 'body': body, + 'lang': lang, + 'isAdult': isAdult + })); + + httpErrorHandler(response, message: "Failed to edit entry"); + + return EntryItem.fromJson(jsonDecode(response.body) as Map); +} + +Future deletePost( + http.Client client, + String instanceHost, + int postID, + ) async { + final response = await client.delete(Uri.https( + instanceHost, + '/api/entry/$postID' + )); + + httpErrorHandler(response, message: "Failed to delete entry"); +} \ No newline at end of file diff --git a/lib/src/api/post_comments.dart b/lib/src/api/post_comments.dart index 66c7b0d..108c66f 100644 --- a/lib/src/api/post_comments.dart +++ b/lib/src/api/post_comments.dart @@ -175,3 +175,39 @@ Future postComment( return Comment.fromJson(jsonDecode(response.body) as Map); } + +Future editComment( + http.Client client, + String instanceHost, + int commentId, + String body, + String lang, + bool? isAdult +) async { + final response = await client.put(Uri.https( + instanceHost, + '/api/post-comments/$commentId' + ), + body: jsonEncode({ + 'body': body, + 'lang': lang, + 'isAdult': isAdult ?? false + })); + + httpErrorHandler(response, message: "Failed to edit comment"); + + return Comment.fromJson(jsonDecode(response.body) as Map); +} + +Future deleteComment( + http.Client client, + String instanceHost, + int commentId, + ) async { + final response = await client.delete(Uri.https( + instanceHost, + '/api/post-comments/$commentId' + )); + + httpErrorHandler(response, message: "Failed to delete comment"); +} \ No newline at end of file diff --git a/lib/src/api/posts.dart b/lib/src/api/posts.dart index 3e98a3b..179d0a3 100644 --- a/lib/src/api/posts.dart +++ b/lib/src/api/posts.dart @@ -144,3 +144,39 @@ Future putFavorite( return PostItem.fromJson(jsonDecode(response.body) as Map); } + +Future editPost( + http.Client client, + String instanceHost, + int postID, + String body, + String lang, + bool isAdult +) async { + final response = await client.put(Uri.https( + instanceHost, + '/api/post/$postID' + ), + body: jsonEncode({ + 'body': body, + 'lang': lang, + 'isAdult': isAdult + })); + + httpErrorHandler(response, message: "Failed to edit post"); + + return PostItem.fromJson(jsonDecode(response.body) as Map); +} + +Future deletePost( + http.Client client, + String instanceHost, + int postID, +) async { + final response = await client.delete(Uri.https( + instanceHost, + '/api/post/$postID' + )); + + httpErrorHandler(response, message: "Failed to delete post"); +} \ No newline at end of file diff --git a/lib/src/screens/entries/entry_comment.dart b/lib/src/screens/entries/entry_comment.dart index e8d3914..bc8a1e5 100644 --- a/lib/src/screens/entries/entry_comment.dart +++ b/lib/src/screens/entries/entry_comment.dart @@ -117,6 +117,29 @@ class _EntryCommentState extends State { newComment.children!.insert(0, newSubComment); widget.onUpdate(newComment); }, + onEdit: whenLoggedIn(context, (body) async { + var newComment = await api_comments.editComment( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + body, + widget.comment.lang, + widget.comment.isAdult + ); + setState(() { + widget.comment.body = newComment.body; + }); + }), + onDelete: whenLoggedIn(context, () async { + await api_comments.deleteComment( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + ); + setState(() { + widget.comment.body = "deleted"; + }); + }), ), ), const SizedBox(height: 4), diff --git a/lib/src/screens/entries/entry_item.dart b/lib/src/screens/entries/entry_item.dart index 99c75cc..f2321ef 100644 --- a/lib/src/screens/entries/entry_item.dart +++ b/lib/src/screens/entries/entry_item.dart @@ -19,11 +19,15 @@ class EntryItem extends StatelessWidget { super.key, this.isPreview = false, this.onReply, + this.onEdit, + this.onDelete, }); final api_entries.EntryItem item; final void Function(api_entries.EntryItem) onUpdate; final Future Function(String)? onReply; + final Future Function(String)? onEdit; + final Future Function()? onDelete; final bool isPreview; _onImageClick(BuildContext context) { @@ -208,6 +212,11 @@ class EntryItem extends StatelessWidget { )); }), onReply: onReply, + onEdit: isLoggedInUser(context, item.user.username, onEdit), + onDelete: isLoggedInUser(context, item.user.username, onDelete), + initEdit: () { + return item.body; + }, leadingWidgets: [ const Icon(Icons.comment), const SizedBox(width: 4), diff --git a/lib/src/screens/entries/entry_page.dart b/lib/src/screens/entries/entry_page.dart index b41b4e7..a30c35d 100644 --- a/lib/src/screens/entries/entry_page.dart +++ b/lib/src/screens/entries/entry_page.dart @@ -5,6 +5,7 @@ import 'package:interstellar/src/api/entries.dart' as api_entries; import 'package:interstellar/src/screens/entries/entry_comment.dart'; import 'package:interstellar/src/screens/entries/entry_item.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; +import 'package:interstellar/src/utils/utils.dart'; import 'package:provider/provider.dart'; class EntryPage extends StatefulWidget { @@ -89,6 +90,31 @@ class _EntryPageState extends State { _pagingController.itemList = newList; }); }, + onEdit: whenLoggedIn(context, (body) async { + final newEntry = await api_entries.editEntry( + context.read().httpClient, + context.read().instanceHost, + widget.item.entryId, + widget.item.title, + widget.item.isOc, + body, + widget.item.lang, + widget.item.isAdult + ); + setState(() { + widget.item.body = newEntry.body; + }); + }), + onDelete: whenLoggedIn(context, () async { + await api_entries.deletePost( + context.read().httpClient, + context.read().instanceHost, + widget.item.entryId, + ); + setState(() { + widget.item.body = "deleted"; + }); + }), ), ), SliverToBoxAdapter( diff --git a/lib/src/screens/explore/magazine_screen.dart b/lib/src/screens/explore/magazine_screen.dart index b8e548d..706779a 100644 --- a/lib/src/screens/explore/magazine_screen.dart +++ b/lib/src/screens/explore/magazine_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/api/content_sources.dart'; import 'package:interstellar/src/api/magazines.dart' as api_magazines; import 'package:interstellar/src/screens/entries/entries_list.dart'; +import 'package:interstellar/src/screens/feed_screen.dart'; +import 'package:interstellar/src/screens/posts/posts_list.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/widgets/avatar.dart'; @@ -21,6 +23,7 @@ class MagazineScreen extends StatefulWidget { class _MagazineScreenState extends State { api_magazines.DetailedMagazine? _data; + FeedMode _feedMode = FeedMode.entries; @override void initState() { @@ -41,71 +44,109 @@ class _MagazineScreenState extends State { } } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(_data?.name ?? '')), - body: EntriesListView( - contentSource: ContentMagazine(widget.magazineId), - details: _data != null - ? Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - if (_data!.icon?.storageUrl != null) - Padding( - padding: const EdgeInsets.only(right: 12), - child: - Avatar(_data!.icon!.storageUrl, radius: 32)), - Expanded( - child: Text( - _data!.title, - style: Theme.of(context).textTheme.titleLarge, - softWrap: true, - ), - ), - OutlinedButton( - style: ButtonStyle( - foregroundColor: _data!.isUserSubscribed == true - ? MaterialStatePropertyAll( - Colors.purple.shade400) - : null), - onPressed: whenLoggedIn(context, () async { - var newValue = await api_magazines.putSubscribe( - context.read().httpClient, - context.read().instanceHost, - _data!.magazineId, - !_data!.isUserSubscribed!); + Widget _magazineDetails() { + return Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + if (_data!.icon?.storageUrl != null) + Padding( + padding: const EdgeInsets.only(right: 12), + child: + Avatar(_data!.icon!.storageUrl, radius: 32)), + Expanded( + child: Text( + _data!.title, + style: Theme.of(context).textTheme.titleLarge, + softWrap: true, + ), + ), + OutlinedButton( + style: ButtonStyle( + foregroundColor: _data!.isUserSubscribed == true + ? MaterialStatePropertyAll( + Colors.purple.shade400) + : null), + onPressed: whenLoggedIn(context, () async { + var newValue = await api_magazines.putSubscribe( + context.read().httpClient, + context.read().instanceHost, + _data!.magazineId, + !_data!.isUserSubscribed!); - if (widget.onUpdate != null) { - widget.onUpdate!(newValue); - } - setState(() { - _data = newValue; - }); - }), - child: Row( - children: [ - const Icon(Icons.group), - Text(' ${intFormat(_data!.subscriptionsCount)}'), - ], - ), - ) - ], - ), - if (_data!.description != null) - Padding( - padding: const EdgeInsets.only(top: 12), - child: Markdown(_data!.description!), - ) + if (widget.onUpdate != null) { + widget.onUpdate!(newValue); + } + setState(() { + _data = newValue; + }); + }), + child: Row( + children: [ + const Icon(Icons.group), + Text(' ${intFormat(_data!.subscriptionsCount)}'), ], ), ) - : null, + ], + ), + if (_data!.description != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Markdown(_data!.description!), + ) + ], ), ); } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_data?.name ?? ''), + actions: [ + SegmentedButton( + segments: const [ + ButtonSegment( + value: FeedMode.entries, + label: Text("Threads"), + ), + ButtonSegment( + value: FeedMode.posts, + label: Text("Posts"), + ), + ], + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity(horizontal: -3, vertical: -3), + ), + selected: {_feedMode}, + onSelectionChanged: (Set newSelection) { + setState(() { + _feedMode = newSelection.first; + }); + }, + ), + ], + ), + body: switch (_feedMode) { + FeedMode.entries => EntriesListView( + contentSource: ContentMagazine(widget.magazineId), + details: _data != null + ? _magazineDetails() + : null, + ), + FeedMode.posts => PostsListView( + contentSource: ContentMagazine(widget.magazineId), + details: _data != null + ? _magazineDetails() + : null, + ), + } + ); + } } diff --git a/lib/src/screens/explore/user_screen.dart b/lib/src/screens/explore/user_screen.dart index 8b09880..1b7fd0d 100644 --- a/lib/src/screens/explore/user_screen.dart +++ b/lib/src/screens/explore/user_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:interstellar/src/api/content_sources.dart'; import 'package:interstellar/src/api/users.dart' as api_users; import 'package:interstellar/src/screens/entries/entries_list.dart'; +import 'package:interstellar/src/screens/feed_screen.dart'; +import 'package:interstellar/src/screens/posts/posts_list.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:interstellar/src/utils/utils.dart'; import 'package:interstellar/src/widgets/avatar.dart'; @@ -21,6 +23,7 @@ class UserScreen extends StatefulWidget { class _UserScreenState extends State { api_users.DetailedUser? _data; + FeedMode _feedMode = FeedMode.entries; @override void initState() { @@ -41,71 +44,109 @@ class _UserScreenState extends State { } } - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(_data?.username ?? '')), - body: EntriesListView( - contentSource: ContentUser(widget.userId), - details: _data != null - ? Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Row( - children: [ - if (_data!.avatar?.storageUrl != null) - Padding( - padding: const EdgeInsets.only(right: 12), - child: Avatar(_data!.avatar!.storageUrl, - radius: 32)), - Expanded( - child: Text( - _data!.username, - style: Theme.of(context).textTheme.titleLarge, - softWrap: true, - ), - ), - OutlinedButton( - style: ButtonStyle( - foregroundColor: _data!.isFollowedByUser == true - ? MaterialStatePropertyAll( - Colors.purple.shade400) - : null), - onPressed: whenLoggedIn(context, () async { - var newValue = await api_users.putFollow( - context.read().httpClient, - context.read().instanceHost, - _data!.userId, - !_data!.isFollowedByUser!); + Widget _userDetails() { + return Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + if (_data!.avatar?.storageUrl != null) + Padding( + padding: const EdgeInsets.only(right: 12), + child: Avatar(_data!.avatar!.storageUrl, + radius: 32)), + Expanded( + child: Text( + _data!.username, + style: Theme.of(context).textTheme.titleLarge, + softWrap: true, + ), + ), + OutlinedButton( + style: ButtonStyle( + foregroundColor: _data!.isFollowedByUser == true + ? MaterialStatePropertyAll( + Colors.purple.shade400) + : null), + onPressed: whenLoggedIn(context, () async { + var newValue = await api_users.putFollow( + context.read().httpClient, + context.read().instanceHost, + _data!.userId, + !_data!.isFollowedByUser!); - if (widget.onUpdate != null) { - widget.onUpdate!(newValue); - } - setState(() { - _data = newValue; - }); - }), - child: Row( - children: [ - const Icon(Icons.group), - Text(' ${intFormat(_data!.followersCount)}'), - ], - ), - ) - ], - ), - if (_data!.about != null) - Padding( - padding: const EdgeInsets.only(top: 12), - child: Markdown(_data!.about!), - ) + if (widget.onUpdate != null) { + widget.onUpdate!(newValue); + } + setState(() { + _data = newValue; + }); + }), + child: Row( + children: [ + const Icon(Icons.group), + Text(' ${intFormat(_data!.followersCount)}'), ], ), ) - : null, + ], + ), + if (_data!.about != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Markdown(_data!.about!), + ) + ], ), ); } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(_data?.username ?? ''), + actions: [ + SegmentedButton( + segments: const [ + ButtonSegment( + value: FeedMode.entries, + label: Text("Threads"), + ), + ButtonSegment( + value: FeedMode.posts, + label: Text("Posts"), + ), + ], + style: const ButtonStyle( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: VisualDensity(horizontal: -3, vertical: -3), + ), + selected: {_feedMode}, + onSelectionChanged: (Set newSelection) { + setState(() { + _feedMode = newSelection.first; + }); + }, + ), + ], + ), + body: switch (_feedMode) { + FeedMode.entries => EntriesListView( + contentSource: ContentUser(widget.userId), + details: _data != null + ? _userDetails() + : null, + ), + FeedMode.posts => PostsListView( + contentSource: ContentUser(widget.userId), + details: _data != null + ? _userDetails() + : null, + ), + } + ); + } } diff --git a/lib/src/screens/posts/post_comment.dart b/lib/src/screens/posts/post_comment.dart index d9b1d94..7d1d492 100644 --- a/lib/src/screens/posts/post_comment.dart +++ b/lib/src/screens/posts/post_comment.dart @@ -117,6 +117,29 @@ class _EntryCommentState extends State { newComment.children!.insert(0, newSubComment); widget.onUpdate(newComment); }, + onEdit: whenLoggedIn(context, (body) async { + var newComment = await api_comments.editComment( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + body, + widget.comment.lang, + widget.comment.isAdult + ); + setState(() { + widget.comment.body = newComment.body; + }); + }), + onDelete: whenLoggedIn(context, () async { + await api_comments.deleteComment( + context.read().httpClient, + context.read().instanceHost, + widget.comment.commentId, + ); + setState(() { + widget.comment.body = "deleted"; + }); + }), ), ), const SizedBox(height: 4), diff --git a/lib/src/screens/posts/post_item.dart b/lib/src/screens/posts/post_item.dart index 2a0bc33..d0a11c2 100644 --- a/lib/src/screens/posts/post_item.dart +++ b/lib/src/screens/posts/post_item.dart @@ -16,11 +16,15 @@ class PostItem extends StatelessWidget { super.key, this.isPreview = false, this.onReply, + this.onEdit, + this.onDelete, }); final api_posts.PostItem item; final void Function(api_posts.PostItem) onUpdate; final Future Function(String)? onReply; + final Future Function(String)? onEdit; + final Future Function()? onDelete; final bool isPreview; _onImageClick(BuildContext context) { @@ -159,6 +163,11 @@ class PostItem extends StatelessWidget { )); }), onReply: onReply, + onEdit: isLoggedInUser(context, item.user.username, onEdit), + onDelete: isLoggedInUser(context, item.user.username, onDelete), + initEdit: () { + return item.body; + }, leadingWidgets: [ const Icon(Icons.comment), const SizedBox(width: 4), diff --git a/lib/src/screens/posts/post_page.dart b/lib/src/screens/posts/post_page.dart index 95a348b..6839b2f 100644 --- a/lib/src/screens/posts/post_page.dart +++ b/lib/src/screens/posts/post_page.dart @@ -5,6 +5,7 @@ import 'package:interstellar/src/api/posts.dart' as api_posts; import 'package:interstellar/src/screens/posts/post_comment.dart'; import 'package:interstellar/src/screens/posts/post_item.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; +import 'package:interstellar/src/utils/utils.dart'; import 'package:provider/provider.dart'; class PostPage extends StatefulWidget { @@ -89,6 +90,29 @@ class _PostPageState extends State { _pagingController.itemList = newList; }); }, + onEdit: whenLoggedIn(context, (body) async { + final newPost = await api_posts.editPost( + context.read().httpClient, + context.read().instanceHost, + widget.item.postId, + body, + widget.item.lang, + widget.item.isAdult + ); + setState(() { + widget.item.body = newPost.body; + }); + }), + onDelete: whenLoggedIn(context, () async { + await api_posts.deletePost( + context.read().httpClient, + context.read().instanceHost, + widget.item.postId, + ); + setState(() { + widget.item.body = "deleted"; + }); + }), ), ), SliverToBoxAdapter( diff --git a/lib/src/screens/posts/posts_list.dart b/lib/src/screens/posts/posts_list.dart index 897bdd5..d520e77 100644 --- a/lib/src/screens/posts/posts_list.dart +++ b/lib/src/screens/posts/posts_list.dart @@ -5,6 +5,7 @@ import 'package:interstellar/src/api/posts.dart' as api_posts; import 'package:interstellar/src/screens/posts/post_item.dart'; import 'package:interstellar/src/screens/posts/post_page.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; +import 'package:interstellar/src/utils/utils.dart'; import 'package:provider/provider.dart'; class PostsListView extends StatefulWidget { diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index 9beb555..e151b5c 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:flutter/widgets.dart'; import 'package:http/http.dart' as http; +import 'package:interstellar/src/api/users.dart'; import 'package:interstellar/src/screens/settings/settings_controller.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; @@ -74,3 +75,6 @@ Map removeNulls(Map map) { }); return map; } + +T? isLoggedInUser(BuildContext context, String username, T? value, {T? otherwise}) => + context.read().selectedAccount.split("@").first == username ? value : otherwise; diff --git a/lib/src/widgets/action_bar.dart b/lib/src/widgets/action_bar.dart index aa098f2..70fcd9d 100644 --- a/lib/src/widgets/action_bar.dart +++ b/lib/src/widgets/action_bar.dart @@ -17,6 +17,9 @@ class ActionBar extends StatefulWidget { final void Function()? onDownVote; final void Function()? onCollapse; final Future Function(String)? onReply; + final Future Function(String)? onEdit; + final void Function()? onDelete; + final String? Function()? initEdit; final List? leadingWidgets; @@ -34,6 +37,9 @@ class ActionBar extends StatefulWidget { this.onDownVote, this.onReply, this.onCollapse, + this.onEdit, + this.onDelete, + this.initEdit, this.leadingWidgets, }); @@ -43,6 +49,8 @@ class ActionBar extends StatefulWidget { class _ActionBarState extends State { TextEditingController? _replyTextController; + TextEditingController? _editTextController; + final MenuController _menuController = MenuController(); @override Widget build(BuildContext context) { @@ -71,9 +79,38 @@ class _ActionBarState extends State { const Spacer(), Padding( padding: const EdgeInsets.only(left: 12), - child: IconButton( - icon: const Icon(Icons.more_vert), - onPressed: () {}, + child: MenuAnchor( + builder: (BuildContext context, MenuController controller, Widget? child) { + return IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () { + if (_menuController.isOpen) { + _menuController.close(); + } else { + _menuController.open(); + } + }, + ); + }, + controller: _menuController, + menuChildren: [ + MenuItemButton( + onPressed: widget.onEdit != null ? () => setState(() { + _editTextController = TextEditingController(); + }) : null, + child: const Padding( + padding: EdgeInsets.all(12), + child: Text("Edit") + ), + ), + MenuItemButton( + onPressed: widget.onDelete, + child: const Padding( + padding: EdgeInsets.all(12), + child: Text("Delete") + ), + ), + ] ), ), if (widget.boosts != null) @@ -146,6 +183,39 @@ class _ActionBarState extends State { ) ], ), + ), + if (widget.onEdit != null && _editTextController != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + MarkdownEditor(_editTextController!..text = (widget.initEdit != null ? widget.initEdit!() : "")!), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: () => setState(() { + _editTextController!.dispose(); + _editTextController = null; + }), + child: const Text('Cancel')), + const SizedBox(width: 8), + FilledButton( + onPressed: () async { + // Wait in case of errors before closing + await widget.onEdit!(_editTextController!.text); + + setState(() { + _editTextController!.dispose(); + _editTextController = null; + }); + }, + child: const Text('Submit')) + ], + ) + ], + ), ) ], );