diff --git a/lib/src/antiMobbing/anti_mobbing_list_bloc.dart b/lib/src/antiMobbing/anti_mobbing_list_bloc.dart index 841400c0..e0459b08 100644 --- a/lib/src/antiMobbing/anti_mobbing_list_bloc.dart +++ b/lib/src/antiMobbing/anti_mobbing_list_bloc.dart @@ -59,7 +59,7 @@ class AntiMobbingListBloc extends Bloc AntiMobbingListStateInitial(); @override - Stream mapEventToState(AntiMobbingListState currentState, AntiMobbingListEvent event) async* { + Stream mapEventToState(AntiMobbingListEvent event) async* { if (event is RequestMessages) { try { _messageListRepository = RepositoryManager.get(RepositoryType.chatMessage, Chat.typeInvite); diff --git a/lib/src/chat/chat.dart b/lib/src/chat/chat.dart index 489c739a..ddb20212 100644 --- a/lib/src/chat/chat.dart +++ b/lib/src/chat/chat.dart @@ -317,11 +317,13 @@ class _ChatState extends State with ChatComposer, CreateChatMixin, InviteM String subTitle; Color color; bool isVerified = false; + String imagePath = ""; if (state is ChatStateSuccess) { name = state.name; subTitle = state.subTitle; color = state.color; isVerified = state.isVerified; + imagePath = state.avatarPath; } else { name = ""; subTitle = ""; @@ -331,6 +333,7 @@ class _ChatState extends State with ChatComposer, CreateChatMixin, InviteM child: Row( children: [ Avatar( + imagePath: imagePath, textPrimary: name, textSecondary: subTitle, color: color, @@ -667,10 +670,17 @@ class _ChatState extends State with ChatComposer, CreateChatMixin, InviteM }); } - _chatTitleTapped() { - navigation.push( + _chatTitleTapped() async{ + await navigation.push( context, - MaterialPageRoute(builder: (context) => ChatProfile(chatId: widget.chatId, messageId: widget.messageId)), + MaterialPageRoute(builder: (context) { + return BlocProvider.value( + value: _chatBloc, + child: ChatProfile(chatId: widget.chatId, messageId: widget.messageId), + ); + }), ); + + _chatBloc.dispatch(RequestChat(chatId: widget.chatId, messageId: widget.messageId)); } } diff --git a/lib/src/chat/chat_add_group_participants.dart b/lib/src/chat/chat_add_group_participants.dart new file mode 100644 index 00000000..20d81791 --- /dev/null +++ b/lib/src/chat/chat_add_group_participants.dart @@ -0,0 +1,227 @@ +/* + * OPEN-XCHANGE legal information + * + * All intellectual property rights in the Software are protected by + * international copyright laws. + * + * + * In some countries OX, OX Open-Xchange and open xchange + * as well as the corresponding Logos OX Open-Xchange and OX are registered + * trademarks of the OX Software GmbH group of companies. + * The use of the Logos is not covered by the Mozilla Public License 2.0 (MPL 2.0). + * Instead, you are allowed to use these Logos according to the terms and + * conditions of the Creative Commons License, Version 2.5, Attribution, + * Non-commercial, ShareAlike, and the interpretation of the term + * Non-commercial applicable to the aforementioned license is published + * on the web site https://www.open-xchange.com/terms-and-conditions/. + * + * Please make sure that third-party modules and libraries are used + * according to their respective licenses. + * + * Any modifications to this package must retain all copyright notices + * of the original copyright holder(s) for the original code used. + * + * After any such modifications, the original and derivative code shall remain + * under the copyright of the copyright holder(s) and/or original author(s) as stated here: + * https://www.open-xchange.com/legal/. The contributing author shall be + * given Attribution for the derivative code and a license granting use. + * + * Copyright (C) 2016-2020 OX Software GmbH + * Mail: info@open-xchange.com + * + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public License 2.0 + * for more details. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ox_coi/src/contact/contact_item_chip.dart'; +import 'package:ox_coi/src/contact/contact_item_selectable.dart'; +import 'package:ox_coi/src/contact/contact_list_bloc.dart'; +import 'package:ox_coi/src/contact/contact_list_event_state.dart'; +import 'package:ox_coi/src/data/contact_repository.dart'; +import 'package:ox_coi/src/l10n/localizations.dart'; +import 'package:ox_coi/src/navigation/navigatable.dart'; +import 'package:ox_coi/src/navigation/navigation.dart'; +import 'package:ox_coi/src/ui/dimensions.dart'; +import 'package:ox_coi/src/utils/toast.dart'; +import 'package:ox_coi/src/utils/widgets.dart'; +import 'package:ox_coi/src/widgets/search.dart'; +import 'package:ox_coi/src/widgets/state_info.dart'; + +import 'chat_change_bloc.dart'; +import 'chat_change_event_state.dart'; + +class ChatAddGroupParticipants extends StatefulWidget { + final int chatId; + final List contactIds; + + ChatAddGroupParticipants({@required this.chatId, @required this.contactIds}); + + @override + _ChatAddGroupParticipantsState createState() => _ChatAddGroupParticipantsState(); +} + +class _ChatAddGroupParticipantsState extends State { + ContactListBloc _contactListBloc = ContactListBloc(); + ChatChangeBloc _chatChangeBloc = ChatChangeBloc(); + Navigation navigation = Navigation(); + + @override + void initState() { + super.initState(); + navigation.current = Navigatable(Type.chatAddGroupParticipants); + _contactListBloc.dispatch(RequestContacts(listTypeOrChatId: ContactRepository.validContacts)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + icon: Icon(Icons.close), + onPressed: () => navigation.pop(context), + ), + title: Text(AppLocalizations.of(context).chatProfileAddParticipantsButtonText), + actions: [ + getSearchAction(), + IconButton( + icon: Icon(Icons.check), + onPressed: () => _onSubmit(), + ) + ], + ), + body: buildList(), + ); + } + + Widget getSearchAction() { + Search search = Search( + onBuildResults: onBuildResultOrSuggestion, + onBuildSuggestion: onBuildResultOrSuggestion, + onClose: onSearchClose, + ); + return IconButton( + icon: Icon(Icons.search), + onPressed: () => search.show(context), + ); + } + + Widget onBuildResultOrSuggestion(String query) { + _contactListBloc.dispatch(SearchContacts(query: query)); + return buildList(); + } + + Widget buildList() { + return BlocBuilder( + bloc: _contactListBloc, + builder: (context, state) { + if (state is ContactListStateSuccess) { + if(state.contactIds.length != widget.contactIds.length){ + return Column( + children: [ + _buildSelectedParticipantList(state.contactsSelected), + Flexible( + child: buildListItems(state), + ), + ], + ); + }else{ + return Center( + child: Text(AppLocalizations.of(context).chatProfileAddParticipantsEmptyList), + ); + } + } else if (state is! ContactListStateFailure) { + return StateInfo(showLoading: true); + } else { + return Icon(Icons.error); + } + }, + ); + } + + ListView buildListItems(ContactListStateSuccess state) { + return ListView.builder( + padding: EdgeInsets.only(top: listItemPadding), + itemCount: state.contactIds.length, + itemBuilder: (BuildContext context, int index) { + if(!widget.contactIds.contains(state.contactIds[index])){ + var contactId = state.contactIds[index]; + var key = createKeyString(contactId, state.contactLastUpdateValues[index]); + bool isSelected = state.contactsSelected.contains(contactId); + return ContactItemSelectable(contactId: contactId, onTap: _itemTapped, isSelected: isSelected, key: key); + }else{ + return Container(); + } + }, + ); + } + + Widget _buildSelectedParticipantList(List selectedContacts) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only( + left: listItemPadding, + right: listItemPadding, + top: listItemPadding, + bottom: listItemPaddingSmall, + ), + child: Text("${selectedContacts.length} ${AppLocalizations.of(context).participants}"), + ), + Container( + padding: EdgeInsets.only(bottom: 4.0), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(), + ), + ), + width: double.infinity, + height: 40.0, + child: selectedContacts.isNotEmpty + ? ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: selectedContacts.length, + itemBuilder: (BuildContext context, int index) { + var selectedContactId = selectedContacts[index]; + return ContactItemChip(contactId: selectedContactId, itemTapped: () => _itemTapped(selectedContactId)); + }) + : Container( + padding: EdgeInsets.only( + left: listItemPadding, + right: listItemPadding, + top: listItemPadding, + ), + child: Text(AppLocalizations.of(context).createGroupNoParticipantsHint), + ), + ), + ], + ); + } + + void onSearchClose() { + _contactListBloc.dispatch(RequestContacts(listTypeOrChatId: ContactRepository.validContacts)); + } + + _itemTapped(int id) { + _contactListBloc.dispatch(ContactsSelectionChanged(id: id)); + } + + _onSubmit() async { + if (_contactListBloc.contactsSelectedCount > 0) { + _chatChangeBloc.dispatch(ChatAddParticipants(chatId: widget.chatId, contactIds: _contactListBloc.contactsSelected)); + navigation.pop(context); + } else { + showToast(AppLocalizations.of(context).createGroupNoParticipantsSelected); + } + } +} diff --git a/lib/src/chat/chat_bloc.dart b/lib/src/chat/chat_bloc.dart index 81a7b70b..ff63dd43 100644 --- a/lib/src/chat/chat_bloc.dart +++ b/lib/src/chat/chat_bloc.dart @@ -62,7 +62,7 @@ class ChatBloc extends Bloc { ChatState get initialState => ChatStateInitial(); @override - Stream mapEventToState(ChatState currentState, ChatEvent event) async* { + Stream mapEventToState(ChatEvent event) async* { if (event is RequestChat) { yield ChatStateLoading(); try { @@ -77,16 +77,16 @@ class ChatBloc extends Bloc { } } else if (event is ChatLoaded) { yield ChatStateSuccess( - name: event.name, - subTitle: event.subTitle, - color: event.color, - freshMessageCount: event.freshMessageCount, - isSelfTalk: event.isSelfTalk, - isGroupChat: event.isGroupChat, - preview: event.preview, - timestamp: event.timestamp, - isVerified: event.isVerified, - ); + name: event.name, + subTitle: event.subTitle, + color: event.color, + freshMessageCount: event.freshMessageCount, + isSelfTalk: event.isSelfTalk, + isGroupChat: event.isGroupChat, + preview: event.preview, + timestamp: event.timestamp, + isVerified: event.isVerified, + avatarPath: event.avatarPath); } } @@ -111,6 +111,7 @@ class ChatBloc extends Bloc { preview: null, timestamp: null, isVerified: false, + avatarPath: null, ), ); } @@ -127,6 +128,7 @@ class ChatBloc extends Bloc { _isGroup = await chat.isGroup(); bool isVerified = await chat.isVerified(); Color color = rgbColorFromInt(colorValue); + String avatarPath = await chat.getProfileImage(); var chatSummary = chat.get(ChatExtension.chatSummary); dispatch( ChatLoaded( @@ -139,6 +141,7 @@ class ChatBloc extends Bloc { preview: chatSummary?.preview, timestamp: chatSummary?.timestamp, isVerified: isVerified, + avatarPath: avatarPath, ), ); } diff --git a/lib/src/chat/chat_change_bloc.dart b/lib/src/chat/chat_change_bloc.dart index 3b259c43..877273b6 100644 --- a/lib/src/chat/chat_change_bloc.dart +++ b/lib/src/chat/chat_change_bloc.dart @@ -48,6 +48,7 @@ import 'package:ox_coi/src/chat/chat_change_event_state.dart'; import 'package:ox_coi/src/data/contact_repository.dart'; import 'package:ox_coi/src/data/repository.dart'; import 'package:ox_coi/src/data/repository_manager.dart'; +import 'package:ox_coi/src/utils/text.dart'; class ChatChangeBloc extends Bloc { Repository _messageListRepository; @@ -57,12 +58,12 @@ class ChatChangeBloc extends Bloc { ChatChangeState get initialState => CreateChatStateInitial(); @override - Stream mapEventToState(ChatChangeState currentState, ChatChangeEvent event) async* { + Stream mapEventToState(ChatChangeEvent event) async* { if (event is CreateChat) { yield CreateChatStateLoading(); try { _messageListRepository = RepositoryManager.get(RepositoryType.chatMessage, event.chatId); - _createChat(contactId: event.contactId, messageId: event.messageId, verified: event.verified, name: event.name, contacts: event.contacts); + _createChat(contactId: event.contactId, messageId: event.messageId, verified: event.verified, name: event.name, contacts: event.contacts, imagePath: event.imagePath); } catch (error) { yield CreateChatStateFailure(error: error.toString()); } @@ -78,10 +79,20 @@ class ChatChangeBloc extends Bloc { _markNoticedChat(event.chatId); } else if (event is ChatMarkMessagesSeen) { _markMessagesSeen(event.messageIds); + } else if (event is ChatAddParticipants) { + _addParticipants(event.chatId, event.contactIds); + } else if (event is ChatRemoveParticipant) { + _removeParticipant(event.chatId, event.contactId); + } else if (event is SetName) { + _setName(event.chatId, event.newName); + } else if (event is SetImagePath) { + _setProfileImage(event.chatId, event.newPath); + } else if (event is SetNameCompleted) { + yield ChangeNameSuccess(); } } - void _createChat({int contactId, int messageId, bool verified, String name, List contacts}) async { + void _createChat({int contactId, int messageId, bool verified, String name, List contacts, String imagePath}) async { Context context = Context(); var chatId; if (contactId != null) { @@ -102,6 +113,9 @@ class ChatChangeBloc extends Bloc { for (int i = 0; i < contacts.length; i++) { context.addContactToChat(chatId, contacts[i]); } + if(!isNullOrEmpty(imagePath)){ + _setProfileImage(chatId, imagePath); + } } _chatRepository.putIfAbsent(id: chatId); @@ -135,12 +149,37 @@ class ChatChangeBloc extends Bloc { if (!_chatRepository.contains(chatId)) { return; } - Chat chat = _chatRepository.get(chatId); - chat.setLastUpdate(); + _chatRepository.get(chatId).setLastUpdate(); } void _markMessagesSeen(List messageIds) async { Context context = Context(); await context.markSeenMessages(messageIds); } + + void _addParticipants(int chatId, List contactIds) async { + Context context = Context(); + for (int i = 0; i < contactIds.length; i++) { + await context.addContactToChat(chatId, contactIds[i]); + } + } + + void _removeParticipant(int chatId, int contactId) async { + Context context = Context(); + await context.removeContactFromChat(chatId, contactId); + RepositoryManager.get(RepositoryType.contact, chatId).remove(id: contactId); + } + + void _setName(int chatId, String newName) async { + Context context = Context(); + await context.setChatName(chatId, newName); + RepositoryManager.get(RepositoryType.chat).get(chatId).set(Chat.methodChatGetName, newName); + dispatch(SetNameCompleted()); + } + + void _setProfileImage(int chatId, String newPath) async { + Context context = Context(); + await context.setChatProfileImage(chatId, newPath); + RepositoryManager.get(RepositoryType.chat).get(chatId).set(Chat.methodChatGetProfileImage, newPath); + } } diff --git a/lib/src/chat/chat_change_event_state.dart b/lib/src/chat/chat_change_event_state.dart index aeae1b96..f3123965 100644 --- a/lib/src/chat/chat_change_event_state.dart +++ b/lib/src/chat/chat_change_event_state.dart @@ -51,6 +51,7 @@ class CreateChat extends ChatChangeEvent { final bool verified; final String name; final List contacts; + final String imagePath; CreateChat({ this.contactId, @@ -59,6 +60,7 @@ class CreateChat extends ChatChangeEvent { this.verified, this.name, this.contacts, + this.imagePath, }); } @@ -98,6 +100,36 @@ class ChatMarkMessagesSeen extends ChatChangeEvent { ChatMarkMessagesSeen({@required this.messageIds}); } +class ChatAddParticipants extends ChatChangeEvent{ + final int chatId; + final List contactIds; + + ChatAddParticipants({@required this.chatId, @required this.contactIds}); +} + +class ChatRemoveParticipant extends ChatChangeEvent{ + final int chatId; + final int contactId; + + ChatRemoveParticipant({@required this.chatId, @required this.contactId}); +} + +class SetName extends ChatChangeEvent{ + final int chatId; + final String newName; + + SetName({@required this.chatId, @required this.newName}); +} + +class SetImagePath extends ChatChangeEvent{ + final int chatId; + final String newPath; + + SetImagePath({@required this.chatId, @required this.newPath}); +} + +class SetNameCompleted extends ChatChangeEvent{} + abstract class ChatChangeState {} class CreateChatStateInitial extends ChatChangeState {} @@ -114,4 +146,6 @@ class CreateChatStateFailure extends ChatChangeState { final String error; CreateChatStateFailure({@required this.error}); -} \ No newline at end of file +} + +class ChangeNameSuccess extends ChatChangeState {} \ No newline at end of file diff --git a/lib/src/chat/chat_composer_bloc.dart b/lib/src/chat/chat_composer_bloc.dart index 191cbef8..446dc2c4 100644 --- a/lib/src/chat/chat_composer_bloc.dart +++ b/lib/src/chat/chat_composer_bloc.dart @@ -63,7 +63,7 @@ class ChatComposerBloc extends Bloc { ChatComposerState get initialState => ChatComposerInitial(); @override - Stream mapEventToState(ChatComposerState currentState, ChatComposerEvent event) async* { + Stream mapEventToState(ChatComposerEvent event) async* { if (event is StartAudioRecording) { try { bool hasContactPermission = await hasPermission(PermissionGroup.microphone); diff --git a/lib/src/chat/chat_create_group_participants.dart b/lib/src/chat/chat_create_group_participants.dart index 5c1d7f25..3f05545c 100644 --- a/lib/src/chat/chat_create_group_participants.dart +++ b/lib/src/chat/chat_create_group_participants.dart @@ -82,8 +82,8 @@ class _ChatCreateGroupParticipantsState extends State navigation.pop(context), ), title: Text(AppLocalizations.of(context).createGroupTitle), diff --git a/lib/src/chat/chat_create_group_settings.dart b/lib/src/chat/chat_create_group_settings.dart index 3e69da0b..c6069f2a 100644 --- a/lib/src/chat/chat_create_group_settings.dart +++ b/lib/src/chat/chat_create_group_settings.dart @@ -40,6 +40,8 @@ * for more details. */ +import 'dart:io'; + import 'package:delta_chat_core/delta_chat_core.dart' as Core; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -55,6 +57,7 @@ import 'package:ox_coi/src/navigation/navigatable.dart'; import 'package:ox_coi/src/navigation/navigation.dart'; import 'package:ox_coi/src/ui/color.dart'; import 'package:ox_coi/src/ui/dimensions.dart'; +import 'package:ox_coi/src/widgets/profile_header.dart'; import 'package:ox_coi/src/widgets/state_info.dart'; import 'package:ox_coi/src/widgets/validatable_text_form_field.dart'; @@ -78,6 +81,7 @@ class _ChatCreateGroupSettingsState extends State with GlobalKey _formKey = GlobalKey(); Repository chatRepository; Navigation navigation = Navigation(); + String _avatar; @override void initState() { @@ -125,6 +129,15 @@ class _ChatCreateGroupSettingsState extends State with return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + Align( + alignment: Alignment.center, + child: ProfileData( + color: accent, + imageActionCallback: _setAvatar, + child: ProfileAvatar( + imagePath: _avatar, + ),) + ), Padding( padding: EdgeInsets.only( left: formHorizontalPadding, @@ -161,6 +174,12 @@ class _ChatCreateGroupSettingsState extends State with ); } + _setAvatar(String avatarPath){ + setState(() { + _avatar = avatarPath; + }); + } + ListView buildListItems(List contactIds) { return ListView.builder( padding: EdgeInsets.all(listItemPadding), @@ -174,7 +193,7 @@ class _ChatCreateGroupSettingsState extends State with _onSubmit() { if (_formKey.currentState.validate()) { - createChatFromGroup(context, false, _groupNameField.controller.text, widget.selectedContacts); + createChatFromGroup(context, false, _groupNameField.controller.text, widget.selectedContacts, _avatar); } } } diff --git a/lib/src/chat/chat_create_mixin.dart b/lib/src/chat/chat_create_mixin.dart index 6a431415..ede17492 100644 --- a/lib/src/chat/chat_create_mixin.dart +++ b/lib/src/chat/chat_create_mixin.dart @@ -61,9 +61,9 @@ mixin CreateChatMixin { return createChatBloc; } - void createChatFromGroup(BuildContext context, bool verified, String name, List contacts, [Function onSuccess]) { + void createChatFromGroup(BuildContext context, bool verified, String name, List contacts, String imagePath, [Function onSuccess]) { ChatChangeBloc chatChangeBloc = _getChatChangeBloc(context, onSuccess); - chatChangeBloc.dispatch(CreateChat(verified: verified, name: name, contacts: contacts)); + chatChangeBloc.dispatch(CreateChat(verified: verified, name: name, contacts: contacts, imagePath: imagePath)); } void createChatFromMessage(BuildContext context, int messageId, int chatId, [Function onSuccess]) { diff --git a/lib/src/chat/chat_event_state.dart b/lib/src/chat/chat_event_state.dart index 0e458102..9941751d 100644 --- a/lib/src/chat/chat_event_state.dart +++ b/lib/src/chat/chat_event_state.dart @@ -63,6 +63,7 @@ class ChatLoaded extends ChatEvent { final String preview; final int timestamp; final bool isVerified; + final String avatarPath; ChatLoaded( {@required this.name, @@ -73,7 +74,8 @@ class ChatLoaded extends ChatEvent { @required this.isGroupChat, @required this.preview, @required this.timestamp, - @required this.isVerified}); + @required this.isVerified, + @required this.avatarPath}); } abstract class ChatState {} @@ -92,6 +94,7 @@ class ChatStateSuccess extends ChatState { final String preview; final int timestamp; final bool isVerified; + final String avatarPath; ChatStateSuccess( {@required this.name, @@ -102,7 +105,8 @@ class ChatStateSuccess extends ChatState { @required this.isGroupChat, @required this.preview, @required this.timestamp, - @required this.isVerified}); + @required this.isVerified, + @required this.avatarPath}); } class ChatStateFailure extends ChatState { diff --git a/lib/src/chat/chat_profile.dart b/lib/src/chat/chat_profile.dart index 001e1655..9a050e68 100644 --- a/lib/src/chat/chat_profile.dart +++ b/lib/src/chat/chat_profile.dart @@ -68,12 +68,12 @@ class ChatProfile extends StatefulWidget { class _ChatProfileState extends State { ChatBloc _chatBloc = ChatBloc(); ContactListBloc _contactListBloc = ContactListBloc(); - final Navigation navigation = Navigation(); + final Navigation _navigation = Navigation(); @override void initState() { super.initState(); - navigation.current = Navigatable(Type.chatProfile); + _navigation.current = Navigatable(Type.chatProfile); _chatBloc.dispatch(RequestChat(chatId: widget.chatId, messageId: widget.messageId)); int listTypeOrChatId; if (widget.chatId == Chat.typeInvite) { @@ -106,7 +106,6 @@ class _ChatProfileState extends State { if (state.isGroupChat) { return ChatProfileGroup( chatId: widget.chatId, - chatName: state.name, chatColor: state.color, isVerified: _isVerified, ); diff --git a/lib/src/chat/chat_profile_group.dart b/lib/src/chat/chat_profile_group.dart index 4d41e2b8..af38c73c 100644 --- a/lib/src/chat/chat_profile_group.dart +++ b/lib/src/chat/chat_profile_group.dart @@ -48,19 +48,24 @@ import 'package:ox_coi/src/chat/chat_profile_group_contact_item.dart'; import 'package:ox_coi/src/contact/contact_list_bloc.dart'; import 'package:ox_coi/src/contact/contact_list_event_state.dart'; import 'package:ox_coi/src/l10n/localizations.dart'; +import 'package:ox_coi/src/navigation/navigatable.dart'; import 'package:ox_coi/src/navigation/navigation.dart'; +import 'package:ox_coi/src/ui/color.dart'; import 'package:ox_coi/src/ui/dimensions.dart'; -import 'package:ox_coi/src/widgets/avatar.dart'; import 'package:ox_coi/src/widgets/profile_body.dart'; import 'package:ox_coi/src/widgets/profile_header.dart'; +import 'chat_add_group_participants.dart'; +import 'chat_bloc.dart'; +import 'chat_event_state.dart'; +import 'edit_name.dart'; + class ChatProfileGroup extends StatefulWidget { final int chatId; - final String chatName; final Color chatColor; final bool isVerified; - ChatProfileGroup({@required this.chatId, @required this.chatName, @required this.chatColor, @required this.isVerified}); + ChatProfileGroup({@required this.chatId, this.chatColor, this.isVerified}); @override _ChatProfileGroupState createState() => _ChatProfileGroupState(); @@ -68,10 +73,15 @@ class ChatProfileGroup extends StatefulWidget { class _ChatProfileGroupState extends State { ContactListBloc _contactListBloc = ContactListBloc(); + ChatChangeBloc _chatChangeBloc = ChatChangeBloc(); + ChatBloc chatBloc; + Navigation _navigation = Navigation(); + String chatName; @override void initState() { super.initState(); + _navigation.current = Navigatable(Type.chatGroupProfile); _contactListBloc.dispatch(RequestContacts(listTypeOrChatId: widget.chatId)); } @@ -83,34 +93,102 @@ class _ChatProfileGroupState extends State { @override Widget build(BuildContext context) { + chatBloc = BlocProvider.of(context); return BlocBuilder( bloc: _contactListBloc, builder: (context, state) { if (state is ContactListStateSuccess) { var appLocalizations = AppLocalizations.of(context); return Column( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ProfileHeader( - dynamicChildren: [ - ProfileHeaderText(text: widget.chatName), - ProfileHeaderText( - text: appLocalizations.chatProfileGroupMemberCounter(state.contactIds.length), - iconData: widget.isVerified ? Icons.verified_user : null, - ) - ], - color: widget.chatColor, - initialsString: Avatar.getInitials(widget.chatName), + BlocBuilder( + bloc: chatBloc, + builder: (context, state) { + if (state is ChatStateSuccess) { + chatName = state.name; + return ProfileData( + color: widget.chatColor, + text: state.name, + textStyle: Theme.of(context).textTheme.title, + iconData: state.isVerified ? Icons.verified_user : null, + imageActionCallback: _editPhotoCallback, + child: Column( + children: [ + Align( + alignment: Alignment.center, + child: ProfileAvatar( + imagePath: state.avatarPath, + ), + ), + Padding( + padding: EdgeInsets.all(20.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ProfileHeaderText(), + IconButton( + icon: Icon( + Icons.edit, + color: accent, + ), + onPressed: _goToEditName) + ], + ), + ), + ], + )); + } else { + return Container(); + } + }, ), - ProfileActionList(tiles: [ - ProfileAction( - iconData: Icons.delete, - text: appLocalizations.chatProfileLeaveGroupButtonText, - onTap: () => showActionDialog(context, ProfileActionType.leave, _leaveGroup), + Padding( + padding: EdgeInsets.only(left: 20.0), + child: ProfileData( + text: appLocalizations.chatProfileGroupMemberCounter(state.contactIds.length), + child: ProfileMemberHeaderText(), + )), + Divider(), + InkWell( + onTap: () => _navigation.push( + context, MaterialPageRoute(builder: (context) => ChatAddGroupParticipants(chatId: widget.chatId, contactIds: state.contactIds))), + child: Container( + padding: const EdgeInsets.only(left: 16.0, bottom: 12.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CircleAvatar( + radius: listAvatarRadius, + backgroundColor: accent, + foregroundColor: onAccent, + child: Icon(Icons.group_add), + ), + Padding( + padding: EdgeInsets.only(left: 4.0), + child: Text( + AppLocalizations.of(context).chatProfileAddParticipantsButtonText, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.subhead.apply(color: accent), + ), + ) + ], + ), ), - ]), - Divider(height: dividerHeight), - _buildGroupMemberList(state) + ), + Padding( + padding: EdgeInsets.only(left: 8.0, right: 8.0), + child: _buildGroupMemberList(state), + ), + Divider( + height: dividerHeight, + ), + ProfileAction( + iconData: Icons.delete, + text: appLocalizations.chatProfileLeaveGroupButtonText, + onTap: () => showActionDialog(context, ProfileActionType.leave, _leaveGroup), + color: Colors.red, + ), ], ); } else { @@ -120,16 +198,20 @@ class _ChatProfileGroupState extends State { ); } + _editPhotoCallback(String avatarPath){ + _chatChangeBloc.dispatch(SetImagePath(chatId: widget.chatId, newPath: avatarPath)); + chatBloc.dispatch(RequestChat(chatId: widget.chatId)); + } + ListView _buildGroupMemberList(ContactListStateSuccess state) { return ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - padding: EdgeInsets.only(top: listItemPadding), itemCount: state.contactIds.length, itemBuilder: (BuildContext context, int index) { var contactId = state.contactIds[index]; var key = "$contactId-${state.contactLastUpdateValues[index]}"; - return ChatProfileGroupContactItem(contactId: contactId, key: key); + return ChatProfileGroupContactItem(chatId: widget.chatId, contactId: contactId, showMoreButton: true, key: key); }); } @@ -140,4 +222,22 @@ class _ChatProfileGroupState extends State { Navigation navigation = Navigation(); navigation.popUntil(context, ModalRoute.withName(Navigation.root)); } + + void _goToEditName() { + _navigation.push( + context, + MaterialPageRoute( + builder: (context) { + return BlocProvider.value( + value: chatBloc, + child: EditName( + chatId: widget.chatId, + actualName: chatName, + title: AppLocalizations.of(context).editGroupNameTitle, + ), + ); + }, + ), + ); + } } diff --git a/lib/src/chat/chat_profile_group_contact_item.dart b/lib/src/chat/chat_profile_group_contact_item.dart index 055a1317..9c34d05a 100644 --- a/lib/src/chat/chat_profile_group_contact_item.dart +++ b/lib/src/chat/chat_profile_group_contact_item.dart @@ -39,43 +39,135 @@ * or FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public License 2.0 * for more details. */ - + +import 'package:delta_chat_core/delta_chat_core.dart' as Core; import 'package:flutter/material.dart'; +import 'package:ox_coi/src/contact/contact_details.dart'; import 'package:ox_coi/src/contact/contact_item_bloc.dart'; import 'package:ox_coi/src/contact/contact_item_builder_mixin.dart'; import 'package:ox_coi/src/contact/contact_item_event_state.dart'; import 'package:ox_coi/src/data/contact_repository.dart'; +import 'package:ox_coi/src/navigation/navigatable.dart'; +import 'package:ox_coi/src/navigation/navigation.dart'; +import 'package:ox_coi/src/chat/chat.dart'; + +import 'chat_change_bloc.dart'; +import 'chat_change_event_state.dart'; +import 'chat_create_mixin.dart'; + +enum GroupParticipantActions{ + info, + sendMessage, + remove +} class ChatProfileGroupContactItem extends StatefulWidget { + final int chatId; final int contactId; + final bool showMoreButton; - ChatProfileGroupContactItem({@required this.contactId, key}) : super(key: Key(key)); + ChatProfileGroupContactItem({this.chatId, this.contactId, this.showMoreButton = false, key}) : super(key: Key(key)); @override - _ChatProfileGroupContactItemState createState() => _ChatProfileGroupContactItemState(); - } + _ChatProfileGroupContactItemState createState() => _ChatProfileGroupContactItemState(); +} - class _ChatProfileGroupContactItemState extends State with ContactItemBuilder{ - ContactItemBloc _contactBloc = ContactItemBloc(); +class _ChatProfileGroupContactItemState extends State with ContactItemBuilder, CreateChatMixin { + ContactItemBloc _contactBloc = ContactItemBloc(); + ChatChangeBloc _chatChangeBloc = ChatChangeBloc(); + Navigation _navigation = Navigation(); + List choices; + void _select(GroupPopupMenu choice) { + switch(choice.action){ + case GroupParticipantActions.info: + goToProfile("",""); + break; + case GroupParticipantActions.sendMessage: + createChat(); + break; + case GroupParticipantActions.remove: + _removeParticipant(); + } + } - @override - void initState() { - super.initState(); - _contactBloc.dispatch(RequestContact(contactId: widget.contactId, listType: ContactRepository.validContacts)); - } + @override + void initState() { + super.initState(); + _contactBloc.dispatch(RequestContact(contactId: widget.contactId, listType: ContactRepository.validContacts)); + if(widget.contactId != Core.Contact.idSelf){ + choices = participantChoices; + }else{ + choices = meChoices; + } + } - @override - void dispose() { + @override + void dispose() { _contactBloc.dispose(); super.dispose(); } @override - Widget build(BuildContext context) { - return getAvatarItemBlocBuilder(_contactBloc, onContactTapped); - } - - onContactTapped(String name, String email) async { - //Not implemented yet - } - } + Widget build(BuildContext context) { + return getAvatarItemBlocBuilder(bloc: _contactBloc, onContactTapped: goToProfile, moreButton: getMoreButton()); + } + + goToProfile(String title, String subtitle) { + _navigation.push( + context, + MaterialPageRoute(builder: (context) => ContactDetails(contactId: widget.contactId)), + ); + } + + createChat(){ + createChatFromContact(context, widget.contactId, _handleCreateChatStateChange); + } + + getMoreButton(){ + return PopupMenuButton( + elevation: 3.2, + onSelected: _select, + itemBuilder: (BuildContext context) { + return choices.map((GroupPopupMenu choice) { + return PopupMenuItem( + value: choice, + child: Text(choice.title), + ); + }).toList(); + }, + ); + } + + void _removeParticipant() { + if(widget.contactId != Core.Contact.idSelf) { + _chatChangeBloc.dispatch(ChatRemoveParticipant(chatId: widget.chatId, contactId: widget.contactId)); + } + } + + _handleCreateChatStateChange(int chatId) { + _navigation.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (context) => Chat(chatId: chatId)), + ModalRoute.withName(Navigation.root), + Navigatable(Type.chat) + ); + } +} + +List participantChoices = [ + GroupPopupMenu(title: 'Info', action: GroupParticipantActions.info), + GroupPopupMenu(title: 'Send message', action: GroupParticipantActions.sendMessage), + GroupPopupMenu(title: 'Remove from group', action: GroupParticipantActions.remove), +]; + +List meChoices = [ + GroupPopupMenu(title: 'Info', action: GroupParticipantActions.info), + GroupPopupMenu(title: 'Send message', action: GroupParticipantActions.sendMessage), +]; + +class GroupPopupMenu { + String title; + GroupParticipantActions action; + + GroupPopupMenu({this.title, this.action}); +} diff --git a/lib/src/chat/chat_profile_single.dart b/lib/src/chat/chat_profile_single.dart index d118f398..52346bb0 100644 --- a/lib/src/chat/chat_profile_single.dart +++ b/lib/src/chat/chat_profile_single.dart @@ -52,7 +52,7 @@ import 'package:ox_coi/src/contact/contact_item_event_state.dart'; import 'package:ox_coi/src/data/contact_repository.dart'; import 'package:ox_coi/src/l10n/localizations.dart'; import 'package:ox_coi/src/navigation/navigation.dart'; -import 'package:ox_coi/src/widgets/avatar.dart'; +import 'package:ox_coi/src/ui/color.dart'; import 'package:ox_coi/src/widgets/profile_body.dart'; import 'package:ox_coi/src/widgets/profile_header.dart'; @@ -97,46 +97,60 @@ class _ChatProfileOneToOneState extends State { bloc: _contactItemBloc, builder: (context, state) { if (state is ContactItemStateSuccess) { - return _buildSingleProfileInfo(state.name, state.email, state.color, state.isVerified); + return _buildSingleProfileInfo(state.name, state.email, state.color, state.isVerified, state.imagePath); } else { return Container(); } }); } - Widget _buildSingleProfileInfo(String chatName, String email, Color color, bool isVerified) { + Widget _buildSingleProfileInfo(String chatName, String email, Color color, bool isVerified, String imagePath) { var appLocalizations = AppLocalizations.of(context); - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ProfileHeader( - dynamicChildren: [ - ProfileHeaderText(text: chatName), - ProfileCopyableHeaderText( - text: email, - toastMessage: appLocalizations.chatProfileClipboardToastMessage, - iconData: isVerified ? Icons.verified_user : null, - ), - ], - color: color, - initialsString: Avatar.getInitials(chatName, email), - ), - ProfileActionList(tiles: [ - if (!widget.isSelfTalk) - ProfileAction( - iconData: Icons.block, - text: appLocalizations.chatProfileBlockContactButtonText, - onTap: () => showActionDialog(context, ProfileActionType.block, _blockContact), - ), - if (!isInvite()) - ProfileAction( - iconData: Icons.delete, - text: appLocalizations.chatProfileDeleteChatButtonText, - onTap: () => showActionDialog(context, ProfileActionType.deleteChat, _deleteChat), - ), - ]), - ], + return Container( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Align( + alignment: Alignment.center, + child: ProfileData( + color: color, + child: ProfileAvatar( + imagePath: imagePath, + ), + )), + ProfileData( + text: chatName, + textStyle: Theme.of(context).textTheme.title, + child: ProfileHeaderText(), + ), + Padding( + padding: EdgeInsets.all(16.0), + child: ProfileData( + text: email, + textStyle: Theme.of(context).textTheme.subtitle, + iconData: isVerified ? Icons.verified_user : null, + child: ProfileCopyableHeaderText( + toastMessage: appLocalizations.chatProfileClipboardToastMessage, + ), + )), + ProfileActionList(tiles: [ + if (!widget.isSelfTalk) + ProfileAction( + iconData: Icons.block, + text: appLocalizations.chatProfileBlockContactButtonText, + onTap: () => showActionDialog(context, ProfileActionType.block, _blockContact), + ), + if (!isInvite()) + ProfileAction( + iconData: Icons.delete, + color: error, + text: appLocalizations.chatProfileDeleteChatButtonText, + onTap: () => showActionDialog(context, ProfileActionType.deleteChat, _deleteChat), + ), + ]), + ], + ), ); } diff --git a/lib/src/chat/edit_name.dart b/lib/src/chat/edit_name.dart new file mode 100644 index 00000000..731f8a8c --- /dev/null +++ b/lib/src/chat/edit_name.dart @@ -0,0 +1,120 @@ +/* + * OPEN-XCHANGE legal information + * + * All intellectual property rights in the Software are protected by + * international copyright laws. + * + * + * In some countries OX, OX Open-Xchange and open xchange + * as well as the corresponding Logos OX Open-Xchange and OX are registered + * trademarks of the OX Software GmbH group of companies. + * The use of the Logos is not covered by the Mozilla Public License 2.0 (MPL 2.0). + * Instead, you are allowed to use these Logos according to the terms and + * conditions of the Creative Commons License, Version 2.5, Attribution, + * Non-commercial, ShareAlike, and the interpretation of the term + * Non-commercial applicable to the aforementioned license is published + * on the web site https://www.open-xchange.com/terms-and-conditions/. + * + * Please make sure that third-party modules and libraries are used + * according to their respective licenses. + * + * Any modifications to this package must retain all copyright notices + * of the original copyright holder(s) for the original code used. + * + * After any such modifications, the original and derivative code shall remain + * under the copyright of the copyright holder(s) and/or original author(s) as stated here: + * https://www.open-xchange.com/legal/. The contributing author shall be + * given Attribution for the derivative code and a license granting use. + * + * Copyright (C) 2016-2020 OX Software GmbH + * Mail: info@open-xchange.com + * + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the Mozilla Public License 2.0 + * for more details. + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:ox_coi/src/l10n/localizations.dart'; +import 'package:ox_coi/src/navigation/navigatable.dart'; +import 'package:ox_coi/src/navigation/navigation.dart'; +import 'package:ox_coi/src/widgets/validatable_text_form_field.dart'; + +import 'chat_bloc.dart'; +import 'chat_change_bloc.dart'; +import 'chat_change_event_state.dart'; +import 'chat_event_state.dart'; + +class EditName extends StatefulWidget { + final int chatId; + final String actualName; + final String title; + + EditName({@required this.chatId, @required this.actualName, @required this.title}); + + @override + _EditNameState createState() => _EditNameState(); +} + +class _EditNameState extends State { + ChatChangeBloc _chatChangeBloc = ChatChangeBloc(); + ChatBloc _chatBloc; + Navigation _navigation = Navigation(); + + ValidatableTextFormField _nameField = ValidatableTextFormField( + (context) => AppLocalizations.of(context).name, + hintText: (context) => AppLocalizations.of(context).setNameTextFieldHint, + needValidation: true, + validationHint: (context) => AppLocalizations.of(context).validatableTextFormFieldHintEmptyString, + ); + GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + _navigation.current = Navigatable(Type.editName); + _chatBloc = BlocProvider.of(context); + _nameField.controller.text = widget.actualName; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: new IconButton( + icon: new Icon(Icons.close), + onPressed: () => _navigation.pop(context), + ), + title: Text(widget.title), + actions: [IconButton(icon: Icon(Icons.check), onPressed: saveNewName)], + ), + body: BlocListener( + bloc: _chatChangeBloc, + listener: (context, state) { + if (state is ChangeNameSuccess) { + _chatBloc.dispatch(RequestChat(chatId: widget.chatId)); + _navigation.pop(context); + } + }, + child: Padding( + padding: EdgeInsets.all(20.0), + child: Form( + key: _formKey, + child: _nameField, + )), + )); + } + + void saveNewName() { + if (_formKey.currentState.validate()) { + _chatChangeBloc.dispatch(SetName(chatId: widget.chatId, newName: _nameField.controller.text)); + } + } +} diff --git a/lib/src/chatlist/chat_list_bloc.dart b/lib/src/chatlist/chat_list_bloc.dart index 7dded757..31709430 100644 --- a/lib/src/chatlist/chat_list_bloc.dart +++ b/lib/src/chatlist/chat_list_bloc.dart @@ -71,7 +71,7 @@ class ChatListBloc extends Bloc { ChatListState get initialState => ChatListStateInitial(); @override - Stream mapEventToState(ChatListState currentState, ChatListEvent event) async* { + Stream mapEventToState(ChatListEvent event) async* { if (event is RequestChatList) { _currentSearch = null; yield ChatListStateLoading(); @@ -113,7 +113,7 @@ class ChatListBloc extends Bloc { void setupChatListListener() { if (_repositoryStreamHandler == null) { - _repositoryStreamHandler = RepositoryMultiEventStreamHandler(Type.publish, [Dcc.Event.incomingMsg, Dcc.Event.msgsChanged], _onChatListChanged); + _repositoryStreamHandler = RepositoryMultiEventStreamHandler(Type.publish, [Dcc.Event.chatModified, Dcc.Event.incomingMsg, Dcc.Event.msgsChanged], _onChatListChanged); _chatRepository.addListener(_repositoryStreamHandler); final messageListObservable = Observable(_messageListBloc.state); diff --git a/lib/src/chatlist/chat_list_item.dart b/lib/src/chatlist/chat_list_item.dart index bf6382f4..d67c7a3c 100644 --- a/lib/src/chatlist/chat_list_item.dart +++ b/lib/src/chatlist/chat_list_item.dart @@ -92,6 +92,7 @@ class _ChatListItemState extends State { int freshMessageCount = 0; int timestamp = 0; String preview; + String imagePath = ""; if (state is ChatStateSuccess) { name = state.name; subTitle = state.subTitle; @@ -99,6 +100,7 @@ class _ChatListItemState extends State { freshMessageCount = state.freshMessageCount; timestamp = state.timestamp; preview = state.preview; + imagePath = state.avatarPath; } else { name = ""; subTitle = ""; @@ -110,6 +112,7 @@ class _ChatListItemState extends State { title: name, subTitle: _chatBloc.isGroup ? subTitle : preview, color: color, + imagePath: imagePath, freshMessageCount: freshMessageCount, timestamp: timestamp, subTitleIcon: _chatBloc.isGroup diff --git a/lib/src/contact/contact_change_bloc.dart b/lib/src/contact/contact_change_bloc.dart index 33cf6821..f0790036 100644 --- a/lib/src/contact/contact_change_bloc.dart +++ b/lib/src/contact/contact_change_bloc.dart @@ -60,7 +60,7 @@ class ContactChangeBloc extends Bloc wit ContactChangeState get initialState => ContactChangeStateInitial(); @override - Stream mapEventToState(ContactChangeState currentState, ContactChangeEvent event) async* { + Stream mapEventToState(ContactChangeEvent event) async* { if (event is ChangeContact) { yield ContactChangeStateLoading(); try { diff --git a/lib/src/contact/contact_details.dart b/lib/src/contact/contact_details.dart index 0b29b160..5918806e 100644 --- a/lib/src/contact/contact_details.dart +++ b/lib/src/contact/contact_details.dart @@ -50,9 +50,9 @@ import 'package:ox_coi/src/data/contact_repository.dart'; import 'package:ox_coi/src/l10n/localizations.dart'; import 'package:ox_coi/src/navigation/navigatable.dart'; import 'package:ox_coi/src/navigation/navigation.dart'; +import 'package:ox_coi/src/ui/color.dart'; import 'package:ox_coi/src/utils/error.dart'; import 'package:ox_coi/src/utils/toast.dart'; -import 'package:ox_coi/src/widgets/avatar.dart'; import 'package:ox_coi/src/widgets/profile_body.dart'; import 'package:ox_coi/src/widgets/profile_header.dart'; import 'package:rxdart/rxdart.dart'; @@ -87,32 +87,43 @@ class ContactDetails extends StatelessWidget with CreateChatMixin { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - ProfileHeader( - dynamicChildren: [ - ProfileHeaderText(text: state.name), - ProfileCopyableHeaderText( - text: state.email, - toastMessage: appLocalizations.chatProfileClipboardToastMessage, - iconData: state.isVerified ? Icons.verified_user : null, - ), - ], - color: state.color, - initialsString: Avatar.getInitials(state.name, state.email), + Align( + alignment: Alignment.center, + child: ProfileData( + color: state.color, + child: ProfileAvatar( + imagePath: state.imagePath, + ), + )), + ProfileData( + text: state.name, + textStyle: Theme.of(context).textTheme.title, + child: ProfileHeaderText(), ), + ProfileData( + text: state.email, + textStyle: Theme.of(context).textTheme.subtitle, + iconData: state.isVerified ? Icons.verified_user : null, + child: ProfileCopyableHeaderText( + toastMessage: appLocalizations.chatProfileClipboardToastMessage, + )), ProfileActionList(tiles: [ ProfileAction( iconData: Icons.chat, text: appLocalizations.contactsOpenChat, + color: accent, onTap: () => createChatFromContact(context, contactId), ), ProfileAction( iconData: Icons.edit, text: appLocalizations.contactChangeEditTitle, + color: accent, onTap: () => _editContact(context, state.name, state.email), ), ProfileAction( iconData: Icons.delete, text: appLocalizations.contactChangeDeleteTitle, + color: error, onTap: () => showActionDialog( context, ProfileActionType.deleteContact, @@ -135,8 +146,8 @@ class ContactDetails extends StatelessWidget with CreateChatMixin { ); } - void _editContact(BuildContext context, String name, String email) { - return _navigation.push( + void _editContact(BuildContext context, String name, String email) async { + return await _navigation.push( context, MaterialPageRoute( builder: (context) => ContactChange( diff --git a/lib/src/contact/contact_import_bloc.dart b/lib/src/contact/contact_import_bloc.dart index b907482c..c048be27 100644 --- a/lib/src/contact/contact_import_bloc.dart +++ b/lib/src/contact/contact_import_bloc.dart @@ -60,7 +60,7 @@ class ContactImportBloc extends Bloc { ContactImportState get initialState => ContactsImportInitial(); @override - Stream mapEventToState(ContactImportState currentState, ContactImportEvent event) async* { + Stream mapEventToState(ContactImportEvent event) async* { if (event is MarkContactsAsInitiallyLoaded) { markContactsAsInitiallyLoaded(); } else if (event is PerformImport) { diff --git a/lib/src/contact/contact_item.dart b/lib/src/contact/contact_item.dart index 2673f9be..04c1dc3b 100644 --- a/lib/src/contact/contact_item.dart +++ b/lib/src/contact/contact_item.dart @@ -93,7 +93,7 @@ class _ContactItemState extends State with ContactItemBuilder, Crea @override Widget build(BuildContext context) { - return getAvatarItemBlocBuilder(_contactBloc, onContactTapped); + return getAvatarItemBlocBuilder(bloc: _contactBloc, onContactTapped: onContactTapped); } onContactTapped(String name, String email) async { diff --git a/lib/src/contact/contact_item_bloc.dart b/lib/src/contact/contact_item_bloc.dart index 8c5007f7..4c512fb1 100644 --- a/lib/src/contact/contact_item_bloc.dart +++ b/lib/src/contact/contact_item_bloc.dart @@ -60,7 +60,7 @@ class ContactItemBloc extends Bloc { ContactItemState get initialState => ContactItemStateInitial(); @override - Stream mapEventToState(ContactItemState currentState, ContactItemEvent event) async* { + Stream mapEventToState(ContactItemEvent event) async* { if (event is RequestContact) { _contactId = event.contactId; _contactRepository = RepositoryManager.get(RepositoryType.contact, event.listType); @@ -71,7 +71,7 @@ class ContactItemBloc extends Bloc { yield ContactItemStateFailure(error: error.toString()); } } else if (event is ContactLoaded) { - yield ContactItemStateSuccess(name: event.name, email: event.email, color: event.color, isVerified: event.isVerified); + yield ContactItemStateSuccess(name: event.name, email: event.email, color: event.color, isVerified: event.isVerified, imagePath: event.imagePath); } } @@ -81,12 +81,14 @@ class ContactItemBloc extends Bloc { String email = await contact.getAddress(); int colorValue = await contact.getColor(); bool isVerified = await contact.isVerified(); + String imagePath = await contact.getProfileImage(); Color color = rgbColorFromInt(colorValue); dispatch(ContactLoaded( name: name, email: email, color: color, isVerified: isVerified, + imagePath: imagePath, )); } } diff --git a/lib/src/contact/contact_item_builder_mixin.dart b/lib/src/contact/contact_item_builder_mixin.dart index 508ce7d2..c9950a4b 100644 --- a/lib/src/contact/contact_item_builder_mixin.dart +++ b/lib/src/contact/contact_item_builder_mixin.dart @@ -73,7 +73,7 @@ mixin ContactItemBuilder { ); } - BlocBuilder getAvatarItemBlocBuilder(ContactItemBloc bloc, Function onContactTapped, [bool isSelected = false]) { + BlocBuilder getAvatarItemBlocBuilder({ContactItemBloc bloc, Function onContactTapped, bool isSelected = false, PopupMenuButton moreButton}) { return BlocBuilder( bloc: bloc, builder: (context, state) { @@ -85,6 +85,8 @@ mixin ContactItemBuilder { avatarIcon: isSelected ? Icons.check : null, onTap: onContactTapped, isVerified: state.isVerified != null ? state.isVerified : false, + imagePath: state.imagePath, + moreButton: moreButton, ); } else if (state is ContactItemStateFailure) { return new Text(state.error); diff --git a/lib/src/contact/contact_item_event_state.dart b/lib/src/contact/contact_item_event_state.dart index 1810928b..81a1054f 100644 --- a/lib/src/contact/contact_item_event_state.dart +++ b/lib/src/contact/contact_item_event_state.dart @@ -57,8 +57,9 @@ class ContactLoaded extends ContactItemEvent { final String email; final Color color; final bool isVerified; + final String imagePath; - ContactLoaded({ @required this.name, @required this.email, @required this.color, @required this.isVerified}); + ContactLoaded({@required this.name, @required this.email, @required this.color, @required this.isVerified, @required this.imagePath}); } abstract class ContactItemState {} @@ -72,8 +73,9 @@ class ContactItemStateSuccess extends ContactItemState { final String email; final Color color; final bool isVerified; + final String imagePath; - ContactItemStateSuccess({@required this.name, @required this.email, @required this.color, @required this.isVerified}); + ContactItemStateSuccess({@required this.name, @required this.email, @required this.color, @required this.isVerified, @required this.imagePath}); } class ContactItemStateFailure extends ContactItemState { diff --git a/lib/src/contact/contact_item_selectable.dart b/lib/src/contact/contact_item_selectable.dart index 09c24e63..4b5e1412 100644 --- a/lib/src/contact/contact_item_selectable.dart +++ b/lib/src/contact/contact_item_selectable.dart @@ -68,7 +68,7 @@ class _ContactItemSelectableState extends State with Cont @override Widget build(BuildContext context) { - return getAvatarItemBlocBuilder(_contactBloc, _onContactTapped, widget.isSelected); + return getAvatarItemBlocBuilder(bloc: _contactBloc, onContactTapped: _onContactTapped, isSelected: widget.isSelected); } _onContactTapped(String name, String email) { diff --git a/lib/src/contact/contact_list_bloc.dart b/lib/src/contact/contact_list_bloc.dart index 2ab2d569..c77549b3 100644 --- a/lib/src/contact/contact_list_bloc.dart +++ b/lib/src/contact/contact_list_bloc.dart @@ -53,7 +53,7 @@ import 'package:ox_coi/src/utils/text.dart'; class ContactListBloc extends Bloc with ContactRepositoryUpdater { Repository _contactRepository; - RepositoryEventStreamHandler _repositoryStreamHandler; + RepositoryMultiEventStreamHandler _repositoryStreamHandler; int _listTypeOrChatId; List _contactsSelected = List(); String _currentSearch; @@ -66,7 +66,7 @@ class ContactListBloc extends Bloc with Cont ContactListState get initialState => ContactListStateInitial(); @override - Stream mapEventToState(ContactListState currentState, ContactListEvent event) async* { + Stream mapEventToState(ContactListEvent event) async* { if (event is RequestContacts) { yield ContactListStateLoading(); try { @@ -111,7 +111,7 @@ class ContactListBloc extends Bloc with Cont void _setupContactListener() async { if (_repositoryStreamHandler == null) { - _repositoryStreamHandler = RepositoryEventStreamHandler(Type.publish, Event.contactsChanged, _onContactsChanged); + _repositoryStreamHandler = RepositoryMultiEventStreamHandler(Type.publish, [Event.contactsChanged, Event.chatModified], _onContactsChanged); _contactRepository.addListener(_repositoryStreamHandler); } } diff --git a/lib/src/contact/contact_list_event_state.dart b/lib/src/contact/contact_list_event_state.dart index 23a3a450..ffd67219 100644 --- a/lib/src/contact/contact_list_event_state.dart +++ b/lib/src/contact/contact_list_event_state.dart @@ -50,6 +50,13 @@ class RequestContacts extends ContactListEvent { RequestContacts({@required this.listTypeOrChatId}); } +class RequestContactsForGroup extends ContactListEvent { + final int listTypeOrChatId; + final int chatId; + + RequestContactsForGroup({@required this.listTypeOrChatId, @required this.chatId}); +} + class ContactsChanged extends ContactListEvent {} class ContactsSelectionChanged extends ContactListEvent { diff --git a/lib/src/data/chat_repository.dart b/lib/src/data/chat_repository.dart index 677bea7a..3ed2f979 100644 --- a/lib/src/data/chat_repository.dart +++ b/lib/src/data/chat_repository.dart @@ -49,7 +49,7 @@ class ChatRepository extends Repository { @override onData(Event event) async { - if (event.hasType(Event.incomingMsg) || event.hasType(Event.msgsChanged)) { + if (event.hasType(Event.incomingMsg) || event.hasType(Event.chatModified) || event.hasType(Event.msgsChanged)) { int chatId = event.data1; await setupChatListAfterUpdate(chatId); } @@ -76,6 +76,9 @@ class ChatRepository extends Repository { if (chatSummary != null) { updatedChat?.set(ChatExtension.chatSummary, chatSummary); } + updatedChat.reloadValue(Chat.methodChatGetSubtitle); + updatedChat.reloadValue(Chat.methodChatGetName); + updatedChat.reloadValue(Chat.methodChatGetProfileImage); updatedChat.setLastUpdate(); } update(ids: chatIds); diff --git a/lib/src/data/contact_repository.dart b/lib/src/data/contact_repository.dart index bb02eab6..fd89ee1c 100644 --- a/lib/src/data/contact_repository.dart +++ b/lib/src/data/contact_repository.dart @@ -55,7 +55,7 @@ class ContactRepository extends Repository with ContactRepositoryUpdate @override onData(Event event) async { - if (event.eventId == Event.contactsChanged) { + if (event.eventId == Event.contactsChanged || event.eventId == Event.chatModified) { List contactIds = await getContactIdsAfterUpdate(_listTypeOrChatId); update(ids: contactIds); } diff --git a/lib/src/debug/debug_viewer_bloc.dart b/lib/src/debug/debug_viewer_bloc.dart index 5a9ce5c5..04e2ff30 100644 --- a/lib/src/debug/debug_viewer_bloc.dart +++ b/lib/src/debug/debug_viewer_bloc.dart @@ -58,7 +58,7 @@ class DebugViewerBloc extends Bloc { DebugViewerState get initialState => DebugViewerStateInitial(); @override - Stream mapEventToState(DebugViewerState currentState, DebugViewerEvent event) async* { + Stream mapEventToState(DebugViewerEvent event) async* { if (event is RequestLog) { try { loadLog(); diff --git a/lib/src/flagged/flagged_bloc.dart b/lib/src/flagged/flagged_bloc.dart index 082707e9..ec647f49 100644 --- a/lib/src/flagged/flagged_bloc.dart +++ b/lib/src/flagged/flagged_bloc.dart @@ -56,8 +56,8 @@ class FlaggedBloc extends Bloc { FlaggedState get initialState => FlaggedStateInitial(); @override - Stream mapEventToState(FlaggedState currentState, FlaggedEvent event) async* { - if (event is RequestFlaggedMessages) { + Stream mapEventToState(FlaggedEvent event) async*{ + if(event is RequestFlaggedMessages){ yield FlaggedStateLoading(); try { _messageListRepository = RepositoryManager.get(RepositoryType.chatMessage, Chat.typeStarred); diff --git a/lib/src/l10n/localizations.dart b/lib/src/l10n/localizations.dart index 4daa10fe..9d30105b 100644 --- a/lib/src/l10n/localizations.dart +++ b/lib/src/l10n/localizations.dart @@ -420,8 +420,18 @@ class AppLocalizations { String get chatProfileLeaveGroupButtonText => Intl.message('Leave group', name: 'chatProfileLeaveGroupButtonText'); + String get chatProfileAddParticipantsButtonText => Intl.message('Add participants', name: 'chatProfileAddParticipantsButtonText'); + + String get chatProfileAddParticipantsEmptyList => Intl.message('All your contacts are in this chat.', name: 'chatProfileAddParticipantsEmptyList'); + + String get chatProfileRemoveParticipantsButtonText => Intl.message('Remove participant', name: 'chatProfileRemoveParticipantsButtonText'); + String chatProfileGroupMemberCounter(memberCount) => - Intl.message('$memberCount member(s)', name: 'chatProfileGroupMemberCounter', args: [memberCount]); + Intl.message('$memberCount Participant(s)', name: 'chatProfileGroupMemberCounter', args: [memberCount]); + + String get setNameTextFieldHint => Intl.message('Set a name', name: 'setNameTextFieldHint'); + + String get editGroupNameTitle => Intl.message('Rename group', name: 'editGroupNameTitle'); // Create chat String get createChatTitle => Intl.message('Create chat', name: 'createChatTitle'); diff --git a/lib/src/log/log_bloc_delegate.dart b/lib/src/log/log_bloc_delegate.dart index 764437b1..f9ae8055 100644 --- a/lib/src/log/log_bloc_delegate.dart +++ b/lib/src/log/log_bloc_delegate.dart @@ -47,12 +47,17 @@ class LogBlocDelegate implements BlocDelegate { final Logger _logger = Logger("blocDelegate"); @override - void onTransition(Transition transition) { + void onTransition(Bloc bloc, Transition transition) { _logger.info(transition.toString()); } @override - void onError(Object error, StackTrace stacktrace) { + void onError(Bloc bloc, Object error, StackTrace stacktrace) { _logger.warning("Error: $error (Stacktrace: $stacktrace)"); } + + @override + void onEvent(Bloc bloc, Object event) { + // TODO: implement onEvent + } } diff --git a/lib/src/log/log_manager.dart b/lib/src/log/log_manager.dart index f2f0c090..f6b84084 100644 --- a/lib/src/log/log_manager.dart +++ b/lib/src/log/log_manager.dart @@ -70,7 +70,7 @@ class LogManager { get currentLogFile => _logFile; void setup({@required bool logToFile, @required Level logLevel}) async { - BlocSupervisor().delegate = LogBlocDelegate(); + BlocSupervisor.delegate = LogBlocDelegate(); if (logToFile) { _logFile = await _setupLogFile(); await manageLogFiles(); diff --git a/lib/src/login/login_bloc.dart b/lib/src/login/login_bloc.dart index 15b517a4..264102f6 100644 --- a/lib/src/login/login_bloc.dart +++ b/lib/src/login/login_bloc.dart @@ -69,9 +69,9 @@ class LoginBloc extends Bloc { LoginState get initialState => LoginStateInitial(); @override - Stream mapEventToState(LoginState currentState, LoginEvent event) async* { - if (event is RequestProviders) { - try { + Stream mapEventToState(LoginEvent event) async* { + if(event is RequestProviders){ + try{ _loadProviders(event.type); } catch (error) { yield LoginStateFailure(error: error.toString()); diff --git a/lib/src/main/main_bloc.dart b/lib/src/main/main_bloc.dart index ac25add7..bc26b71c 100644 --- a/lib/src/main/main_bloc.dart +++ b/lib/src/main/main_bloc.dart @@ -66,7 +66,7 @@ class MainBloc extends Bloc { MainState get initialState => MainStateInitial(); @override - Stream mapEventToState(MainState currentState, MainEvent event) async* { + Stream mapEventToState(MainEvent event) async* { if (event is PrepareApp) { yield MainStateLoading(); try { diff --git a/lib/src/message/message_attachment_bloc.dart b/lib/src/message/message_attachment_bloc.dart index 14b4a51d..d05fe0e2 100644 --- a/lib/src/message/message_attachment_bloc.dart +++ b/lib/src/message/message_attachment_bloc.dart @@ -56,7 +56,7 @@ class MessageAttachmentBloc extends Bloc MessageAttachmentStateInitial(); @override - Stream mapEventToState(MessageAttachmentState currentState, MessageAttachmentEvent event) async* { + Stream mapEventToState(MessageAttachmentEvent event) async* { if (event is RequestAttachment) { _messageListRepository = RepositoryManager.get(RepositoryType.chatMessage, event.chatId); yield MessageAttachmentStateLoading(); diff --git a/lib/src/message/message_change_bloc.dart b/lib/src/message/message_change_bloc.dart index ae7b31b3..ec5f469f 100644 --- a/lib/src/message/message_change_bloc.dart +++ b/lib/src/message/message_change_bloc.dart @@ -55,7 +55,7 @@ class MessageChangeBloc extends Bloc { MessageChangeState get initialState => MessageChangeStateInitial(); @override - Stream mapEventToState(MessageChangeState currentState, MessageChangeEvent event) async* { + Stream mapEventToState(MessageChangeEvent event) async* { if (event is DeleteMessage) { yield MessageChangeStateLoading(); try { diff --git a/lib/src/message/message_item_bloc.dart b/lib/src/message/message_item_bloc.dart index 45591be8..4a5414e5 100644 --- a/lib/src/message/message_item_bloc.dart +++ b/lib/src/message/message_item_bloc.dart @@ -62,7 +62,7 @@ class MessageItemBloc extends Bloc { MessageItemState get initialState => MessageItemStateInitial(); @override - Stream mapEventToState(MessageItemState currentState, MessageItemEvent event) async* { + Stream mapEventToState(MessageItemEvent event) async* { if (event is RequestMessage) { try { var chatId = event.chatId; diff --git a/lib/src/message/message_list_bloc.dart b/lib/src/message/message_list_bloc.dart index 4f647b4a..a68e3d67 100644 --- a/lib/src/message/message_list_bloc.dart +++ b/lib/src/message/message_list_bloc.dart @@ -61,7 +61,7 @@ class MessageListBloc extends Bloc with Invi MessageListState get initialState => MessagesStateInitial(); @override - Stream mapEventToState(MessageListState currentState, MessageListEvent event) async* { + Stream mapEventToState(MessageListEvent event) async* { if (event is RequestMessages) { yield MessagesStateLoading(); try { diff --git a/lib/src/navigation/navigatable.dart b/lib/src/navigation/navigatable.dart index 5d491224..11be4b2e 100644 --- a/lib/src/navigation/navigatable.dart +++ b/lib/src/navigation/navigatable.dart @@ -46,10 +46,12 @@ import 'package:flutter/foundation.dart'; enum Type { antiMobbingList, chat, + chatAddGroupParticipants, chatCreate, chatCreateGroupParticipants, chatCreateGroupSettings, chatDeleteDialog, + chatGroupProfile, chatList, chatLeaveGroupDialog, chatProfile, @@ -64,6 +66,7 @@ enum Type { contactProfile, contactUnblockDialog, debugViewer, + editName, flagged, login, loginProviderList, diff --git a/lib/src/navigation/navigation.dart b/lib/src/navigation/navigation.dart index 32871e1f..bf3e9bd1 100644 --- a/lib/src/navigation/navigation.dart +++ b/lib/src/navigation/navigation.dart @@ -106,10 +106,10 @@ class Navigation { Navigation._internal(); - void push(BuildContext context, MaterialPageRoute route) { + Future push(BuildContext context, MaterialPageRoute route) { _logger.info("Push"); Navigatable savedNavigatable = _navigationStack.last; - Navigator.push(context, route).then((value) { + return Navigator.push(context, route).then((value) { current = savedNavigatable; }); } @@ -138,9 +138,9 @@ class Navigation { }); } - void pop(BuildContext context) { + void pop(BuildContext context, {Object result}) { _logger.info("Pop latest"); - Navigator.pop(context); + Navigator.pop(context, result); } void popUntil(BuildContext context, RoutePredicate predicate) { diff --git a/lib/src/qr/qr_bloc.dart b/lib/src/qr/qr_bloc.dart index 9cb64408..6189a897 100644 --- a/lib/src/qr/qr_bloc.dart +++ b/lib/src/qr/qr_bloc.dart @@ -62,7 +62,7 @@ class QrBloc extends Bloc{ QrState get initialState => QrStateInitial(); @override - Stream mapEventToState(QrState currentState, QrEvent event) async*{ + Stream mapEventToState(QrEvent event) async*{ if(event is RequestQrText){ yield QrStateLoading(progress: 0); try{ diff --git a/lib/src/settings/settings_about_bloc.dart b/lib/src/settings/settings_about_bloc.dart index 3be47296..5c12d66e 100644 --- a/lib/src/settings/settings_about_bloc.dart +++ b/lib/src/settings/settings_about_bloc.dart @@ -51,7 +51,7 @@ class SettingsAboutBloc extends Bloc { SettingsAboutState get initialState => SettingsAboutStateInitial(); @override - Stream mapEventToState(SettingsAboutState currentState, SettingsAboutEvent event) async* { + Stream mapEventToState(SettingsAboutEvent event) async* { if (event is RequestAbout) { try { loadAbout(); diff --git a/lib/src/settings/settings_anti_mobbing_bloc.dart b/lib/src/settings/settings_anti_mobbing_bloc.dart index 11fc64ae..b061c0af 100644 --- a/lib/src/settings/settings_anti_mobbing_bloc.dart +++ b/lib/src/settings/settings_anti_mobbing_bloc.dart @@ -49,7 +49,7 @@ class SettingsAntiMobbingBloc extends Bloc SettingsAntiMobbingStateInitial(); @override - Stream mapEventToState(SettingsAntiMobbingState currentState, SettingsAntiMobbingEvent event) async* { + Stream mapEventToState(SettingsAntiMobbingEvent event) async* { if (event is RequestSetting) { try { loadSetting(); diff --git a/lib/src/settings/settings_autocrypt_bloc.dart b/lib/src/settings/settings_autocrypt_bloc.dart index a63f3f9e..c9b5a612 100644 --- a/lib/src/settings/settings_autocrypt_bloc.dart +++ b/lib/src/settings/settings_autocrypt_bloc.dart @@ -53,7 +53,7 @@ class SettingsAutocryptBloc extends Bloc SettingsAutocryptStateInitial(); @override - Stream mapEventToState(SettingsAutocryptState currentState, SettingsAutocryptEvent event) async* { + Stream mapEventToState(SettingsAutocryptEvent event) async* { if (event is PrepareKeyTransfer) { _prepareKeyTransfer(event.chatId, event.messageId); } else if (event is KeyTransferPrepared) { diff --git a/lib/src/settings/settings_chat_bloc.dart b/lib/src/settings/settings_chat_bloc.dart index 3e9f5bc9..91b7712c 100644 --- a/lib/src/settings/settings_chat_bloc.dart +++ b/lib/src/settings/settings_chat_bloc.dart @@ -52,7 +52,7 @@ class SettingsChatBloc extends Bloc { SettingsChatState get initialState => SettingsChatStateInitial(); @override - Stream mapEventToState(SettingsChatState currentState, SettingsChatEvent event) async* { + Stream mapEventToState(SettingsChatEvent event) async* { if (event is RequestValues) { try { _requestValues(); diff --git a/lib/src/settings/settings_debug_bloc.dart b/lib/src/settings/settings_debug_bloc.dart index 129eb9e5..7d0cd1e5 100644 --- a/lib/src/settings/settings_debug_bloc.dart +++ b/lib/src/settings/settings_debug_bloc.dart @@ -51,7 +51,7 @@ class SettingsDebugBloc extends Bloc { SettingsDebugState get initialState => SettingsDebugStateInitial(); @override - Stream mapEventToState(SettingsDebugState currentState, SettingsDebugEvent event) async* { + Stream mapEventToState(SettingsDebugEvent event) async* { if (event is RequestDebug) { try { loadDebug(); diff --git a/lib/src/settings/settings_security_bloc.dart b/lib/src/settings/settings_security_bloc.dart index 07357f61..8b2f8ec8 100644 --- a/lib/src/settings/settings_security_bloc.dart +++ b/lib/src/settings/settings_security_bloc.dart @@ -65,7 +65,7 @@ class SettingsSecurityBloc extends Bloc SettingsSecurityStateInitial(); @override - Stream mapEventToState(SettingsSecurityState currentState, SettingsSecurityEvent event) async* { + Stream mapEventToState(SettingsSecurityEvent event) async* { if (event is ExportKeys) { yield SettingsSecurityStateLoading(type: SettingsSecurityType.exportKeys); try { diff --git a/lib/src/share/share_bloc.dart b/lib/src/share/share_bloc.dart index aea46e0c..4cbae7f5 100644 --- a/lib/src/share/share_bloc.dart +++ b/lib/src/share/share_bloc.dart @@ -58,8 +58,8 @@ class ShareBloc extends Bloc { ShareState get initialState => ShareStateInitial(); @override - Stream mapEventToState(ShareState currentState, ShareEvent event) async* { - if (event is RequestChatsAndContacts) { + Stream mapEventToState(ShareEvent event) async*{ + if(event is RequestChatsAndContacts){ yield ShareStateLoading(); try { createShareList(); diff --git a/lib/src/ui/dimensions.dart b/lib/src/ui/dimensions.dart index 6625267e..411b36b6 100644 --- a/lib/src/ui/dimensions.dart +++ b/lib/src/ui/dimensions.dart @@ -108,6 +108,11 @@ const profileVerticalPadding = 8.0; const profileSectionsVerticalPadding = 36.0; const profileAvatarPlaceholderIconSize = 60.0; const profileAvatarMaxRadius = 64.0; +const profileAvatarSize = 128.0; +const profileAvatarBorderRadius = 20.0; + +const profileEditPhotoButtonRightPosition = 8.0; +const profileEditPhotoButtonBottomPosition = 24.0; const editUserAvatarVerticalPadding = 24.0; const editUserAvatarEditIconSize = 36.0; diff --git a/lib/src/user/user_bloc.dart b/lib/src/user/user_bloc.dart index 8a32c38f..370f84bb 100644 --- a/lib/src/user/user_bloc.dart +++ b/lib/src/user/user_bloc.dart @@ -51,7 +51,7 @@ class UserBloc extends Bloc { UserState get initialState => UserStateInitial(); @override - Stream mapEventToState(UserState currentState, UserEvent event) async* { + Stream mapEventToState(UserEvent event) async* { if (event is RequestUser) { yield UserStateLoading(); try { diff --git a/lib/src/user/user_change_bloc.dart b/lib/src/user/user_change_bloc.dart index 1a9f6ca0..f9453e9e 100644 --- a/lib/src/user/user_change_bloc.dart +++ b/lib/src/user/user_change_bloc.dart @@ -53,7 +53,7 @@ class UserChangeBloc extends Bloc { UserChangeState get initialState => UserChangeStateInitial(); @override - Stream mapEventToState(UserChangeState currentState, UserChangeEvent event) async* { + Stream mapEventToState(UserChangeEvent event) async* { if (event is RequestUser) { yield UserChangeStateLoading(); try { diff --git a/lib/src/user/user_profile.dart b/lib/src/user/user_profile.dart index efd2a7e3..4d9dfb9f 100644 --- a/lib/src/user/user_profile.dart +++ b/lib/src/user/user_profile.dart @@ -40,8 +40,6 @@ * for more details. */ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:ox_coi/src/data/config.dart'; @@ -55,8 +53,8 @@ import 'package:ox_coi/src/ui/dimensions.dart'; import 'package:ox_coi/src/user/user_bloc.dart'; import 'package:ox_coi/src/user/user_event_state.dart'; import 'package:ox_coi/src/user/user_settings.dart'; -import 'package:ox_coi/src/utils/widgets.dart'; import 'package:ox_coi/src/widgets/placeholder_text.dart'; +import 'package:ox_coi/src/widgets/profile_header.dart'; class UserProfile extends RootChild { UserProfile({State state}) : super(state: state); @@ -166,7 +164,7 @@ class _ProfileState extends State { child: Text(AppLocalizations.of(context).profileEditButton), onPressed: editUserSettings, ), - Padding(padding:EdgeInsets.all(chatProfileButtonPadding)), + Padding(padding: EdgeInsets.all(chatProfileButtonPadding)), RaisedButton( color: accent, textColor: onAccent, @@ -181,21 +179,12 @@ class _ProfileState extends State { ); } - CircleAvatar buildAvatar(Config config) { - var hasAvatarPath = config.avatarPath == null || config.avatarPath.isEmpty; - return hasAvatarPath - ? CircleAvatar( - maxRadius: profileAvatarMaxRadius, - child: Icon( - Icons.person, - size: profileAvatarPlaceholderIconSize, - ), - ) - : CircleAvatar( - key: createKey(config.lastUpdate), - maxRadius: profileAvatarMaxRadius, - backgroundImage: FileImage(File(config.avatarPath)), - ); + ProfileData buildAvatar(Config config) { + return ProfileData( + color: accent, + child: ProfileAvatar( + imagePath: config.avatarPath, + )); } Widget getSettings() { @@ -222,6 +211,4 @@ class _ProfileState extends State { MaterialPageRoute(builder: (context) => QrCode(chatId: 0)), ); } - - } diff --git a/lib/src/user/user_settings.dart b/lib/src/user/user_settings.dart index b26ceba1..89a051d9 100644 --- a/lib/src/user/user_settings.dart +++ b/lib/src/user/user_settings.dart @@ -40,12 +40,8 @@ * for more details. */ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:image_cropper/image_cropper.dart'; -import 'package:image_picker/image_picker.dart'; import 'package:ox_coi/src/data/config.dart'; import 'package:ox_coi/src/l10n/localizations.dart'; import 'package:ox_coi/src/navigation/navigatable.dart'; @@ -54,6 +50,8 @@ import 'package:ox_coi/src/ui/color.dart'; import 'package:ox_coi/src/ui/dimensions.dart'; import 'package:ox_coi/src/user/user_change_bloc.dart'; import 'package:ox_coi/src/user/user_change_event_state.dart'; +import 'package:ox_coi/src/utils/text.dart'; +import 'package:ox_coi/src/widgets/profile_header.dart'; import 'package:rxdart/rxdart.dart'; class UserSettings extends StatefulWidget { @@ -68,7 +66,7 @@ class _UserSettingsState extends State { TextEditingController _usernameController = TextEditingController(); TextEditingController _statusController = TextEditingController(); - File _avatar; + String _avatar; @override void initState() { @@ -86,7 +84,7 @@ class _UserSettingsState extends State { _statusController.text = config.status; String avatarPath = config.avatarPath; if (avatarPath != null && avatarPath.isNotEmpty) { - _avatar = File(config.avatarPath); + _avatar = config.avatarPath; } } else if (state is UserChangeStateApplied) { navigation.pop(context); @@ -128,28 +126,14 @@ class _UserSettingsState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Padding(padding: EdgeInsets.only(top: editUserAvatarVerticalPadding)), - new GestureDetector( - onTap: () => _showAvatarSourceChooser(), - child: Stack( - children: [ - _avatar != null - ? CircleAvatar( - maxRadius: profileAvatarMaxRadius, - backgroundImage: FileImage(_avatar), - ) - : CircleAvatar( - maxRadius: profileAvatarMaxRadius, - ), - CircleAvatar( - maxRadius: profileAvatarMaxRadius, - backgroundColor: Colors.transparent, - child: Icon( - Icons.edit, - size: editUserAvatarEditIconSize, - color: onPrimary, - ), - ), - ], + Align( + alignment: Alignment.center, + child: ProfileData( + color: accent, + imageActionCallback: _setAvatar, + child: ProfileAvatar( + imagePath: _avatar, + ), )), Padding( padding: EdgeInsets.only(left: listItemPaddingBig, right: listItemPaddingBig), @@ -173,61 +157,14 @@ class _UserSettingsState extends State { ); } - _showAvatarSourceChooser() { - showModalBottomSheet( - context: context, - builder: (BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: Icon(Icons.photo), - title: Text(AppLocalizations.of(context).gallery), - onTap: () => _getNewAvatarPath(ImageSource.gallery), - ), - ListTile( - leading: Icon(Icons.camera_alt), - title: Text(AppLocalizations.of(context).camera), - onTap: () => _getNewAvatarPath(ImageSource.camera), - ), - ListTile( - leading: Icon(Icons.delete), - title: Text(AppLocalizations.of(context).userSettingsRemoveImage), - onTap: () => _removeAvatar(), - ) - ], - ); - }); - } - - _getNewAvatarPath(ImageSource source) async { - navigation.pop(context); - File newAvatar = await ImagePicker.pickImage(source: source); - if (newAvatar != null) { - File croppedAvatar = await ImageCropper.cropImage( - sourcePath: newAvatar.path, - ratioX: editUserAvatarRation, - ratioY: editUserAvatarRation, - maxWidth: editUserAvatarImageMaxSize, - maxHeight: editUserAvatarImageMaxSize, - ); - if (croppedAvatar != null) { - setState(() { - _avatar = croppedAvatar; - }); - } - } - } - - _removeAvatar() { - navigation.pop(context); + _setAvatar(String avatarPath){ setState(() { - _avatar = null; + _avatar = avatarPath; }); } void _saveChanges() async { - String avatarPath = _avatar != null ? _avatar.path : null; + String avatarPath = !isNullOrEmpty(_avatar) ? _avatar : null; _userChangeBloc.dispatch(UserPersonalDataChanged(username: _usernameController.text, status: _statusController.text, avatarPath: avatarPath)); } } diff --git a/lib/src/widgets/avatar.dart b/lib/src/widgets/avatar.dart index 1ffdf407..fbe5d1ac 100644 --- a/lib/src/widgets/avatar.dart +++ b/lib/src/widgets/avatar.dart @@ -62,20 +62,22 @@ class Avatar extends StatelessWidget { if (imagePath != null && imagePath.isNotEmpty) { avatarImage = FileImage(File(imagePath)); } else { + avatarImage = FileImage(File("")); initials = getInitials(textPrimary, textSecondary); } - if (avatarImage == null && isNullOrEmpty(initials)) { - return Container( + return Container( + alignment: Alignment.center, height: listAvatarDiameter, width: listAvatarDiameter, - ); - } - return CircleAvatar( - radius: listAvatarRadius, - foregroundColor: onPrimary, - backgroundColor: color != null ? color : Colors.transparent, - child: avatarImage != null ? avatarImage : new Text(initials), - ); + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(profileAvatarBorderRadius), + color: color, + image: DecorationImage( + fit: BoxFit.fill, + image: avatarImage, + ), + ), + child: isNullOrEmpty(imagePath) ? Text(initials, style: Theme.of(context).textTheme.subhead.apply(color: onPrimary),) : Container()); } static String getInitials(String textPrimary, [String textSecondary]) { @@ -88,4 +90,3 @@ class Avatar extends StatelessWidget { return ""; } } - diff --git a/lib/src/widgets/avatar_list_item.dart b/lib/src/widgets/avatar_list_item.dart index 87e7b590..e4f9e01f 100644 --- a/lib/src/widgets/avatar_list_item.dart +++ b/lib/src/widgets/avatar_list_item.dart @@ -60,21 +60,24 @@ class AvatarListItem extends StatelessWidget { final int timestamp; final bool isVerified; final bool isInvite; + final bool isGroupListItem; + final PopupMenuButton moreButton; - AvatarListItem({ - @required this.title, - @required this.subTitle, - @required this.onTap, - this.avatarIcon, - this.imagePath, - this.color, - this.freshMessageCount = 0, - this.titleIcon, - this.subTitleIcon, - this.timestamp = 0, - this.isVerified = false, - this.isInvite = false, - }); + AvatarListItem( + {@required this.title, + @required this.subTitle, + @required this.onTap, + this.avatarIcon, + this.imagePath, + this.color, + this.freshMessageCount = 0, + this.titleIcon, + this.subTitleIcon, + this.timestamp = 0, + this.isVerified = false, + this.isInvite = false, + this.isGroupListItem = false, + this.moreButton}); @override Widget build(BuildContext context) { @@ -170,10 +173,14 @@ class AvatarListItem extends StatelessWidget { ), ], ), - Divider(), + !isGroupListItem ? Padding(padding: EdgeInsets.all(8.0)) : Divider(), ], ), ), + Visibility( + visible: moreButton != null, + child: Container(child: moreButton), + ) ], ), ), diff --git a/lib/src/widgets/profile_body.dart b/lib/src/widgets/profile_body.dart index 021e4c10..f876d747 100644 --- a/lib/src/widgets/profile_body.dart +++ b/lib/src/widgets/profile_body.dart @@ -43,7 +43,6 @@ import 'package:flutter/material.dart'; import 'package:ox_coi/src/l10n/localizations.dart'; import 'package:ox_coi/src/navigation/navigatable.dart'; -import 'package:ox_coi/src/ui/color.dart'; import 'package:ox_coi/src/utils/dialog_builder.dart'; class ProfileActionList extends StatelessWidget { @@ -64,18 +63,22 @@ class ProfileAction extends StatelessWidget { final IconData iconData; final String text; final Function onTap; + final Color color; - const ProfileAction({@required this.iconData, @required this.text, @required this.onTap, Key key}) : super(key: key); + const ProfileAction({@required this.iconData, @required this.text, @required this.onTap, this.color, Key key}) : super(key: key); @override Widget build(BuildContext context) { return ListTile( leading: Icon( iconData, - color: accent, + color: color, ), title: Text( text, + style: TextStyle( + color: color + ), ), onTap: onTap, ); diff --git a/lib/src/widgets/profile_header.dart b/lib/src/widgets/profile_header.dart index 7233acad..869aea28 100644 --- a/lib/src/widgets/profile_header.dart +++ b/lib/src/widgets/profile_header.dart @@ -45,109 +45,220 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:ox_coi/src/ui/color.dart'; import 'package:ox_coi/src/ui/dimensions.dart'; +import 'package:image_cropper/image_cropper.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:ox_coi/src/l10n/localizations.dart'; +import 'package:ox_coi/src/navigation/navigation.dart'; import 'package:ox_coi/src/utils/clipboard.dart'; import 'package:ox_coi/src/utils/text.dart'; -import 'package:ox_coi/src/utils/widgets.dart'; -class ProfileHeader extends StatelessWidget { - final String initialsString; - - final List dynamicChildren; +class ProfileData extends InheritedWidget { final Color color; - final String imagePath; - final int lastUpdate; + final String text; + final IconData iconData; + final TextStyle textStyle; + final Function imageActionCallback; - ProfileHeader({ - @required this.initialsString, - this.dynamicChildren, + const ProfileData({ + Key key, + @required Widget child, this.color, - this.imagePath, - this.lastUpdate, - }); + this.text, + this.iconData, + this.textStyle, + this.imageActionCallback, + }) : assert(child != null), + super(key: key, child: child); + + static ProfileData of(BuildContext context) { + return context.inheritFromWidgetOfExactType(ProfileData) as ProfileData; + } + + @override + bool updateShouldNotify(ProfileData old) { + return true; + } +} + +class ProfileAvatar extends StatelessWidget { + final String imagePath; + + ProfileAvatar({this.imagePath}); @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: chatProfileVerticalPadding), - child: isNullOrEmpty(imagePath) - ? CircleAvatar( - maxRadius: profileAvatarMaxRadius, - backgroundColor: color, - child: Text( - initialsString, - style: Theme.of(context).textTheme.display3.apply(color: onPrimary), - ), + double avatarSize = profileAvatarSize; + File imageFile; + if (isNullOrEmpty(imagePath)) { + imageFile = File(""); + } else { + imageFile = File(imagePath); + } + + Navigation _navigation = Navigation(); + + _getNewAvatarPath(ImageSource source) async { + _navigation.pop(context); + File newAvatar = await ImagePicker.pickImage(source: source); + if (newAvatar != null) { + File croppedAvatar = await ImageCropper.cropImage( + sourcePath: newAvatar.path, + ratioX: editUserAvatarRation, + ratioY: editUserAvatarRation, + maxWidth: editUserAvatarImageMaxSize, + maxHeight: editUserAvatarImageMaxSize, + ); + if (croppedAvatar != null) { + ProfileData.of(context).imageActionCallback(croppedAvatar.path); + } + } + } + + _removeAvatar() { + _navigation.pop(context); + ProfileData.of(context).imageActionCallback(""); + } + + _editPhoto() { + showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: Icon(Icons.photo), + title: Text(AppLocalizations.of(context).gallery), + onTap: () => _getNewAvatarPath(ImageSource.gallery), + ), + ListTile( + leading: Icon(Icons.camera_alt), + title: Text(AppLocalizations.of(context).camera), + onTap: () => _getNewAvatarPath(ImageSource.camera), + ), + ListTile( + leading: Icon(Icons.delete), + title: Text(AppLocalizations.of(context).userSettingsRemoveImage), + onTap: () => _removeAvatar(), ) - : CircleAvatar( - key: lastUpdate ?? createKey(lastUpdate), - maxRadius: profileAvatarMaxRadius, - backgroundColor: color, - backgroundImage: FileImage(File(imagePath)), + ], + ); + }); + } + + + + return Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: chatProfileVerticalPadding), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: ProfileData.of(context).color, + borderRadius: BorderRadius.circular(profileAvatarBorderRadius), + image: DecorationImage( + fit: BoxFit.fill, + image: FileImage(imageFile), ), + ), + height: avatarSize, + width: avatarSize, + )), + Visibility( + visible: ProfileData.of(context).imageActionCallback != null, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: chatProfileVerticalPadding), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: ProfileData.of(context).color, + borderRadius: BorderRadius.circular(profileAvatarBorderRadius), + gradient: LinearGradient(begin: FractionalOffset.topCenter, end: FractionalOffset.bottomCenter, colors: [ + Colors.black.withOpacity(0.0), + Colors.black.withOpacity(0.5), + ], stops: [ + 0.7, + 1.0 + ])), + height: avatarSize, + width: avatarSize, + )), ), - for (var child in dynamicChildren) Padding(padding: EdgeInsets.only(top: 8.0), child: child), - Padding( - padding: const EdgeInsets.only(top: chatProfileVerticalPadding), - child: Divider( - height: dividerHeight, - ), - ) + Visibility( + visible: ProfileData.of(context).imageActionCallback != null, + child: Positioned( + bottom: profileEditPhotoButtonBottomPosition, + right: profileEditPhotoButtonRightPosition, + child: InkWell( + child: Icon( + Icons.add_a_photo, + color: onPrimary, + ), + onTap: _editPhoto, + ))) ], ); } } class ProfileHeaderText extends StatelessWidget { - final String text; + const ProfileHeaderText({Key key}) : super(key: key); - final IconData iconData; + @override + Widget build(BuildContext context) { + var content = Text( + ProfileData.of(context).text, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: ProfileData.of(context).textStyle, + ); + return Flexible( + child: ProfileData.of(context).iconData != null + ? Row( + children: [ + Icon(ProfileData.of(context).iconData), + Padding( + padding: const EdgeInsets.only(left: iconTextPadding), + child: content, + ), + ], + ) + : content); + } +} - const ProfileHeaderText({Key key, @required this.text, this.iconData}) : super(key: key); +class ProfileMemberHeaderText extends StatelessWidget { + const ProfileMemberHeaderText({Key key}) : super(key: key); @override Widget build(BuildContext context) { - var content = Text(text, style: Theme.of(context).textTheme.subhead); - return iconData != null - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(iconData), - Padding( - padding: const EdgeInsets.only(left: iconTextPadding), - child: content, - ), - ], - ) - : content; + return Text( + ProfileData.of(context).text, + style: Theme.of(context).textTheme.subtitle, + ); } } class ProfileCopyableHeaderText extends StatelessWidget { - final String text; final String toastMessage; - final IconData iconData; - const ProfileCopyableHeaderText({Key key, @required this.text, @required this.toastMessage, this.iconData}) : super(key: key); + const ProfileCopyableHeaderText({Key key, @required this.toastMessage}) : super(key: key); @override Widget build(BuildContext context) { return InkWell( onTap: () { - copyToClipboardWithToast(text: text, toastText: toastMessage); + copyToClipboardWithToast(text: ProfileData.of(context).text, toastText: toastMessage); }, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.only(right: iconTextPadding), - child: ProfileHeaderText(text: text, iconData: iconData), - ), + ProfileHeaderText(), + Padding(padding: EdgeInsets.all(iconTextPadding)), Icon(Icons.content_copy), ], ), ); } -} +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index cf2e802d..28fefeb4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,29 +11,29 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - bloc: ^0.9.3 - contacts_service: ^0.2.8 + bloc: ^0.14.4 + contacts_service: ^0.2.1 cupertino_icons: ^0.1.2 date_format: ^1.0.6 downloads_path_provider: ^0.1.0 - firebase_messaging: ^5.1.1 - file_picker: ^1.3.8 - flutter_bloc: ^0.7.0 - flutter_local_notifications: ^0.7.1+3 - flutter_sound: ^1.4.2 - fluttertoast: ^3.1.0 - image_cropper: ^1.0.2 - image_picker: ^0.6.0+17 + firebase_messaging: ^5.0.0 + file_picker: ^1.3.3 + flutter_bloc: ^0.19.1 + flutter_local_notifications: ^0.7.0 + flutter_sound: ^1.3.7 + fluttertoast: ^3.0.4 + image_cropper: ^1.0.0 + image_picker: ^0.5.0+3 logging: ^0.11.3+2 - open_file: ^2.0.3 - package_info: ^0.4.0+5 - path_provider: ^1.1.2 - permission_handler: ^3.2.0 - rxdart: ^0.20.0 # ^0.21.0 exists but 0.20.0 is used for compatibility reasons with bloc: ^0.9.3 - shared_preferences: ^0.5.3+4 - url_launcher: ^5.1.0 - video_player: ^0.10.1+4 - qr_flutter: ^2.1.0+55 + open_file: ^2.0.1+1 + package_info: ^0.4.0+2 + path_provider: ^0.5.0+1 + permission_handler: ^3.0.0 + rxdart: ^0.22.0 + shared_preferences: ^0.5.1+1 + url_launcher: ^5.0.2 + video_player: ^0.10.0+4 + qr_flutter: ^2.0.0 qr_mobile_vision: ^0.2.2 delta_chat_core: path: ../flutter-deltachat-core/ # Intended path outside the project context