2023-05-08 11:18:09 +00:00
|
|
|
import 'package:flutter/foundation.dart';
|
2022-11-18 21:50:15 +00:00
|
|
|
import 'package:flutter/material.dart';
|
2023-03-20 02:16:30 +00:00
|
|
|
import 'package:go_router/go_router.dart';
|
2022-11-21 21:21:45 +00:00
|
|
|
import 'package:logging/logging.dart';
|
2023-05-08 11:18:09 +00:00
|
|
|
import 'package:provider/provider.dart';
|
2023-05-08 15:14:02 +00:00
|
|
|
import 'package:relatica/utils/snackbar_builder.dart';
|
|
|
|
import 'package:result_monad/result_monad.dart';
|
2022-11-18 21:50:15 +00:00
|
|
|
|
2023-03-20 02:16:30 +00:00
|
|
|
import '../../globals.dart';
|
2023-05-08 11:18:09 +00:00
|
|
|
import '../../models/filters/timeline_entry_filter.dart';
|
2023-01-07 16:30:16 +00:00
|
|
|
import '../../models/flattened_tree_item.dart';
|
2022-11-18 21:50:15 +00:00
|
|
|
import '../../models/timeline_entry.dart';
|
2023-05-08 11:18:09 +00:00
|
|
|
import '../../services/timeline_entry_filter_service.dart';
|
2023-03-20 02:16:30 +00:00
|
|
|
import '../../services/timeline_manager.dart';
|
|
|
|
import '../../utils/active_profile_selector.dart';
|
|
|
|
import '../../utils/clipboard_utils.dart';
|
2023-05-08 11:18:09 +00:00
|
|
|
import '../../utils/filter_runner.dart';
|
2023-04-06 14:04:13 +00:00
|
|
|
import '../../utils/html_to_edit_text_helper.dart';
|
2023-04-13 14:30:09 +00:00
|
|
|
import '../../utils/responsive_sizes_calculator.dart';
|
2022-11-23 20:48:09 +00:00
|
|
|
import '../../utils/url_opening_utils.dart';
|
2023-04-27 19:19:51 +00:00
|
|
|
import '../html_text_viewer_control.dart';
|
2023-01-05 19:58:56 +00:00
|
|
|
import '../media_attachment_viewer_control.dart';
|
2022-11-18 21:50:15 +00:00
|
|
|
import '../padding.dart';
|
|
|
|
import 'interactions_bar_control.dart';
|
2023-03-19 20:27:57 +00:00
|
|
|
import 'link_preview_control.dart';
|
2022-11-23 02:59:08 +00:00
|
|
|
import 'status_header_control.dart';
|
2022-11-18 21:50:15 +00:00
|
|
|
|
2023-01-07 16:30:16 +00:00
|
|
|
class FlattenedTreeEntryControl extends StatefulWidget {
|
|
|
|
final FlattenedTreeItem originalItem;
|
2022-12-13 12:17:35 +00:00
|
|
|
final bool openRemote;
|
|
|
|
final bool showStatusOpenButton;
|
2022-11-18 21:50:15 +00:00
|
|
|
|
2023-01-07 16:30:16 +00:00
|
|
|
const FlattenedTreeEntryControl(
|
2022-12-13 12:17:35 +00:00
|
|
|
{super.key,
|
|
|
|
required this.originalItem,
|
|
|
|
required this.openRemote,
|
|
|
|
required this.showStatusOpenButton});
|
2022-11-18 23:31:28 +00:00
|
|
|
|
|
|
|
@override
|
2023-01-07 16:30:16 +00:00
|
|
|
State<FlattenedTreeEntryControl> createState() => _StatusControlState();
|
2022-11-18 23:31:28 +00:00
|
|
|
}
|
|
|
|
|
2023-01-07 16:30:16 +00:00
|
|
|
class _StatusControlState extends State<FlattenedTreeEntryControl> {
|
|
|
|
static final _logger = Logger('$FlattenedTreeEntryControl');
|
2022-11-18 21:50:15 +00:00
|
|
|
|
2022-11-22 18:36:57 +00:00
|
|
|
var showContent = true;
|
2023-05-08 11:18:09 +00:00
|
|
|
var showFilteredPost = false;
|
2022-12-13 12:25:03 +00:00
|
|
|
var showComments = false;
|
2023-05-08 11:18:09 +00:00
|
|
|
var isProcessing = false;
|
2022-12-13 12:25:03 +00:00
|
|
|
|
2023-01-07 16:30:16 +00:00
|
|
|
FlattenedTreeItem get item => widget.originalItem;
|
2022-11-21 21:21:45 +00:00
|
|
|
|
2023-01-07 16:30:16 +00:00
|
|
|
TimelineEntry get entry => item.timelineEntry;
|
2022-11-22 18:36:57 +00:00
|
|
|
|
2023-01-07 16:30:16 +00:00
|
|
|
bool get isPost => entry.parentId.isEmpty;
|
2022-11-22 16:43:16 +00:00
|
|
|
|
|
|
|
bool get hasComments => entry.engagementSummary.repliesCount > 0;
|
|
|
|
|
2023-05-08 11:18:09 +00:00
|
|
|
var filteringInfo = FilterResult.show;
|
2023-03-20 02:16:30 +00:00
|
|
|
|
2022-11-22 18:36:57 +00:00
|
|
|
@override
|
|
|
|
void initState() {
|
2023-05-08 11:18:09 +00:00
|
|
|
super.initState();
|
2022-11-22 18:36:57 +00:00
|
|
|
showContent = entry.spoilerText.isEmpty;
|
2022-12-19 14:43:06 +00:00
|
|
|
showComments = isPost ? false : true;
|
2022-11-22 18:36:57 +00:00
|
|
|
}
|
|
|
|
|
2022-11-18 21:50:15 +00:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-01-07 16:30:16 +00:00
|
|
|
_logger.finest('Building ${entry.toShortString()}');
|
2023-05-08 11:18:09 +00:00
|
|
|
final filterService = context
|
|
|
|
.watch<ActiveProfileSelector<TimelineEntryFilterService>>()
|
|
|
|
.activeEntry
|
|
|
|
.value;
|
|
|
|
|
|
|
|
filteringInfo = filterService.checkTimelineEntry(entry);
|
|
|
|
|
2023-01-07 16:30:16 +00:00
|
|
|
const otherPadding = 8.0;
|
|
|
|
final leftPadding = otherPadding + (widget.originalItem.level * 15.0);
|
|
|
|
final color = widget.originalItem.level.isOdd
|
2023-03-19 21:42:10 +00:00
|
|
|
? Theme.of(context).secondaryHeaderColor
|
|
|
|
: Theme.of(context).dialogBackgroundColor;
|
2023-05-08 11:18:09 +00:00
|
|
|
|
|
|
|
if (filteringInfo.isFiltered &&
|
|
|
|
filteringInfo.action == TimelineEntryFilterAction.hide) {
|
|
|
|
return kReleaseMode
|
|
|
|
? const SizedBox()
|
|
|
|
: Container(
|
|
|
|
height: 10,
|
|
|
|
color: Colors.red,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
late final Widget body;
|
|
|
|
if (filteringInfo.isFiltered && !showFilteredPost) {
|
|
|
|
body = buildHiddenBody(context, filteringInfo);
|
|
|
|
} else {
|
|
|
|
body = buildMainWidgetBody(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
final bodyCard = Container(
|
2023-03-19 21:42:10 +00:00
|
|
|
decoration: BoxDecoration(
|
|
|
|
color: color,
|
|
|
|
border: Border.all(width: 0.5),
|
|
|
|
borderRadius: BorderRadius.circular(5.0),
|
|
|
|
boxShadow: [
|
|
|
|
BoxShadow(
|
|
|
|
color: Theme.of(context).dividerColor,
|
|
|
|
blurRadius: 2,
|
2023-05-08 11:18:09 +00:00
|
|
|
offset: const Offset(4, 4),
|
2023-03-19 21:42:10 +00:00
|
|
|
spreadRadius: 0.1,
|
|
|
|
blurStyle: BlurStyle.normal,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
),
|
2023-05-08 11:18:09 +00:00
|
|
|
child: body,
|
|
|
|
);
|
|
|
|
return Padding(
|
|
|
|
padding: EdgeInsets.only(
|
|
|
|
left: leftPadding,
|
|
|
|
right: otherPadding,
|
|
|
|
top: otherPadding,
|
|
|
|
bottom: otherPadding,
|
|
|
|
),
|
|
|
|
child: bodyCard,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildMainWidgetBody(BuildContext context) {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(5.0),
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
children: [
|
|
|
|
Row(
|
|
|
|
children: [
|
|
|
|
Expanded(
|
|
|
|
child: StatusHeaderControl(
|
|
|
|
entry: entry,
|
2023-03-20 02:16:30 +00:00
|
|
|
),
|
2023-03-19 21:42:10 +00:00
|
|
|
),
|
2023-05-08 11:18:09 +00:00
|
|
|
if (filteringInfo.isFiltered)
|
|
|
|
IconButton(
|
|
|
|
onPressed: () => setState(() {
|
|
|
|
showFilteredPost = false;
|
|
|
|
}),
|
|
|
|
icon: const Icon(Icons.hide_source)),
|
|
|
|
buildMenuControl(context),
|
2023-03-19 21:42:10 +00:00
|
|
|
],
|
2023-05-08 11:18:09 +00:00
|
|
|
),
|
|
|
|
const VerticalPadding(
|
|
|
|
height: 5,
|
|
|
|
),
|
|
|
|
if (entry.spoilerText.isNotEmpty)
|
|
|
|
TextButton(
|
|
|
|
onPressed: () {
|
|
|
|
setState(() {
|
|
|
|
showContent = !showContent;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
child: Text(
|
|
|
|
'Content Summary: ${entry.spoilerText} (Click to ${showContent ? "Hide" : "Show"}}')),
|
|
|
|
if (showContent) ...[
|
|
|
|
buildContentField(context),
|
2023-03-19 21:42:10 +00:00
|
|
|
const VerticalPadding(
|
|
|
|
height: 5,
|
|
|
|
),
|
2023-05-08 11:18:09 +00:00
|
|
|
if (entry.linkPreviewData != null)
|
|
|
|
LinkPreviewControl(preview: entry.linkPreviewData!),
|
|
|
|
buildMediaBar(context),
|
2022-12-13 04:04:32 +00:00
|
|
|
],
|
2023-05-08 11:18:09 +00:00
|
|
|
const VerticalPadding(
|
|
|
|
height: 5,
|
|
|
|
),
|
|
|
|
InteractionsBarControl(
|
|
|
|
entry: entry,
|
|
|
|
isMine: item.isMine,
|
|
|
|
showOpenControl: widget.showStatusOpenButton,
|
|
|
|
),
|
|
|
|
const VerticalPadding(
|
|
|
|
height: 5,
|
|
|
|
),
|
|
|
|
],
|
2022-11-18 21:50:15 +00:00
|
|
|
),
|
|
|
|
);
|
2023-05-08 11:18:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildHiddenBody(BuildContext context, FilterResult result) {
|
2023-01-07 16:30:16 +00:00
|
|
|
return Padding(
|
2023-05-08 11:18:09 +00:00
|
|
|
padding: const EdgeInsets.all(5.0),
|
|
|
|
child: Column(
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
children: [
|
|
|
|
if (result.isFiltered &&
|
|
|
|
result.action == TimelineEntryFilterAction.warn)
|
|
|
|
TextButton(
|
|
|
|
onPressed: () {
|
|
|
|
setState(() {
|
|
|
|
showFilteredPost = true;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
child: Text(
|
|
|
|
'${result.trippingFilterName} filtered post. Click to show'),
|
|
|
|
),
|
|
|
|
],
|
2023-01-07 16:30:16 +00:00
|
|
|
),
|
|
|
|
);
|
2022-11-18 21:50:15 +00:00
|
|
|
}
|
|
|
|
|
2023-05-08 11:18:09 +00:00
|
|
|
Widget buildContentField(BuildContext context) {
|
2023-04-27 19:19:51 +00:00
|
|
|
return HtmlTextViewerControl(
|
|
|
|
content: entry.body,
|
|
|
|
onTapUrl: (url) async =>
|
|
|
|
await openUrlStringInSystembrowser(context, url, 'link'),
|
2022-11-18 21:50:15 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
Widget buildMediaBar(BuildContext context) {
|
|
|
|
final items = entry.mediaAttachments;
|
|
|
|
if (items.isEmpty) {
|
|
|
|
return const SizedBox();
|
|
|
|
}
|
|
|
|
return SizedBox(
|
2023-04-13 14:30:09 +00:00
|
|
|
height: ResponsiveSizesCalculator(context).maxThumbnailHeight,
|
2022-11-18 21:50:15 +00:00
|
|
|
child: ListView.separated(
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
itemBuilder: (context, index) {
|
2023-01-29 22:21:47 +00:00
|
|
|
return MediaAttachmentViewerControl(
|
|
|
|
attachments: items,
|
|
|
|
index: index,
|
2023-04-13 14:30:09 +00:00
|
|
|
width: items.length > 1
|
|
|
|
? ResponsiveSizesCalculator(context).maxThumbnailWidth
|
|
|
|
: ResponsiveSizesCalculator(context).viewPortalWidth,
|
2023-04-19 15:45:45 +00:00
|
|
|
height: ResponsiveSizesCalculator(context).maxThumbnailHeight,
|
2023-01-29 22:21:47 +00:00
|
|
|
);
|
2022-11-18 21:50:15 +00:00
|
|
|
},
|
|
|
|
separatorBuilder: (context, index) {
|
|
|
|
return HorizontalPadding();
|
|
|
|
},
|
|
|
|
itemCount: items.length));
|
|
|
|
}
|
2023-03-20 02:16:30 +00:00
|
|
|
|
|
|
|
Widget buildMenuControl(BuildContext context) {
|
|
|
|
const editStatus = 'Edit';
|
|
|
|
const deleteStatus = 'Delete';
|
|
|
|
const goToPost = 'Open Post';
|
|
|
|
const copyText = 'Copy Post Text';
|
|
|
|
const copyUrl = 'Copy URL';
|
|
|
|
const openExternal = 'Open In Browser';
|
|
|
|
final options = [
|
|
|
|
if (widget.showStatusOpenButton && !widget.openRemote) goToPost,
|
|
|
|
if (item.isMine && !item.timelineEntry.youReshared) editStatus,
|
|
|
|
if (item.isMine) deleteStatus,
|
|
|
|
copyText,
|
|
|
|
openExternal,
|
|
|
|
copyUrl,
|
|
|
|
];
|
|
|
|
|
|
|
|
return PopupMenuButton<String>(onSelected: (menuOption) async {
|
|
|
|
if (!mounted) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (menuOption) {
|
|
|
|
case goToPost:
|
|
|
|
context.push(
|
|
|
|
'/post/view/${item.timelineEntry.id}/${item.timelineEntry.id}');
|
|
|
|
break;
|
|
|
|
case editStatus:
|
|
|
|
if (item.timelineEntry.parentId.isEmpty) {
|
|
|
|
context.push('/post/edit/${item.timelineEntry.id}');
|
|
|
|
} else {
|
|
|
|
context.push('/comment/edit/${item.timelineEntry.id}');
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case deleteStatus:
|
|
|
|
deleteEntry();
|
|
|
|
break;
|
|
|
|
case openExternal:
|
|
|
|
await openUrlStringInSystembrowser(
|
|
|
|
context,
|
|
|
|
item.timelineEntry.externalLink,
|
|
|
|
'Post',
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case copyUrl:
|
|
|
|
await copyToClipboard(
|
|
|
|
context: context,
|
|
|
|
text: item.timelineEntry.externalLink,
|
|
|
|
message: 'Post link copied to clipboard',
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
case copyText:
|
|
|
|
await copyToClipboard(
|
|
|
|
context: context,
|
2023-04-06 14:04:13 +00:00
|
|
|
text: htmlToSimpleText(item.timelineEntry.body),
|
2023-03-20 02:16:30 +00:00
|
|
|
message: 'Post link copied to clipboard',
|
|
|
|
);
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
//do nothing
|
|
|
|
}
|
|
|
|
}, itemBuilder: (context) {
|
|
|
|
return options
|
|
|
|
.map((o) => PopupMenuItem(value: o, child: Text(o)))
|
|
|
|
.toList();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<void> deleteEntry() async {
|
|
|
|
setState(() {
|
|
|
|
isProcessing = true;
|
|
|
|
});
|
|
|
|
final confirm =
|
|
|
|
await showYesNoDialog(context, 'Delete ${isPost ? "Post" : "Comment"}');
|
|
|
|
if (confirm == true) {
|
|
|
|
await getIt<ActiveProfileSelector<TimelineManager>>()
|
|
|
|
.activeEntry
|
2023-05-08 15:14:02 +00:00
|
|
|
.transformAsync(
|
|
|
|
(tm) async => await tm.deleteEntryById(item.timelineEntry.id))
|
|
|
|
.match(onSuccess: (_) {
|
|
|
|
isProcessing = false;
|
|
|
|
if (!isPost && context.canPop()) {
|
|
|
|
context.pop();
|
|
|
|
}
|
|
|
|
}, onError: (e) {
|
|
|
|
isProcessing = false;
|
|
|
|
buildSnackbar(
|
|
|
|
context,
|
|
|
|
'Error deleting ${isPost ? "Post" : "Comment"}: $e',
|
|
|
|
);
|
|
|
|
});
|
2023-03-20 02:16:30 +00:00
|
|
|
}
|
2023-05-08 15:14:02 +00:00
|
|
|
setState(() {});
|
2023-03-20 02:16:30 +00:00
|
|
|
}
|
2022-11-18 21:50:15 +00:00
|
|
|
}
|