import 'dart:typed_data'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:comunic/helpers/groups_helper.dart'; import 'package:comunic/helpers/server_config_helper.dart'; import 'package:comunic/models/advanced_group_info.dart'; import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/group.dart'; import 'package:comunic/models/new_group_conversation.dart'; import 'package:comunic/ui/dialogs/input_user_password_dialog.dart'; import 'package:comunic/ui/dialogs/multi_choices_dialog.dart'; import 'package:comunic/ui/dialogs/virtual_directory_dialog.dart'; import 'package:comunic/ui/routes/main_route/main_route.dart'; import 'package:comunic/ui/widgets/async_screen_widget.dart'; import 'package:comunic/ui/widgets/comunic_back_button_widget.dart'; import 'package:comunic/ui/widgets/group_icon_widget.dart'; import 'package:comunic/ui/widgets/safe_state.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/ui/widgets/settings/text_settings_edit_tile.dart'; import 'package:comunic/utils/files_utils.dart'; import 'package:comunic/utils/identicon_utils.dart'; import 'package:comunic/utils/input_utils.dart'; import 'package:comunic/utils/intl_utils.dart'; import 'package:comunic/utils/log_utils.dart'; import 'package:comunic/utils/ui_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; /// Groups settings screen /// /// @author Pierre Hubert class GroupSettingsScreen extends StatefulWidget { final int groupID; const GroupSettingsScreen({Key? key, required this.groupID}) : assert(groupID != null), super(key: key); @override _GroupSettingsScreenState createState() => _GroupSettingsScreenState(); } class _GroupSettingsScreenState extends SafeState { AdvancedGroupInfo? _groupSettings; final _key = GlobalKey(); Future _refresh() async { _groupSettings = await GroupsHelper().getSettings(widget.groupID); } Future _updateSettings() async { try { await GroupsHelper.setSettings(_groupSettings!); } catch (e, stack) { print("Could not update group settings! $e\n$stack"); showSimpleSnack(context, tr("Could not update group settings!")!); } _key.currentState!.refresh(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: ComunicBackButton(), title: Text(tr("Group settings")!), ), body: _buildBody(), ); } Widget _buildBody() { return AsyncScreenWidget( key: _key, onReload: _refresh, onBuild: _buildContent, errorMessage: tr("Could not get group settings!")!, showOldDataWhileUpdating: true, ); } Widget _buildContent() { return SettingsList( sections: [ HeadSpacerSection(), _buildGeneralSection(), _buildAccessRestrictions(), _buildConversationsArea(), _buildGroupLogoArea(), _buildDangerZone(), ], ); } SettingsSection _buildGeneralSection() { return SettingsSection( title: tr("General information"), tiles: [ // Group ID SettingsTile( title: tr("Group ID"), subtitle: _groupSettings!.id.toString(), ), // Group name TextEditSettingsTile( title: tr("Group name")!, currValue: _groupSettings!.name, onChanged: (s) { _groupSettings!.name = s; _updateSettings(); }), // Group virtual directory SettingsTile( title: tr("Virtual directory (optional)"), subtitle: _groupSettings!.virtualDirectory, onPressed: (_) async { final newDir = await showVirtualDirectoryDialog( context: context, initialDirectory: _groupSettings!.virtualDirectory, id: _groupSettings!.id, type: VirtualDirectoryTargetType.GROUP, ); if (newDir == null) return; _groupSettings!.virtualDirectory = newDir; _updateSettings(); }, ), // Group URL TextEditSettingsTile( title: tr("Group URL (optional)")!, currValue: _groupSettings!.url, checkInput: validateUrl, allowEmptyValues: true, onChanged: (s) { _groupSettings!.url = s; _updateSettings(); }, ), // Group description TextEditSettingsTile( title: tr("Group description (optional)")!, currValue: _groupSettings!.description, maxLines: 3, maxLength: 255, allowEmptyValues: true, onChanged: (s) { _groupSettings!.description = s; _updateSettings(); }), ], ); } List> get _visibilityLevels => [ MultiChoiceEntry( id: GroupVisibilityLevel.OPEN, title: tr("Open group")!, subtitle: tr("Group information & public posts are available to everyone."), ), MultiChoiceEntry( id: GroupVisibilityLevel.PRIVATE, title: tr("Private group")!, subtitle: tr("The group is accessible to accepted members only."), ), MultiChoiceEntry( id: GroupVisibilityLevel.SECRETE, title: tr("Secrete group")!, subtitle: tr("The group is visible only to invited members."), ), ]; List> get _registrationLevels => [ MultiChoiceEntry( id: GroupRegistrationLevel.OPEN, title: tr("Open registration")!, subtitle: tr( "Everyone can choose to join the group without moderator approval"), ), MultiChoiceEntry( id: GroupRegistrationLevel.MODERATED, title: tr("Moderated registration")!, subtitle: tr( "Everyone can request a membership, but a moderator review the request"), ), MultiChoiceEntry( id: GroupRegistrationLevel.CLOSED, title: tr("Closed registration")!, subtitle: tr( "The only way to join the group is to be invited by a moderator"), ), ]; List> get _postsCreationLevels => [ MultiChoiceEntry( id: GroupPostCreationLevel.MEMBERS, title: tr("All members")!, subtitle: tr("All the members of the group can create posts on the group"), ), MultiChoiceEntry( id: GroupPostCreationLevel.MODERATORS, title: tr("Moderators only")!, subtitle: tr( "Only moderators and administrators of the group can create posts on it"), ), ]; SettingsSection _buildAccessRestrictions() => SettingsSection( title: tr("Access restrictions"), tiles: [ // Group visibility MultiChoicesSettingsTile( title: tr("Group visibility")!, choices: _visibilityLevels, currentValue: _groupSettings!.visibilityLevel, onChanged: (dynamic v) { _groupSettings!.visibilityLevel = v; _updateSettings(); }), // Group registration level MultiChoicesSettingsTile( title: tr("Group registration level")!, choices: _registrationLevels, currentValue: _groupSettings!.registrationLevel, onChanged: (dynamic v) { _groupSettings!.registrationLevel = v; _updateSettings(); }), // Group posts creation levels MultiChoicesSettingsTile( title: tr("Posts creation level")!, choices: _postsCreationLevels, currentValue: _groupSettings!.postCreationLevel, onChanged: (dynamic s) { _groupSettings!.postCreationLevel = s; _updateSettings(); }), // Groups members list visibility SettingsTile.switchTile( title: tr("Make members list public"), onToggle: (s) { _groupSettings!.isMembersListPublic = s; _updateSettings(); }, switchValue: _groupSettings!.isMembersListPublic, titleMaxLines: 2, ) ], ); List> get _conversationMinMembershipLevel => [ MultiChoiceEntry( id: GroupMembershipLevel.ADMINISTRATOR, title: tr("Administrators only")!, subtitle: tr( "Only the administrators of the group can access the conversation"), ), MultiChoiceEntry( id: GroupMembershipLevel.MODERATOR, title: tr("Moderators and administrators")!, subtitle: tr( "Only moderators and administrators of the group can access the conversation"), ), MultiChoiceEntry( id: GroupMembershipLevel.MEMBER, title: tr("All members")!, subtitle: tr( "All the members of the group can access the conversation"), ), ]; SettingsSection _buildConversationsArea() => SettingsSection( title: tr("Group conversations"), tiles: _groupSettings!.conversations! .map( (e) { SettingsTile tile = MultiChoicesSettingsTile( title: e.name!, choices: _conversationMinMembershipLevel, currentValue: e.groupMinMembershipLevel, leading: e.hasLogo ? CachedNetworkImage( imageUrl: e.logoURL!, width: 30, ) : Icon(Icons.group, size: 30), onChanged: (c) => _changeConversationVisibility(e, c), trailing: IconButton( icon: Icon(Icons.delete), onPressed: () => _deleteConversation(e), ), ); return tile; }, ) .toList() .cast() ..add( SettingsTile( title: tr("Create a new conversation"), onPressed: _createNewGroupConversation, ), ), ); void _createNewGroupConversation(BuildContext context) async { try { final name = await askUserString( context: context, title: tr("New conversation name")!, message: tr("Please give a name to the new conversation")!, defaultValue: "", hint: tr("Name")!, minLength: 1, maxLength: ServerConfigurationHelper .config!.conversationsPolicy.maxConversationNameLen, ); if (name == null) return; final visibility = await showMultiChoicesDialog( context: context, choices: _conversationMinMembershipLevel, defaultChoice: GroupMembershipLevel.MEMBER, title: tr("Conversation visibility"), ); if (visibility == null) return; await GroupsHelper.createGroupConversation(NewGroupConversation( groupID: _groupSettings!.id, name: name, minMembershipLevel: visibility, )); _key.currentState!.refresh(); } catch (e, s) { logError(e, s); snack(context, tr("Failed to create a conversation!")!); } } void _changeConversationVisibility( Conversation conv, GroupMembershipLevel? newLevel) async { try { await GroupsHelper.setConversationVisibility(conv.id, newLevel); _key.currentState!.refresh(); } catch (e, s) { logError(e, s); snack(context, tr("Failed to change conversation visibility level!")!); } } void _deleteConversation(Conversation conv) async { try { if (!await showConfirmDialog( context: context, message: tr("Do you really want to delete this conversation?"))) return; await GroupsHelper.deleteConversation(conv.id); _key.currentState!.refresh(); } catch (e, s) { logError(e, s); snack(context, tr("Failed to delete conversation!")!); } } SettingsSection _buildGroupLogoArea() { return SettingsSection( title: tr("Group logo"), tiles: [ // Current logo SettingsTile( title: tr("Current logo"), leading: GroupIcon(group: _groupSettings!), ), // Upload a new logo SettingsTile( title: tr("Upload a new logo"), onPressed: (_) => _uploadNewLogo(), ), // Generate a new random logo SettingsTile( title: tr("Generate a new random logo"), onPressed: (_) => _generateRandomLogo(), ), // Delete current logo SettingsTile( title: tr("Delete logo"), onPressed: (_) => _deleteLogo(), ), ], ); } /// Upload a new logo for the group void _uploadNewLogo() async { try { final logo = await pickImage(context); if (logo == null) return; await _doUploadLogo(logo.bytes as Uint8List?); } catch (e, stack) { print("Could not upload new logo! $e\n$stack"); showSimpleSnack(context, tr("Could not upload new logo!")!); } } /// Generate a new random logo for the group void _generateRandomLogo() async { try { final newLogo = await genIdenticon(context); await _doUploadLogo(newLogo); } catch (e, stack) { print("Could not generate new logo! $e\n$stack"); showSimpleSnack(context, tr("Could not generate new random logo!")!); } } Future _doUploadLogo(Uint8List? bytes) async { await GroupsHelper.uploadNewLogo(_groupSettings!.id, bytes); _key.currentState!.refresh(); } /// Delete previous group logo void _deleteLogo() async { try { if (!await showConfirmDialog( context: context, message: tr("Do you really want to delete the logo of this group ?"))) return; await GroupsHelper.deleteLogo(_groupSettings!.id); _key.currentState!.refresh(); } catch (e, s) { print("Could not delete group logo! $e\n$s"); showSimpleSnack(context, tr("Could not delete group logo!")!); } } SettingsSection _buildDangerZone() { return SettingsSection( title: tr("Danger zone"), tiles: [ SettingsTile( title: tr("Delete group"), onPressed: (_) => _deleteGroup(), ), ], ); } /// Delete the group void _deleteGroup() async { try { final password = await showUserPasswordDialog(context); if (password == null) return; if (!await showConfirmDialog( context: context, message: tr( "Do you really want to delete this group ? All the posts related to it will be permanently deleted!"))) return; await GroupsHelper.deleteGroup(_groupSettings!.id, password); MainController.of(context)!.popPage(); } catch (e, s) { print("Could not delete the group! $e\n$s"); showSimpleSnack(context, tr("Could not delete the group")!); } } }