import 'package:cached_network_image/cached_network_image.dart'; import 'package:comunic/helpers/conversations_helper.dart'; import 'package:comunic/helpers/server_config_helper.dart'; import 'package:comunic/helpers/users_helper.dart'; import 'package:comunic/lists/users_list.dart'; import 'package:comunic/models/conversation.dart'; import 'package:comunic/models/new_conversation.dart'; import 'package:comunic/models/new_conversation_settings.dart'; import 'package:comunic/models/user.dart'; import 'package:comunic/ui/dialogs/color_picker_dialog.dart'; import 'package:comunic/ui/dialogs/pick_file_dialog.dart'; import 'package:comunic/ui/routes/main_route/main_route.dart'; import 'package:comunic/ui/tiles/simple_user_tile.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/pick_user_widget.dart'; import 'package:comunic/utils/account_utils.dart'; import 'package:comunic/utils/color_utils.dart'; import 'package:comunic/utils/dart_color.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'; /// Create / Update conversation screen /// /// @author Pierre HUBERT enum _MembersMenuChoices { TOGGLE_ADMIN_STATUS, REMOVE } class UpdateConversationScreen extends StatefulWidget { final convID; const UpdateConversationScreen({ Key? key, this.convID, }) : super(key: key); @override State createState() => _UpdateConversationScreen(); } class _UpdateConversationScreen extends State { late Conversation _conversation; TextEditingController _nameController = TextEditingController(); TextEditingController _colorController = TextEditingController(); UsersList _members = UsersList(); Set _admins = Set(); bool _followConversation = true; bool? _canEveryoneAddMembers = true; String? _image; String get _conversationColor => _colorController.text; Color? get _color { if (_conversationColor == null || _conversationColor.isEmpty) return null; try { return HexColor(_conversationColor); } catch (e, s) { logError(e, s); return null; } } get isUpdating => widget.convID != null; get isAdmin => !isUpdating || _conversation.isAdmin; bool get _isGroupConversation => isUpdating && _conversation.isGroupConversation; bool get _canAddMembers => (isAdmin || _conversation.canEveryoneAddMembers!) && (!isUpdating || !_conversation.isManaged); get _isValid => _members.length > 0; Future _init() async { if (!isUpdating) { _admins.add(userID()); return; } _conversation = await ConversationsHelper().getSingle(widget.convID, force: true); _nameController.text = _conversation.name ?? ""; _colorController.text = _conversation.color == null ? "" : "#${colorToHex(_conversation.color)}"; _members = await UsersHelper().getList(_conversation.membersID); _admins = _conversation.adminsID; _followConversation = _conversation.following; _canEveryoneAddMembers = _conversation.canEveryoneAddMembers; _image = _conversation.logoURL; setState(() {}); } @override Widget build(BuildContext context) => Scaffold( appBar: AppBar( leading: ComunicBackButton(), title: Text(isUpdating ? tr("Update conversation")! : tr("Create a conversation")!), actions: [ IconButton( icon: Icon(Icons.check), onPressed: _isValid ? _submitForm : null) ], ), body: AsyncScreenWidget( onReload: _init, onBuild: _buildBody, errorMessage: tr("Failed to load conversation settings!")!, ), ); Widget _buildBody() { return SingleChildScrollView( child: Container( padding: EdgeInsets.all(8.0), child: Column( children: [ _isGroupConversation ? Text(tr("This conversation is managed by a group")!) : Container(), // Conversation name TextField( controller: _nameController, decoration: InputDecoration( labelText: tr("Conversation name (optional)"), alignLabelWithHint: true, enabled: isAdmin, ), ), // Conversation color TextField( controller: _colorController, onChanged: (s) => setState(() {}), decoration: InputDecoration( labelText: tr("Conversation color (optional)"), alignLabelWithHint: true, enabled: isAdmin, suffixIcon: IconButton( icon: Icon(Icons.colorize), color: _color, onPressed: isAdmin ? _pickColor : null, )), ), // Follow conversation ? Row( children: [ Switch.adaptive( value: _followConversation, onChanged: (b) => setState(() { _followConversation = b; }), ), Text(tr("Follow conversation")!) ], ), // Allow every members of the conversation to add users ? _isGroupConversation ? Container() : Row( children: [ Switch.adaptive( value: _canEveryoneAddMembers!, onChanged: isAdmin ? (b) => setState(() { _canEveryoneAddMembers = b; }) : null, ), Flexible( child: Text(tr( "Allow all members of the conversation to add users")!)) ], ), // Add a member to the conversation PickUserWidget( resetOnChoose: true, keepFocusOnChoose: true, label: tr("Add member")!, enabled: _canAddMembers, onSelectUser: (user) => _addMember(user)), //Conversation members Column( children: _members.map((f) => _buildMemberTile(f)).toList(), ), // Conversation image isUpdating ? _buildConversationImageWidget() : Container(), ], ), ), ); } Widget _buildMemberTile(User user) => SimpleUserTile( user: user, subtitle: _admins.contains(user.id) ? tr("Admin") : tr("Member"), trailing: _canAddMembers ? PopupMenuButton<_MembersMenuChoices>( onSelected: (choice) => _membersMenuItemSelected(user, choice), itemBuilder: (c) => >[ PopupMenuItem( child: Text(tr("Toggle admin status")!), value: _MembersMenuChoices.TOGGLE_ADMIN_STATUS, enabled: isUpdating && isAdmin && user.id != userID(), ), PopupMenuItem( child: Text(tr("Remove")!), value: _MembersMenuChoices.REMOVE, enabled: isAdmin && user.id != userID(), ), ], ) : null, ); void _pickColor() async { final color = await showColorPickerDialog(context, _color); setState(() => _colorController.text = color == null ? "" : "#${colorToHex(color)}"); } /// An option of the members menu has been selected void _membersMenuItemSelected(User user, _MembersMenuChoices choice) { if (choice == null) return; switch (choice) { case _MembersMenuChoices.REMOVE: _removeMember(user); break; case _MembersMenuChoices.TOGGLE_ADMIN_STATUS: _toggleAdminStatus(user); break; } } void _addMember(User user) async { try { if (_members.contains(user)) return; if (isUpdating) await ConversationsHelper.addMember(_conversation.id, user.id); setState(() => _members.insert(0, user)); } catch (e, s) { logError(e, s); snack(context, tr("Failed to add member to conversation!")!); } } void _removeMember(User user) async { try { if (isUpdating) await ConversationsHelper.removeMember(_conversation.id, user.id); setState(() { _members.removeWhere((u) => u.id == user.id); _admins.remove(user.id); }); } catch (e, s) { logError(e, s); snack(context, tr("Failed to remove member!")!); } } void _toggleAdminStatus(User user) async { try { final setAdmin = !_admins.contains(user.id); await ConversationsHelper.setAdmin(_conversation.id!, user.id, setAdmin); setState(() { if (!setAdmin) _admins.remove(user.id); else _admins.add(user.id); }); } catch (e, s) { logError(e, s); snack(context, tr("Failed to toggle admin status of user!")!); } } /// Submit the conversation Future _submitForm() async { try { // Create the conversation if (!isUpdating) { final conversationID = await ConversationsHelper.createConversation( NewConversation( name: _nameController.text, members: _members.map((element) => element.id).toList(), follow: _followConversation, canEveryoneAddMembers: _canEveryoneAddMembers!, color: _color)) ; MainController.of(context)!.popPage(); MainController.of(context)!.openConversationById(conversationID); return; } // Update conversation settings final newSettings = NewConversationsSettings( convID: _conversation.id!, following: _followConversation, isComplete: isAdmin, name: _nameController.text, canEveryoneAddMembers: _canEveryoneAddMembers, color: _color, ); await ConversationsHelper.updateConversation(newSettings); MainController.of(context)!.popPage(); } catch (e, s) { logError(e, s); snack(context, tr("Failed to update conversation settings!")!); } } /// Conversation image management Widget _buildConversationImageWidget() => Column( children: [ SizedBox(height: 10), Text(tr("Conversation logo")!, style: TextStyle(fontWeight: FontWeight.bold)), SizedBox(height: 5), _image == null ? Text("No logo defined yet.") : CachedNetworkImage(imageUrl: _image!), SizedBox(height: 5), isAdmin ? Row( mainAxisAlignment: MainAxisAlignment.center, children: [ OutlinedButton( onPressed: _uploadNewLogo, child: Text(tr("Change logo")!), ), SizedBox(width: 5), _image == null ? Container() : ElevatedButton( onPressed: _deleteLogo, child: Text(tr("Delete logo")!), style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red)), ), ], ) : Container(), SizedBox(height: 10), ], ); /// Upload new conversation logo Future _uploadNewLogo() async { try { final newLogo = await showPickFileDialog( context: context, allowedMimeTypes: ["image/png", "image/jpeg", "image/gif"], imageMaxWidth: srvConfig!.conversationsPolicy.maxLogoWidth, imageMaxHeight: srvConfig!.conversationsPolicy.maxLogoHeight, ); if (newLogo == null) return; await ConversationsHelper.changeImage(_conversation.id, newLogo); final newConvSettings = await ConversationsHelper().getSingle(_conversation.id, force: true); setState(() => _image = newConvSettings.logoURL); } catch (e, s) { logError(e, s); snack(context, tr("Failed to change conversation logo !")!); } } /// Delete conversation logo Future _deleteLogo() async { try { if (!await showConfirmDialog( context: context, message: tr("Do you really want to delete this logo?"))) return; await ConversationsHelper.removeLogo(_conversation.id); setState(() => _image = null); } catch (e, s) { logError(e, s); snack(context, tr("Failed to remove conversation logo!")!); } } }