diff --git a/assets/i18n/en-US.json b/assets/i18n/en-US.json index f9a2216..f66ed6f 100644 --- a/assets/i18n/en-US.json +++ b/assets/i18n/en-US.json @@ -48,6 +48,28 @@ "deletePublisherHint": "Are you sure to delete this publisher? This will also deleted all the post and collections under this publisher.", "somethingWentWrong": "Something went wrong...", "deletePost": "Delete Post", + "safetyReport": "Report", + "safetyReportTitle": "Safety Report", + "safetyReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.", + "safetyReportType": "Report Type", + "safetyReportReason": "Additional Details", + "safetyReportReasonHint": "Please provide more details about the issue...", + "safetyReportSubmit": "Submit Report", + "safetyReportSubmitting": "Submitting...", + "safetyReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.", + "safetyReportError": "Failed to submit report. Please try again.", + "safetyReportReasonRequired": "Please provide details about the issue", + "safetyReportTypeSpam": "Spam or Misleading", + "safetyReportTypeHarassment": "Harassment or Abuse", + "safetyReportTypeHateSpeech": "Hate Speech", + "safetyReportTypeViolence": "Violence or Threats", + "safetyReportTypeAdultContent": "Adult Content", + "safetyReportTypeIntellectualProperty": "Intellectual Property Violation", + "safetyReportTypeOther": "Other", + "safetyReportTypeInappropriate": "Inappropriate Content", + "safetyReportTypeCopyright": "Copyright Violation", + "safetyReportSuccessTitle": "Report Submitted", + "safetyReportErrorTitle": "Error", "deletePostHint": "Are you sure to delete this post?", "copyLink": "Copy Link", "postCreateAccountTitle": "Thanks for joining!", @@ -566,5 +588,27 @@ "uploadingFiles": "Uploading files...", "sharedSuccessfully": "Shared successfully!", "navigateToChat": "Navigate to Chat", - "wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?" + "wouldYouLikeToNavigateToChat": "Would you like to navigate to the chat?", + "abuseReport": "Report", + "abuseReportTitle": "Report Content", + "abuseReportDescription": "Help us keep the community safe by reporting inappropriate content or behavior.", + "abuseReportType": "Report Type", + "abuseReportReason": "Additional Details", + "abuseReportReasonHint": "Please provide more details about the issue...", + "abuseReportSubmit": "Submit Report", + "abuseReportSuccess": "Report submitted successfully. Thank you for helping keep our community safe.", + "abuseReportError": "Failed to submit report. Please try again.", + "abuseReportReasonRequired": "Please provide details about the issue", + "abuseReportSuccessTitle": "Report Submitted", + "abuseReportErrorTitle": "Error", + "abuseReportTypeSpam": "Spam or Misleading", + "abuseReportTypeHarassment": "Harassment or Abuse", + "abuseReportTypeInappropriate": "Inappropriate Content", + "abuseReportTypeViolence": "Violence or Threats", + "abuseReportTypeCopyright": "Copyright Violation", + "abuseReportTypeImpersonation": "Impersonation", + "abuseReportTypeOffensiveContent": "Offensive Content", + "abuseReportTypePrivacyViolation": "Privacy Violation", + "abuseReportTypeIllegalContent": "Illegal Content", + "abuseReportTypeOther": "Other" } diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index 3c701c5..01a102f 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'dart:math' as math; import 'package:island/models/embed.dart'; import 'package:island/models/post.dart'; +import 'package:island/pods/config.dart'; import 'package:island/pods/network.dart'; import 'package:island/pods/userinfo.dart'; import 'package:island/route.gr.dart'; @@ -20,6 +21,7 @@ import 'package:island/widgets/content/cloud_file_collection.dart'; import 'package:island/widgets/content/cloud_files.dart'; import 'package:island/widgets/content/embed/link.dart'; import 'package:island/widgets/content/markdown.dart'; +import 'package:island/widgets/safety/abuse_report_helper.dart'; import 'package:island/widgets/post/post_replies_sheet.dart'; import 'package:island/widgets/share/share_sheet.dart'; import 'package:material_symbols_icons/symbols.dart'; @@ -125,18 +127,29 @@ class PostItem extends HookConsumerWidget { context.router.push(PostComposeRoute(forwardedPost: item)); }, ), + MenuSeparator(), MenuAction( title: 'share'.tr(), image: MenuImage.icon(Symbols.share), callback: () { showShareSheetLink( context: context, - link: 'https://solsynth.dev/posts/${item.id}', + link: '${ref.read(serverUrlProvider)}/posts/${item.id}', title: 'sharePost'.tr(), toSystem: true, ); }, ), + MenuAction( + title: 'abuseReport'.tr(), + image: MenuImage.icon(Symbols.flag), + callback: () { + showAbuseReportSheet( + context, + resourceIdentifier: 'posts:${item.id}', + ); + }, + ), ], ); }, diff --git a/lib/widgets/safety/abuse_report_helper.dart b/lib/widgets/safety/abuse_report_helper.dart new file mode 100644 index 0000000..99945d6 --- /dev/null +++ b/lib/widgets/safety/abuse_report_helper.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:island/widgets/safety/abuse_report_sheet.dart'; + +/// Helper function to show the safety report sheet +/// +/// [context] - The build context +/// [resourceIdentifier] - The identifier of the resource being reported (e.g., post ID, user ID, etc.) +/// [initialReason] - Optional initial reason text to pre-fill the form +Future showAbuseReportSheet( + BuildContext context, { + required String resourceIdentifier, + String? initialReason, +}) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: true, + builder: + (context) => AbuseReportSheet( + resourceIdentifier: resourceIdentifier, + initialReason: initialReason, + ), + ); +} diff --git a/lib/widgets/safety/abuse_report_sheet.dart b/lib/widgets/safety/abuse_report_sheet.dart new file mode 100644 index 0000000..93ed69f --- /dev/null +++ b/lib/widgets/safety/abuse_report_sheet.dart @@ -0,0 +1,184 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:island/pods/network.dart'; +import 'package:island/widgets/alert.dart'; +import 'package:island/widgets/content/sheet.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +class AbuseReportSheet extends HookConsumerWidget { + final String resourceIdentifier; + final String? initialReason; + + const AbuseReportSheet({ + super.key, + required this.resourceIdentifier, + this.initialReason, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final reasonController = useTextEditingController( + text: initialReason ?? '', + ); + final selectedType = useState(0); + final isSubmitting = useState(false); + + final reportTypes = [ + {'value': 0, 'label': 'abuseReportTypeCopyright'.tr()}, + {'value': 1, 'label': 'abuseReportTypeHarassment'.tr()}, + {'value': 2, 'label': 'abuseReportTypeImpersonation'.tr()}, + {'value': 3, 'label': 'abuseReportTypeOffensiveContent'.tr()}, + {'value': 4, 'label': 'abuseReportTypeSpam'.tr()}, + {'value': 5, 'label': 'abuseReportTypePrivacyViolation'.tr()}, + {'value': 6, 'label': 'abuseReportTypeIllegalContent'.tr()}, + {'value': 7, 'label': 'abuseReportTypeOther'.tr()}, + ]; + + Future submitReport() async { + isSubmitting.value = true; + + try { + final client = ref.read(apiClientProvider); + await client.post( + '/safety/reports', + data: { + 'resource_identifier': resourceIdentifier, + 'type': selectedType.value, + 'reason': reasonController.text.trim(), + }, + ); + + if (context.mounted) { + Navigator.of(context).pop(); + showDialog( + context: context, + builder: + (contextDialog) => AlertDialog( + icon: const Icon( + Icons.check_circle, + color: Colors.green, + size: 36, + ), + title: Text('abuseReportSuccessTitle'.tr()), + content: Text('abuseReportSuccess'.tr()), + actions: [ + TextButton( + onPressed: () { + Navigator.of(contextDialog).pop(); + Navigator.of(context).pop(); + }, + child: const Text('OK'), + ), + ], + ), + ); + } + } catch (err) { + showErrorAlert(err); + } finally { + isSubmitting.value = false; + } + } + + return SheetScaffold( + titleText: 'abuseReportTitle'.tr(), + child: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Information text + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Symbols.info, + size: 20, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const Gap(8), + Expanded( + child: Text( + 'abuseReportDescription'.tr(), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ), + const Gap(24), + + // Report type selection + Text( + 'abuseReportType'.tr(), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + const Gap(12), + ...reportTypes.map((type) { + return RadioListTile( + value: type['value'] as int, + groupValue: selectedType.value, + onChanged: (value) => selectedType.value = value!, + title: Text(type['label'] as String), + contentPadding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + ); + }), + const Gap(24), + + // Reason text field + Text( + 'abuseReportReason'.tr(), + style: Theme.of( + context, + ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + ), + const Gap(8), + TextField( + controller: reasonController, + maxLines: 4, + decoration: InputDecoration( + hintText: 'abuseReportReasonHint'.tr(), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + filled: true, + fillColor: Theme.of(context).colorScheme.surface, + ), + ), + const Gap(24), + + // Submit button + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: isSubmitting.value ? null : submitReport, + child: + isSubmitting.value + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Text('abuseReportSubmit'.tr()), + ), + ), + const Gap(16), + ], + ), + ), + ); + } +}