diff --git a/lib/helpers/server_config_helper.dart b/lib/helpers/server_config_helper.dart new file mode 100644 index 0000000..fb1a00c --- /dev/null +++ b/lib/helpers/server_config_helper.dart @@ -0,0 +1,44 @@ +import 'package:comunic/models/api_request.dart'; +import 'package:comunic/models/server_config.dart'; + +/// Server configuration helper +/// +/// @author Pierre Hubert + +class ServerConfigurationHelper { + static ServerConfig _config; + + /// Make sure the configuration has been correctly loaded + static Future ensureLoaded() async { + if (_config != null) return; + + final response = + (await APIRequest.withoutLogin("server/config").execWithThrow()) + .getObject(); + + final dataConservationPolicy = response["data_conservation_policy"]; + + _config = ServerConfig( + 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"], + ), + ); + } + + /// Get current server configuration, throwing if it is not loaded yet + static ServerConfig get config { + if (_config == null) + throw Exception( + "Trying to access server configuration but it is not loaded yet!"); + + return _config; + } +} diff --git a/lib/helpers/settings_helper.dart b/lib/helpers/settings_helper.dart index 9ceb71c..f6f429a 100644 --- a/lib/helpers/settings_helper.dart +++ b/lib/helpers/settings_helper.dart @@ -1,6 +1,7 @@ import 'package:comunic/enums/user_page_visibility.dart'; import 'package:comunic/models/account_image_settings.dart'; import 'package:comunic/models/api_request.dart'; +import 'package:comunic/models/data_conservation_policy_settings.dart'; import 'package:comunic/models/general_settings.dart'; import 'package:comunic/models/new_emoji.dart'; import 'package:comunic/models/security_settings.dart'; @@ -193,4 +194,42 @@ class SettingsHelper { .addString("security_answer_2", newSettings.securityAnswer2) .execWithThrow(); } + + /// Get account data conservation policy settings + /// + /// Throws in case of failure + static Future + getDataConservationPolicy() async { + final response = + (await APIRequest.withLogin("settings/get_data_conservation_policy") + .execWithThrow()) + .getObject(); + + return DataConservationPolicySettings( + inactiveAccountLifeTime: response["inactive_account_lifetime"], + notificationLifetime: response["notification_lifetime"], + commentsLifetime: response["comments_lifetime"], + postsLifetime: response["posts_lifetime"], + conversationMessagesLifetime: + response["conversation_messages_lifetime"], + likesLifetime: response["likes_lifetime"]); + } + + /// Apply new data conservation policy settings + /// + /// Throws in case of failure + static Future setDataConservationPolicy( + String password, DataConservationPolicySettings newSettings) async { + await APIRequest(uri: "settings/set_data_conservation_policy", needLogin: true) + .addString("password", password) + .addInt("inactive_account_lifetime", + newSettings.inactiveAccountLifeTime ?? 0) + .addInt("notification_lifetime", newSettings.notificationLifetime ?? 0) + .addInt("comments_lifetime", newSettings.commentsLifetime ?? 0) + .addInt("posts_lifetime", newSettings.postsLifetime ?? 0) + .addInt("conversation_messages_lifetime", + newSettings.conversationMessagesLifetime ?? 0) + .addInt("likes_lifetime", newSettings.likesLifetime ?? 0) + .execWithThrow(); + } } diff --git a/lib/models/data_conservation_policy_settings.dart b/lib/models/data_conservation_policy_settings.dart new file mode 100644 index 0000000..6049d2a --- /dev/null +++ b/lib/models/data_conservation_policy_settings.dart @@ -0,0 +1,21 @@ +/// Data conservation policy settings +/// +/// @author Pierre Hubert + +class DataConservationPolicySettings { + int inactiveAccountLifeTime; + int notificationLifetime; + int commentsLifetime; + int postsLifetime; + int conversationMessagesLifetime; + int likesLifetime; + + DataConservationPolicySettings({ + this.inactiveAccountLifeTime, + this.notificationLifetime, + this.commentsLifetime, + this.postsLifetime, + this.conversationMessagesLifetime, + this.likesLifetime, + }); +} diff --git a/lib/models/server_config.dart b/lib/models/server_config.dart new file mode 100644 index 0000000..e43ca9f --- /dev/null +++ b/lib/models/server_config.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; + +/// Server static configuration +/// +/// @author Pierre Hubert + +class ServerDataConservationPolicy { + final int minInactiveAccountLifetime; + final int minNotificationLifetime; + final int minCommentsLifetime; + final int minPostsLifetime; + final int minConversationMessagesLifetime; + final int minLikesLifetime; + + const ServerDataConservationPolicy({ + @required this.minInactiveAccountLifetime, + @required this.minNotificationLifetime, + @required this.minCommentsLifetime, + @required this.minPostsLifetime, + @required this.minConversationMessagesLifetime, + @required this.minLikesLifetime, + }) : assert(minInactiveAccountLifetime != null), + assert(minNotificationLifetime != null), + assert(minCommentsLifetime != null), + assert(minPostsLifetime != null), + assert(minConversationMessagesLifetime != null), + assert(minLikesLifetime != null); +} + +class ServerConfig { + final ServerDataConservationPolicy dataConservationPolicy; + + const ServerConfig({ + @required this.dataConservationPolicy, + }) : assert(dataConservationPolicy != null); +} diff --git a/lib/ui/routes/settings/account_privacy_settings.dart b/lib/ui/routes/settings/account_privacy_settings.dart index 8c771cb..e45ce00 100644 --- a/lib/ui/routes/settings/account_privacy_settings.dart +++ b/lib/ui/routes/settings/account_privacy_settings.dart @@ -1,6 +1,13 @@ import 'package:comunic/helpers/account_helper.dart'; +import 'package:comunic/helpers/server_config_helper.dart'; +import 'package:comunic/helpers/settings_helper.dart'; +import 'package:comunic/models/data_conservation_policy_settings.dart'; +import 'package:comunic/models/server_config.dart'; import 'package:comunic/ui/dialogs/input_user_password_dialog.dart'; +import 'package:comunic/ui/dialogs/multi_choices_dialog.dart'; +import 'package:comunic/ui/widgets/async_screen_widget.dart'; import 'package:comunic/ui/widgets/settings/header_spacer_section.dart'; +import 'package:comunic/ui/widgets/settings/multi_choices_settings_tile.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/cupertino.dart'; @@ -17,19 +24,68 @@ class AccountPrivacySettings extends StatefulWidget { } class _AccountPrivacySettingsState extends State { + final _key = GlobalKey(); + ServerConfig _serverConfig; + DataConservationPolicySettings _userSettings; + String _cachedPassword; + + Future _loadSettings() async { + await ServerConfigurationHelper.ensureLoaded(); + _serverConfig = ServerConfigurationHelper.config; + _userSettings = await SettingsHelper.getDataConservationPolicy(); + } + @override Widget build(BuildContext context) { - return SettingsList(sections: [ - HeadSpacerSection(), - SettingsSection(title: tr("Privacy settings"), tiles: [ - SettingsTile( - title: tr("Delete your account"), - subtitle: - tr("Permanently delete your account and all data related to it."), - onPressed: (_) => _deleteAccount(), - ) - ]) - ]); + return AsyncScreenWidget( + key: _key, + onReload: _loadSettings, + onBuild: () => SettingsList(sections: [ + HeadSpacerSection(), + SettingsSection( + title: tr("Data conservation policy"), + tiles: _dataConservationPolicyTiles, + ), + SettingsSection(title: tr("Danger zone"), tiles: [ + SettingsTile( + title: tr("Delete your account"), + subtitle: tr( + "Permanently delete your account and all data related to it."), + onPressed: (_) => _deleteAccount(), + ) + ]) + ]), + errorMessage: tr("Failed to load privacy settings!"), + ); + } + + List get _dataConservationPolicyTiles => [ + DataConservationPolicyTile( + value: _userSettings.notificationLifetime, + title: tr("Automatically delete unread notification after"), + onChange: (val) { + _userSettings.notificationLifetime = val; + _updateDataConservationPolicy(); + }, + minValue: + _serverConfig.dataConservationPolicy.minNotificationLifetime, + ), + ]; + + void _updateDataConservationPolicy() async { + try { + if (_cachedPassword == null) + _cachedPassword = await showUserPasswordDialog(context); + + await SettingsHelper.setDataConservationPolicy( + _cachedPassword, _userSettings); + + _key.currentState.refresh(); + } catch (e, s) { + print("Could not update data conservation policy! $e\n$s"); + showSimpleSnack( + context, tr("Failed to update data conservation policy!")); + } } /// Permanently delete user account @@ -61,6 +117,98 @@ class _AccountPrivacySettingsState extends State { } } +class DataConservationPolicyTile extends SettingsTile { + final int value; + final String title; + final Function(int) onChange; + final int minValue; + + const DataConservationPolicyTile({ + @required this.value, + @required this.title, + @required this.onChange, + @required this.minValue, + }) : assert(title != null), + assert(onChange != null), + assert(minValue != null); + + @override + Widget build(BuildContext context) { + return MultiChoicesSettingsTile( + title: title, + choices: _choices, + currentValue: _roundValue, + onChanged: onChange, + ); + } + + int get _day => 60 * 60 * 24; + + int get _month => _day * 30; + + int get _year => _day * 365; + + int get _roundValue { + if (this.value == null) return 0; + + return _choices.firstWhere((element) => element.id >= this.value).id; + } + + List> get _choices => [ + MultiChoiceEntry(id: 0, title: tr("Never"), hidden: false), + MultiChoiceEntry( + id: _day * 7, + title: tr("7 days"), + hidden: _day * 7 < minValue, + ), + MultiChoiceEntry( + id: _day * 15, + title: tr("15 days"), + hidden: _day * 15 < minValue, + ), + MultiChoiceEntry( + id: _month, + title: tr("1 month"), + hidden: _month < minValue, + ), + MultiChoiceEntry( + id: _month * 3, + title: tr("3 months"), + hidden: _month * 3 < minValue, + ), + MultiChoiceEntry( + id: _month * 6, + title: tr("6 months"), + hidden: _month * 6 < minValue, + ), + MultiChoiceEntry( + id: _year, + title: tr("1 year"), + hidden: _year < minValue, + ), + MultiChoiceEntry( + id: _year * 2, + title: tr("2 years"), + hidden: _year * 5 < minValue, + ), + MultiChoiceEntry( + id: _year * 5, + title: tr("5 years"), + hidden: _year * 5 < minValue, + ), + MultiChoiceEntry( + id: _year * 10, + title: tr("10 years"), + hidden: _year * 10 < minValue, + ), + MultiChoiceEntry( + id: _year * 50, + title: tr("50 years"), + hidden: _year * 50 < minValue, + ), + ]; +} + class _LastChanceDeleteAccountDialog extends StatelessWidget { @override Widget build(BuildContext context) {