From 7023271fe93cb34e5ab2b4ee5dd705a3ffff88b8 Mon Sep 17 00:00:00 2001 From: Linloir <3145078758@qq.com> Date: Sun, 23 Oct 2022 17:58:57 +0800 Subject: [PATCH] New Feature: - Unread message bubble! --- lib/chat/cubit/chat_cubit.dart | 10 +- lib/home/home_page.dart | 2 +- .../view/contact_page/view/contact_tile.dart | 2 +- .../cubit/{ => msg_list}/msg_list_cubit.dart | 4 +- .../cubit/{ => msg_list}/msg_list_state.dart | 0 .../cubit/msg_tile/msg_tile_cubit.dart | 89 ++++++ .../cubit/msg_tile/msg_tile_state.dart | 21 ++ lib/home/view/message_page/mesage_page.dart | 7 +- .../message_page/models/message_info.dart | 15 +- .../view/message_page/view/message_tile.dart | 266 +++++++++++------- .../local_service_repository.dart | 150 ++++++++-- pubspec.yaml | 2 +- 12 files changed, 425 insertions(+), 143 deletions(-) rename lib/home/view/message_page/cubit/{ => msg_list}/msg_list_cubit.dart (98%) rename lib/home/view/message_page/cubit/{ => msg_list}/msg_list_state.dart (100%) create mode 100644 lib/home/view/message_page/cubit/msg_tile/msg_tile_cubit.dart create mode 100644 lib/home/view/message_page/cubit/msg_tile/msg_tile_state.dart diff --git a/lib/chat/cubit/chat_cubit.dart b/lib/chat/cubit/chat_cubit.dart index eee8888..a726d81 100644 --- a/lib/chat/cubit/chat_cubit.dart +++ b/lib/chat/cubit/chat_cubit.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-13 14:03:56 - * @LastEditTime : 2022-10-23 13:07:08 + * @LastEditTime : 2022-10-23 17:22:46 * @Description : */ @@ -209,6 +209,14 @@ class ChatCubit extends Cubit { if(response.type == TCPResponseType.forwardMessage) { response as ForwardMessageResponse; if(response.message.senderID == userID || response.message.recieverID == userID) { + if(response.message.senderID == userID) { + //Update read history + localServiceRepository.setReadHistory( + userid: response.message.recieverID, + targetid: userID, + timestamp: response.message.timeStamp + ); + } // Message storage will be handled by home bloc listener //Emit new state var newHistory = ChatHistory( diff --git a/lib/home/home_page.dart b/lib/home/home_page.dart index e2c0c8e..c2850b2 100644 --- a/lib/home/home_page.dart +++ b/lib/home/home_page.dart @@ -12,7 +12,7 @@ import 'package:tcp_client/home/cubit/home_cubit.dart'; import 'package:tcp_client/home/cubit/home_state.dart'; import 'package:tcp_client/home/view/contact_page/contact_page.dart'; import 'package:tcp_client/home/view/contact_page/cubit/contact_cubit.dart'; -import 'package:tcp_client/home/view/message_page/cubit/msg_list_cubit.dart'; +import 'package:tcp_client/home/view/message_page/cubit/msg_list/msg_list_cubit.dart'; import 'package:tcp_client/home/view/message_page/mesage_page.dart'; import 'package:tcp_client/home/view/profile_page/profile_page.dart'; import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; diff --git a/lib/home/view/contact_page/view/contact_tile.dart b/lib/home/view/contact_page/view/contact_tile.dart index 4ec43ee..cddab3f 100644 --- a/lib/home/view/contact_page/view/contact_tile.dart +++ b/lib/home/view/contact_page/view/contact_tile.dart @@ -16,7 +16,7 @@ import 'package:tcp_client/home/cubit/home_cubit.dart'; import 'package:tcp_client/home/cubit/home_state.dart'; import 'package:tcp_client/home/view/contact_page/cubit/contact_cubit.dart'; import 'package:tcp_client/home/view/contact_page/models/contact_model.dart'; -import 'package:tcp_client/home/view/message_page/cubit/msg_list_cubit.dart'; +import 'package:tcp_client/home/view/message_page/cubit/msg_list/msg_list_cubit.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/tcp_repository.dart'; diff --git a/lib/home/view/message_page/cubit/msg_list_cubit.dart b/lib/home/view/message_page/cubit/msg_list/msg_list_cubit.dart similarity index 98% rename from lib/home/view/message_page/cubit/msg_list_cubit.dart rename to lib/home/view/message_page/cubit/msg_list/msg_list_cubit.dart index 73db5ec..3dd6157 100644 --- a/lib/home/view/message_page/cubit/msg_list_cubit.dart +++ b/lib/home/view/message_page/cubit/msg_list/msg_list_cubit.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-12 23:38:31 - * @LastEditTime : 2022-10-21 23:14:02 + * @LastEditTime : 2022-10-23 16:30:24 * @Description : */ @@ -9,7 +9,7 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tcp_client/home/view/message_page/cubit/msg_list_state.dart'; +import 'package:tcp_client/home/view/message_page/cubit/msg_list/msg_list_state.dart'; import 'package:tcp_client/home/view/message_page/models/message_info.dart'; import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; import 'package:tcp_client/repositories/tcp_repository/models/tcp_request.dart'; diff --git a/lib/home/view/message_page/cubit/msg_list_state.dart b/lib/home/view/message_page/cubit/msg_list/msg_list_state.dart similarity index 100% rename from lib/home/view/message_page/cubit/msg_list_state.dart rename to lib/home/view/message_page/cubit/msg_list/msg_list_state.dart diff --git a/lib/home/view/message_page/cubit/msg_tile/msg_tile_cubit.dart b/lib/home/view/message_page/cubit/msg_tile/msg_tile_cubit.dart new file mode 100644 index 0000000..443bd5d --- /dev/null +++ b/lib/home/view/message_page/cubit/msg_tile/msg_tile_cubit.dart @@ -0,0 +1,89 @@ +/* + * @Author : Linloir + * @Date : 2022-10-23 16:30:45 + * @LastEditTime : 2022-10-23 17:46:28 + * @Description : + */ + +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tcp_client/home/view/message_page/cubit/msg_tile/msg_tile_state.dart'; +import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; +import 'package:tcp_client/repositories/tcp_repository/models/tcp_response.dart'; +import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; + +class MessageTileCubit extends Cubit { + MessageTileCubit({ + required this.tcpRepository, + required this.localServiceRepository, + required this.targetID + }): super(const MessageTileState(unreadCount: 0)) { + Future(() async { + return await localServiceRepository.getUnreadCount( + userid: (await SharedPreferences.getInstance()).getInt('userid')!, + targetid: targetID + ); + }).then((value) => emit(state + value)); + subscription = tcpRepository.responseStreamBroadcast.listen(_onResponse); + } + + final TCPRepository tcpRepository; + final LocalServiceRepository localServiceRepository; + final int targetID; + late final StreamSubscription subscription; + + Future _onResponse(TCPResponse response) async { + var pref = await SharedPreferences.getInstance(); + var userID = pref.getInt('userid'); + if(userID == null) { + return; + } + var readHistoryTimestamp = await localServiceRepository.fetchReadHistory( + userid: userID, + targetid: targetID + ); + if(response.type == TCPResponseType.fetchMessage) { + //Count unread incoming message count + response as FetchMessageResponse; + var addCnt = 0; + for(var message in response.messages) { + if(message.senderID == targetID && message.recieverID == userID) { + if(readHistoryTimestamp < message.timeStamp) { + addCnt += 1; + } + } + } + if(!isClosed) { + emit(state + addCnt); + } + } + else if(response.type == TCPResponseType.forwardMessage) { + //Count unread incoming message count + response as ForwardMessageResponse; + if(response.message.senderID == targetID && response.message.recieverID == userID) { + if(readHistoryTimestamp < response.message.timeStamp) { + if(!isClosed) { + emit(state + 1); + } + } + else { + if(!isClosed) { + emit(const MessageTileState(unreadCount: 0)); + } + } + } + } + } + + void clearUnread() { + emit(const MessageTileState(unreadCount: 0)); + } + + @override + Future close() { + subscription.cancel(); + return super.close(); + } +} diff --git a/lib/home/view/message_page/cubit/msg_tile/msg_tile_state.dart b/lib/home/view/message_page/cubit/msg_tile/msg_tile_state.dart new file mode 100644 index 0000000..03ddd52 --- /dev/null +++ b/lib/home/view/message_page/cubit/msg_tile/msg_tile_state.dart @@ -0,0 +1,21 @@ +/* + * @Author : Linloir + * @Date : 2022-10-23 16:30:52 + * @LastEditTime : 2022-10-23 16:40:47 + * @Description : + */ + +import 'package:equatable/equatable.dart'; + +class MessageTileState extends Equatable { + final int unreadCount; + + const MessageTileState({required this.unreadCount}); + + MessageTileState operator +(int other) { + return MessageTileState(unreadCount: unreadCount + other); + } + + @override + List get props => [unreadCount]; +} diff --git a/lib/home/view/message_page/mesage_page.dart b/lib/home/view/message_page/mesage_page.dart index 298a1ec..7c92bbc 100644 --- a/lib/home/view/message_page/mesage_page.dart +++ b/lib/home/view/message_page/mesage_page.dart @@ -1,15 +1,15 @@ /* * @Author : Linloir * @Date : 2022-10-11 11:05:18 - * @LastEditTime : 2022-10-17 13:35:35 + * @LastEditTime : 2022-10-23 17:38:30 * @Description : */ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.dart'; -import 'package:tcp_client/home/view/message_page/cubit/msg_list_cubit.dart'; -import 'package:tcp_client/home/view/message_page/cubit/msg_list_state.dart'; +import 'package:tcp_client/home/view/message_page/cubit/msg_list/msg_list_cubit.dart'; +import 'package:tcp_client/home/view/message_page/cubit/msg_list/msg_list_state.dart'; import 'package:tcp_client/home/view/message_page/view/message_tile.dart'; class MessagePage extends StatelessWidget { @@ -31,6 +31,7 @@ class MessagePage extends StatelessWidget { physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), itemBuilder: (context, index) { return MessageTile( + key: ValueKey(state.messageList[index].targetUser), userID: state.messageList[index].targetUser, message: state.messageList[index].message, ); diff --git a/lib/home/view/message_page/models/message_info.dart b/lib/home/view/message_page/models/message_info.dart index 9167d3f..32384b5 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-18 11:25:36 + * @LastEditTime : 2022-10-23 16:30:08 * @Description : */ @@ -14,9 +14,20 @@ class MessageInfo extends Equatable { const MessageInfo({ this.message, - required this.targetUser + required this.targetUser, }); + MessageInfo copyWith({ + Message? message, + int? targetUser, + int? unreadCount + }) { + return MessageInfo( + message: message ?? this.message, + targetUser: targetUser ?? this.targetUser, + ); + } + @override 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 12b331c..87cb6a9 100644 --- a/lib/home/view/message_page/view/message_tile.dart +++ b/lib/home/view/message_page/view/message_tile.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-13 13:17:52 - * @LastEditTime : 2022-10-23 10:09:09 + * @LastEditTime : 2022-10-23 17:55:44 * @Description : */ @@ -10,6 +10,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:tcp_client/chat/chat_page.dart'; import 'package:tcp_client/common/avatar/avatar.dart'; import 'package:tcp_client/common/username/username.dart'; +import 'package:tcp_client/home/view/message_page/cubit/msg_tile/msg_tile_cubit.dart'; +import 'package:tcp_client/home/view/message_page/cubit/msg_tile/msg_tile_state.dart'; import 'package:tcp_client/repositories/common_models/message.dart'; import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart'; import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; @@ -27,118 +29,168 @@ class MessageTile extends StatelessWidget { @override Widget build(BuildContext context) { - return IntrinsicHeight( - child: Stack( - fit: StackFit.expand, - children: [ - InkWell( - onTap: () { - Navigator.of(context).push(ChatPage.route( - userRepository: context.read(), - localServiceRepository: context.read(), - tcpRepository: context.read(), - userID: userID - )); - }, - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 24.0 - ), - child: Row( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - IgnorePointer( - child: UserAvatar(userid: userID), + return BlocProvider( + key: ValueKey(userID), + create: (context) { + return MessageTileCubit( + tcpRepository: context.read(), + localServiceRepository: context.read(), + targetID: userID + ); + }, + child: Builder( + key: ValueKey(userID), + builder: (context) => IntrinsicHeight( + child: Stack( + fit: StackFit.expand, + children: [ + InkWell( + onTap: () { + if(message != null) { + context.read().setReadHistory( + userid: message!.recieverID == userID ? message!.senderID : message!.recieverID, + targetid: userID, + timestamp: message!.timeStamp + ); + } + context.read().clearUnread(); + Navigator.of(context).push(ChatPage.route( + userRepository: context.read(), + localServiceRepository: context.read(), + tcpRepository: context.read(), + userID: userID + )); + }, + ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 24.0 ), - // 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: 16,), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(height: 6,), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 2.0, - horizontal: 0 - ), - child: IgnorePointer( - child: UserNameText(userid: userID, fontWeight: FontWeight.bold,), - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - vertical: 2.0 - ), - child: IgnorePointer( - child: Text( - message?.type == MessageType.image ? '[Image]' : message?.contentDecoded ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IgnorePointer( + child: 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: 16,), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox(height: 6,), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 2.0, + horizontal: 0 + ), + child: IgnorePointer( + child: UserNameText(userid: userID, fontWeight: FontWeight.bold,), ), ), - ), + Padding( + padding: const EdgeInsets.symmetric( + vertical: 2.0 + ), + child: IgnorePointer( + child: Text( + message?.type == MessageType.image ? '[Image]' : message?.contentDecoded ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + ), + ), + ), + ), + const SizedBox(height: 6,), + ], ), - const SizedBox(height: 6,), - ], - ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if(message != null) + Padding( + padding: const EdgeInsets.only( + top: 8.0, + bottom: 8.0 + ), + child: Align( + alignment: Alignment.topCenter, + child: IgnorePointer( + child: Text( + getTimeStamp(message!.timeStamp) + ), + ), + ), + ), + BlocBuilder( + builder: (context, state) { + return state.unreadCount == 0 ? Container() : Container( + margin: const EdgeInsets.only( + bottom: 8.0 + ), + padding: const EdgeInsets.symmetric( + horizontal: 10.0, + vertical: 4.0 + ), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: Colors.blue.withOpacity(0.9) + ), + child: Text( + '${state.unreadCount > 99 ? '99+' : state.unreadCount}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ); + } + ), + ], + ), + ], ), - if(message != null) - Padding( - padding: const EdgeInsets.symmetric( - vertical: 8.0, - horizontal: 0 - ), - child: Align( - alignment: Alignment.topCenter, - child: IgnorePointer( - child: Text( - getTimeStamp(message!.timeStamp) - ), - ), - ), - ), - - ], - ), + ), + ], ), - ], + ), ), ); } diff --git a/lib/repositories/local_service_repository/local_service_repository.dart b/lib/repositories/local_service_repository/local_service_repository.dart index 7d725d7..2b9cd96 100644 --- a/lib/repositories/local_service_repository/local_service_repository.dart +++ b/lib/repositories/local_service_repository/local_service_repository.dart @@ -1,7 +1,7 @@ /* * @Author : Linloir * @Date : 2022-10-11 10:56:02 - * @LastEditTime : 2022-10-23 13:49:23 + * @LastEditTime : 2022-10-23 17:10:13 * @Description : Local Service Repository */ @@ -73,29 +73,38 @@ class LocalServiceRepository { ); ''' ); + await txn.execute( + ''' + create table readhistory ( + userid int not null, + targetid int not null, + timestamp int not null, + primary key (userid, targetid) + ) + ''' + ); }); - // await db.execute( - // ''' - // create table msgs ( - // userid integer not null, - // targetid integer not null, - // contenttype text not null, - // content text not null, - // timestamp int not null, - // md5encoded text primary key, - // filemd5 text - // ); - // create table users ( - // userid integer primary key, - // username text not null, - // avatar text - // ); - // create table files ( - // filemd5 text primary key, - // dir text not null - // ); - // ''' - // ); + } + + static Future _updateDatabaseToVer2(Database db) async { + await db.transaction((txn) async { + db.execute( + ''' + create table readhistory ( + userid int not null, + targetid int not null, + timestamp int not null, + primary key (userid, targetid) + ) + ''' + ); + }); + } + + static FutureOr _onDatabaseUpgrade(Database db, int curVer, int newVer) async { + if(curVer == 1 && newVer == 2) { + await _updateDatabaseToVer2(db); + } } static Future create({ @@ -104,8 +113,9 @@ class LocalServiceRepository { }) async { var database = await openDatabase( databaseFilePath, - version: 1, - onCreate: _onDatabaseCreate + version: 2, + onCreate: _onDatabaseCreate, + onUpgrade: _onDatabaseUpgrade, ); return LocalServiceRepository._internal(database: database); } @@ -457,4 +467,94 @@ class LocalServiceRepository { var imageContent = await image.readAsBytes(); return imageContent; } + + Future setReadHistory({ + required int userid, + required int targetid, + required int timestamp + }) async { + await _database.transaction((txn) async { + var result = await txn.query( + 'readhistory', + where: 'userid = ? and targetid = ?', + whereArgs: [ + userid, + targetid + ] + ); + if(result.isEmpty) { + await txn.insert( + 'readhistory', + { + 'userid': userid, + 'targetid': targetid, + 'timestamp': timestamp + } + ); + return; + } + if(result[0]['timestamp'] as int > timestamp) { + return; + } + await txn.update( + 'readhistory', + { + 'timestamp': timestamp + }, + where: 'userid = ? and targetid = ?', + whereArgs: [ + userid, + targetid + ] + ); + }); + } + + Future fetchReadHistory({ + required int userid, + required int targetid + }) async { + return await _database.transaction((txn) async { + var result = await txn.query( + 'readhistory', + where: 'userid = ? and targetid = ?', + whereArgs: [ + userid, + targetid, + ], + ); + if(result.isEmpty) { + txn.insert( + 'readhistory', + { + 'userid': userid, + 'targetid': targetid, + 'timestamp': 0 + }, + ); + return 0; + } + return result[0]['timestamp'] as int; + }); + } + + Future getUnreadCount({ + required int userid, + required int targetid + }) async { + return await _database.transaction((txn) async { + var result = await txn.query( + 'msgs left outer join readhistory on msgs.userid = readhistory.targetid and msgs.targetid = readhistory.userid', + columns: [ + 'msgs.md5encoded' + ], + where: 'msgs.userid = ? and msgs.targetid = ? and msgs.timestamp > readhistory.timestamp', + whereArgs: [ + targetid, + userid + ] + ); + return result.length; + }); + } } diff --git a/pubspec.yaml b/pubspec.yaml index b9ace87..b956608 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,7 +17,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 2.0.1 +version: 2.2.0 environment: sdk: '>=2.18.2 <3.0.0'