diff --git a/lib/enums/report_target_type.dart b/lib/enums/report_target_type.dart new file mode 100644 index 0000000..84a4f30 --- /dev/null +++ b/lib/enums/report_target_type.dart @@ -0,0 +1,28 @@ +/// What kind of content that can be reported +enum ReportTargetType { + Post, + Comment, + Conversation, + ConversationMessage, + User, + Group +} + +extension ReportTargetExt on ReportTargetType { + String get apiId { + switch (this) { + case ReportTargetType.Post: + return "post"; + case ReportTargetType.Comment: + return "comment"; + case ReportTargetType.Conversation: + return "conversation"; + case ReportTargetType.ConversationMessage: + return "conversation_message"; + case ReportTargetType.User: + return "user"; + case ReportTargetType.Group: + return "group"; + } + } +} diff --git a/lib/helpers/server_config_helper.dart b/lib/helpers/server_config_helper.dart index d9b5492..1f04969 100644 --- a/lib/helpers/server_config_helper.dart +++ b/lib/helpers/server_config_helper.dart @@ -23,77 +23,88 @@ class ServerConfigurationHelper { final dataConservationPolicy = response["data_conservation_policy"]; final conversationsPolicy = response["conversations_policy"]; final accountInformationPolicy = response["account_info_policy"]; + final reportPolicy = response["report_policy"]; _config = ServerConfig( - minSupportedMobileVersion: - Version.parse(response["min_supported_mobile_version"]), - termsURL: response["terms_url"], - privacyPolicyURL: response["privacy_policy_url"], - contactEmail: response["contact_email"], - playStoreURL: response["play_store_url"], - androidDirectDownloadURL: response["android_direct_download_url"], - banner: banner == null - ? null - : Banner( - enabled: banner["enabled"], - expire: banner["expire"], - nature: BannerNatureExt.fromStr(banner["nature"]), - message: Map.from(banner["message"]) - .map((key, value) => MapEntry(key, value.toString())), - link: banner["link"]), - notificationsPolicy: NotificationsPolicy( - hasFirebase: pushNotificationsPolicy["has_firebase"], - hasIndependent: pushNotificationsPolicy["has_independent"], - ), - passwordPolicy: PasswordPolicy( - allowMailInPassword: passwordPolicy["allow_email_in_password"], - allowNameInPassword: passwordPolicy["allow_name_in_password"], - minPasswordLength: passwordPolicy["min_password_length"], - minNumberUpperCaseLetters: - passwordPolicy["min_number_upper_case_letters"], - minNumberLowerCaseLetters: - passwordPolicy["min_number_lower_case_letters"], - minNumberDigits: passwordPolicy["min_number_digits"], - minNumberSpecialCharacters: - passwordPolicy["min_number_special_characters"], - minCategoriesPresence: passwordPolicy["min_categories_presence"], - ), - dataConservationPolicy: ServerDataConservationPolicy( - minInactiveAccountLifetime: - dataConservationPolicy["min_inactive_account_lifetime"], - minNotificationLifetime: - dataConservationPolicy["min_notification_lifetime"], - minCommentsLifetime: dataConservationPolicy["min_comments_lifetime"], - minPostsLifetime: dataConservationPolicy["min_posts_lifetime"], - minConversationMessagesLifetime: - dataConservationPolicy["min_conversation_messages_lifetime"], - minLikesLifetime: dataConservationPolicy["min_likes_lifetime"], - ), - conversationsPolicy: ConversationsPolicy( - maxConversationNameLen: - conversationsPolicy["max_conversation_name_len"], - minMessageLen: conversationsPolicy["min_message_len"], - maxMessageLen: conversationsPolicy["max_message_len"], - allowedFilesType: - conversationsPolicy["allowed_files_type"].cast(), - filesMaxSize: conversationsPolicy["files_max_size"], - writingEventInterval: conversationsPolicy["writing_event_interval"], - writingEventLifetime: conversationsPolicy["writing_event_lifetime"], - maxMessageImageWidth: conversationsPolicy["max_message_image_width"], - maxMessageImageHeight: conversationsPolicy["max_message_image_height"], - maxThumbnailWidth: conversationsPolicy["max_thumbnail_width"], - maxThumbnailHeight: conversationsPolicy["max_thumbnail_height"], - maxLogoWidth: conversationsPolicy["max_logo_width"], - maxLogoHeight: conversationsPolicy["max_logo_height"], - ), - accountInformationPolicy: AccountInformationPolicy( - minFirstNameLength: accountInformationPolicy["min_first_name_length"], - maxFirstNameLength: accountInformationPolicy["max_first_name_length"], - minLastNameLength: accountInformationPolicy["min_last_name_length"], - maxLastNameLength: accountInformationPolicy["max_last_name_length"], - maxLocationLength: accountInformationPolicy["max_location_length"], - ), - ); + minSupportedMobileVersion: + Version.parse(response["min_supported_mobile_version"]), + termsURL: response["terms_url"], + privacyPolicyURL: response["privacy_policy_url"], + contactEmail: response["contact_email"], + playStoreURL: response["play_store_url"], + androidDirectDownloadURL: response["android_direct_download_url"], + banner: banner == null + ? null + : Banner( + enabled: banner["enabled"], + expire: banner["expire"], + nature: BannerNatureExt.fromStr(banner["nature"]), + message: Map.from(banner["message"]) + .map((key, value) => MapEntry(key, value.toString())), + link: banner["link"]), + notificationsPolicy: NotificationsPolicy( + hasFirebase: pushNotificationsPolicy["has_firebase"], + hasIndependent: pushNotificationsPolicy["has_independent"], + ), + passwordPolicy: PasswordPolicy( + allowMailInPassword: passwordPolicy["allow_email_in_password"], + allowNameInPassword: passwordPolicy["allow_name_in_password"], + minPasswordLength: passwordPolicy["min_password_length"], + minNumberUpperCaseLetters: + passwordPolicy["min_number_upper_case_letters"], + minNumberLowerCaseLetters: + passwordPolicy["min_number_lower_case_letters"], + minNumberDigits: passwordPolicy["min_number_digits"], + minNumberSpecialCharacters: + passwordPolicy["min_number_special_characters"], + minCategoriesPresence: passwordPolicy["min_categories_presence"], + ), + dataConservationPolicy: ServerDataConservationPolicy( + minInactiveAccountLifetime: + dataConservationPolicy["min_inactive_account_lifetime"], + minNotificationLifetime: + dataConservationPolicy["min_notification_lifetime"], + minCommentsLifetime: dataConservationPolicy["min_comments_lifetime"], + minPostsLifetime: dataConservationPolicy["min_posts_lifetime"], + minConversationMessagesLifetime: + dataConservationPolicy["min_conversation_messages_lifetime"], + minLikesLifetime: dataConservationPolicy["min_likes_lifetime"], + ), + conversationsPolicy: ConversationsPolicy( + maxConversationNameLen: + conversationsPolicy["max_conversation_name_len"], + minMessageLen: conversationsPolicy["min_message_len"], + maxMessageLen: conversationsPolicy["max_message_len"], + allowedFilesType: + conversationsPolicy["allowed_files_type"].cast(), + filesMaxSize: conversationsPolicy["files_max_size"], + writingEventInterval: conversationsPolicy["writing_event_interval"], + writingEventLifetime: conversationsPolicy["writing_event_lifetime"], + maxMessageImageWidth: conversationsPolicy["max_message_image_width"], + maxMessageImageHeight: + conversationsPolicy["max_message_image_height"], + maxThumbnailWidth: conversationsPolicy["max_thumbnail_width"], + maxThumbnailHeight: conversationsPolicy["max_thumbnail_height"], + maxLogoWidth: conversationsPolicy["max_logo_width"], + maxLogoHeight: conversationsPolicy["max_logo_height"], + ), + accountInformationPolicy: AccountInformationPolicy( + minFirstNameLength: accountInformationPolicy["min_first_name_length"], + maxFirstNameLength: accountInformationPolicy["max_first_name_length"], + minLastNameLength: accountInformationPolicy["min_last_name_length"], + maxLastNameLength: accountInformationPolicy["max_last_name_length"], + maxLocationLength: accountInformationPolicy["max_location_length"], + ), + reportPolicy: reportPolicy == null + ? null + : ReportPolicy( + causes: List.from(reportPolicy["causes"] + .map((cause) => ReportCause( + id: cause["id"], + label: new Map.from(cause["label"]))) + .toList()), + maxCommentLength: reportPolicy["max_comment_length"], + )); } /// Get current server configuration, throwing if it is not loaded yet @@ -109,4 +120,4 @@ class ServerConfigurationHelper { /// Shortcut for server configuration ServerConfig? get srvConfig => ServerConfigurationHelper.config; -bool get showBanner => srvConfig!.banner != null && srvConfig!.banner!.visible; \ No newline at end of file +bool get showBanner => srvConfig!.banner != null && srvConfig!.banner!.visible; diff --git a/lib/models/report_target.dart b/lib/models/report_target.dart new file mode 100644 index 0000000..ae90b19 --- /dev/null +++ b/lib/models/report_target.dart @@ -0,0 +1,8 @@ +import 'package:comunic/enums/report_target_type.dart'; + +class ReportTarget { + final ReportTargetType type; + final int targetId; + + const ReportTarget(this.type, this.targetId); +} diff --git a/lib/models/server_config.dart b/lib/models/server_config.dart index 5ca50c4..1b02ff9 100644 --- a/lib/models/server_config.dart +++ b/lib/models/server_config.dart @@ -1,4 +1,5 @@ import 'package:comunic/utils/date_utils.dart'; +import 'package:comunic/utils/intl_utils.dart'; import 'package:version/version.dart'; /// Server static configuration @@ -137,6 +138,23 @@ class Banner { bool get visible => enabled && (expire == null || expire! > time()); } +class ReportCause { + final String id; + final Map label; + + const ReportCause({required this.id, required this.label}); + + String get localeLabel => + label.containsKey(shortLang) ? label[shortLang]! : label["en"]!; +} + +class ReportPolicy { + final List causes; + final int maxCommentLength; + + const ReportPolicy({required this.causes, required this.maxCommentLength}); +} + class ServerConfig { final Version minSupportedMobileVersion; final String termsURL; @@ -150,6 +168,7 @@ class ServerConfig { final ServerDataConservationPolicy dataConservationPolicy; final ConversationsPolicy conversationsPolicy; final AccountInformationPolicy accountInformationPolicy; + final ReportPolicy? reportPolicy; const ServerConfig({ required this.minSupportedMobileVersion, @@ -164,5 +183,8 @@ class ServerConfig { required this.dataConservationPolicy, required this.conversationsPolicy, required this.accountInformationPolicy, + required this.reportPolicy, }); + + bool get isReportingEnabled => this.reportPolicy != null; } diff --git a/lib/ui/dialogs/report_dialog.dart b/lib/ui/dialogs/report_dialog.dart new file mode 100644 index 0000000..9459949 --- /dev/null +++ b/lib/ui/dialogs/report_dialog.dart @@ -0,0 +1,80 @@ +import 'package:comunic/helpers/server_config_helper.dart'; +import 'package:comunic/models/report_target.dart'; +import 'package:comunic/models/server_config.dart'; +import 'package:comunic/ui/widgets/dialogs/cancel_dialog_button.dart'; +import 'package:comunic/utils/intl_utils.dart'; +import 'package:flutter/material.dart'; + +enum _Status { ChooseCause, GiveComment, Sending } + +/// Show a dialog to report some content +Future showReportDialog({ + required BuildContext ctx, + required ReportTarget target, +}) async { + await showDialog( + context: ctx, + builder: (ctx) => _ReportDialog( + target: target, + )); +} + +class _ReportDialog extends StatefulWidget { + final ReportTarget target; + + const _ReportDialog({Key? key, required this.target}) : super(key: key); + + @override + State<_ReportDialog> createState() => _ReportDialogState(); +} + +class _ReportDialogState extends State<_ReportDialog> { + var _cause = srvConfig!.reportPolicy!.causes.length - 1; + var _status = _Status.ChooseCause; + + List get _causes => srvConfig!.reportPolicy!.causes; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(tr("Report abuse")!), + content: Container(width: 100, height: 200, child: _buildContent()), + actions: _status == _Status.Sending + ? [] + : [ + CancelDialogButton(), + MaterialButton( + onPressed: null, + child: Text( + (_status == _Status.ChooseCause ? tr("Next")! : tr("Send")!) + .toUpperCase()), + ) + ], + ); + } + + Widget _buildContent() { + if (_status == _Status.Sending) + return Center(child: CircularProgressIndicator()); + + if (_status == _Status.ChooseCause) + return Column( + children: [ + Text(tr("Please choose the reason of your report (you can scroll to access all reasons):")!), + Expanded( + child: ListView.builder( + itemBuilder: (c, i) => RadioListTile( + dense: false, + title: Text(_causes[i].localeLabel), + value: i, + groupValue: _cause, + onChanged: (_i) => setState(() => _cause = i)), + itemCount: _causes.length, + ), + ), + ], + ); + + throw Exception("todo"); + } +} diff --git a/lib/ui/screens/friends_list_screen.dart b/lib/ui/screens/friends_list_screen.dart index 96a119d..a3dcd05 100644 --- a/lib/ui/screens/friends_list_screen.dart +++ b/lib/ui/screens/friends_list_screen.dart @@ -1,10 +1,13 @@ import 'dart:math'; +import 'package:comunic/enums/report_target_type.dart'; import 'package:comunic/helpers/friends_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/lists/friends_list.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/friend.dart'; +import 'package:comunic/models/report_target.dart'; +import 'package:comunic/ui/dialogs/report_dialog.dart'; import 'package:comunic/ui/tiles/accepted_friend_tile.dart'; import 'package:comunic/ui/tiles/pending_friend_tile.dart'; import 'package:comunic/ui/widgets/safe_state.dart'; @@ -151,11 +154,13 @@ class _FriendsListScreenState extends SafeState { onOpenPrivateConversation: _openPrivateConversation, onSetFollowing: _setFollowingFriend, onRequestDelete: _deleteFriend, + onReportFriend: _reportFriend, ) : PendingFriendTile( friend: _friendsList![i], user: _usersInfo.getUser(_friendsList![i].id), onRespond: _respondRequest, + onReport: _reportFriend, ); }), ), @@ -216,6 +221,10 @@ class _FriendsListScreenState extends SafeState { _refreshList(); } + /// Report a friend + Future _reportFriend(Friend friend) async => await showReportDialog( + ctx: context, target: ReportTarget(ReportTargetType.User, friend.id)); + /// Open a private conversation for a given [friend] Future _openPrivateConversation(Friend friend) async { await openPrivateConversation(context, friend.id); diff --git a/lib/ui/tiles/accepted_friend_tile.dart b/lib/ui/tiles/accepted_friend_tile.dart index 7bd2aa1..15a368f 100644 --- a/lib/ui/tiles/accepted_friend_tile.dart +++ b/lib/ui/tiles/accepted_friend_tile.dart @@ -1,3 +1,4 @@ +import 'package:comunic/helpers/server_config_helper.dart'; import 'package:comunic/models/friend.dart'; import 'package:comunic/models/user.dart'; import 'package:comunic/ui/widgets/account_image_widget.dart'; @@ -10,11 +11,17 @@ import 'package:flutter/material.dart'; /// /// @author Pierre HUBERT -enum _FriendMenuChoices { REMOVE, TOGGLE_FOLLOWING, PRIVATE_CONVERSATION } +enum _FriendMenuChoices { + REMOVE, + TOGGLE_FOLLOWING, + PRIVATE_CONVERSATION, + REPORT +} typedef OnRequestDeleteFriend = void Function(Friend); typedef OnSetFollowing = void Function(Friend, bool); typedef OnOpenPrivateConversation = void Function(Friend); +typedef OnReportFriend = void Function(Friend); class AcceptedFriendTile extends StatelessWidget { final Friend friend; @@ -22,6 +29,7 @@ class AcceptedFriendTile extends StatelessWidget { final OnRequestDeleteFriend onRequestDelete; final OnSetFollowing onSetFollowing; final OnOpenPrivateConversation onOpenPrivateConversation; + final OnReportFriend onReportFriend; const AcceptedFriendTile({ Key? key, @@ -30,6 +38,7 @@ class AcceptedFriendTile extends StatelessWidget { required this.onRequestDelete, required this.onSetFollowing, required this.onOpenPrivateConversation, + required this.onReportFriend, }) : super(key: key); @override @@ -75,7 +84,15 @@ class AcceptedFriendTile extends StatelessWidget { child: Text(tr("Remove")!), value: _FriendMenuChoices.REMOVE, ), - ], + ]..addAll(srvConfig!.isReportingEnabled + ? [ + // Report user + PopupMenuItem( + child: Text(tr("Report abuse")!), + value: _FriendMenuChoices.REPORT, + ), + ] + : []), onSelected: _selectedMenuOption, ), ); @@ -97,6 +114,10 @@ class AcceptedFriendTile extends StatelessWidget { case _FriendMenuChoices.REMOVE: onRequestDelete(friend); break; + + case _FriendMenuChoices.REPORT: + onReportFriend(friend); + break; } } } diff --git a/lib/ui/tiles/pending_friend_tile.dart b/lib/ui/tiles/pending_friend_tile.dart index 34efa9c..8e23012 100644 --- a/lib/ui/tiles/pending_friend_tile.dart +++ b/lib/ui/tiles/pending_friend_tile.dart @@ -1,5 +1,7 @@ +import 'package:comunic/helpers/server_config_helper.dart'; import 'package:comunic/models/friend.dart'; import 'package:comunic/models/user.dart'; +import 'package:comunic/ui/tiles/accepted_friend_tile.dart'; import 'package:comunic/ui/widgets/account_image_widget.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:flutter/material.dart'; @@ -14,13 +16,15 @@ class PendingFriendTile extends StatelessWidget { final Friend friend; final User user; final RespondFriendshipRequestCallback onRespond; + final OnReportFriend onReport; - const PendingFriendTile( - {Key? key, - required this.friend, - required this.user, - required this.onRespond}) - : super(key: key); + const PendingFriendTile({ + Key? key, + required this.friend, + required this.user, + required this.onRespond, + required this.onReport, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -56,7 +60,19 @@ class PendingFriendTile extends StatelessWidget { style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red)), onPressed: () => onRespond(friend, false), - ) + ), + + // Report button + srvConfig!.isReportingEnabled + ? IconButton( + visualDensity: VisualDensity.compact, + onPressed: () => onReport(friend), + icon: Icon( + Icons.flag, + size: 15.0, + ), + ) + : Container() ], ), ),