mirror of
https://gitlab.com/mysocialportal/relatica
synced 2024-10-18 11:13:31 +00:00
Add being able to add comments to posts.
This commit is contained in:
parent
8bb8bd7cc8
commit
f224925540
8 changed files with 220 additions and 27 deletions
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.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 {
|
||||
final id = widget.entry.id;
|
||||
_logger.finest('Trying to un-reshare $id');
|
||||
|
@ -100,7 +105,8 @@ class _InteractionsBarControlState extends State<InteractionsBarControl> {
|
|||
? await unResharePost()
|
||||
: await resharePost(),
|
||||
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)),
|
||||
]),
|
||||
],
|
||||
);
|
||||
|
|
|
@ -17,6 +17,7 @@ import '../../utils/dateutils.dart';
|
|||
import '../../utils/snackbar_builder.dart';
|
||||
import '../padding.dart';
|
||||
import 'interactions_bar_control.dart';
|
||||
import 'status_header_control.dart';
|
||||
|
||||
class StatusControl extends StatefulWidget {
|
||||
final EntryTreeItem originalItem;
|
||||
|
@ -57,7 +58,7 @@ class _StatusControlState extends State<StatusControl> {
|
|||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buildHeader(context),
|
||||
StatusHeaderControl(entry: entry),
|
||||
const VerticalPadding(
|
||||
height: 5,
|
||||
),
|
||||
|
|
61
lib/controls/timeline/status_header_control.dart
Normal file
61
lib/controls/timeline/status_header_control.dart
Normal file
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -142,15 +142,18 @@ class FriendicaClient {
|
|||
));
|
||||
}
|
||||
|
||||
FutureResult<TimelineEntry, ExecError> createNewPost(
|
||||
{required String text, String spoilerText = ''}) async {
|
||||
_logger.finest(() => 'Creating post');
|
||||
FutureResult<TimelineEntry, ExecError> createNewStatus(
|
||||
{required String text,
|
||||
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 body = {
|
||||
'status': text,
|
||||
if (spoilerText.isNotEmpty) 'spoiler_text': spoilerText,
|
||||
if (inReplyToId.isNotEmpty) 'in_reply_to_id': inReplyToId,
|
||||
};
|
||||
print(body);
|
||||
final result = await _postUrl(url, body);
|
||||
if (result.isFailure) {
|
||||
return result.errorCast();
|
||||
|
|
|
@ -100,5 +100,28 @@ final appRouter = GoRouter(
|
|||
builder: (context, state) =>
|
||||
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'),
|
||||
),
|
||||
]),
|
||||
]);
|
||||
|
|
|
@ -1,34 +1,60 @@
|
|||
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:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../controls/padding.dart';
|
||||
import '../controls/timeline/status_header_control.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import '../services/timeline_manager.dart';
|
||||
import '../utils/snackbar_builder.dart';
|
||||
|
||||
class EditorScreen extends StatefulWidget {
|
||||
final String id;
|
||||
final String parentId;
|
||||
|
||||
const EditorScreen({super.key, this.id = ''});
|
||||
const EditorScreen({super.key, this.id = '', this.parentId = ''});
|
||||
|
||||
@override
|
||||
State<EditorScreen> createState() => _EditorScreenState();
|
||||
}
|
||||
|
||||
class _EditorScreenState extends State<EditorScreen> {
|
||||
static final _logger = Logger('$EditorScreen');
|
||||
final contentController = 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) {
|
||||
buildSnackbar(context, "Can't submit an empty post/comment");
|
||||
return;
|
||||
}
|
||||
final result = await manager.createNewPost(
|
||||
final result = await manager.createNewStatus(
|
||||
contentController.text,
|
||||
spoilerText: spoilerController.text,
|
||||
inReplyToId: widget.parentId,
|
||||
);
|
||||
|
||||
if (result.isFailure) {
|
||||
|
@ -40,6 +66,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
print('Build editor $isComment $parentEntry');
|
||||
final manager = context.read<TimelineManager>();
|
||||
|
||||
return Scaffold(
|
||||
|
@ -54,10 +81,12 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
if (isComment && parentEntry != null)
|
||||
buildCommentPreview(context, parentEntry!),
|
||||
TextFormField(
|
||||
controller: spoilerController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '$statusType spoiler text (optional)',
|
||||
labelText: '$statusType Spoiler Text (optional)',
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
|
@ -71,7 +100,8 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
maxLines: 10,
|
||||
controller: contentController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '$statusType content',
|
||||
labelText: '$statusType Content',
|
||||
alignLabelWithHint: true,
|
||||
border: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
|
@ -85,7 +115,7 @@ class _EditorScreenState extends State<EditorScreen> {
|
|||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async => createPost(context, manager),
|
||||
onPressed: () async => createStatus(context, manager),
|
||||
child: const Text('Submit'),
|
||||
),
|
||||
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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,8 +21,19 @@ class EntryManagerService extends ChangeNotifier {
|
|||
_parentPostIds.clear();
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> createNewPost(String text,
|
||||
{String spoilerText = ''}) async {
|
||||
Result<TimelineEntry, ExecError> getEntryById(String id) {
|
||||
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');
|
||||
final auth = getIt<AuthService>();
|
||||
final clientResult = auth.currentClient;
|
||||
|
@ -33,18 +44,30 @@ class EntryManagerService extends ChangeNotifier {
|
|||
|
||||
final client = clientResult.value;
|
||||
final result = await client
|
||||
.createNewPost(
|
||||
.createNewStatus(
|
||||
text: text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
)
|
||||
.andThenSuccessAsync((item) async {
|
||||
await processNewItems([item], client.credentials.username, null);
|
||||
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) {
|
||||
_logger.finest('${post.id} post updated after reshare');
|
||||
return _nodeToTreeItem(_postNodes[post.id]!, auth.currentId);
|
||||
return result.mapValue((status) {
|
||||
_logger.finest('${status.id} status created');
|
||||
return true;
|
||||
}).mapError(
|
||||
(error) {
|
||||
_logger.finest('Error creating post: $error');
|
||||
|
@ -126,7 +149,6 @@ class EntryManagerService extends ChangeNotifier {
|
|||
_entries[item.id] = item;
|
||||
_parentPostIds[item.id] = parentPostId;
|
||||
}
|
||||
;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -137,8 +159,12 @@ class EntryManagerService extends ChangeNotifier {
|
|||
final postNode = _postNodes.putIfAbsent(item.id, () => _Node(item.id));
|
||||
postNodesToReturn.add(postNode);
|
||||
} 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);
|
||||
_parentPostIds[item.id] = parentPostNode.id;
|
||||
if (parentPostNode.getChildById(item.id) == null) {
|
||||
final newNode = _Node(item.id);
|
||||
final injectionNode = parentPostNode.id == item.parentId
|
||||
|
@ -282,7 +308,6 @@ class EntryManagerService extends ChangeNotifier {
|
|||
}
|
||||
final entry = _entries[node.id]!;
|
||||
final isMine = entry.authorId == currentId;
|
||||
print('Author: ${entry.authorId}, Mine: $currentId => IsMine? $isMine');
|
||||
return EntryTreeItem(
|
||||
_entries[node.id]!,
|
||||
isMine: isMine,
|
||||
|
|
|
@ -7,6 +7,7 @@ import '../models/TimelineIdentifiers.dart';
|
|||
import '../models/entry_tree_item.dart';
|
||||
import '../models/exec_error.dart';
|
||||
import '../models/timeline.dart';
|
||||
import '../models/timeline_entry.dart';
|
||||
import 'entry_manager_service.dart';
|
||||
|
||||
enum TimelineRefreshType {
|
||||
|
@ -26,18 +27,25 @@ class TimelineManager extends ChangeNotifier {
|
|||
notifyListeners();
|
||||
}
|
||||
|
||||
FutureResult<EntryTreeItem, ExecError> createNewPost(String text,
|
||||
{String spoilerText = ''}) async {
|
||||
final result = await getIt<EntryManagerService>().createNewPost(
|
||||
FutureResult<bool, ExecError> createNewStatus(String text,
|
||||
{String spoilerText = '', String inReplyToId = ''}) async {
|
||||
final result = await getIt<EntryManagerService>().createNewStatus(
|
||||
text,
|
||||
spoilerText: spoilerText,
|
||||
inReplyToId: inReplyToId,
|
||||
);
|
||||
if (result.isSuccess) {
|
||||
_logger.finest('Notifying listeners of new status created');
|
||||
notifyListeners();
|
||||
}
|
||||
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
|
||||
Result<List<EntryTreeItem>, ExecError> getTimeline(TimelineIdentifiers type) {
|
||||
_logger.finest('Getting timeline $type');
|
||||
|
@ -95,7 +103,7 @@ class TimelineManager extends ChangeNotifier {
|
|||
late final int highestId;
|
||||
switch (refreshType) {
|
||||
case TimelineRefreshType.refresh:
|
||||
lowestId = timeline.highestStatusId;
|
||||
lowestId = timeline.highestStatusId + 1;
|
||||
highestId = 0;
|
||||
break;
|
||||
case TimelineRefreshType.loadOlder:
|
||||
|
|
Loading…
Reference in a new issue