From f93a95d14138cffa1eb1e7dde4040e665ce78ec8 Mon Sep 17 00:00:00 2001 From: Linloir <3145078758@qq.com> Date: Thu, 13 Oct 2022 23:30:38 +0800 Subject: [PATCH] More Codes - Search Page (ugly) --- lib/common/avatar/avatar.dart | 69 ++++++++++ lib/common/avatar/cubit/avatar_cubit.dart | 28 ++++ lib/common/avatar/cubit/avatar_state.dart | 20 +++ lib/common/username/cubit/username_cubit.dart | 28 ++++ lib/common/username/cubit/username_state.dart | 20 +++ lib/common/username/username.dart | 42 ++++++ lib/home/cubit/home_cubit.dart | 2 +- lib/home/home_page.dart | 127 ++++++++++++------ lib/home/view/contact_page/contact_page.dart | 63 ++++----- .../view/contact_page/view/contact_tile.dart | 4 +- .../message_page/cubit/msg_list_cubit.dart | 18 +-- .../message_page/cubit/msg_list_state.dart | 20 +-- lib/home/view/message_page/mesage_page.dart | 31 +++-- .../message_page/models/message_info.dart | 8 +- .../view/message_page/view/message_tile.dart | 79 ++++++----- .../tcp_repository/models/tcp_request.dart | 20 ++- .../user_repository/user_repository.dart | 79 +++++++++++ lib/search/cubit/search_cubit.dart | 74 +++++++++- lib/search/cubit/search_state.dart | 30 ++++- lib/search/model/history_result.dart | 22 +++ lib/search/model/search_key.dart | 6 - lib/search/model/user_result.dart | 18 +++ lib/search/search_page.dart | 95 ++++++++++++- lib/search/view/history_tile.dart | 98 ++++++++++++++ lib/search/view/search_bar.dart | 10 +- lib/search/view/user_tile.dart | 73 ++++++++++ pubspec.lock | 7 + pubspec.yaml | 1 + 28 files changed, 920 insertions(+), 172 deletions(-) create mode 100644 lib/common/avatar/avatar.dart create mode 100644 lib/common/avatar/cubit/avatar_cubit.dart create mode 100644 lib/common/avatar/cubit/avatar_state.dart create mode 100644 lib/common/username/cubit/username_cubit.dart create mode 100644 lib/common/username/cubit/username_state.dart create mode 100644 lib/common/username/username.dart create mode 100644 lib/repositories/user_repository/user_repository.dart create mode 100644 lib/search/model/history_result.dart delete mode 100644 lib/search/model/search_key.dart create mode 100644 lib/search/model/user_result.dart create mode 100644 lib/search/view/history_tile.dart create mode 100644 lib/search/view/user_tile.dart diff --git a/lib/common/avatar/avatar.dart b/lib/common/avatar/avatar.dart new file mode 100644 index 0000000..2507d06 --- /dev/null +++ b/lib/common/avatar/avatar.dart @@ -0,0 +1,69 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 21:49:53 + * @LastEditTime : 2022-10-13 22:17:17 + * @Description : + */ + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tcp_client/common/avatar/cubit/avatar_cubit.dart'; +import 'package:tcp_client/common/avatar/cubit/avatar_state.dart'; +import 'package:tcp_client/repositories/user_repository/user_repository.dart'; + +class UserAvatar extends StatelessWidget { + const UserAvatar({required this.userid, super.key}); + + final int userid; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: AvatarCubit( + userid: userid, + userRepository: context.read() + ), + builder: (context, state) { + if(state.userInfo.avatarEncoded == null) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(5.0), + boxShadow: [BoxShadow(blurRadius: 10.0, color: Colors.grey[850]!.withOpacity(0.15))] + ), + child: Text( + state.userInfo.userName[0], + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w300, + color: Colors.white, + shadows: [Shadow(blurRadius: 5.0, color: Colors.white.withOpacity(0.15))] + ), + ), + ); + } + else { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.0), + boxShadow: [BoxShadow(blurRadius: 10.0, color: Colors.grey[850]!.withOpacity(0.15))] + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(5.0), + child: OverflowBox( + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.fitWidth, + child: Image.memory(base64.decode(state.userInfo.avatarEncoded!)), + ), + ) + ), + ); + } + }, + ); + } +} diff --git a/lib/common/avatar/cubit/avatar_cubit.dart b/lib/common/avatar/cubit/avatar_cubit.dart new file mode 100644 index 0000000..755bdf2 --- /dev/null +++ b/lib/common/avatar/cubit/avatar_cubit.dart @@ -0,0 +1,28 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 21:50:14 + * @LastEditTime : 2022-10-13 22:03:01 + * @Description : + */ + +import 'package:bloc/bloc.dart'; +import 'package:tcp_client/common/avatar/cubit/avatar_state.dart'; +import 'package:tcp_client/repositories/common_models/userinfo.dart'; +import 'package:tcp_client/repositories/user_repository/user_repository.dart'; + +class AvatarCubit extends Cubit { + AvatarCubit({ + required int userid, + required this.userRepository + }): super(AvatarState(userInfo: userRepository.getUserInfo(userid: userid))) { + userRepository.userInfoStreamBroadcast.listen(onFetchedUserInfo); + } + + final UserRepository userRepository; + + void onFetchedUserInfo(UserInfo userInfo) { + if(userInfo.userID == state.userInfo.userID) { + emit(AvatarState(userInfo: userInfo)); + } + } +} \ No newline at end of file diff --git a/lib/common/avatar/cubit/avatar_state.dart b/lib/common/avatar/cubit/avatar_state.dart new file mode 100644 index 0000000..13844fd --- /dev/null +++ b/lib/common/avatar/cubit/avatar_state.dart @@ -0,0 +1,20 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 21:50:07 + * @LastEditTime : 2022-10-13 22:02:11 + * @Description : + */ + +import 'package:equatable/equatable.dart'; +import 'package:tcp_client/repositories/common_models/userinfo.dart'; + +class AvatarState extends Equatable { + const AvatarState({ + required this.userInfo, + }); + + final UserInfo userInfo; + + @override + List get props => [userInfo.userID, userInfo.avatarEncoded]; +} diff --git a/lib/common/username/cubit/username_cubit.dart b/lib/common/username/cubit/username_cubit.dart new file mode 100644 index 0000000..0914761 --- /dev/null +++ b/lib/common/username/cubit/username_cubit.dart @@ -0,0 +1,28 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 21:50:14 + * @LastEditTime : 2022-10-13 22:05:52 + * @Description : + */ + +import 'package:bloc/bloc.dart'; +import 'package:tcp_client/common/username/cubit/username_state.dart'; +import 'package:tcp_client/repositories/common_models/userinfo.dart'; +import 'package:tcp_client/repositories/user_repository/user_repository.dart'; + +class UsernameCubit extends Cubit { + UsernameCubit({ + required int userid, + required this.userRepository + }): super(UsernameState(userInfo: userRepository.getUserInfo(userid: userid))) { + userRepository.userInfoStreamBroadcast.listen(onFetchedUserInfo); + } + + final UserRepository userRepository; + + void onFetchedUserInfo(UserInfo userInfo) { + if(userInfo.userID == state.userInfo.userID) { + emit(UsernameState(userInfo: userInfo)); + } + } +} \ No newline at end of file diff --git a/lib/common/username/cubit/username_state.dart b/lib/common/username/cubit/username_state.dart new file mode 100644 index 0000000..a582171 --- /dev/null +++ b/lib/common/username/cubit/username_state.dart @@ -0,0 +1,20 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 21:50:07 + * @LastEditTime : 2022-10-13 22:05:43 + * @Description : + */ + +import 'package:equatable/equatable.dart'; +import 'package:tcp_client/repositories/common_models/userinfo.dart'; + +class UsernameState extends Equatable { + const UsernameState({ + required this.userInfo, + }); + + final UserInfo userInfo; + + @override + List get props => [userInfo.userID, userInfo.avatarEncoded]; +} diff --git a/lib/common/username/username.dart b/lib/common/username/username.dart new file mode 100644 index 0000000..aa624bd --- /dev/null +++ b/lib/common/username/username.dart @@ -0,0 +1,42 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 22:05:12 + * @LastEditTime : 2022-10-13 22:21:42 + * @Description : + */ + +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tcp_client/common/username/cubit/username_cubit.dart'; +import 'package:tcp_client/common/username/cubit/username_state.dart'; +import 'package:tcp_client/repositories/user_repository/user_repository.dart'; + +class UserNameText extends StatelessWidget { + const UserNameText({ + required this.userid, + this.fontWeight = FontWeight.normal, + super.key + }); + + final int userid; + final FontWeight fontWeight; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + bloc: UsernameCubit( + userid: userid, + userRepository: context.read() + ), + builder: (context, state) { + return Text( + state.userInfo.userName, + style: TextStyle( + fontSize: 18, + fontWeight: fontWeight + ), + ); + } + ); + } +} diff --git a/lib/home/cubit/home_cubit.dart b/lib/home/cubit/home_cubit.dart index b2ef9e5..2c22bf8 100644 --- a/lib/home/cubit/home_cubit.dart +++ b/lib/home/cubit/home_cubit.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-13 14:02:28 - * @LastEditTime : 2022-10-13 16:43:49 + * @LastEditTime : 2022-10-13 23:02:04 * @Description : */ diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index 09e2297..fe9217f 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 11:05:08 - * @LastEditTime : 2022-10-13 16:55:48 + * @LastEditTime : 2022-10-13 23:02:55 * @Description : */ @@ -15,9 +15,11 @@ import 'package:tcp_client/home/view/message_page/cubit/msg_list_cubit.dart'; import 'package:tcp_client/home/view/message_page/mesage_page.dart'; import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; +import 'package:tcp_client/repositories/user_repository/user_repository.dart'; +import 'package:tcp_client/search/search_page.dart'; class HomePage extends StatelessWidget { - HomePage({ + const HomePage({ required this.localServiceRepository, required this.tcpRepository, super.key @@ -26,8 +28,6 @@ class HomePage extends StatelessWidget { final LocalServiceRepository localServiceRepository; final TCPRepository tcpRepository; - final PageController _controller = PageController(); - static Route route({ required LocalServiceRepository localServiceRepository, required TCPRepository tcpRepository @@ -38,47 +38,98 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return MultiBlocProvider( + return MultiRepositoryProvider( providers: [ - BlocProvider( - create: (context) => MessageListCubit( - localServiceRepository: localServiceRepository, - tcpRepository: tcpRepository - ), - ), - BlocProvider( - create: (context) => ContactCubit( + RepositoryProvider( + create: (context) => UserRepository( localServiceRepository: localServiceRepository, tcpRepository: tcpRepository ), ), - BlocProvider( - create: (context) => HomeCubit( - localServiceRepository: localServiceRepository, - tcpRepository: tcpRepository - ), - ) + RepositoryProvider.value(value: localServiceRepository), + RepositoryProvider.value(value: tcpRepository), ], - child: BlocListener( - listenWhen:(previous, current) => current.page != previous.page, - listener: (context, state) { - _controller.animateToPage( - state.page.value, - duration: const Duration(milliseconds: 375), - curve: Curves.easeInOutCubicEmphasized - ); - }, - child: Scaffold( - body: PageView( - controller: _controller, - children: const [ - MessagePage(), - ContactPage() - ], + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => MessageListCubit( + localServiceRepository: localServiceRepository, + tcpRepository: tcpRepository + ), ), - ), - ) - + BlocProvider( + create: (context) => ContactCubit( + localServiceRepository: localServiceRepository, + tcpRepository: tcpRepository + ), + ), + BlocProvider( + create: (context) => HomeCubit( + localServiceRepository: localServiceRepository, + tcpRepository: tcpRepository + ), + ) + ], + child: HomePageView(), + ), ); } } + +class HomePageView extends StatelessWidget { + HomePageView({super.key}); + + final PageController _controller = PageController(); + + @override + Widget build(BuildContext context) { + return BlocListener( + listenWhen:(previous, current) => current.page != previous.page, + listener: (context, state) { + _controller.animateToPage( + state.page.value, + duration: const Duration(milliseconds: 375), + curve: Curves.easeInOutCubicEmphasized + ); + }, + child: Scaffold( + appBar: AppBar( + title: BlocBuilder( + builder: (context, state) { + return Text( + state.page.literal, + style: const TextStyle( + fontWeight: FontWeight.bold + ), + ); + }, + ), + actions: [ + IconButton( + icon: const Icon(Icons.search_rounded), + onPressed: () { + Navigator.of(context).push(SearchPage.route( + localServiceRepository: context.read(), + tcpRepository: context.read(), + userRepository: context.read() + )); + }, + ) + ], + ), + body: Center( + child: BlocBuilder( + builder:(context, state) => PageView( + controller: _controller, + onPageChanged: (value) => context.read().switchPage(HomePagePosition.fromValue(value)), + children: const [ + MessagePage(), + ContactPage() + ], + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/home/view/contact_page/contact_page.dart b/lib/home/view/contact_page/contact_page.dart index 385e983..ce0e093 100644 --- a/lib/home/view/contact_page/contact_page.dart +++ b/lib/home/view/contact_page/contact_page.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-12 23:36:07 - * @LastEditTime : 2022-10-13 16:10:57 + * @LastEditTime : 2022-10-13 22:59:25 * @Description : */ @@ -18,36 +18,39 @@ class ContactPage extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - return AzListView( - data: state.indexedData, - itemCount: state.contacts.length, - itemBuilder: (context, index) { - return ContactTile( - userInfo: state.contacts[index], - ); - }, - physics: const BouncingScrollPhysics(), - susItemBuilder: (context, index) { - return Container( - height: 40, - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.only(left: 16.0), - color: Colors.grey[200], - alignment: Alignment.centerLeft, - child: Text( - ContactModel(userInfo: state.contacts[index]).getSuspensionTag(), - softWrap: false, - style: TextStyle( - fontSize: 14.0, - color: Colors.grey[700], + return Container( + color: Colors.green, + child: BlocBuilder( + builder: (context, state) { + return AzListView( + data: state.indexedData, + itemCount: state.contacts.length, + itemBuilder: (context, index) { + return ContactTile( + userInfo: state.contacts[index], + ); + }, + physics: const BouncingScrollPhysics(), + susItemBuilder: (context, index) { + return Container( + height: 40, + width: MediaQuery.of(context).size.width, + padding: const EdgeInsets.only(left: 16.0), + color: Colors.grey[200], + alignment: Alignment.centerLeft, + child: Text( + ContactModel(userInfo: state.contacts[index]).getSuspensionTag(), + softWrap: false, + style: TextStyle( + fontSize: 14.0, + color: Colors.grey[700], + ), ), - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ); } } diff --git a/lib/home/view/contact_page/view/contact_tile.dart b/lib/home/view/contact_page/view/contact_tile.dart index 44334a3..8e93ee5 100644 --- a/lib/home/view/contact_page/view/contact_tile.dart +++ b/lib/home/view/contact_page/view/contact_tile.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-13 14:02:00 - * @LastEditTime : 2022-10-13 16:44:03 + * @LastEditTime : 2022-10-13 22:26:07 * @Description : */ @@ -32,7 +32,7 @@ class ContactTile extends StatelessWidget { InkWell( onTap: () { Navigator.of(context).push(ChatPage.route(userInfo: userInfo)); - context.read().addEmptyMessageOf(user: userInfo); + context.read().addEmptyMessageOf(targetUser: userInfo.userID); context.read().switchPage(HomePagePosition.message); }, ), diff --git a/lib/home/view/message_page/cubit/msg_list_cubit.dart b/lib/home/view/message_page/cubit/msg_list_cubit.dart index 7601ace..3902a66 100644 --- a/lib/home/view/message_page/cubit/msg_list_cubit.dart +++ b/lib/home/view/message_page/cubit/msg_list_cubit.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-12 23:38:31 - * @LastEditTime : 2022-10-13 16:12:53 + * @LastEditTime : 2022-10-13 22:27:29 * @Description : */ @@ -25,11 +25,11 @@ class MessageListCubit extends Cubit { final LocalServiceRepository localServiceRepository; final TCPRepository tcpRepository; - void addEmptyMessageOf({required UserInfo user}) { - if(state.messageList.any((element) => element.userInfo.userID == user.userID)) { + void addEmptyMessageOf({required int targetUser}) { + if(state.messageList.any((element) => element.targetUser == targetUser)) { return; } - var newList = [MessageInfo(userInfo: user)]; + var newList = [MessageInfo(targetUser: targetUser)]; emit(MessageListState(messageList: newList..addAll(state.messageList))); } @@ -50,12 +50,9 @@ class MessageListCubit extends Cubit { //therefore only insert to map if the target user does not exist if(!addedUserSet.contains(targetUser)) { addedUserSet.add(targetUser); - var targetUserInfo = await localServiceRepository.fetchUserInfoViaID(userid: targetUser); - //TODO: Maybe need to add API in tcp repository to fetch user info via id - targetUserInfo ??= UserInfo(userid: targetUser, username: targetUser.toString()); //Create message info latestMessages.add(MessageInfo( - userInfo: targetUserInfo, + targetUser: targetUser, message: message )); } @@ -74,12 +71,9 @@ class MessageListCubit extends Cubit { var targetUser = response.message.senderID == curUser ? response.message.recieverID : response.message.senderID; - var targetUserInfo = await localServiceRepository.fetchUserInfoViaID(userid: targetUser); - //TODO: Maybe need to add API in tcp repository to fetch user info via id - targetUserInfo ??= UserInfo(userid: targetUser, username: targetUser.toString()); emit(state.updateWithSingle( messageInfo: MessageInfo( - userInfo: targetUserInfo, + targetUser: targetUser, message: response.message ) )); diff --git a/lib/home/view/message_page/cubit/msg_list_state.dart b/lib/home/view/message_page/cubit/msg_list_state.dart index 844325c..9520000 100644 --- a/lib/home/view/message_page/cubit/msg_list_state.dart +++ b/lib/home/view/message_page/cubit/msg_list_state.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-12 23:37:49 - * @LastEditTime : 2022-10-13 11:09:54 + * @LastEditTime : 2022-10-13 22:24:22 * @Description : */ @@ -22,7 +22,7 @@ class MessageListState extends Equatable { }) { var newList = [messageInfo]; for(var msgInfo in messageList) { - if(msgInfo.userInfo.userID == messageInfo.userInfo.userID) { + if(msgInfo.targetUser == messageInfo.targetUser) { continue; } newList.add(msgInfo); @@ -41,11 +41,11 @@ class MessageListState extends Equatable { insertListIndex < orderedNewMessages.length && origListIndex < messageList.length ) { - if(addedUsers.contains(orderedNewMessages[insertListIndex].userInfo.userID)) { + if(addedUsers.contains(orderedNewMessages[insertListIndex].targetUser)) { insertListIndex += 1; continue; } - if(addedUsers.contains(messageList[origListIndex].userInfo.userID)) { + if(addedUsers.contains(messageList[origListIndex].targetUser)) { origListIndex += 1; continue; } @@ -54,35 +54,35 @@ class MessageListState extends Equatable { (orderedNewMessages[insertListIndex].message?.timeStamp ?? 0) ) { newList.add(messageList[origListIndex]); - addedUsers.add(messageList[origListIndex].userInfo.userID); + addedUsers.add(messageList[origListIndex].targetUser); origListIndex += 1; continue; } else { newList.add(orderedNewMessages[insertListIndex]); - addedUsers.add(orderedNewMessages[insertListIndex].userInfo.userID); + addedUsers.add(orderedNewMessages[insertListIndex].targetUser); insertListIndex += 1; continue; } } //Add the messages left while(origListIndex < messageList.length) { - if(addedUsers.contains(messageList[origListIndex].userInfo.userID)) { + if(addedUsers.contains(messageList[origListIndex].targetUser)) { origListIndex += 1; continue; } newList.add(messageList[origListIndex]); - addedUsers.add(messageList[origListIndex].userInfo.userID); + addedUsers.add(messageList[origListIndex].targetUser); origListIndex += 1; continue; } while(insertListIndex < orderedNewMessages.length) { - if(addedUsers.contains(orderedNewMessages[insertListIndex].userInfo.userID)) { + if(addedUsers.contains(orderedNewMessages[insertListIndex].targetUser)) { origListIndex += 1; continue; } newList.add(orderedNewMessages[insertListIndex]); - addedUsers.add(orderedNewMessages[insertListIndex].userInfo.userID); + addedUsers.add(orderedNewMessages[insertListIndex].targetUser); origListIndex += 1; continue; } diff --git a/lib/home/view/message_page/mesage_page.dart b/lib/home/view/message_page/mesage_page.dart index 2aaf32d..8f1e15e 100644 --- a/lib/home/view/message_page/mesage_page.dart +++ b/lib/home/view/message_page/mesage_page.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 11:05:18 - * @LastEditTime : 2022-10-13 16:11:24 + * @LastEditTime : 2022-10-13 22:59:10 * @Description : */ @@ -18,19 +18,22 @@ class MessagePage extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return ListView.separated( - itemBuilder: (context, index) { - return MessageTile( - userInfo: state.messageList[index].userInfo, - message: state.messageList[index].message, - ); - }, - separatorBuilder: (context, index) { - return const Divider( - height: 0.5, - ); - }, - itemCount: state.messageList.length + return Container( + color: Colors.blue, + child: ListView.separated( + itemBuilder: (context, index) { + return MessageTile( + userID: state.messageList[index].targetUser, + message: state.messageList[index].message, + ); + }, + separatorBuilder: (context, index) { + return const Divider( + height: 0.5, + ); + }, + itemCount: state.messageList.length + ), ); } ); diff --git a/lib/home/view/message_page/models/message_info.dart b/lib/home/view/message_page/models/message_info.dart index a32e9df..84d3fdc 100644 --- a/lib/home/view/message_page/models/message_info.dart +++ b/lib/home/view/message_page/models/message_info.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-12 23:48:54 - * @LastEditTime : 2022-10-12 23:50:17 + * @LastEditTime : 2022-10-13 22:24:01 * @Description : */ @@ -11,13 +11,13 @@ import 'package:tcp_client/repositories/common_models/userinfo.dart'; class MessageInfo extends Equatable { final Message? message; - final UserInfo userInfo; + final int targetUser; const MessageInfo({ this.message, - required this.userInfo + required this.targetUser }); @override - List get props => [message?.contentmd5, userInfo]; + List get props => [message?.contentmd5, targetUser]; } diff --git a/lib/home/view/message_page/view/message_tile.dart b/lib/home/view/message_page/view/message_tile.dart index 5d4cea6..e9daff8 100644 --- a/lib/home/view/message_page/view/message_tile.dart +++ b/lib/home/view/message_page/view/message_tile.dart @@ -1,24 +1,26 @@ /* * @Author : Linloir * @Date : 2022-10-13 13:17:52 - * @LastEditTime : 2022-10-13 14:57:14 + * @LastEditTime : 2022-10-13 22:23:31 * @Description : */ import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:tcp_client/common/avatar/avatar.dart'; +import 'package:tcp_client/common/username/username.dart'; import 'package:tcp_client/repositories/common_models/message.dart'; import 'package:tcp_client/repositories/common_models/userinfo.dart'; class MessageTile extends StatelessWidget { const MessageTile({ - required this.userInfo, + required this.userID, this.message, super.key }); - final UserInfo userInfo; + final int userID; final Message? message; @override @@ -33,37 +35,38 @@ class MessageTile extends StatelessWidget { mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.center, children: [ - if(userInfo.avatarEncoded != null && userInfo.avatarEncoded!.isEmpty) - Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5.0), - border: Border.all( - color: Colors.grey[700]!, - width: 1.0 - ) - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(5.0), - child: OverflowBox( - alignment: Alignment.center, - child: FittedBox( - fit: BoxFit.fitWidth, - child: Image.memory(base64Decode(userInfo.avatarEncoded!)), - ), - ) - ), - ), - if(userInfo.avatarEncoded == null || userInfo.avatarEncoded!.isEmpty) - Container( - color: Colors.grey, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5.0), - border: Border.all( - color: Colors.grey[700]!, - width: 1.0 - ) - ), - ), + UserAvatar(userid: userID), + // if(userInfo.avatarEncoded != null && userInfo.avatarEncoded!.isEmpty) + // Container( + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(5.0), + // border: Border.all( + // color: Colors.grey[700]!, + // width: 1.0 + // ) + // ), + // child: ClipRRect( + // borderRadius: BorderRadius.circular(5.0), + // child: OverflowBox( + // alignment: Alignment.center, + // child: FittedBox( + // fit: BoxFit.fitWidth, + // child: Image.memory(base64Decode(userInfo.avatarEncoded!)), + // ), + // ) + // ), + // ), + // if(userInfo.avatarEncoded == null || userInfo.avatarEncoded!.isEmpty) + // Container( + // color: Colors.grey, + // decoration: BoxDecoration( + // borderRadius: BorderRadius.circular(5.0), + // border: Border.all( + // color: Colors.grey[700]!, + // width: 1.0 + // ) + // ), + // ), const SizedBox(width: 12,), Expanded( child: Column( @@ -73,13 +76,7 @@ class MessageTile extends StatelessWidget { vertical: 8.0, horizontal: 0 ), - child: Text( - userInfo.userName, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), + child: UserNameText(userid: userID, fontWeight: FontWeight.bold,) ), const Spacer(), Padding( diff --git a/lib/repositories/tcp_repository/models/tcp_request.dart b/lib/repositories/tcp_repository/models/tcp_request.dart index e450c19..a308a05 100644 --- a/lib/repositories/tcp_repository/models/tcp_request.dart +++ b/lib/repositories/tcp_repository/models/tcp_request.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 09:44:03 - * @LastEditTime : 2022-10-12 13:56:56 + * @LastEditTime : 2022-10-13 23:04:49 * @Description : Abstract TCP request class */ @@ -108,10 +108,19 @@ class LogoutRequest extends TCPRequest { } class GetProfileRequest extends TCPRequest { - const GetProfileRequest({required int? token}): super(type: TCPRequestType.profile, token: token); + const GetProfileRequest({ + required int userid, + required int? token + }): _userid = userid, super(type: TCPRequestType.profile, token: token); + + final int _userid; + + int get userid => _userid; @override - Map get body => {}; + Map get body => { + 'userid': _userid + }; } class ModifyPasswordRequest extends TCPRequest { @@ -205,14 +214,15 @@ class FetchFileRequest extends TCPRequest { class SearchUserRequest extends TCPRequest { final String _username; - const SearchUserRequest({required String username, required int? token}): _username = username, super(type: TCPRequestType.searchUser, token: token); + SearchUserRequest({required String username, required int? token}): + _username = base64.encode(utf8.encode(username)), super(type: TCPRequestType.searchUser, token: token); @override Map get body => { 'username': _username, }; - String get username => _username; + String get username => utf8.decode(base64.decode(_username)); } class AddContactRequest extends TCPRequest { diff --git a/lib/repositories/user_repository/user_repository.dart b/lib/repositories/user_repository/user_repository.dart new file mode 100644 index 0000000..c434a04 --- /dev/null +++ b/lib/repositories/user_repository/user_repository.dart @@ -0,0 +1,79 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 20:18:14 + * @LastEditTime : 2022-10-13 21:26:16 + * @Description : Repository to cache user info + */ + +import 'dart:async'; + +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tcp_client/repositories/common_models/userinfo.dart'; +import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; +import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart'; +import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart'; +import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; + +class UserRepository { + final Map users = {}; + final LocalServiceRepository localServiceRepository; + final TCPRepository tcpRepository; + + final StreamController _userInfoStreamController = StreamController(); + Stream? _userInfoStreamBroadcast; + Stream get userInfoStreamBroadcast { + _userInfoStreamBroadcast ??= _userInfoStreamController.stream.asBroadcastStream(); + return _userInfoStreamBroadcast!; + } + + UserRepository({ + required this.localServiceRepository, + required this.tcpRepository + }) { + tcpRepository.responseStreamBroadcast.listen(_onResponse); + } + + Future _onResponse(TCPResponse response) async { + if(response.type == TCPResponseType.profile && response.status == TCPResponseStatus.ok) { + response as GetProfileResponse; + users.update(response.userInfo!.userID, (value) => response.userInfo!, ifAbsent: () => response.userInfo!); + _userInfoStreamController.add(response.userInfo!); + localServiceRepository.storeUserInfo(userInfo: response.userInfo!); + } + } + + //Fetch user info + //1. Check if the user info is in the map + // if so, return the user, otherwise consult the database + //2. If the database has the user info + // add it to the map and stream, otherwise consult the tcp repository + //3. Pass the control to tcp response handler + UserInfo getUserInfo({required int userid}) { + if(users.containsKey(userid)) { + return users[userid]!; + } + Future(() async { + //Consult the database for info + return await localServiceRepository.fetchUserInfoViaID(userid: userid); + }).then((userInfo) async { + if(userInfo == null) { + //Consult the tcp server for info + tcpRepository.pushRequest(GetProfileRequest( + userid: userid, + token: (await SharedPreferences.getInstance()).getInt('token') + )); + } + else { + //Add to map + users.update(userid, (value) => userInfo, ifAbsent: () => userInfo); + //Push to stream + _userInfoStreamController.add(userInfo); + } + }); + //Return a mock userinfo + return UserInfo( + userid: userid, + username: userid.toString(), + ); + } +} diff --git a/lib/search/cubit/search_cubit.dart b/lib/search/cubit/search_cubit.dart index 694c7b1..6df0a34 100644 --- a/lib/search/cubit/search_cubit.dart +++ b/lib/search/cubit/search_cubit.dart @@ -1,6 +1,78 @@ /* * @Author : Linloir * @Date : 2022-10-13 17:09:25 - * @LastEditTime : 2022-10-13 17:09:26 + * @LastEditTime : 2022-10-13 23:25:20 * @Description : */ + +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; +import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart'; +import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart'; +import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; +import 'package:tcp_client/search/cubit/search_state.dart'; +import 'package:tcp_client/search/model/history_result.dart'; +import 'package:tcp_client/search/model/user_result.dart'; + +class SearchCubit extends Cubit { + SearchCubit({ + required this.localServiceRepository, + required this.tcpRepository + }): super(const SearchState.empty()) { + subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse); + } + + final LocalServiceRepository localServiceRepository; + final TCPRepository tcpRepository; + late final StreamSubscription subscription; + + void onKeyChanged(String newKey) { + EasyDebounce.debounce( + 'Search', + const Duration(milliseconds: 500), + () => _performSearch(newKey) + ); + } + + Future _performSearch(String newKey) async { + tcpRepository.pushRequest(SearchUserRequest( + username: newKey, + token: (await SharedPreferences.getInstance()).getInt('token') + )); + var histories = await localServiceRepository.findMessages(pattern: newKey); + var currentUserID = (await SharedPreferences.getInstance()).getInt('userid'); + var historyResults = histories.map((msg) { + var targetID = msg.senderID == currentUserID ? msg.recieverID : msg.senderID; + return HistorySearchResult(contact: targetID, message: msg); + }).toList(); + emit(state.copyWith(historyResults: historyResults)); + } + + Future _onResponse(TCPResponse response) async { + switch(response.type) { + case TCPResponseType.searchUser: { + response as SearchUserResponse; + //TODO: Maybe server search should be ambigious + var userInfo = response.userInfo; + emit(state.copyWith( + userResults: [ + if(userInfo != null) UserSearchResult(userInfo: userInfo) + ] + )); + break; + } + default: break; + } + } + + //Override dispose to cancel the subscription + @override + Future close() { + subscription.cancel(); + return super.close(); + } +} diff --git a/lib/search/cubit/search_state.dart b/lib/search/cubit/search_state.dart index c72a573..a5a8033 100644 --- a/lib/search/cubit/search_state.dart +++ b/lib/search/cubit/search_state.dart @@ -1,6 +1,34 @@ /* * @Author : Linloir * @Date : 2022-10-13 17:08:56 - * @LastEditTime : 2022-10-13 17:08:57 + * @LastEditTime : 2022-10-13 17:42:33 * @Description : */ + +import 'package:equatable/equatable.dart'; +import 'package:tcp_client/search/model/history_result.dart'; +import 'package:tcp_client/search/model/user_result.dart'; + +class SearchState extends Equatable { + const SearchState({ + required this.historyResults, + required this.userResults + }); + const SearchState.empty(): historyResults = const [], userResults = const []; + + final List historyResults; + final List userResults; + + SearchState copyWith({ + List? historyResults, + List? userResults + }) { + return SearchState( + historyResults: historyResults ?? this.historyResults, + userResults: userResults ?? this.userResults + ); + } + + @override + List get props => [historyResults, userResults]; +} diff --git a/lib/search/model/history_result.dart b/lib/search/model/history_result.dart new file mode 100644 index 0000000..4d95cec --- /dev/null +++ b/lib/search/model/history_result.dart @@ -0,0 +1,22 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 17:30:26 + * @LastEditTime : 2022-10-13 21:28:46 + * @Description : + */ + +import 'package:equatable/equatable.dart'; +import 'package:tcp_client/repositories/common_models/message.dart'; + +class HistorySearchResult extends Equatable { + final int contact; + final Message message; + + const HistorySearchResult({ + required this.contact, + required this.message + }); + + @override + List get props => [contact, message.contentmd5]; +} diff --git a/lib/search/model/search_key.dart b/lib/search/model/search_key.dart deleted file mode 100644 index 09b92ab..0000000 --- a/lib/search/model/search_key.dart +++ /dev/null @@ -1,6 +0,0 @@ -/* - * @Author : Linloir - * @Date : 2022-10-13 17:09:46 - * @LastEditTime : 2022-10-13 17:09:46 - * @Description : - */ diff --git a/lib/search/model/user_result.dart b/lib/search/model/user_result.dart new file mode 100644 index 0000000..495b956 --- /dev/null +++ b/lib/search/model/user_result.dart @@ -0,0 +1,18 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 17:30:36 + * @LastEditTime : 2022-10-13 22:39:25 + * @Description : + */ + +import 'package:equatable/equatable.dart'; +import 'package:tcp_client/repositories/common_models/userinfo.dart'; + +class UserSearchResult extends Equatable { + final UserInfo userInfo; + + const UserSearchResult({required this.userInfo}); + + @override + List get props => [userInfo.userID]; +} diff --git a/lib/search/search_page.dart b/lib/search/search_page.dart index 941fc12..dac0a9b 100644 --- a/lib/search/search_page.dart +++ b/lib/search/search_page.dart @@ -1,21 +1,106 @@ /* * @Author : Linloir * @Date : 2022-10-13 17:04:12 - * @LastEditTime : 2022-10-13 17:08:13 + * @LastEditTime : 2022-10-13 23:24:13 * @Description : */ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; +import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; +import 'package:tcp_client/repositories/user_repository/user_repository.dart'; +import 'package:tcp_client/search/cubit/search_cubit.dart'; +import 'package:tcp_client/search/cubit/search_state.dart'; +import 'package:tcp_client/search/view/history_tile.dart'; import 'package:tcp_client/search/view/search_bar.dart'; +import 'package:tcp_client/search/view/user_tile.dart'; class SearchPage extends StatelessWidget { - const SearchPage({super.key}); + const SearchPage({ + required this.localServiceRepository, + required this.tcpRepository, + required this.userRepository, + super.key + }); + + final LocalServiceRepository localServiceRepository; + final TCPRepository tcpRepository; + final UserRepository userRepository; + + static Route route({ + required LocalServiceRepository localServiceRepository, + required TCPRepository tcpRepository, + required UserRepository userRepository + }) => MaterialPageRoute(builder: (context) => SearchPage( + localServiceRepository: localServiceRepository, + tcpRepository: tcpRepository, + userRepository: userRepository, + )); @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const SearchBar(), + return RepositoryProvider.value( + value: userRepository, + child: BlocProvider( + create: (context) => SearchCubit( + localServiceRepository: localServiceRepository, + tcpRepository: tcpRepository + ), + child: Scaffold( + appBar: AppBar( + title: const SearchBar(), + ), + body: BlocBuilder( + builder:(context, state) { + return ListView( + physics: const BouncingScrollPhysics(), + children: [ + if(state.userResults.isNotEmpty) + ...[ + const SizedBox(height: 16.0,), + const Text( + 'Users', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20 + ), + ), + const SizedBox(height: 8.0,), + ...state.userResults.map((e) => UserTile(userInfo: e.userInfo)) + ], + if(state.historyResults.isNotEmpty) + ...[ + const SizedBox(height: 16.0,), + const Text( + 'Histories', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20 + ), + ), + const SizedBox(height: 8.0,), + ...state.historyResults.map((e) => HistoryTile( + userID: e.contact, + message: e.message + )) + ], + if(state.historyResults.isEmpty && state.userResults.isEmpty) + const Align( + alignment: Alignment.center, + child: Text( + 'No result found', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20 + ), + ), + ) + ], + ); + }, + ), + ), ), ); } diff --git a/lib/search/view/history_tile.dart b/lib/search/view/history_tile.dart new file mode 100644 index 0000000..71d546b --- /dev/null +++ b/lib/search/view/history_tile.dart @@ -0,0 +1,98 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 21:41:49 + * @LastEditTime : 2022-10-13 22:31:37 + * @Description : + */ + +import 'package:flutter/material.dart'; +import 'package:tcp_client/common/avatar/avatar.dart'; +import 'package:tcp_client/common/username/username.dart'; +import 'package:tcp_client/repositories/common_models/message.dart'; + +class HistoryTile extends StatelessWidget { + const HistoryTile({ + required this.userID, + required this.message, + super.key + }); + + final int userID; + final Message message; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 24.0 + ), + child: IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + UserAvatar(userid: userID), + const SizedBox(width: 12,), + Expanded( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 0 + ), + child: UserNameText(userid: userID, fontWeight: FontWeight.bold,) + ), + const Spacer(), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 4.0 + ), + child: Text( + message.contentDecoded, + style: const TextStyle( + fontSize: 16, + ), + ), + ) + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 0 + ), + child: Align( + alignment: Alignment.topCenter, + child: Text( + getTimeStamp(message.timeStamp) + ), + ), + ), + ], + ), + ), + ); + } + + String getTimeStamp(int timeStamp) { + var date = DateTime.fromMillisecondsSinceEpoch(timeStamp); + var weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + //If date is today, return time + if(date.day == DateTime.now().day) { + return '${date.hour}:${date.minute}'; + } + //If date is yesterday, return 'yesterday' + if(date.day == DateTime.now().day - 1) { + return 'yesterday'; + } + //If date is within this week, return the weekday in english + if(date.weekday < DateTime.now().weekday) { + return weekdays[date.weekday - 1]; + } + //Otherwise return the date in english + return '${date.month}/${date.day}'; + } +} \ No newline at end of file diff --git a/lib/search/view/search_bar.dart b/lib/search/view/search_bar.dart index 8b342b4..f103a4c 100644 --- a/lib/search/view/search_bar.dart +++ b/lib/search/view/search_bar.dart @@ -1,17 +1,23 @@ /* * @Author : Linloir * @Date : 2022-10-13 17:06:52 - * @LastEditTime : 2022-10-13 17:06:53 + * @LastEditTime : 2022-10-13 21:35:11 * @Description : */ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:tcp_client/search/cubit/search_cubit.dart'; class SearchBar extends StatelessWidget { const SearchBar({super.key}); @override Widget build(BuildContext context) { - return + return TextField( + onChanged: (value) { + context.read().onKeyChanged(value); + }, + ); } } diff --git a/lib/search/view/user_tile.dart b/lib/search/view/user_tile.dart new file mode 100644 index 0000000..5179aa1 --- /dev/null +++ b/lib/search/view/user_tile.dart @@ -0,0 +1,73 @@ +/* + * @Author : Linloir + * @Date : 2022-10-13 21:41:41 + * @LastEditTime : 2022-10-13 23:26:46 + * @Description : + */ + +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:tcp_client/repositories/common_models/userinfo.dart'; + +class UserTile extends StatelessWidget { + const UserTile({ + required this.userInfo, + super.key + }); + + final UserInfo userInfo; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + if(userInfo.avatarEncoded != null && userInfo.avatarEncoded!.isEmpty) + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5.0), + border: Border.all( + color: Colors.grey[700]!, + width: 1.0 + ) + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(5.0), + child: OverflowBox( + alignment: Alignment.center, + child: FittedBox( + fit: BoxFit.fitWidth, + child: Image.memory(base64.decode(userInfo.avatarEncoded!)), + ), + ) + ), + ), + if(userInfo.avatarEncoded == null || userInfo.avatarEncoded!.isEmpty) + Container( + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(5.0), + border: Border.all( + color: Colors.grey[700]!, + width: 1.0 + ) + ), + ), + const SizedBox(width: 12,), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12.0 + ), + child: Text( + userInfo.userName, + style: const TextStyle( + fontSize: 18.0 + ), + ), + ) + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 39c2bec..583f7b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -71,6 +71,13 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" + easy_debounce: + dependency: "direct main" + description: + name: easy_debounce + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.2+1" equatable: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 234e27b..1891b63 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: pull_to_refresh: ^2.0.0 azlistview: ^2.0.0 lpinyin: ^2.0.3 + easy_debounce: ^2.0.2+1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons.