1
0
mirror of https://gitlab.com/comunic/comunicmobile synced 2025-07-01 14:13:29 +00:00

14 Commits

23 changed files with 606 additions and 131 deletions

View File

@ -340,6 +340,7 @@
"Failed to remove conversation logo!": "Erreur lors de la suppression du logo de la conversation !",
"Failed to remove member!": "Echec de la suppression d'un membre !",
"Failed to send a file!": "Erreur lors de l'envoi d'un fichier !",
"Failed to send report!": "Erreur lors de l'envoi du signalement !",
"Failed to start recording!": "Erreur lors du lancement de l'enregistrement !",
"Failed to toggle admin status of user!": "Echec du changement du status administrateur d'un membre !",
"Failed to update conversation settings!": "Echec de la mise à jour des paramètres de la conversation !",
@ -500,6 +501,7 @@
"Please check back soon!": "Revenez plus tard finir la configuration de l'application !",
"Please choose new account image visibility level:": "Veuillez choisir un nouveau niveau de visibilité pour votre image de compte :",
"Please choose now the Forez group you want to join...": "Veuillez maintenant choisir le groupe #Forez que vous souhaitez rejoindre...",
"Please choose the reason of your report:": "Veuillez indiquer la raison de votre signalement",
"Please click on the day you will be in the plain, so that everyone gets informed ! ;)": "Veuillez cliquer sur les jours où vous serez présent dans la plaine du Forez, que tout le monde soit au courant ;)",
"Please enter message content: ": "Veuillez entrer le contenu du message :",
"Please enter new message content:": "Veuillez entrer le contenu du nouveau message :",
@ -534,6 +536,7 @@
"Question 1": "Question 1",
"Question 2": "Question 2",
"Ready": "Prêt",
"Reason of report": "Raison du signalement",
"Receive notifications for the conversations you follow.": "Recevoir des notifications push pour les conversations que vous suivez",
"Record audio": "Faire un enregistrement audio",
"Recording...": "Enregistrement...",
@ -543,6 +546,8 @@
"Remove": "Supprimer",
"Remove selected image": "Supprimer l'image sélectionnée",
"Replace image": "Remplacer l'image",
"Report abuse": "Signaler un abus",
"Report successfully saved. Thank you for your contribution!": "Le report a bien été pris en compte. Merci pour votre contribution !",
"Request membership": "Demander de rejoindre le groupe",
"Requested": "En attente",
"Respond to survey": "Répondre au sondage",
@ -642,8 +647,11 @@
"You can search the people you know and ask them to become your friends!": "Vous pouvez rechercher vos connaissances et les demander en amis !",
"You can search the people you know and ask them to become your friends!\\n\\nThis will help you to reach them to exchange information!": "Vous pouvez rechercher vos connaissances et les demander en amis, pour entrer facilement en contact avec elles !",
"You can use this virtual directory.": "Vous pouvez utiliser ce répertoire virtuel.",
"You do not have any conversation yet!": "Vous n'avez pas encore de conversation !",
"You do not have any friend yet!": "Vous n'avez pas encore d'amis sur Comunic !",
"You do not have any notification now.": "Vous n'avez pas de notification pour l'intant.",
"You do not have any unread conversation yet...": "Vous n'avez aucune conversation non lue pour le moment...",
"You have already sent a report for this resource!": "Vous avez déjà soumis un signalement pour cette ressource !",
"You must accept the Terms Of Service to continue.": "Vous devez accepter les Conditions d'utilisation pour continuer.",
"You security questions have been successfully updated!": "Vos questions de sécurité ont été mises avec succès !",
"You will need to restart the application to apply changes": "Vous aurez besoin de redémarrer l'application pour appliquer les changements",

View File

@ -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";
}
}
}

View File

@ -0,0 +1,38 @@
import 'package:comunic/enums/report_target_type.dart';
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/report_target.dart';
import 'package:comunic/models/server_config.dart';
/// Reports Helper
///
/// @author Pierre Hubert
enum ReportResult {
Success,
ErrorAlreadyReported,
Error,
}
class ReportHelper {
/// Send a new report to the server
static Future<ReportResult> sendReport({
required ReportCause cause,
required ReportTarget target,
required String comment,
}) async {
final response = await APIRequest.withLogin("reports/create", args: {
"cause": cause.id,
"target_type": target.type.apiId,
"target_id": target.id.toString(),
"comment": comment
}).exec();
if (response.isOK) return ReportResult.Success;
print("Failed to send report: ${response.content}");
if (response.code == 409) return ReportResult.ErrorAlreadyReported;
return ReportResult.Error;
}
}

View File

@ -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<String, dynamic>.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<String>(),
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<String, dynamic>.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<String>(),
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<String, String>.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;
bool get showBanner => srvConfig!.banner != null && srvConfig!.banner!.visible;

View File

@ -6,6 +6,7 @@ import 'package:comunic/lists/comments_list.dart';
import 'package:comunic/models/displayed_content.dart';
import 'package:comunic/models/like_element.dart';
import 'package:comunic/models/survey.dart';
import 'package:comunic/utils/account_utils.dart' as account;
/// Single post information
///
@ -76,6 +77,8 @@ class Post implements LikeElement {
access == UserAccessLevels.FULL ||
access == UserAccessLevels.INTERMEDIATE;
bool get isOwner => userID == account.userID();
@override
LikesType get likeType => LikesType.POST;
}

View File

@ -0,0 +1,8 @@
import 'package:comunic/enums/report_target_type.dart';
class ReportTarget {
final ReportTargetType type;
final int id;
const ReportTarget(this.type, this.id);
}

View File

@ -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<String, String> label;
const ReportCause({required this.id, required this.label});
String get localeLabel =>
label.containsKey(shortLang) ? label[shortLang]! : label["en"]!;
}
class ReportPolicy {
final List<ReportCause> 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;
}

View File

@ -0,0 +1,140 @@
import 'package:comunic/helpers/report_helper.dart';
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/dialogs/alert_dialog.dart';
import 'package:comunic/ui/routes/main_route/main_route.dart';
import 'package:comunic/ui/widgets/comunic_back_button_widget.dart';
import 'package:comunic/ui/widgets/safe_state.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<void> showReportDialog({
required BuildContext ctx,
required ReportTarget target,
}) async {
MainController.of(ctx)!.push(
_ReportDialog(target: target),
canShowAsDialog: true,
hideNavBar: true,
);
}
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 SafeState<_ReportDialog> {
var _cause = srvConfig!.reportPolicy!.causes.length - 1;
var _status = _Status.ChooseCause;
final _commentController = TextEditingController();
List<ReportCause> get _causes => srvConfig!.reportPolicy!.causes;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: ComunicBackButton(),
title: Text(tr("Report abuse")!),
actions: _status == _Status.Sending
? []
: [
IconButton(
onPressed: _goNextStep,
icon: Icon(Icons.check),
),
]),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: _buildContent(),
),
);
}
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:")!),
SizedBox(height: 15),
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,
),
),
],
);
if (_status == _Status.GiveComment) {
return Column(
children: [
Text("You can optionally describe the reason of your report:"),
SizedBox(height: 15),
TextField(
controller: _commentController,
decoration: InputDecoration(hintText: tr("Reason of report")),
minLines: 5,
maxLines: 5,
maxLength: srvConfig!.reportPolicy!.maxCommentLength,
)
],
);
}
throw Exception("Unknown status!");
}
void _goNextStep() async {
if (_status == _Status.ChooseCause) {
setState(() => _status = _Status.GiveComment);
return;
}
// Send report
try {
setState(() => _status = _Status.Sending);
final result = await ReportHelper.sendReport(
cause: _causes[_cause],
target: widget.target,
comment: _commentController.value.text,
);
// In case of success
if (result == ReportResult.Success) {
await alert(context,
tr("Report successfully saved. Thank you for your contribution!"));
MainController.of(context)!.popPage();
} else if (result == ReportResult.ErrorAlreadyReported) {
await alert(
context, tr("You have already sent a report for this resource!"));
MainController.of(context)!.popPage();
} else {
await alert(context, tr("Failed to send report!"));
}
} catch (e, s) {
print("$e $s");
alert(context, tr("Failed to send report!"));
} finally {
setState(() => _status = _Status.GiveComment);
}
}
}

View File

@ -1,4 +1,8 @@
import 'package:comunic/enums/report_target_type.dart';
import 'package:comunic/models/advanced_group_info.dart';
import 'package:comunic/models/group.dart';
import 'package:comunic/models/report_target.dart';
import 'package:comunic/ui/dialogs/report_dialog.dart';
import 'package:comunic/ui/routes/main_route/main_route.dart';
import 'package:comunic/ui/screens/group_sections/about_group_section.dart';
import 'package:comunic/ui/screens/group_sections/forez_presence_section.dart';
@ -65,7 +69,10 @@ class _AuthorizedGroupPageScreenState
// About the group
_GroupPageTab(
widget: (c) => AboutGroupSection(group: _group),
widget: (c) => AboutGroupSection(
group: _group,
onReportGroup: _reportGroup,
),
label: tr("About")!,
),
@ -197,6 +204,10 @@ class _AuthorizedGroupPageScreenState
),
);
}
/// Report group
void _reportGroup(Group g) => showReportDialog(
ctx: context, target: ReportTarget(ReportTargetType.Group, g.id));
}
class _GroupPageTab {

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:comunic/enums/report_target_type.dart';
import 'package:comunic/helpers/conversations_helper.dart';
import 'package:comunic/helpers/events_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart';
@ -11,7 +12,9 @@ import 'package:comunic/models/config.dart';
import 'package:comunic/models/conversation.dart';
import 'package:comunic/models/conversation_message.dart';
import 'package:comunic/models/new_conversation_message.dart';
import 'package:comunic/models/report_target.dart';
import 'package:comunic/ui/dialogs/pick_file_dialog.dart';
import 'package:comunic/ui/dialogs/report_dialog.dart';
import 'package:comunic/ui/routes/main_route/main_route.dart';
import 'package:comunic/ui/tiles/conversation_message_tile.dart';
import 'package:comunic/ui/tiles/server_conversation_message_tile.dart';
@ -504,6 +507,7 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
onRequestMessageStats: _requestMessageStats,
onRequestMessageUpdate: _updateMessage,
onRequestMessageDelete: _deleteMessage,
onReportMessage: _reportMessage,
);
Widget _buildDateWidget(DateTime dt) => Center(
@ -753,4 +757,9 @@ class _ConversationScreenState extends SafeState<ConversationScreen> {
if (!await _conversationsHelper.deleteMessage(message.id))
showSimpleSnack(context, tr("Could not delete conversation message!")!);
}
/// Report message
void _reportMessage(ConversationMessage msg) => showReportDialog(
ctx: context,
target: ReportTarget(ReportTargetType.ConversationMessage, msg.id!));
}

View File

@ -1,6 +1,7 @@
import 'dart:math';
import 'package:comunic/enums/load_error_level.dart';
import 'package:comunic/enums/report_target_type.dart';
import 'package:comunic/helpers/conversations_helper.dart';
import 'package:comunic/helpers/events_helper.dart';
import 'package:comunic/helpers/groups_helper.dart';
@ -9,6 +10,8 @@ import 'package:comunic/lists/conversations_list.dart';
import 'package:comunic/lists/groups_list.dart';
import 'package:comunic/lists/users_list.dart';
import 'package:comunic/models/conversation.dart';
import 'package:comunic/models/report_target.dart';
import 'package:comunic/ui/dialogs/report_dialog.dart';
import 'package:comunic/ui/routes/main_route/main_route.dart';
import 'package:comunic/ui/screens/create_conversation_screen.dart';
import 'package:comunic/ui/tiles/conversation_tile.dart';
@ -160,6 +163,11 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
_loadConversationsList(false);
}
/// Handle conversation report request
void _reportConversation(Conversation conversation) => showReportDialog(
ctx: context,
target: ReportTarget(ReportTargetType.Conversation, conversation.id!));
@override
Widget build(BuildContext context) {
if (_error == LoadErrorLevel.MAJOR) return _buildErrorCard();
@ -201,6 +209,7 @@ class _ConversationScreenState extends SafeState<ConversationsListScreen> {
},
onRequestUpdate: _updateConversation,
onRequestLeave: _requestLeaveConversation,
onReport: _reportConversation,
);
},
itemCount: max(_list!.length, 1),

View File

@ -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<FriendsListScreen> {
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<FriendsListScreen> {
_refreshList();
}
/// Report a friend
Future<void> _reportFriend(Friend friend) async => await showReportDialog(
ctx: context, target: ReportTarget(ReportTargetType.User, friend.id));
/// Open a private conversation for a given [friend]
Future<void> _openPrivateConversation(Friend friend) async {
await openPrivateConversation(context, friend.id);

View File

@ -1,3 +1,4 @@
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/advanced_group_info.dart';
import 'package:comunic/models/group.dart';
import 'package:comunic/utils/date_utils.dart';
@ -11,10 +12,12 @@ import 'package:url_launcher/url_launcher.dart';
class AboutGroupSection extends StatelessWidget {
final AdvancedGroupInfo group;
final Function(Group) onReportGroup;
const AboutGroupSection({
Key? key,
required this.group,
required this.onReportGroup,
}) : super(key: key);
@override
@ -103,6 +106,15 @@ class AboutGroupSection extends StatelessWidget {
subtitle: Text(tr("Forez special features enabled")!),
)
: Container(),
// Report group
srvConfig!.isReportingEnabled
? ListTile(
textColor: Colors.red,
leading: Icon(Icons.flag, color: Colors.red),
title: Text(tr("Report abuse")!),
onTap: () => onReportGroup(group),
)
: Container(),
],
);
}

View File

@ -1,6 +1,10 @@
import 'package:comunic/enums/report_target_type.dart';
import 'package:comunic/helpers/groups_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/lists/groups_list.dart';
import 'package:comunic/models/group.dart';
import 'package:comunic/models/report_target.dart';
import 'package:comunic/ui/dialogs/report_dialog.dart';
import 'package:comunic/ui/routes/main_route/main_route.dart';
import 'package:comunic/ui/widgets/group_icon_widget.dart';
import 'package:comunic/ui/widgets/group_membership_widget.dart';
@ -70,9 +74,26 @@ class _GroupsListScreenState extends SafeState<GroupsListScreen> {
group: g,
onUpdated: () => _refreshIndicatorKey.currentState!.show(),
),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteGroup(g)),
trailing: IntrinsicWidth(
child: Row(
children: [
// Remove membership
IconButton(
icon: Icon(Icons.delete),
onPressed: () => _deleteGroup(g)),
// Report button
srvConfig!.isReportingEnabled && !g.isAtLeastMember
? IconButton(
onPressed: () => _reportGroup(g),
icon: Icon(
Icons.flag,
),
)
: Container()
],
),
),
onTap: () => MainController.of(context)!.openGroup(g.id),
))
.toList(),
@ -122,6 +143,10 @@ class _GroupsListScreenState extends SafeState<GroupsListScreen> {
_refreshIndicatorKey.currentState!.show();
}
/// Report a group
void _reportGroup(Group g) => showReportDialog(
ctx: context, target: ReportTarget(ReportTargetType.Group, g.id));
/// Add a group
void _createGroup() async {
try {

View File

@ -20,8 +20,7 @@ enum _PageStatus { LOADING, ERROR, READY }
class UserPageScreen extends StatefulWidget {
final int userID;
const UserPageScreen({Key? key, required this.userID})
: super(key: key);
const UserPageScreen({Key? key, required this.userID}) : super(key: key);
@override
_UserPageScreenState createState() => _UserPageScreenState();
@ -33,7 +32,7 @@ class _UserPageScreenState extends SafeState<UserPageScreen> {
// Objects members
_PageStatus _status = _PageStatus.LOADING;
AdvancedUserInfo? _userInfo;
AdvancedUserInfo? _userInfo;
FriendStatus? _frienshipStatus;
final _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
@ -42,7 +41,7 @@ class _UserPageScreenState extends SafeState<UserPageScreen> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
if(_userInfo?.id != widget.userID)
if (_userInfo?.id == widget.userID) return;
_getUserInfo();
}

View File

@ -1,8 +1,12 @@
import 'package:comunic/enums/report_target_type.dart';
import 'package:comunic/enums/user_page_visibility.dart';
import 'package:comunic/helpers/friends_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/advanced_user_info.dart';
import 'package:comunic/models/displayed_content.dart';
import 'package:comunic/models/friend_status.dart';
import 'package:comunic/models/report_target.dart';
import 'package:comunic/ui/dialogs/report_dialog.dart';
import 'package:comunic/ui/widgets/FrienshipStatusWidget.dart';
import 'package:comunic/ui/widgets/async_screen_widget.dart';
import 'package:comunic/ui/widgets/text_widget.dart';
@ -21,7 +25,7 @@ class AboutUserSection extends StatefulWidget {
const AboutUserSection({
Key? key,
required this.user,
}) : super(key: key);
}) : super(key: key);
@override
_AboutUserSectionState createState() => _AboutUserSectionState();
@ -129,6 +133,21 @@ class _AboutUserSectionState extends State<AboutUserSection> {
? tr("Public page")!
: tr("Private page")!)),
),
// Report user
!widget.user.isCurrentUser && srvConfig!.isReportingEnabled
? ListTile(
textColor: Colors.red,
leading: Icon(Icons.flag, color: Colors.red),
title: Text(tr("Report abuse")!),
onTap: _reportAbuse,
)
: Container(),
],
);
/// Report user
void _reportAbuse() => showReportDialog(
ctx: context,
target: ReportTarget(ReportTargetType.User, widget.user.id));
}

View File

@ -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;
}
}
}

View File

@ -1,3 +1,4 @@
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/comment.dart';
import 'package:comunic/models/user.dart';
import 'package:comunic/ui/widgets/account_image_widget.dart';
@ -13,13 +14,14 @@ import 'package:flutter/material.dart';
///
/// @author Pierre HUBERT
enum _CommentAction { DELETE, UPDATE }
enum _CommentAction { DELETE, UPDATE, REPORT }
class CommentTile extends StatelessWidget {
final Comment comment;
final User user;
final void Function(Comment) onUpdateComment;
final void Function(Comment) onDeleteComment;
final void Function(Comment) onReportComment;
const CommentTile({
Key? key,
@ -27,6 +29,7 @@ class CommentTile extends StatelessWidget {
required this.user,
required this.onUpdateComment,
required this.onDeleteComment,
required this.onReportComment,
}) : super(key: key);
@override
@ -37,34 +40,13 @@ class CommentTile extends StatelessWidget {
user.displayName,
),
subtitle: _buildCommentContent(),
trailing: Text(
diffTimeFromNowToStr(comment.timeSent)!,
style: TextStyle(fontSize: 10.0),
),
trailing: _buildTrailing(),
);
}
Widget _buildAccountImageWidget() {
return PopupMenuButton<_CommentAction>(
child: AccountImageWidget(
user: user,
),
onSelected: _selectedMenuOption,
itemBuilder: (c) => [
// Update comment content
PopupMenuItem(
enabled: comment.isOwner,
child: Text(tr("Update")!),
value: _CommentAction.UPDATE,
),
// Delete comment
PopupMenuItem(
enabled: comment.isOwner,
child: Text(tr("Delete")!),
value: _CommentAction.DELETE,
),
],
return AccountImageWidget(
user: user,
);
}
@ -101,6 +83,49 @@ class CommentTile extends StatelessWidget {
);
}
Widget _buildTrailing() {
return IntrinsicWidth(
child: Row(
children: [
Text(
diffTimeFromNowToStr(comment.timeSent)!,
style: TextStyle(fontSize: 10.0),
),
SizedBox(width: 5),
PopupMenuButton<_CommentAction>(
padding: EdgeInsets.all(0),
child: InkWell(
child: Icon(Icons.adaptive.more, size: 20),
),
onSelected: _selectedMenuOption,
itemBuilder: (c) => [
// Update comment content
PopupMenuItem(
enabled: comment.isOwner,
child: Text(tr("Update")!),
value: _CommentAction.UPDATE,
),
// Delete comment
PopupMenuItem(
enabled: comment.isOwner,
child: Text(tr("Delete")!),
value: _CommentAction.DELETE,
),
]..addAll(srvConfig!.isReportingEnabled && !comment.isOwner
? [
PopupMenuItem(
child: Text(tr("Report abuse")!),
value: _CommentAction.REPORT,
)
]
: []),
)
],
),
);
}
/// A menu option has been selected
void _selectedMenuOption(_CommentAction value) {
switch (value) {
@ -113,6 +138,11 @@ class CommentTile extends StatelessWidget {
case _CommentAction.DELETE:
onDeleteComment(comment);
break;
// Report comment
case _CommentAction.REPORT:
onReportComment(comment);
break;
}
}
}

View File

@ -1,3 +1,4 @@
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/models/conversation_message.dart';
import 'package:comunic/models/user.dart';
import 'package:comunic/ui/widgets/conversation_file_tile.dart';
@ -17,6 +18,7 @@ enum _MenuChoices {
DELETE,
REQUEST_UPDATE_CONTENT,
GET_STATS,
REPORT
}
typedef OnRequestMessageStats = void Function(ConversationMessage);
@ -29,6 +31,7 @@ class ConversationMessageTile extends StatelessWidget {
final OnRequestMessageStats onRequestMessageStats;
final OnRequestMessageUpdate onRequestMessageUpdate;
final OnRequestMessageDelete onRequestMessageDelete;
final Function(ConversationMessage) onReportMessage;
const ConversationMessageTile({
Key? key,
@ -37,6 +40,7 @@ class ConversationMessageTile extends StatelessWidget {
required this.onRequestMessageStats,
required this.onRequestMessageUpdate,
required this.onRequestMessageDelete,
required this.onReportMessage,
}) : super(key: key);
@override
@ -90,6 +94,14 @@ class ConversationMessageTile extends StatelessWidget {
value: _MenuChoices.DELETE,
child: Text(tr("Delete")!),
),
// Report the message
PopupMenuItem(
enabled:
srvConfig!.isReportingEnabled && !message.isOwner,
value: _MenuChoices.REPORT,
child: Text(tr("Report abuse")!),
),
]..removeWhere((element) => !element.enabled),
),
)
@ -133,6 +145,10 @@ class ConversationMessageTile extends StatelessWidget {
case _MenuChoices.DELETE:
onRequestMessageDelete(message);
break;
case _MenuChoices.REPORT:
onReportMessage(message);
break;
}
}
}

View File

@ -1,4 +1,5 @@
import 'package:comunic/helpers/conversations_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/lists/groups_list.dart';
import 'package:comunic/lists/users_list.dart';
import 'package:comunic/models/config.dart';
@ -18,7 +19,7 @@ typedef OpenConversationCallback = void Function(Conversation);
typedef RequestLeaveConversationCallback = void Function(Conversation);
typedef RequestUpdateConversationCallback = void Function(Conversation);
enum _PopupMenuChoices { UPDATE, LEAVE }
enum _PopupMenuChoices { UPDATE, LEAVE, REPORT }
class ConversationTile extends StatelessWidget {
final Conversation conversation;
@ -27,6 +28,7 @@ class ConversationTile extends StatelessWidget {
final OpenConversationCallback onOpen;
final RequestUpdateConversationCallback onRequestUpdate;
final RequestLeaveConversationCallback onRequestLeave;
final Function(Conversation) onReport;
const ConversationTile({
Key? key,
@ -36,7 +38,8 @@ class ConversationTile extends StatelessWidget {
required this.onOpen,
required this.onRequestUpdate,
required this.onRequestLeave,
}) : super(key: key);
required this.onReport,
}) : super(key: key);
_buildSubInformation(IconData icon, String content) {
return Row(
@ -116,18 +119,26 @@ class ConversationTile extends StatelessWidget {
onLongPressOpenMenu: (position) {
showMenu<_PopupMenuChoices>(
context: context,
position: position,
items: [
PopupMenuItem(
child: Text(tr("Update")!),
value: _PopupMenuChoices.UPDATE,
),
PopupMenuItem(
child: Text(tr("Leave")!),
value: _PopupMenuChoices.LEAVE,
)
]).then(_conversationMenuCallback);
context: context,
position: position,
items: [
PopupMenuItem(
child: Text(tr("Update")!),
value: _PopupMenuChoices.UPDATE,
),
PopupMenuItem(
child: Text(tr("Leave")!),
value: _PopupMenuChoices.LEAVE,
)
]..addAll(srvConfig!.isReportingEnabled
? [
PopupMenuItem(
child: Text(tr("Report abuse")!),
value: _PopupMenuChoices.REPORT,
)
]
: []))
.then(_conversationMenuCallback);
},
);
@ -158,7 +169,11 @@ class ConversationTile extends StatelessWidget {
onRequestLeave(conversation);
break;
default:
case _PopupMenuChoices.REPORT:
onReport(conversation);
break;
case null:
break;
}
}

View File

@ -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()
],
),
),

View File

@ -2,15 +2,19 @@ import 'dart:math';
import 'package:comunic/enums/post_kind.dart';
import 'package:comunic/enums/post_visibility_level.dart';
import 'package:comunic/enums/report_target_type.dart';
import 'package:comunic/helpers/comments_helper.dart';
import 'package:comunic/helpers/posts_helper.dart';
import 'package:comunic/helpers/server_config_helper.dart';
import 'package:comunic/lists/groups_list.dart';
import 'package:comunic/lists/users_list.dart';
import 'package:comunic/models/comment.dart';
import 'package:comunic/models/new_comment.dart';
import 'package:comunic/models/post.dart';
import 'package:comunic/models/report_target.dart';
import 'package:comunic/models/user.dart';
import 'package:comunic/ui/dialogs/post_visibility_picker_dialog.dart';
import 'package:comunic/ui/dialogs/report_dialog.dart';
import 'package:comunic/ui/tiles/comment_tile.dart';
import 'package:comunic/ui/widgets/account_image_widget.dart';
import 'package:comunic/ui/widgets/countdown_widget.dart';
@ -41,7 +45,7 @@ const TextStyle _userNameStyle = TextStyle(
fontSize: 16.0);
/// Post actions
enum _PostActions { DELETE, UPDATE_CONTENT }
enum _PostActions { DELETE, UPDATE_CONTENT, REPORT }
class PostTile extends StatefulWidget {
final Post post;
@ -152,7 +156,14 @@ class _PostTileState extends State<PostTile> {
value: _PostActions.DELETE,
enabled: widget.post.canDelete,
),
],
]..addAll(srvConfig!.isReportingEnabled && !widget.post.isOwner
? [
PopupMenuItem(
child: Text(tr("Report abuse")!),
value: _PostActions.REPORT,
)
]
: []),
onSelected: _selectedPostMenuAction,
)
],
@ -356,6 +367,7 @@ class _PostTileState extends State<PostTile> {
user: widget.usersInfo.getUser(comment.userID),
onUpdateComment: _updateCommentContent,
onDeleteComment: _deleteComment,
onReportComment: _reportComment,
);
},
);
@ -549,6 +561,10 @@ class _PostTileState extends State<PostTile> {
}
}
/// Process a report request
void _reportComment(Comment comment) => showReportDialog(
ctx: context, target: ReportTarget(ReportTargetType.Comment, comment.id));
/// Method called each time the user has selected an option
void _selectedPostMenuAction(_PostActions value) {
switch (value) {
@ -561,6 +577,11 @@ class _PostTileState extends State<PostTile> {
case _PostActions.DELETE:
confirmDelete();
break;
// Report content
case _PostActions.REPORT:
reportContent();
break;
}
}
@ -620,4 +641,9 @@ class _PostTileState extends State<PostTile> {
widget.onDeletedPost(widget.post);
}
/// Report post
void reportContent() async => await showReportDialog(
ctx: context,
target: ReportTarget(ReportTargetType.Post, widget.post.id));
}

View File

@ -11,7 +11,7 @@ description: Comunic client
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.1.11+18
version: 1.1.12+19
environment:
sdk: '>=2.12.0 <3.0.0'