diff --git a/sama_chat_client/lib/src/features/conversation/bloc/conversation_bloc.dart b/sama_chat_client/lib/src/features/conversation/bloc/conversation_bloc.dart index 31df15b..6d0e82a 100644 --- a/sama_chat_client/lib/src/features/conversation/bloc/conversation_bloc.dart +++ b/sama_chat_client/lib/src/features/conversation/bloc/conversation_bloc.dart @@ -99,9 +99,6 @@ class ConversationBloc extends Bloc { on<_ConversationUpdated>( _onConversationUpdated, ); - on( - _onConversationDeleted, - ); on( _onTypingStatusStartReceived, transformer: typingThrottleDroppable(), @@ -358,13 +355,6 @@ class ConversationBloc extends Bloc { emit(state.copyWith(conversation: event.conversation)); } - Future _onConversationDeleted( - ConversationDeleted event, Emitter emit) async { - await conversationRepository.deleteConversation(state.conversation) - ? emit(state.copyWith(status: ConversationStatus.delete)) - : emit(state.copyWith(status: ConversationStatus.failure)); - } - Future _onTypingStatusStartReceived( TypingStatusStartReceived event, Emitter emit) async { var user = await userRepository.getUserById(event.from); diff --git a/sama_chat_client/lib/src/features/conversation/bloc/conversation_event.dart b/sama_chat_client/lib/src/features/conversation/bloc/conversation_event.dart index 6ae58fc..68b7f89 100644 --- a/sama_chat_client/lib/src/features/conversation/bloc/conversation_event.dart +++ b/sama_chat_client/lib/src/features/conversation/bloc/conversation_event.dart @@ -72,10 +72,6 @@ final class _ConversationUpdated extends ConversationEvent { const _ConversationUpdated(this.conversation); } -final class ConversationDeleted extends ConversationEvent { - const ConversationDeleted(); -} - final class TypingStatusStartReceived extends ConversationEvent { final String from; diff --git a/sama_chat_client/lib/src/features/conversation/bloc/conversation_state.dart b/sama_chat_client/lib/src/features/conversation/bloc/conversation_state.dart index 7607b71..dc36188 100644 --- a/sama_chat_client/lib/src/features/conversation/bloc/conversation_state.dart +++ b/sama_chat_client/lib/src/features/conversation/bloc/conversation_state.dart @@ -1,6 +1,6 @@ part of 'conversation_bloc.dart'; -enum ConversationStatus { initial, success, failure, delete } +enum ConversationStatus { initial, success, failure } class TypingMessageStatus { final TypingState typingState; diff --git a/sama_chat_client/lib/src/features/conversation/view/conversation_page.dart b/sama_chat_client/lib/src/features/conversation/view/conversation_page.dart index d148571..2a7030d 100644 --- a/sama_chat_client/lib/src/features/conversation/view/conversation_page.dart +++ b/sama_chat_client/lib/src/features/conversation/view/conversation_page.dart @@ -21,6 +21,7 @@ import '../../../shared/ui/colors.dart'; import '../../../shared/utils/string_utils.dart'; import '../../../shared/widget/loaders.dart'; import '../../../shared/widget/typing_indicator.dart'; +import '../../conversation_delete/bloc/conversation_delete_bloc.dart'; import '../../group_info/view/group_info_page.dart'; import '../bloc/ai_message/ai_message_bloc.dart'; import '../bloc/conversation_bloc.dart'; @@ -65,6 +66,12 @@ class ConversationPage extends StatelessWidget { RepositoryProvider.of(context), ), ), + BlocProvider( + create: (context) => ConversationDeleteBloc( + conversationRepository: + RepositoryProvider.of(context), + ), + ), BlocProvider( create: (context) => MediaAttachmentBloc( attachmentsRepository: @@ -80,78 +87,102 @@ class ConversationPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - buildWhen: (previous, current) => previous.choose != current.choose, - builder: (BuildContext context, state) { - return PopScope( - onPopInvokedWithResult: (didPop, result) { - if (didPop) return; - context - .read() - .add(const SelectMessagesMode(false)); - }, - canPop: !state.choose, - child: Scaffold( - appBar: AppBar( - toolbarHeight: 64, - centerTitle: false, - titleSpacing: 0.0, - backgroundColor: smokyBorough, - surfaceTintColor: Colors.transparent, - title: BlocBuilder( - builder: (BuildContext context, aiState) { - return aiState.status == AiMessageStatus.processing - ? const TitleLoader( - black, - Text('AI processing', - style: TextStyle(color: black, fontSize: 20.0))) - : ConnectionTitle( - color: black, - title: title, - ); - }), - actions: [_PopupMenuButton()], - ), - body: Column( - children: [ - BlocListener( - listener: (context, state) { - if (state.status == ConnectionStatus.connected) { - BlocProvider.of(context) - .add(const MessagesRequested(refresh: true)); - } - }, - child: const Flexible(child: MessagesList())), - SafeArea( - child: !state.choose - ? context.read().state.status == - SharingIntentStatus.processing - ? BlocListener( - listener: (context, sendState) { - if (sendState.status == - SendMessageStatus.success || - sendState.status == - SendMessageStatus.failure) { - context + return BlocListener( + listener: (context, state) { + switch (state.status) { + case ConversationDeleteStatus.initial: + break; + case ConversationDeleteStatus.success: + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.popUntil(context, (route) => route.isFirst); + }); + break; + case ConversationDeleteStatus.failure: + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + duration: const Duration(seconds: 3), + content: Text(state.errorMessage ?? '')), + ); + } + }, + child: BlocBuilder( + buildWhen: (previous, current) => previous.choose != current.choose, + builder: (BuildContext context, state) { + return PopScope( + onPopInvokedWithResult: (didPop, result) { + if (didPop) return; + context + .read() + .add(const SelectMessagesMode(false)); + }, + canPop: !state.choose, + child: Scaffold( + appBar: AppBar( + toolbarHeight: 64, + centerTitle: false, + titleSpacing: 0.0, + backgroundColor: smokyBorough, + surfaceTintColor: Colors.transparent, + title: BlocBuilder( + builder: (BuildContext context, aiState) { + return aiState.status == AiMessageStatus.processing + ? const TitleLoader( + black, + Text('AI processing', + style: TextStyle( + color: black, fontSize: 20.0))) + : ConnectionTitle( + color: black, + title: title, + ); + }), + actions: [_PopupMenuButton()], + ), + body: Column( + children: [ + BlocListener( + listener: (context, state) { + if (state.status == ConnectionStatus.connected) { + BlocProvider.of(context).add( + const MessagesRequested(refresh: true)); + } + }, + child: const Flexible(child: MessagesList())), + SafeArea( + child: !state.choose + ? context .read() - .add(SharingIntentCompleted()); - } - }, - child: ConnectionChecker( - child: MessageInput( - sharedMessage: context + .state + .status == + SharingIntentStatus.processing + ? BlocListener( + listener: (context, sendState) { + if (sendState.status == + SendMessageStatus.success || + sendState.status == + SendMessageStatus.failure) { + context .read() - .state - .sharedFiles - .firstOrNull)), - ) - : const MessageInput() - : const SelectInput()) - ], - ), - )); - }); + .add(SharingIntentCompleted()); + } + }, + child: ConnectionChecker( + child: MessageInput( + sharedMessage: context + .read() + .state + .sharedFiles + .firstOrNull)), + ) + : const MessageInput() + : const SelectInput()) + ], + ), + )); + })); } Widget get title => BlocBuilder( @@ -271,9 +302,13 @@ class _PopupMenuButton extends StatelessWidget { context .read() .add(const TextMessageClear()); - context - .read() - .add(const ConversationDeleted()); + context.read().add( + ConversationDeleted( + chat: context + .read() + .state + .conversation)); + Navigator.of(context).pop(); }, ), ], diff --git a/sama_chat_client/lib/src/features/conversation/view/messages_list.dart b/sama_chat_client/lib/src/features/conversation/view/messages_list.dart index 47e8392..e68c619 100644 --- a/sama_chat_client/lib/src/features/conversation/view/messages_list.dart +++ b/sama_chat_client/lib/src/features/conversation/view/messages_list.dart @@ -171,11 +171,6 @@ class _MessagesListState extends State { }); case ConversationStatus.initial: return const Center(child: CircularProgressIndicator()); - case ConversationStatus.delete: - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.popUntil(context, (route) => route.isFirst); - }); - return const SizedBox.shrink(); } }, ), diff --git a/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_bloc.dart b/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_bloc.dart new file mode 100644 index 0000000..3de11d7 --- /dev/null +++ b/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_bloc.dart @@ -0,0 +1,29 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import '../../../repository/conversation/conversation_repository.dart'; +import '../../../db/models/conversation_model.dart'; + +part 'conversation_delete_event.dart'; + +part 'conversation_delete_state.dart'; + +class ConversationDeleteBloc + extends Bloc { + ConversationDeleteBloc({required this.conversationRepository}) + : super(const ConversationDeleteState()) { + on(_onConversationDeleted); + } + + final ConversationRepository conversationRepository; + + Future _onConversationDeleted( + ConversationDeleted event, + Emitter emit, + ) async { + await conversationRepository.deleteConversation(event.chat) + ? emit(state.copyWith(status: ConversationDeleteStatus.success)) + : emit(state.copyWith( + status: ConversationDeleteStatus.failure, + errorMessage: 'Failed to delete conversation.')); + } +} diff --git a/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_event.dart b/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_event.dart new file mode 100644 index 0000000..27973a6 --- /dev/null +++ b/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_event.dart @@ -0,0 +1,20 @@ +part of 'conversation_delete_bloc.dart'; + +sealed class ConversationDeleteEvent extends Equatable { + const ConversationDeleteEvent(); + + @override + List get props => []; +} + +final class ConversationDeleted extends ConversationDeleteEvent { + const ConversationDeleted({required this.chat}); + + final ConversationModel chat; + + @override + List get props => [chat]; + + @override + String toString() => 'ConversationDeleted { chat: $chat }'; +} diff --git a/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_state.dart b/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_state.dart new file mode 100644 index 0000000..d8ded9e --- /dev/null +++ b/sama_chat_client/lib/src/features/conversation_delete/bloc/conversation_delete_state.dart @@ -0,0 +1,30 @@ +part of 'conversation_delete_bloc.dart'; + +enum ConversationDeleteStatus { initial, success, failure } + +final class ConversationDeleteState extends Equatable { + const ConversationDeleteState({ + this.status = ConversationDeleteStatus.initial, + this.errorMessage, + this.informationMessage, + }); + + final ConversationDeleteStatus status; + final String? errorMessage; + final String? informationMessage; + + ConversationDeleteState copyWith({ + ConversationDeleteStatus? status, + String? errorMessage, + String? informationMessage, + }) { + return ConversationDeleteState( + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + informationMessage: informationMessage ?? this.informationMessage, + ); + } + + @override + List get props => [status, errorMessage, informationMessage]; +} diff --git a/sama_chat_client/lib/src/features/conversations_list/view/conversations_list.dart b/sama_chat_client/lib/src/features/conversations_list/view/conversations_list.dart index ac8878f..a9a540b 100644 --- a/sama_chat_client/lib/src/features/conversations_list/view/conversations_list.dart +++ b/sama_chat_client/lib/src/features/conversations_list/view/conversations_list.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../navigation/constants.dart'; +import '../../../db/models/conversation_model.dart'; +import '../../../shared/connection/view/connection_checker.dart'; +import '../../../shared/ui/colors.dart'; import '../../../shared/utils/observer_utils.dart'; +import '../../../shared/widget/swipe_to.dart'; +import '../../conversation_delete/bloc/conversation_delete_bloc.dart'; import '../conversations_list.dart'; class ConversationsList extends StatefulWidget { @@ -29,7 +33,22 @@ class _ConversationsListState extends State with RouteAware { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocListener( + listener: (context, state) { + switch (state.status) { + case ConversationDeleteStatus.initial: + case ConversationDeleteStatus.success: + break; + case ConversationDeleteStatus.failure: + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar( + SnackBar( + duration: const Duration(seconds: 3), + content: Text(state.errorMessage ?? '')), + ); + } + }, child: BlocBuilder( builder: (context, state) { switch (state.status) { case ConversationsStatus.failure: @@ -68,8 +87,7 @@ class _ConversationsListState extends State with RouteAware { var typing = state.typingStatuses[chat.id]; return index >= state.conversations.length ? const BottomLoader() - : ConversationListItem( - conversation: chat, typingStatus: typing); + : buildChat(chat, typing); }, itemCount: state.conversations.length, // itemCount: state.hasReachedMax @@ -83,6 +101,54 @@ class _ConversationsListState extends State with RouteAware { return const Center(child: CircularProgressIndicator()); } }, + )); + } + + Widget buildChat(ConversationModel chat, TypingChatStatus? typing) { + return SwipeTo( + key: Key(chat.id.toString()), + stickToRight: true, + direction: DismissDirection.endToStart, + onSwipe: () { + print('onSwipe'); + connectionChecker( + context, + () => showModalBottomSheet( + context: context, + builder: (BuildContext bc) { + return SafeArea( + child: SizedBox( + height: 50, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.only(top: 5), + child: TextButton( + style: const ButtonStyle( + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + context + .read() + .add(ConversationDeleted(chat: chat)); + Navigator.pop(context); + }, + child: const Text('Delete chat'), + )), + ], + ), + )); + }, + )); + }, + actionIcon: const Icon( + Icons.delete_forever_outlined, + color: black, + size: 25, + ), + child: ConversationListItem(conversation: chat, typingStatus: typing), ); } diff --git a/sama_chat_client/lib/src/features/conversations_list/view/conversations_page.dart b/sama_chat_client/lib/src/features/conversations_list/view/conversations_page.dart index 23e2145..9ef5e33 100644 --- a/sama_chat_client/lib/src/features/conversations_list/view/conversations_page.dart +++ b/sama_chat_client/lib/src/features/conversations_list/view/conversations_page.dart @@ -9,25 +9,31 @@ import '../../../shared/connection/view/connection_checker.dart'; import '../../../shared/connection/view/connection_title.dart'; import '../../../shared/sharing/bloc/sharing_intent_bloc.dart'; import '../../../shared/ui/colors.dart'; +import '../../conversation_delete/bloc/conversation_delete_bloc.dart'; import '../conversations_list.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); - static BlocProvider route() { - return BlocProvider( - create: (context) { - final bloc = ConversationsBloc( - conversationRepository: - RepositoryProvider.of(context)) - ..add(const ConversationsFetched()); - if (context.read().state.status == - ConnectionStatus.connected) { - bloc.add(const ConversationsFetched(refresh: true)); - } - return bloc; - }, - child: const HomePage()); + static MultiBlocProvider route() { + return MultiBlocProvider(providers: [ + BlocProvider(create: (context) { + final bloc = ConversationsBloc( + conversationRepository: + RepositoryProvider.of(context)) + ..add(const ConversationsFetched()); + if (context.read().state.status == + ConnectionStatus.connected) { + bloc.add(const ConversationsFetched(refresh: true)); + } + return bloc; + }), + BlocProvider( + create: (context) => ConversationDeleteBloc( + conversationRepository: + RepositoryProvider.of(context), + )), + ], child: const HomePage()); } @override diff --git a/sama_chat_client/lib/src/repository/conversation/conversation_repository.dart b/sama_chat_client/lib/src/repository/conversation/conversation_repository.dart index 7c9d728..c0304d6 100644 --- a/sama_chat_client/lib/src/repository/conversation/conversation_repository.dart +++ b/sama_chat_client/lib/src/repository/conversation/conversation_repository.dart @@ -394,7 +394,12 @@ class ConversationRepository { } Future deleteConversation(ConversationModel conversation) async { - var result = await api.deleteConversation(conversation.id); + bool result; + try { + result = await api.deleteConversation(conversation.id); + } catch (_) { + result = false; + } if (result) await localDatasource.removeConversationLocal(conversation.id); _conversationsController.add(conversation); return result;