Add being able to add comments to posts.

merge-requests/67/merge
Hank Grabowski 2022-11-22 21:59:08 -05:00
rodzic 8bb8bd7cc8
commit f224925540
8 zmienionych plików z 220 dodań i 27 usunięć

Wyświetl plik

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import '../../globals.dart'; import '../../globals.dart';
@ -61,6 +62,10 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
}); });
} }
Future<void> addComment() async {
context.push('/comment/new?parent_id=${widget.entry.id}');
}
Future<void> unResharePost() async { Future<void> unResharePost() async {
final id = widget.entry.id; final id = widget.entry.id;
_logger.finest('Trying to un-reshare $id'); _logger.finest('Trying to un-reshare $id');
@ -100,7 +105,8 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
? await unResharePost() ? await unResharePost()
: await resharePost(), : await resharePost(),
icon: icon:
Icon(isReshared ? Icons.repeat_on_outlined : Icons.repeat)) Icon(isReshared ? Icons.repeat_on_outlined : Icons.repeat)),
IconButton(onPressed: addComment, icon: Icon(Icons.add_comment)),
]), ]),
], ],
); );

Wyświetl plik

@ -17,6 +17,7 @@ import '../../utils/dateutils.dart';
import '../../utils/snackbar_builder.dart'; import '../../utils/snackbar_builder.dart';
import '../padding.dart'; import '../padding.dart';
import 'interactions_bar_control.dart'; import 'interactions_bar_control.dart';
import 'status_header_control.dart';
class StatusControl extends StatefulWidget { class StatusControl extends StatefulWidget {
final EntryTreeItem originalItem; final EntryTreeItem originalItem;
@ -57,7 +58,7 @@ class _StatusControlState extends State<StatusControl> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildHeader(context), StatusHeaderControl(entry: entry),
const VerticalPadding( const VerticalPadding(
height: 5, height: 5,
), ),

Wyświetl plik

@ -0,0 +1,61 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/models/timeline_entry.dart';
import '../../globals.dart';
import '../../models/connection.dart';
import '../../services/connections_manager.dart';
import '../../utils/dateutils.dart';
import '../padding.dart';
class StatusHeaderControl extends StatelessWidget {
final TimelineEntry entry;
const StatusHeaderControl({super.key, required this.entry});
@override
Widget build(BuildContext context) {
final author = getIt<ConnectionsManager>()
.getById(entry.authorId)
.getValueOrElse(() => Connection());
return Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CachedNetworkImage(
imageUrl: author.avatarUrl.toString(),
width: 32.0,
),
const HorizontalPadding(),
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
author.name,
style: Theme.of(context).textTheme.bodyText1,
),
Row(
children: [
Text(
ElapsedDateUtils.epochSecondsToString(
entry.backdatedTimestamp),
style: Theme.of(context).textTheme.caption,
),
const HorizontalPadding(),
Icon(
entry.isPublic ? Icons.public : Icons.lock,
color: Theme.of(context).hintColor,
size: Theme.of(context).textTheme.caption?.fontSize,
),
],
),
Text(
entry.id,
),
],
),
],
);
}
}

Wyświetl plik

@ -142,15 +142,18 @@ class FriendicaClient {
)); ));
} }
FutureResult<TimelineEntry, ExecError> createNewPost( FutureResult<TimelineEntry, ExecError> createNewStatus(
{required String text, String spoilerText = ''}) async { {required String text,
_logger.finest(() => 'Creating post'); String spoilerText = '',
String inReplyToId = ''}) async {
_logger.finest(() =>
'Creating status ${inReplyToId.isNotEmpty ? "In Reply to: " : ""} $inReplyToId');
final url = Uri.parse('https://$serverName/api/v1/statuses'); final url = Uri.parse('https://$serverName/api/v1/statuses');
final body = { final body = {
'status': text, 'status': text,
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText, if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
}; };
print(body);
final result = await _postUrl(url, body); final result = await _postUrl(url, body);
if (result.isFailure) { if (result.isFailure) {
return result.errorCast(); return result.errorCast();

Wyświetl plik

@ -100,5 +100,28 @@ final appRouter = GoRouter(
builder: (context, state) => builder: (context, state) =>
EditorScreen(id: state.params['id'] ?? 'Not Found'), EditorScreen(id: state.params['id'] ?? 'Not Found'),
), ),
]) ]),
GoRoute(
path: '/comment',
redirect: (context, state) {
print('post state redirect');
if (state.location == '/comment') {
return '/comment/new';
}
return null;
},
routes: [
GoRoute(
path: 'new',
builder: (context, state) => EditorScreen(
parentId: state.queryParams['parent_id'] ?? '',
),
),
GoRoute(
path: 'edit/:id',
builder: (context, state) =>
EditorScreen(id: state.params['id'] ?? 'Not Found'),
),
]),
]); ]);

Wyświetl plik

@ -1,34 +1,60 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html_core/flutter_widget_from_html_core.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../controls/padding.dart'; import '../controls/padding.dart';
import '../controls/timeline/status_header_control.dart';
import '../models/timeline_entry.dart';
import '../services/timeline_manager.dart'; import '../services/timeline_manager.dart';
import '../utils/snackbar_builder.dart'; import '../utils/snackbar_builder.dart';
class EditorScreen extends StatefulWidget { class EditorScreen extends StatefulWidget {
final String id; final String id;
final String parentId;
const EditorScreen({super.key, this.id = ''}); const EditorScreen({super.key, this.id = '', this.parentId = ''});
@override @override
State<EditorScreen> createState() => _EditorScreenState(); State<EditorScreen> createState() => _EditorScreenState();
} }
class _EditorScreenState extends State<EditorScreen> { class _EditorScreenState extends State<EditorScreen> {
static final _logger = Logger('$EditorScreen');
final contentController = TextEditingController(); final contentController = TextEditingController();
final spoilerController = TextEditingController(); final spoilerController = TextEditingController();
TimelineEntry? parentEntry;
String get statusType => 'Post'; bool get isComment => widget.parentId.isNotEmpty;
Future<void> createPost(BuildContext context, TimelineManager manager) async { String get statusType => widget.parentId.isEmpty ? 'Post' : 'Comment';
@override
void initState() {
if (!isComment) {
return;
}
final manager = context.read<TimelineManager>();
manager.getEntryById(widget.parentId).match(onSuccess: (entry) {
spoilerController.text = entry.spoilerText;
parentEntry = entry;
}, onError: (error) {
_logger.finest('Error trying to get parent entry: $error');
});
}
Future<void> createStatus(
BuildContext context, TimelineManager manager) async {
if (contentController.text.isEmpty) { if (contentController.text.isEmpty) {
buildSnackbar(context, "Can't submit an empty post/comment"); buildSnackbar(context, "Can't submit an empty post/comment");
return; return;
} }
final result = await manager.createNewPost( final result = await manager.createNewStatus(
contentController.text, contentController.text,
spoilerText: spoilerController.text, spoilerText: spoilerController.text,
inReplyToId: widget.parentId,
); );
if (result.isFailure) { if (result.isFailure) {
@ -40,6 +66,7 @@ class _EditorScreenState extends State<EditorScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print('Build editor $isComment $parentEntry');
final manager = context.read<TimelineManager>(); final manager = context.read<TimelineManager>();
return Scaffold( return Scaffold(
@ -54,10 +81,12 @@ class _EditorScreenState extends State<EditorScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
if (isComment && parentEntry != null)
buildCommentPreview(context, parentEntry!),
TextFormField( TextFormField(
controller: spoilerController, controller: spoilerController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: '$statusType spoiler text (optional)', labelText: '$statusType Spoiler Text (optional)',
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).backgroundColor, color: Theme.of(context).backgroundColor,
@ -71,7 +100,8 @@ class _EditorScreenState extends State<EditorScreen> {
maxLines: 10, maxLines: 10,
controller: contentController, controller: contentController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: '$statusType content', labelText: '$statusType Content',
alignLabelWithHint: true,
border: OutlineInputBorder( border: OutlineInputBorder(
borderSide: BorderSide( borderSide: BorderSide(
color: Theme.of(context).backgroundColor, color: Theme.of(context).backgroundColor,
@ -85,7 +115,7 @@ class _EditorScreenState extends State<EditorScreen> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: () async => createPost(context, manager), onPressed: () async => createStatus(context, manager),
child: const Text('Submit'), child: const Text('Submit'),
), ),
const HorizontalPadding(), const HorizontalPadding(),
@ -103,4 +133,40 @@ class _EditorScreenState extends State<EditorScreen> {
), ),
); );
} }
Widget buildCommentPreview(BuildContext context, TimelineEntry entry) {
print('Build preview');
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Comment for status: ',
style: Theme.of(context).textTheme.bodyLarge,
),
Card(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StatusHeaderControl(entry: entry),
const VerticalPadding(height: 3),
if (entry.spoilerText.isNotEmpty) ...[
Text(
'Content Summary: ${entry.spoilerText}',
style: Theme.of(context).textTheme.bodyLarge,
),
const VerticalPadding(height: 3)
],
HtmlWidget(entry.body),
],
),
),
),
const VerticalPadding(),
],
);
}
} }

Wyświetl plik

@ -21,8 +21,19 @@ class EntryManagerService extends ChangeNotifier {
_parentPostIds.clear(); _parentPostIds.clear();
} }
FutureResult<EntryTreeItem, ExecError> createNewPost(String text, Result<TimelineEntry, ExecError> getEntryById(String id) {
{String spoilerText = ''}) async { if (_entries.containsKey(id)) {
return Result.ok(_entries[id]!);
}
return Result.error(ExecError(
type: ErrorType.notFound,
message: 'Timeline entry not found: $id',
));
}
FutureResult<bool, ExecError> createNewStatus(String text,
{String spoilerText = '', String inReplyToId = ''}) async {
_logger.finest('Creating new post: $text'); _logger.finest('Creating new post: $text');
final auth = getIt<AuthService>(); final auth = getIt<AuthService>();
final clientResult = auth.currentClient; final clientResult = auth.currentClient;
@ -33,18 +44,30 @@ class EntryManagerService extends ChangeNotifier {
final client = clientResult.value; final client = clientResult.value;
final result = await client final result = await client
.createNewPost( .createNewStatus(
text: text, text: text,
spoilerText: spoilerText, spoilerText: spoilerText,
inReplyToId: inReplyToId,
) )
.andThenSuccessAsync((item) async { .andThenSuccessAsync((item) async {
await processNewItems([item], client.credentials.username, null); await processNewItems([item], client.credentials.username, null);
return item; return item;
}).andThenSuccessAsync((item) async {
if (inReplyToId.isNotEmpty) {
late final rootPostId;
if (_postNodes.containsKey(inReplyToId)) {
rootPostId = inReplyToId;
} else {
rootPostId = _parentPostIds[inReplyToId];
}
await refreshPost(rootPostId);
}
return item;
}); });
return result.mapValue((post) { return result.mapValue((status) {
_logger.finest('${post.id} post updated after reshare'); _logger.finest('${status.id} status created');
return _nodeToTreeItem(_postNodes[post.id]!, auth.currentId); return true;
}).mapError( }).mapError(
(error) { (error) {
_logger.finest('Error creating post: $error'); _logger.finest('Error creating post: $error');
@ -126,7 +149,6 @@ class EntryManagerService extends ChangeNotifier {
_entries[item.id] = item; _entries[item.id] = item;
_parentPostIds[item.id] = parentPostId; _parentPostIds[item.id] = parentPostId;
} }
;
}); });
} }
@ -137,8 +159,12 @@ class EntryManagerService extends ChangeNotifier {
final postNode = _postNodes.putIfAbsent(item.id, () => _Node(item.id)); final postNode = _postNodes.putIfAbsent(item.id, () => _Node(item.id));
postNodesToReturn.add(postNode); postNodesToReturn.add(postNode);
} else { } else {
final parentPostNode = _postNodes[_parentPostIds[item.id]]!; final parentParentPostId = _postNodes.containsKey(item.parentId)
? item.parentId
: _parentPostIds[item.parentId];
final parentPostNode = _postNodes[parentParentPostId]!;
postNodesToReturn.add(parentPostNode); postNodesToReturn.add(parentPostNode);
_parentPostIds[item.id] = parentPostNode.id;
if (parentPostNode.getChildById(item.id) == null) { if (parentPostNode.getChildById(item.id) == null) {
final newNode = _Node(item.id); final newNode = _Node(item.id);
final injectionNode = parentPostNode.id == item.parentId final injectionNode = parentPostNode.id == item.parentId
@ -282,7 +308,6 @@ class EntryManagerService extends ChangeNotifier {
} }
final entry = _entries[node.id]!; final entry = _entries[node.id]!;
final isMine = entry.authorId == currentId; final isMine = entry.authorId == currentId;
print('Author: ${entry.authorId}, Mine: $currentId => IsMine? $isMine');
return EntryTreeItem( return EntryTreeItem(
_entries[node.id]!, _entries[node.id]!,
isMine: isMine, isMine: isMine,

Wyświetl plik

@ -7,6 +7,7 @@ import '../models/TimelineIdentifiers.dart';
import '../models/entry_tree_item.dart'; import '../models/entry_tree_item.dart';
import '../models/exec_error.dart'; import '../models/exec_error.dart';
import '../models/timeline.dart'; import '../models/timeline.dart';
import '../models/timeline_entry.dart';
import 'entry_manager_service.dart'; import 'entry_manager_service.dart';
enum TimelineRefreshType { enum TimelineRefreshType {
@ -26,18 +27,25 @@ class TimelineManager extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
FutureResult<EntryTreeItem, ExecError> createNewPost(String text, FutureResult<bool, ExecError> createNewStatus(String text,
{String spoilerText = ''}) async { {String spoilerText = '', String inReplyToId = ''}) async {
final result = await getIt<EntryManagerService>().createNewPost( final result = await getIt<EntryManagerService>().createNewStatus(
text, text,
spoilerText: spoilerText, spoilerText: spoilerText,
inReplyToId: inReplyToId,
); );
if (result.isSuccess) { if (result.isSuccess) {
_logger.finest('Notifying listeners of new status created');
notifyListeners(); notifyListeners();
} }
return result; return result;
} }
Result<TimelineEntry, ExecError> getEntryById(String id) {
_logger.finest('Getting entry for $id');
return getIt<EntryManagerService>().getEntryById(id);
}
// refresh timeline gets statuses newer than the newest in that timeline // refresh timeline gets statuses newer than the newest in that timeline
Result<List<EntryTreeItem>, ExecError> getTimeline(TimelineIdentifiers type) { Result<List<EntryTreeItem>, ExecError> getTimeline(TimelineIdentifiers type) {
_logger.finest('Getting timeline $type'); _logger.finest('Getting timeline $type');
@ -95,7 +103,7 @@ class TimelineManager extends ChangeNotifier {
late final int highestId; late final int highestId;
switch (refreshType) { switch (refreshType) {
case TimelineRefreshType.refresh: case TimelineRefreshType.refresh:
lowestId = timeline.highestStatusId; lowestId = timeline.highestStatusId + 1;
highestId = 0; highestId = 0;
break; break;
case TimelineRefreshType.loadOlder: case TimelineRefreshType.loadOlder: