Compare commits
	
		
			2 Commits
		
	
	
		
			8bc8556f06
			...
			01da729365
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 01da729365 | |||
| cef313b356 | 
| @@ -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" | ||||
| } | ||||
|   | ||||
| @@ -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}', | ||||
|                 ); | ||||
|               }, | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|   | ||||
							
								
								
									
										24
									
								
								lib/widgets/safety/abuse_report_helper.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								lib/widgets/safety/abuse_report_helper.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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<void> showAbuseReportSheet( | ||||
|   BuildContext context, { | ||||
|   required String resourceIdentifier, | ||||
|   String? initialReason, | ||||
| }) { | ||||
|   return showModalBottomSheet<void>( | ||||
|     context: context, | ||||
|     isScrollControlled: true, | ||||
|     useRootNavigator: true, | ||||
|     builder: | ||||
|         (context) => AbuseReportSheet( | ||||
|           resourceIdentifier: resourceIdentifier, | ||||
|           initialReason: initialReason, | ||||
|         ), | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										184
									
								
								lib/widgets/safety/abuse_report_sheet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								lib/widgets/safety/abuse_report_sheet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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<int>(0); | ||||
|     final isSubmitting = useState<bool>(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<void> 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<int>( | ||||
|                 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), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -133,14 +133,6 @@ class _ShareSheetState extends ConsumerState<ShareSheet> { | ||||
|     super.dispose(); | ||||
|   } | ||||
|  | ||||
|   void _handleClose() { | ||||
|     if (widget.onClose != null) { | ||||
|       widget.onClose!(); | ||||
|     } else { | ||||
|       Navigator.of(context).pop(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   Future<void> _shareToPost() async { | ||||
|     setState(() => _isLoading = true); | ||||
|     try { | ||||
| @@ -813,63 +805,6 @@ class _CompactShareOption extends StatelessWidget { | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _ShareOption extends StatelessWidget { | ||||
|   final IconData icon; | ||||
|   final String title; | ||||
|   final String subtitle; | ||||
|   final VoidCallback? onTap; | ||||
|  | ||||
|   const _ShareOption({ | ||||
|     required this.icon, | ||||
|     required this.title, | ||||
|     required this.subtitle, | ||||
|     this.onTap, | ||||
|   }); | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Card( | ||||
|       margin: EdgeInsets.zero, | ||||
|       child: ListTile( | ||||
|         leading: Icon( | ||||
|           icon, | ||||
|           color: | ||||
|               onTap != null | ||||
|                   ? Theme.of(context).colorScheme.primary | ||||
|                   : Theme.of( | ||||
|                     context, | ||||
|                   ).colorScheme.onSurfaceVariant.withOpacity(0.5), | ||||
|         ), | ||||
|         title: Text( | ||||
|           title, | ||||
|           style: TextStyle( | ||||
|             color: | ||||
|                 onTap != null | ||||
|                     ? null | ||||
|                     : Theme.of( | ||||
|                       context, | ||||
|                     ).colorScheme.onSurfaceVariant.withOpacity(0.5), | ||||
|           ), | ||||
|         ), | ||||
|         subtitle: Text( | ||||
|           subtitle, | ||||
|           style: TextStyle( | ||||
|             color: | ||||
|                 onTap != null | ||||
|                     ? Theme.of(context).colorScheme.onSurfaceVariant | ||||
|                     : Theme.of( | ||||
|                       context, | ||||
|                     ).colorScheme.onSurfaceVariant.withOpacity(0.5), | ||||
|           ), | ||||
|         ), | ||||
|         trailing: onTap != null ? const Icon(Symbols.chevron_right) : null, | ||||
|         onTap: onTap, | ||||
|         enabled: onTap != null, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class _ContentPreview extends StatelessWidget { | ||||
|   final ShareContent content; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user