diff --git a/sama_chat_client/lib/src/db/db_service.dart b/sama_chat_client/lib/src/db/db_service.dart index 73a45e40..20ff9812 100644 --- a/sama_chat_client/lib/src/db/db_service.dart +++ b/sama_chat_client/lib/src/db/db_service.dart @@ -57,7 +57,7 @@ class DatabaseService { /// //////////////////////////////// Future> getAllConversationsLocal( - DateTime? ltDate) async { + DateTime? ltDate, int? limit, String? type) async { var filter = ConversationModel_.type .equals('u') .and(ConversationModel_.lastMessageBind.notEquals(0).and( @@ -65,13 +65,19 @@ class DatabaseService { .lessThanDate(ltDate ?? DateTime.now()))) .or(ConversationModel_.type.equals('g')); + var condition = + ConversationModel_.updatedAt.lessThanDate(ltDate ?? DateTime.now()); + if (type != null) { + condition = condition.and(ConversationModel_.type.equals(type)); + } + final query = store! .box() // .query(filtered ? filter : null) - .query( - ConversationModel_.updatedAt.lessThanDate(ltDate ?? DateTime.now())) + .query(condition) .order(ConversationModel_.updatedAt, flags: Order.descending) - .build(); + .build() + ..limit = limit ?? 0; final results = await query.findAsync(); query.close(); return results; @@ -264,6 +270,13 @@ class DatabaseService { user.bid = userInDb?.bid; if (userInDb?.avatar?.fileId == user.avatar?.fileId) { user.avatar?.bid = userInDb?.avatar?.bid; + if (user.avatar?.imageUrl != userInDb?.avatar?.imageUrl) { + //to check + print('user.avatar putAsync'); + await store! + .box() + .putAsync(user.avatar!, mode: PutMode.update); + } } } return store!.box().putAndGetManyAsync(items, mode: PutMode.put); diff --git a/sama_chat_client/lib/src/db/local/conversation_local_datasource.dart b/sama_chat_client/lib/src/db/local/conversation_local_datasource.dart index fe1d7563..59e6ea49 100644 --- a/sama_chat_client/lib/src/db/local/conversation_local_datasource.dart +++ b/sama_chat_client/lib/src/db/local/conversation_local_datasource.dart @@ -8,10 +8,11 @@ class ConversationLocalDatasource { // ConversationLocalDataSource(this.databaseService); Future> getAllConversationsLocal( - {DateTime? ltDate}) async { + {DateTime? ltDate, int? limit, String? type}) async { print('getAllConversationsLocal'); try { - return await _databaseService.getAllConversationsLocal(ltDate); + return await _databaseService.getAllConversationsLocal( + ltDate, limit, type); } catch (e) { print('getAllConversationsLocal e ${e.toString()}'); throw DatabaseException(e.toString()); 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 6cd65e3c..d0b64c77 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 @@ -76,11 +76,15 @@ class _MessagesListState extends State { }) ], child: Stack(children: [ - BlocSelector( - selector: (state) => state.status, - builder: (context, status) { + BlocSelector( + selector: (state) => ( + status: state.status, + initial: state.initial, + ), + builder: (context, data) { var state = context.read().state; - switch (status) { + switch (data.status) { case ConversationStatus.failure: WidgetsBinding.instance .addPostFrameCallback((_) => ScaffoldMessenger.of(context) @@ -94,7 +98,7 @@ class _MessagesListState extends State { success: case ConversationStatus.success: if (state.messages.isEmpty) { - return state.initial + return data.initial ? const Center(child: CircularProgressIndicator()) : Center( child: Container( diff --git a/sama_chat_client/lib/src/features/conversation/widgets/forward_messages/forward_messages_widget.dart b/sama_chat_client/lib/src/features/conversation/widgets/forward_messages/forward_messages_widget.dart index 68ecd993..33e0c087 100644 --- a/sama_chat_client/lib/src/features/conversation/widgets/forward_messages/forward_messages_widget.dart +++ b/sama_chat_client/lib/src/features/conversation/widgets/forward_messages/forward_messages_widget.dart @@ -5,7 +5,7 @@ import '../../../../repository/conversation/conversation_repository.dart'; import '../../../../repository/global_search/global_search_repository.dart'; import '../../../../repository/messages/messages_repository.dart'; import '../../../conversation_create/bloc/conversation_create_bloc.dart'; -import '../../../search/bloc/global_search_bloc.dart'; +import '../../../global_search/bloc/global_search_bloc.dart'; import '../../bloc/forward_message/forward_messages_bloc.dart'; import '../../models/chat_message.dart'; import 'forward_search_form.dart'; diff --git a/sama_chat_client/lib/src/features/conversation/widgets/forward_messages/forward_search_form.dart b/sama_chat_client/lib/src/features/conversation/widgets/forward_messages/forward_search_form.dart index 15a54c5f..efd5557b 100644 --- a/sama_chat_client/lib/src/features/conversation/widgets/forward_messages/forward_search_form.dart +++ b/sama_chat_client/lib/src/features/conversation/widgets/forward_messages/forward_search_form.dart @@ -9,10 +9,10 @@ import '../../../../navigation/constants.dart'; import '../../../../shared/ui/colors.dart'; import '../../../conversation_create/bloc/conversation_create_bloc.dart'; import '../../../conversation_create/bloc/conversation_create_state.dart'; -import '../../../search/bloc/global_search_bloc.dart'; -import '../../../search/bloc/global_search_state.dart'; -import '../../../search/view/search_bar.dart'; -import '../../../search/view/search_form.dart'; +import '../../../global_search/bloc/global_search_bloc.dart'; +import '../../../global_search/bloc/global_search_state.dart'; +import '../../../global_search/view/search_bar.dart'; +import '../../../global_search/view/search_result.dart'; import '../../bloc/conversation_bloc.dart'; import '../../bloc/forward_message/forward_messages_bloc.dart'; import '../../models/chat_message.dart'; @@ -51,7 +51,9 @@ class ForwardSearchForm extends StatelessWidget { child: Column( spacing: 4, children: [ - const GlobalSearchBar(), + const Padding( + padding: EdgeInsets.only(bottom: 8), + child: GlobalSearchBar()), _SearchBody(forwardMessages), ], )))); diff --git a/sama_chat_client/lib/src/features/conversation_create/view/conversation_create_form.dart b/sama_chat_client/lib/src/features/conversation_create/view/conversation_create_form.dart deleted file mode 100644 index 3e76e2b2..00000000 --- a/sama_chat_client/lib/src/features/conversation_create/view/conversation_create_form.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import '../../../navigation/constants.dart'; -import '../../../shared/ui/colors.dart'; -import '../../search/view/search_bar.dart'; -import '../../search/view/search_form.dart'; - -class ConversationCreateForm extends StatefulWidget { - const ConversationCreateForm({super.key}); - - @override - State createState() { - return ConversationCreateFormState(); - } -} - -class ConversationCreateFormState extends State { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: black, - iconTheme: const IconThemeData( - color: white, - ), - title: const Text( - 'Create chat', - style: TextStyle(color: white), - ), - centerTitle: true, - ), - body: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const GlobalSearchBar(), - Row(children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.fromLTRB(15, 20, 15, 16), - child: TextButton.icon( - style: const ButtonStyle( - backgroundColor: WidgetStatePropertyAll(slateBlue), - ), - onPressed: () => context.push(groupCreateScreenPath), - icon: const Icon(Icons.group_outlined, - color: lightWhite, size: 25), - label: const Text( - 'Create group', - style: TextStyle( - color: lightWhite, - fontSize: 16, - fontWeight: FontWeight.w500), - ), - ), - ), - ), - ]), - const Expanded( - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: EdgeInsets.only(bottom: 8.0, top: 8.0), - child: Align( - alignment: Alignment.centerLeft, - child: Text( - 'List of users:', - style: TextStyle( - fontWeight: FontWeight.bold, fontSize: 16), - ), - ), - ), - SearchForm(searchType: SearchType.both) - ])) - ]), - )); - } -} diff --git a/sama_chat_client/lib/src/features/conversation_create/view/conversation_create_page.dart b/sama_chat_client/lib/src/features/conversation_create/view/conversation_create_page.dart deleted file mode 100644 index 7d1c1161..00000000 --- a/sama_chat_client/lib/src/features/conversation_create/view/conversation_create_page.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; - -import '../../../db/models/conversation_model.dart'; -import '../../../navigation/constants.dart'; -import '../../../repository/conversation/conversation_repository.dart'; -import '../../../repository/global_search/global_search_repository.dart'; -import '../../../shared/ui/view/loading_overlay.dart'; -import '../../conversation_create/bloc/conversation_create_bloc.dart'; -import '../../conversation_create/bloc/conversation_create_state.dart'; -import '../../search/bloc/global_search_bloc.dart'; -import '../../conversation_create/view/conversation_create_form.dart'; - -class ConversationCreatePage extends StatelessWidget { - const ConversationCreatePage({super.key}); - - static MultiBlocProvider route() { - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => GlobalSearchBloc( - globalSearchRepository: - RepositoryProvider.of(context), - ), - ), - BlocProvider( - create: (context) => ConversationCreateBloc( - conversationRepository: - RepositoryProvider.of(context), - ), - ), - ], - child: const ConversationCreatePage(), - ); - } - - @override - Widget build(BuildContext context) { - final LoadingOverlay loadingOverlay = LoadingOverlay(); - return BlocListener( - listener: (context, state) { - if (state is ConversationCreatedLoading) { - loadingOverlay.show(context); - } else if (state is ConversationCreatedState) { - loadingOverlay.hide(); - ConversationModel conversation = state.conversation; - context.go('$conversationListScreenPath/$conversationScreenSubPath', - extra: conversation); - } else if (state is ConversationCreatedStateError) { - loadingOverlay.hide(); - ScaffoldMessenger.of(context) - ..hideCurrentSnackBar() - ..showSnackBar( - SnackBar(content: Text(state.error ?? '')), - ); - } - }, - child: const ConversationCreateForm()); - } -} diff --git a/sama_chat_client/lib/src/features/conversation_group_create/bloc/group_bloc.dart b/sama_chat_client/lib/src/features/conversation_group_create/bloc/group_bloc.dart index 3821ee99..5b0eb426 100644 --- a/sama_chat_client/lib/src/features/conversation_group_create/bloc/group_bloc.dart +++ b/sama_chat_client/lib/src/features/conversation_group_create/bloc/group_bloc.dart @@ -6,6 +6,8 @@ import 'package:formz/formz.dart'; import 'package:image_picker/image_picker.dart'; import '../../../db/models/user_model.dart'; +import '../../../repository/conversation/conversation_repository.dart'; +import '../../../repository/user/user_repository.dart'; import '../models/avatar.dart'; import '../models/groupname.dart'; import '../models/participants.dart'; @@ -15,7 +17,11 @@ part 'group_event.dart'; part 'group_state.dart'; class GroupBloc extends Bloc { - GroupBloc() : super(const GroupState()) { + final ConversationRepository conversationRepository; + final UserRepository userRepository; + + GroupBloc(this.conversationRepository, this.userRepository) + : super(const GroupState()) { on(_onGroupnameChanged); on(_onGroupAvatarPicked); on(_onGroupParticipantsAdded); diff --git a/sama_chat_client/lib/src/features/conversation_group_create/view/group_create_form.dart b/sama_chat_client/lib/src/features/conversation_group_create/view/group_create_form.dart index 574e1fe0..4808afdb 100644 --- a/sama_chat_client/lib/src/features/conversation_group_create/view/group_create_form.dart +++ b/sama_chat_client/lib/src/features/conversation_group_create/view/group_create_form.dart @@ -8,11 +8,11 @@ import '../../../features/conversation_create/bloc/conversation_create_event.dar import '../../../shared/ui/colors.dart'; import '../../../shared/ui/view/participants_forms.dart'; import '../../../shared/ui/view/text_button_forms.dart'; -import '../../../shared/utils/api_utils.dart'; import '../../../shared/utils/screen_factor.dart'; import '../../conversation_create/bloc/conversation_create_bloc.dart'; import '../../conversations_list/widgets/avatar_letter_icon.dart'; -import '../../search/view/search_bar.dart'; +import '../../global_search/view/search_bar.dart'; +import '../../search/bloc/search_bloc.dart'; import '../bloc/group_bloc.dart'; import '../models/groupname.dart'; @@ -39,7 +39,7 @@ class GroupCreateFormState extends State { leading: const BackButton(color: white), centerTitle: true, title: const Text( - 'Group create', + 'New group', style: TextStyle(color: white), )), body: BlocListener( @@ -63,30 +63,37 @@ class GroupCreateFormState extends State { } }, child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - const GlobalSearchBar(), - Expanded( - child: BlocBuilder( - buildWhen: (previous, current) { - return previous.participants != current.participants; - }, builder: (context, state) { - var users = state.participants.value; - return ParticipantsForm( - users: List.of(users), - onAddParticipants: (user) { - context - .read() - .add(GroupParticipantsAdded(user)); - }, - onRemoveParticipants: (user) { - context - .read() - .add(GroupParticipantsRemoved(user)); - }, - ); - })) - ]), + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 10, + children: [ + const GlobalSearchBar(hintText: 'Search for people to add'), + Expanded( + child: BlocBuilder( + buildWhen: (previous, current) { + return previous.participants != current.participants; + }, builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 25), + child: ParticipantsForm( + participants: List.of(state.participants.value), + users: context.select( + (SearchBloc bloc) => bloc.state.users, + ), + onAddParticipants: (user) { + context + .read() + .add(GroupParticipantsAdded(user)); + }, + onRemoveParticipants: (user) { + context + .read() + .add(GroupParticipantsRemoved(user)); + }, + )); + })) + ]), )), floatingActionButton: BlocBuilder(buildWhen: (previous, current) { diff --git a/sama_chat_client/lib/src/features/conversation_group_create/view/group_create_page.dart b/sama_chat_client/lib/src/features/conversation_group_create/view/group_create_page.dart index e92206e6..617bb157 100644 --- a/sama_chat_client/lib/src/features/conversation_group_create/view/group_create_page.dart +++ b/sama_chat_client/lib/src/features/conversation_group_create/view/group_create_page.dart @@ -6,10 +6,12 @@ import '../../../db/models/conversation_model.dart'; import '../../../navigation/constants.dart'; import '../../../repository/conversation/conversation_repository.dart'; import '../../../repository/global_search/global_search_repository.dart'; +import '../../../repository/user/user_repository.dart'; import '../../../shared/ui/view/loading_overlay.dart'; import '../../conversation_create/bloc/conversation_create_bloc.dart'; import '../../conversation_create/bloc/conversation_create_state.dart'; -import '../../search/bloc/global_search_bloc.dart'; +import '../../global_search/bloc/global_search_bloc.dart'; +import '../../search/bloc/search_bloc.dart'; import '../bloc/group_bloc.dart'; import 'group_create_form.dart'; @@ -32,7 +34,14 @@ class GroupCreatePage extends StatelessWidget { ), ), BlocProvider( - create: (context) => GroupBloc(), + create: (context) => GroupBloc( + RepositoryProvider.of(context), + RepositoryProvider.of(context)), + ), + BlocProvider( + create: (context) => SearchBloc( + RepositoryProvider.of(context), + RepositoryProvider.of(context)), ), ], child: const GroupCreatePage(), 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 543d7c13..23e21455 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 @@ -102,7 +102,7 @@ class ChatAppBar extends StatelessWidget implements PreferredSizeWidget { actions: [ ConnectionChecker( child: IconButton( - onPressed: () => context.push(conversationCreateScreenPath), + onPressed: () => context.push(searchScreenPath), icon: const Icon( Icons.edit_note_outlined, color: lightWhite, diff --git a/sama_chat_client/lib/src/features/search/bloc/global_search_bloc.dart b/sama_chat_client/lib/src/features/global_search/bloc/global_search_bloc.dart similarity index 93% rename from sama_chat_client/lib/src/features/search/bloc/global_search_bloc.dart rename to sama_chat_client/lib/src/features/global_search/bloc/global_search_bloc.dart index 7e5c3a15..2559196a 100644 --- a/sama_chat_client/lib/src/features/search/bloc/global_search_bloc.dart +++ b/sama_chat_client/lib/src/features/global_search/bloc/global_search_bloc.dart @@ -2,8 +2,8 @@ import 'package:bloc/bloc.dart'; import 'package:stream_transform/stream_transform.dart'; import '../../../repository/global_search/global_search_repository.dart'; -import '../bloc/global_search_event.dart'; -import '../bloc/global_search_state.dart'; +import 'global_search_event.dart'; +import 'global_search_state.dart'; import '../models/models.dart'; const _duration = Duration(milliseconds: 300); diff --git a/sama_chat_client/lib/src/features/search/bloc/global_search_event.dart b/sama_chat_client/lib/src/features/global_search/bloc/global_search_event.dart similarity index 100% rename from sama_chat_client/lib/src/features/search/bloc/global_search_event.dart rename to sama_chat_client/lib/src/features/global_search/bloc/global_search_event.dart diff --git a/sama_chat_client/lib/src/features/search/bloc/global_search_state.dart b/sama_chat_client/lib/src/features/global_search/bloc/global_search_state.dart similarity index 100% rename from sama_chat_client/lib/src/features/search/bloc/global_search_state.dart rename to sama_chat_client/lib/src/features/global_search/bloc/global_search_state.dart diff --git a/sama_chat_client/lib/src/features/search/models/models.dart b/sama_chat_client/lib/src/features/global_search/models/models.dart similarity index 100% rename from sama_chat_client/lib/src/features/search/models/models.dart rename to sama_chat_client/lib/src/features/global_search/models/models.dart diff --git a/sama_chat_client/lib/src/features/search/models/search_result.dart b/sama_chat_client/lib/src/features/global_search/models/search_result.dart similarity index 100% rename from sama_chat_client/lib/src/features/search/models/search_result.dart rename to sama_chat_client/lib/src/features/global_search/models/search_result.dart diff --git a/sama_chat_client/lib/src/features/search/models/search_result_error.dart b/sama_chat_client/lib/src/features/global_search/models/search_result_error.dart similarity index 100% rename from sama_chat_client/lib/src/features/search/models/search_result_error.dart rename to sama_chat_client/lib/src/features/global_search/models/search_result_error.dart diff --git a/sama_chat_client/lib/src/features/search/view/search_bar.dart b/sama_chat_client/lib/src/features/global_search/view/search_bar.dart similarity index 92% rename from sama_chat_client/lib/src/features/search/view/search_bar.dart rename to sama_chat_client/lib/src/features/global_search/view/search_bar.dart index df51fb03..74cea3e8 100644 --- a/sama_chat_client/lib/src/features/search/view/search_bar.dart +++ b/sama_chat_client/lib/src/features/global_search/view/search_bar.dart @@ -7,7 +7,9 @@ import '../bloc/global_search_bloc.dart'; import '../bloc/global_search_event.dart'; class GlobalSearchBar extends StatefulWidget implements PreferredSizeWidget { - const GlobalSearchBar({super.key}); + final String? hintText; + + const GlobalSearchBar({this.hintText = 'Search', super.key}); @override State createState() => _GlobalSearchBarState(); @@ -64,7 +66,7 @@ class _GlobalSearchBarState extends State { borderRadius: BorderRadius.circular(20.0), borderSide: const BorderSide(color: slateBlue, width: 2), ), - hintText: 'Search', + hintText: widget.hintText, ), ), ); @@ -75,7 +77,7 @@ class _GlobalSearchBarState extends State { _textController.text = ''; _globalSearchBloc.add(const TextChanged(text: '')); } else { - context.pop(); + FocusScope.of(context).unfocus(); } } } diff --git a/sama_chat_client/lib/src/features/global_search/view/search_result.dart b/sama_chat_client/lib/src/features/global_search/view/search_result.dart new file mode 100644 index 00000000..75db83c0 --- /dev/null +++ b/sama_chat_client/lib/src/features/global_search/view/search_result.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../db/models/conversation_model.dart'; +import '../../../db/models/user_model.dart'; +import '../../../shared/ui/colors.dart'; +import '../../conversation_create/bloc/conversation_create_bloc.dart'; +import '../../conversation_create/bloc/conversation_create_event.dart'; +import '../../conversations_list/conversations_list.dart'; +import '../../conversations_list/widgets/avatar_letter_icon.dart'; + +enum SearchType { + users, + chats, + both, +} + +class SearchResults extends StatelessWidget { + const SearchResults(this.users, this.conversations, + {super.key, this.searchType = SearchType.both, this.chatOnTap}); + + final List? users; + final List? conversations; + final SearchType searchType; + final void Function(ConversationModel)? chatOnTap; + + Widget _header(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Container( + padding: const EdgeInsets.only(left: 18.0), + width: double.maxFinite, + color: gainsborough, //define the background color + child: Text( + title, + style: const TextStyle(fontSize: 18), + ), + ), + ); + } + + Widget _emptyListText(String title) { + return Padding( + padding: const EdgeInsets.all(10.0), + child: Text( + title, + style: const TextStyle( + fontWeight: FontWeight.w300, + fontSize: 16, + ), + textAlign: TextAlign.center, + ), + ); + } + + @override + Widget build(BuildContext context) { + final userList = users == null + ? null + : users!.isEmpty + ? _emptyListText('We couldn\'t find the specified users') + : ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: users!.length, + itemBuilder: (BuildContext context, int index) { + final user = users![index]; + return ListTile( + leading: AvatarLetterIcon( + name: user.login!, avatar: user.avatar), + title: Text( + user.login!, + style: const TextStyle( + fontWeight: FontWeight.w500, fontSize: 20), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + contentPadding: + const EdgeInsets.fromLTRB(18.0, 8.0, 18.0, 8.0), + onTap: () { + context + .read() + .add(ConversationCreated(user: user, type: 'u')); + }, + ); + }, + ); + + final chatList = conversations == null + ? null + : conversations!.isEmpty + ? _emptyListText('We couldn\'t find the specified chats') + : ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: conversations!.length, + itemBuilder: (BuildContext context, int index) { + final chat = conversations![index]; + return ConversationListItem( + conversation: chat, onTap: () => chatOnTap?.call(chat)); + }, + ); + + return MediaQuery.removePadding( + context: context, + removeTop: true, + child: ListView( + padding: const EdgeInsets.only(top: 10.0), + children: [ + if (searchType == SearchType.both || + searchType == SearchType.users) ...[ + if (searchType == SearchType.both) + if (userList != null) ...[_header('Users'), userList], + ], + if (searchType == SearchType.both || + searchType == SearchType.chats) ...[ + if (searchType == SearchType.both) + if (chatList != null) ...[_header('Chats'), chatList], + ], + ], + ), + ); + } +} diff --git a/sama_chat_client/lib/src/features/group_info/view/group_info_form.dart b/sama_chat_client/lib/src/features/group_info/view/group_info_form.dart index f7b59fdc..5a69b82f 100644 --- a/sama_chat_client/lib/src/features/group_info/view/group_info_form.dart +++ b/sama_chat_client/lib/src/features/group_info/view/group_info_form.dart @@ -16,8 +16,8 @@ import '../../../shared/utils/screen_factor.dart'; import '../../../shared/utils/string_utils.dart'; import '../../../shared/widget/keyboard_listener.dart'; import '../../conversations_list/widgets/avatar_letter_icon.dart'; -import '../../search/bloc/global_search_bloc.dart'; -import '../../search/view/search_bar.dart'; +import '../../global_search/bloc/global_search_bloc.dart'; +import '../../global_search/view/search_bar.dart'; import '../bloc/group_info_bloc.dart'; import '../models/models.dart'; @@ -220,7 +220,7 @@ class _ParticipantsHeaderForm extends StatelessWidget { Widget build(BuildContext context) { var state = context.read().state; return ListTile( - contentPadding: const EdgeInsets.all(8.0), + contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), leading: Text( '${state.participants.value.length} ${state.participants.value.length > 1 ? 'members' : 'member'}', style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 20), @@ -274,10 +274,11 @@ class _ParticipantsListForm extends StatelessWidget { if (user.id == ownerId) const Text( 'admin', - style: TextStyle(fontWeight: FontWeight.w300, fontSize: 18), + style: TextStyle( + fontWeight: FontWeight.w300, fontSize: 18, height: 1.0), ), ]), - contentPadding: const EdgeInsets.fromLTRB(8.0, 8.0, 8.0, 8.0), + contentPadding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 12.0), onTap: () { user.id == currentUserId ? context.push(profilePath) @@ -350,7 +351,7 @@ void _showSearchScreenDialog(BuildContext context) { state.participants.value ..remove(state.currentUser)); return ParticipantsForm( - users: List.of(currentParticipants) + participants: List.of(currentParticipants) ..addAll(state.addParticipants.value), nonRemovableUsers: currentParticipants, onAddParticipants: (user) { diff --git a/sama_chat_client/lib/src/features/search/bloc/search_bloc.dart b/sama_chat_client/lib/src/features/search/bloc/search_bloc.dart new file mode 100644 index 00000000..229fd334 --- /dev/null +++ b/sama_chat_client/lib/src/features/search/bloc/search_bloc.dart @@ -0,0 +1,46 @@ +import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; + +import '../../../db/models/models.dart'; +import '../../../repository/conversation/conversation_repository.dart'; +import '../../../repository/user/user_repository.dart'; + +part 'search_event.dart'; + +part 'search_state.dart'; + +class SearchBloc extends Bloc { + final ConversationRepository conversationRepository; + final UserRepository userRepository; + + SearchBloc(this.conversationRepository, this.userRepository) + : super(const SearchState()) { + on(_onUsersRecent); + + add(UsersRecent()); + } + + Future _onUsersRecent(event, emit) async { + var lim = 10; + var currentUserId = await userRepository.getCurrentUserId(); + var chats = await conversationRepository.getStoredConversations( + limit: lim, type: 'u'); + if (chats.length < lim) { + chats.addAll(await conversationRepository.getStoredConversations( + limit: lim, type: 'g')); + } + List users = chats + .map((chat) => chat.participants.toList()) + .flattenedToSet + .where((u) => u.id != currentUserId) + .take(lim) + .toList(); + + emit( + state.copyWith( + users: users, + ), + ); + } +} diff --git a/sama_chat_client/lib/src/features/search/bloc/search_event.dart b/sama_chat_client/lib/src/features/search/bloc/search_event.dart new file mode 100644 index 00000000..97af1f8f --- /dev/null +++ b/sama_chat_client/lib/src/features/search/bloc/search_event.dart @@ -0,0 +1,10 @@ +part of 'search_bloc.dart'; + +sealed class SearchEvent extends Equatable { + const SearchEvent(); + + @override + List get props => []; +} + +final class UsersRecent extends SearchEvent {} diff --git a/sama_chat_client/lib/src/features/search/bloc/search_state.dart b/sama_chat_client/lib/src/features/search/bloc/search_state.dart new file mode 100644 index 00000000..7c2acd3d --- /dev/null +++ b/sama_chat_client/lib/src/features/search/bloc/search_state.dart @@ -0,0 +1,16 @@ +part of 'search_bloc.dart'; + +final class SearchState extends Equatable { + const SearchState({this.users = const []}); + + final List users; + + SearchState copyWith({ + List? users, + }) { + return SearchState(users: users ?? this.users); + } + + @override + List get props => [users]; +} diff --git a/sama_chat_client/lib/src/features/search/view/search_form.dart b/sama_chat_client/lib/src/features/search/view/search_form.dart index 1cee97c6..9ba70bb0 100644 --- a/sama_chat_client/lib/src/features/search/view/search_form.dart +++ b/sama_chat_client/lib/src/features/search/view/search_form.dart @@ -1,40 +1,116 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; - -import '../../../db/models/conversation_model.dart'; -import '../../../db/models/user_model.dart'; -import '../../../features/search/view/search_bar.dart'; +import '../../../db/models/models.dart'; import '../../../navigation/constants.dart'; import '../../../shared/ui/colors.dart'; +import '../../global_search/bloc/global_search_bloc.dart'; +import '../../global_search/bloc/global_search_state.dart'; +import '../../global_search/view/search_bar.dart'; +import '../../global_search/view/search_result.dart'; import '../../conversation_create/bloc/conversation_create_bloc.dart'; -import '../../conversation_create/bloc/conversation_create_event.dart'; import '../../conversation_create/bloc/conversation_create_state.dart'; -import '../../conversations_list/conversations_list.dart'; -import '../../conversations_list/widgets/avatar_letter_icon.dart'; -import '../bloc/global_search_bloc.dart'; -import '../bloc/global_search_state.dart'; +import '../bloc/search_bloc.dart'; -class SearchForm extends StatelessWidget { - const SearchForm({this.searchType = SearchType.both, super.key}); +class SearchForm extends StatefulWidget { + const SearchForm({super.key}); - final SearchType searchType; + @override + State createState() { + return SearchFormState(); + } +} + +class SearchFormState extends State { + @override + void initState() { + super.initState(); + } @override Widget build(BuildContext context) { - // final LoadingOverlay loadingOverlay = LoadingOverlay(); + return Scaffold( + appBar: AppBar( + backgroundColor: black, + iconTheme: const IconThemeData( + color: white, + ), + title: const Text( + 'New chat', + style: TextStyle(color: white), + ), + centerTitle: true, + ), + body: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const GlobalSearchBar(hintText: 'Search name or user'), + Row(children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 20, 15, 15), + child: TextButton.icon( + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(slateBlue), + ), + onPressed: () => context.push(groupCreateScreenPath), + icon: const Icon(Icons.group_outlined, + color: lightWhite, size: 25), + label: const Text( + 'New group', + style: TextStyle( + color: lightWhite, + fontSize: 16, + fontWeight: FontWeight.w500), + ), + ), + ), + ), + ]), + Expanded( + child: Align( + alignment: AlignmentGeometry.topCenter, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: BlocBuilder( + builder: (context, state) => UserChatForm( + searchType: SearchType.both, + users: state.users, + chatOnTap: (chat) { + context.go( + '$conversationListScreenPath/$conversationScreenSubPath', + extra: chat); + }), + ), + ))) + ]))); + } +} +class UserChatForm extends StatelessWidget { + const UserChatForm( + {this.searchType = SearchType.both, + this.chatOnTap, + this.users, + super.key}); + + final SearchType searchType; + final Function(ConversationModel)? chatOnTap; + final List? users; + + @override + Widget build(BuildContext context) { return BlocListener( listener: (context, state) { if (state is ConversationCreatedLoading) { - // loadingOverlay.show(context);// for now disable } else if (state is ConversationCreatedState) { - // loadingOverlay.hide();// for now disable ConversationModel conversation = state.conversation; context.go('$conversationListScreenPath/$conversationScreenSubPath', extra: conversation); } else if (state is ConversationCreatedStateError) { - // loadingOverlay.hide();// for now disable ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( @@ -45,10 +121,13 @@ class SearchForm extends StatelessWidget { child: BlocBuilder( builder: (context, state) { return switch (state) { - SearchStateEmpty() => const Padding( - padding: EdgeInsets.only(top: 18.0), - child: Text('Please start typing to find user or chat'), - ), + SearchStateEmpty() => users?.isEmpty ?? true + ? const Padding( + padding: EdgeInsets.only(top: 18.0), + child: Text('Please start typing to find user or chat'), + ) + : SearchResults(users, null, + searchType: searchType, chatOnTap: chatOnTap), SearchStateLoading() => const Padding( padding: EdgeInsets.only(top: 18.0), child: CircularProgressIndicator.adaptive(), @@ -57,124 +136,12 @@ class SearchForm extends StatelessWidget { padding: const EdgeInsets.only(top: 18.0), child: Text(state.error), ), - SearchStateSuccess() => Expanded( - child: SearchResults(state.users, state.conversations, - searchType: searchType)), + SearchStateSuccess() => SearchResults( + state.users, state.conversations, + searchType: searchType, chatOnTap: chatOnTap), }; }, ), ); } } - -enum SearchType { - users, - chats, - both, -} - -class SearchResults extends StatelessWidget { - const SearchResults(this.users, this.conversations, - {super.key, this.searchType = SearchType.both, this.chatOnTap}); - - final List? users; - final List conversations; - final SearchType searchType; - final void Function(ConversationModel)? chatOnTap; - - Widget _header(String title) { - return Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 8.0), - child: Container( - padding: const EdgeInsets.only(left: 18.0), - width: double.maxFinite, - color: gainsborough, //define the background color - child: Text( - title, - style: const TextStyle(fontSize: 18), - ), - ), - ); - } - - Widget _emptyListText(String title) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Text( - title, - style: const TextStyle( - fontWeight: FontWeight.w300, - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - ); - } - - @override - Widget build(BuildContext context) { - final userList = users == null - ? null - : users!.isEmpty - ? _emptyListText('We couldn\'t find the specified users') - : ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: users!.length, - itemBuilder: (BuildContext context, int index) { - final user = users![index]; - return ListTile( - leading: AvatarLetterIcon( - name: user.login!, avatar: user.avatar), - title: Text( - user.login!, - style: const TextStyle( - fontWeight: FontWeight.w500, fontSize: 20), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - contentPadding: - const EdgeInsets.fromLTRB(18.0, 8.0, 18.0, 8.0), - onTap: () { - context - .read() - .add(ConversationCreated(user: user, type: 'u')); - }, - ); - }, - ); - - final conversationList = conversations.isEmpty - ? _emptyListText('We couldn\'t find the specified chats') - : ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: conversations.length, - itemBuilder: (BuildContext context, int index) { - final chat = conversations[index]; - return ConversationListItem( - conversation: chat, onTap: () => chatOnTap?.call(chat)); - }, - ); - - return MediaQuery.removePadding( - context: context, - removeTop: true, - child: ListView( - padding: const EdgeInsets.only(top: 10.0), - children: [ - if (searchType == SearchType.both || - searchType == SearchType.users) ...[ - if (searchType == SearchType.both) - if (userList != null) ...[_header('Users'), userList], - ], - if (searchType == SearchType.both || - searchType == SearchType.chats) ...[ - if (searchType == SearchType.both) _header('Chats'), - conversationList, - ], - ], - ), - ); - } -} diff --git a/sama_chat_client/lib/src/features/search/view/search_page.dart b/sama_chat_client/lib/src/features/search/view/search_page.dart new file mode 100644 index 00000000..bc942bf1 --- /dev/null +++ b/sama_chat_client/lib/src/features/search/view/search_page.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../repository/conversation/conversation_repository.dart'; +import '../../../repository/global_search/global_search_repository.dart'; +import '../../../repository/user/user_repository.dart'; +import '../../conversation_create/bloc/conversation_create_bloc.dart'; +import '../../global_search/bloc/global_search_bloc.dart'; +import '../bloc/search_bloc.dart'; +import 'search_form.dart'; + +class SearchPage extends StatelessWidget { + const SearchPage({super.key}); + + static MultiBlocProvider route() { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => GlobalSearchBloc( + globalSearchRepository: + RepositoryProvider.of(context), + ), + ), + BlocProvider( + create: (context) => ConversationCreateBloc( + conversationRepository: + RepositoryProvider.of(context), + ), + ), + BlocProvider( + create: (context) => SearchBloc( + RepositoryProvider.of(context), + RepositoryProvider.of(context)), + ), + ], + child: const SearchPage(), + ); + } + + @override + Widget build(BuildContext context) { + return const SearchForm(); + } +} diff --git a/sama_chat_client/lib/src/navigation/app_router.dart b/sama_chat_client/lib/src/navigation/app_router.dart index 9e34f0a3..6416515f 100644 --- a/sama_chat_client/lib/src/navigation/app_router.dart +++ b/sama_chat_client/lib/src/navigation/app_router.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; -import '../features/conversation_create/view/conversation_create_page.dart'; +import '../features/search/view/search_page.dart'; import '../features/group_info/view/group_info_page.dart'; import '../features/conversation_group_create/view/group_create_page.dart'; import '../features/conversations_list/view/conversations_page.dart'; @@ -66,9 +66,9 @@ GoRouter router(BuildContext context, navigatorKey) => GoRouter( }, ), GoRoute( - path: conversationCreateScreenPath, + path: searchScreenPath, builder: (context, state) { - return ConversationCreatePage.route(); + return SearchPage.route(); }, ), GoRoute( diff --git a/sama_chat_client/lib/src/navigation/constants.dart b/sama_chat_client/lib/src/navigation/constants.dart index c9fcd645..4ae844a1 100644 --- a/sama_chat_client/lib/src/navigation/constants.dart +++ b/sama_chat_client/lib/src/navigation/constants.dart @@ -4,7 +4,7 @@ const String splashScreenPath = '/splash'; const String conversationListScreenPath = '/conversations'; const String conversationScreenSubPath = 'conversation'; const String groupCreateScreenPath = '/group_create'; -const String conversationCreateScreenPath = '/conversation_create'; +const String searchScreenPath = '/conversation_user_search'; const String profilePath = '/profile'; const String userInfoPath = '/user_info'; const String groupInfoPath = '/group_info'; 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 65701a4c..7c9d728d 100644 --- a/sama_chat_client/lib/src/repository/conversation/conversation_repository.dart +++ b/sama_chat_client/lib/src/repository/conversation/conversation_repository.dart @@ -186,8 +186,10 @@ class ConversationRepository { _conversationsController.add(updatedConversation); } - Future> getStoredConversations() async { - var conversations = await localDatasource.getAllConversationsLocal(); + Future> getStoredConversations( + {DateTime? ltDate, int? limit, String? type}) async { + var conversations = await localDatasource.getAllConversationsLocal( + ltDate: ltDate, limit: limit, type: type); return conversations.whereNot((c) => _chatsFilter(c)).toList(); } diff --git a/sama_chat_client/lib/src/repository/global_search/global_search_repository.dart b/sama_chat_client/lib/src/repository/global_search/global_search_repository.dart index 40407e06..a233a535 100644 --- a/sama_chat_client/lib/src/repository/global_search/global_search_repository.dart +++ b/sama_chat_client/lib/src/repository/global_search/global_search_repository.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:sama_sdk/api/api.dart'; import '../../db/models/models.dart'; -import '../../features/search/models/models.dart'; +import '../../features/global_search/models/models.dart'; import '../conversation/conversation_repository.dart'; import '../user/user_repository.dart'; diff --git a/sama_chat_client/lib/src/repository/user/user_repository.dart b/sama_chat_client/lib/src/repository/user/user_repository.dart index b5a80bd7..a0a5774b 100644 --- a/sama_chat_client/lib/src/repository/user/user_repository.dart +++ b/sama_chat_client/lib/src/repository/user/user_repository.dart @@ -101,7 +101,7 @@ class UserRepository { return participants; } - Future> getUsersByCids(List cids) async { + Future> fetchUsersByCids(List cids) async { return (await fetchParticipants(cids)) .$2 .map((element) => element.toUserModel()) diff --git a/sama_chat_client/lib/src/shared/secure_storage.dart b/sama_chat_client/lib/src/shared/secure_storage.dart index 61b82b66..54b578bd 100644 --- a/sama_chat_client/lib/src/shared/secure_storage.dart +++ b/sama_chat_client/lib/src/shared/secure_storage.dart @@ -34,7 +34,7 @@ class SecureStorage { } saveCurrentUser(user); } - +//ToDo RP remove all fields to safe but id and deviceId Future saveCurrentUser(UserModel user) async { if (user.id != null) { _storage.write(key: storageUserId, value: user.id); diff --git a/sama_chat_client/lib/src/shared/ui/view/participants_forms.dart b/sama_chat_client/lib/src/shared/ui/view/participants_forms.dart index ebf09f0f..a4a7dc5d 100644 --- a/sama_chat_client/lib/src/shared/ui/view/participants_forms.dart +++ b/sama_chat_client/lib/src/shared/ui/view/participants_forms.dart @@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../db/models/user_model.dart'; import '../../../features/conversations_list/widgets/avatar_letter_icon.dart'; -import '../../../features/search/bloc/global_search_bloc.dart'; -import '../../../features/search/bloc/global_search_state.dart'; +import '../../../features/global_search/bloc/global_search_bloc.dart'; +import '../../../features/global_search/bloc/global_search_state.dart'; import '../../utils/api_utils.dart'; import '../../utils/screen_factor.dart'; import '../../utils/string_utils.dart'; @@ -12,13 +12,15 @@ import '../colors.dart'; class ParticipantsForm extends StatelessWidget { const ParticipantsForm( - {required this.users, + {required this.participants, required this.onAddParticipants, required this.onRemoveParticipants, + this.users, this.nonRemovableUsers, super.key}); - final List users; + final List participants; + final List? users; final List? nonRemovableUsers; final ValueSetter onAddParticipants; final ValueSetter onRemoveParticipants; @@ -26,35 +28,19 @@ class ParticipantsForm extends StatelessWidget { @override Widget build(BuildContext context) { return Column(mainAxisSize: MainAxisSize.min, children: [ - const Align( - alignment: Alignment.centerLeft, - child: Padding( - padding: EdgeInsets.only(top: 8.0, bottom: 8.0), - child: Text('Add participants', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), - ), - ), LimitedBox( maxHeight: screenHeight / 5.5, child: Padding( padding: const EdgeInsets.only(bottom: 8.0), child: ParticipantsList( - users: users, + users: participants, nonRemovableUsers: nonRemovableUsers, onRemoveParticipants: onRemoveParticipants), ), ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0, top: 8.0), - child: Align( - alignment: Alignment.centerLeft, - child: Text('List of users ${users.length}/$maxParticipantsCount', - style: - const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), - ), - ), _SearchBody( - selectedUsers: users, + selectedUsers: participants, + users: users, onAddParticipants: onAddParticipants, onRemoveParticipants: onRemoveParticipants) ]); @@ -64,10 +50,12 @@ class ParticipantsForm extends StatelessWidget { class _SearchBody extends StatelessWidget { const _SearchBody( {required this.selectedUsers, + required this.users, required this.onAddParticipants, required this.onRemoveParticipants}); final List selectedUsers; + final List? users; final ValueSetter onAddParticipants; final ValueSetter onRemoveParticipants; @@ -76,10 +64,14 @@ class _SearchBody extends StatelessWidget { return BlocBuilder( builder: (context, state) { return switch (state) { - SearchStateEmpty() => const Padding( - padding: EdgeInsets.only(top: 18.0), - child: Text('Please start typing to find user'), - ), + SearchStateEmpty() => users?.isEmpty ?? true + ? const SizedBox.shrink() + : Expanded( + child: _SearchResults( + users: users!, + selectedUsers: selectedUsers, + onAddParticipants: onAddParticipants, + onRemoveParticipants: onRemoveParticipants)), SearchStateLoading() => const Padding( padding: EdgeInsets.only(top: 18.0), child: CircularProgressIndicator.adaptive(), @@ -159,7 +151,7 @@ class _SearchResults extends StatelessWidget { ); }, separatorBuilder: (context, index) { - return const Divider(color: lightMallow); + return const Divider(color: Colors.transparent, height: 4); }, );