New Feature:

- Unread message bubble!
This commit is contained in:
Linloir 2022-10-23 17:58:57 +08:00
parent ed10d0aec4
commit 7023271fe9
No known key found for this signature in database
GPG Key ID: 58EEB209A0F2C366
12 changed files with 425 additions and 143 deletions

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-13 14:03:56 * @Date : 2022-10-13 14:03:56
* @LastEditTime : 2022-10-23 13:07:08 * @LastEditTime : 2022-10-23 17:22:46
* @Description : * @Description :
*/ */
@ -209,6 +209,14 @@ class ChatCubit extends Cubit<ChatState> {
if(response.type == TCPResponseType.forwardMessage) { if(response.type == TCPResponseType.forwardMessage) {
response as ForwardMessageResponse; response as ForwardMessageResponse;
if(response.message.senderID == userID || response.message.recieverID == userID) { 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 // Message storage will be handled by home bloc listener
//Emit new state //Emit new state
var newHistory = ChatHistory( var newHistory = ChatHistory(

View File

@ -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/cubit/home_state.dart';
import 'package:tcp_client/home/view/contact_page/contact_page.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/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/message_page/mesage_page.dart';
import 'package:tcp_client/home/view/profile_page/profile_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'; import 'package:tcp_client/repositories/local_service_repository/local_service_repository.dart';

View File

@ -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/cubit/home_state.dart';
import 'package:tcp_client/home/view/contact_page/cubit/contact_cubit.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/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/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_request.dart';
import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart'; import 'package:tcp_client/repositories/tcp_repository/tcp_repository.dart';

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-12 23:38:31 * @Date : 2022-10-12 23:38:31
* @LastEditTime : 2022-10-21 23:14:02 * @LastEditTime : 2022-10-23 16:30:24
* @Description : * @Description :
*/ */
@ -9,7 +9,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:shared_preferences/shared_preferences.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/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/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_request.dart';

View File

@ -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<MessageTileState> {
MessageTileCubit({
required this.tcpRepository,
required this.localServiceRepository,
required this.targetID
}): super(const MessageTileState(unreadCount: 0)) {
Future<int>(() 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<void> _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<void> close() {
subscription.cancel();
return super.close();
}
}

View File

@ -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<Object> get props => [unreadCount];
}

View File

@ -1,15 +1,15 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-11 11:05:18 * @Date : 2022-10-11 11:05:18
* @LastEditTime : 2022-10-17 13:35:35 * @LastEditTime : 2022-10-23 17:38:30
* @Description : * @Description :
*/ */
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:pull_to_refresh_flutter3/pull_to_refresh_flutter3.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/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_state.dart';
import 'package:tcp_client/home/view/message_page/view/message_tile.dart'; import 'package:tcp_client/home/view/message_page/view/message_tile.dart';
class MessagePage extends StatelessWidget { class MessagePage extends StatelessWidget {
@ -31,6 +31,7 @@ class MessagePage extends StatelessWidget {
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
itemBuilder: (context, index) { itemBuilder: (context, index) {
return MessageTile( return MessageTile(
key: ValueKey(state.messageList[index].targetUser),
userID: state.messageList[index].targetUser, userID: state.messageList[index].targetUser,
message: state.messageList[index].message, message: state.messageList[index].message,
); );

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-12 23:48:54 * @Date : 2022-10-12 23:48:54
* @LastEditTime : 2022-10-18 11:25:36 * @LastEditTime : 2022-10-23 16:30:08
* @Description : * @Description :
*/ */
@ -14,9 +14,20 @@ class MessageInfo extends Equatable {
const MessageInfo({ const MessageInfo({
this.message, 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 @override
List<Object> get props => [message?.contentmd5 ?? '', targetUser]; List<Object> get props => [message?.contentmd5 ?? '', targetUser];
} }

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-13 13:17:52 * @Date : 2022-10-13 13:17:52
* @LastEditTime : 2022-10-23 10:09:09 * @LastEditTime : 2022-10-23 17:55:44
* @Description : * @Description :
*/ */
@ -10,6 +10,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:tcp_client/chat/chat_page.dart'; import 'package:tcp_client/chat/chat_page.dart';
import 'package:tcp_client/common/avatar/avatar.dart'; import 'package:tcp_client/common/avatar/avatar.dart';
import 'package:tcp_client/common/username/username.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/common_models/message.dart';
import 'package:tcp_client/repositories/local_service_repository/local_service_repository.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/tcp_repository/tcp_repository.dart';
@ -27,118 +29,168 @@ class MessageTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return IntrinsicHeight( return BlocProvider<MessageTileCubit>(
child: Stack( key: ValueKey(userID),
fit: StackFit.expand, create: (context) {
children: [ return MessageTileCubit(
InkWell( tcpRepository: context.read<TCPRepository>(),
onTap: () { localServiceRepository: context.read<LocalServiceRepository>(),
Navigator.of(context).push(ChatPage.route( targetID: userID
userRepository: context.read<UserRepository>(), );
localServiceRepository: context.read<LocalServiceRepository>(), },
tcpRepository: context.read<TCPRepository>(), child: Builder(
userID: userID key: ValueKey(userID),
)); builder: (context) => IntrinsicHeight(
}, child: Stack(
), fit: StackFit.expand,
Padding( children: [
padding: const EdgeInsets.symmetric( InkWell(
vertical: 8.0, onTap: () {
horizontal: 24.0 if(message != null) {
), context.read<LocalServiceRepository>().setReadHistory(
child: Row( userid: message!.recieverID == userID ? message!.senderID : message!.recieverID,
mainAxisSize: MainAxisSize.max, targetid: userID,
crossAxisAlignment: CrossAxisAlignment.center, timestamp: message!.timeStamp
children: [ );
IgnorePointer( }
child: UserAvatar(userid: userID), context.read<MessageTileCubit>().clearUnread();
Navigator.of(context).push(ChatPage.route(
userRepository: context.read<UserRepository>(),
localServiceRepository: context.read<LocalServiceRepository>(),
tcpRepository: context.read<TCPRepository>(),
userID: userID
));
},
),
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 24.0
), ),
// if(userInfo.avatarEncoded != null && userInfo.avatarEncoded!.isEmpty) child: Row(
// Container( mainAxisSize: MainAxisSize.max,
// decoration: BoxDecoration( crossAxisAlignment: CrossAxisAlignment.center,
// borderRadius: BorderRadius.circular(5.0), children: [
// border: Border.all( IgnorePointer(
// color: Colors.grey[700]!, child: UserAvatar(userid: userID),
// width: 1.0 ),
// ) // if(userInfo.avatarEncoded != null && userInfo.avatarEncoded!.isEmpty)
// ), // Container(
// child: ClipRRect( // decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(5.0), // borderRadius: BorderRadius.circular(5.0),
// child: OverflowBox( // border: Border.all(
// alignment: Alignment.center, // color: Colors.grey[700]!,
// child: FittedBox( // width: 1.0
// fit: BoxFit.fitWidth, // )
// child: Image.memory(base64Decode(userInfo.avatarEncoded!)), // ),
// ), // child: ClipRRect(
// ) // borderRadius: BorderRadius.circular(5.0),
// ), // child: OverflowBox(
// ), // alignment: Alignment.center,
// if(userInfo.avatarEncoded == null || userInfo.avatarEncoded!.isEmpty) // child: FittedBox(
// Container( // fit: BoxFit.fitWidth,
// color: Colors.grey, // child: Image.memory(base64Decode(userInfo.avatarEncoded!)),
// decoration: BoxDecoration( // ),
// borderRadius: BorderRadius.circular(5.0), // )
// border: Border.all( // ),
// color: Colors.grey[700]!, // ),
// width: 1.0 // if(userInfo.avatarEncoded == null || userInfo.avatarEncoded!.isEmpty)
// ) // Container(
// ), // color: Colors.grey,
// ), // decoration: BoxDecoration(
const SizedBox(width: 16,), // borderRadius: BorderRadius.circular(5.0),
Expanded( // border: Border.all(
child: Column( // color: Colors.grey[700]!,
crossAxisAlignment: CrossAxisAlignment.start, // width: 1.0
mainAxisAlignment: MainAxisAlignment.center, // )
children: [ // ),
const SizedBox(height: 6,), // ),
Padding( const SizedBox(width: 16,),
padding: const EdgeInsets.symmetric( Expanded(
vertical: 2.0, child: Column(
horizontal: 0 crossAxisAlignment: CrossAxisAlignment.start,
), mainAxisAlignment: MainAxisAlignment.center,
child: IgnorePointer( children: [
child: UserNameText(userid: userID, fontWeight: FontWeight.bold,), const SizedBox(height: 6,),
), Padding(
), padding: const EdgeInsets.symmetric(
Padding( vertical: 2.0,
padding: const EdgeInsets.symmetric( horizontal: 0
vertical: 2.0 ),
), child: IgnorePointer(
child: IgnorePointer( child: UserNameText(userid: userID, fontWeight: FontWeight.bold,),
child: Text(
message?.type == MessageType.image ? '[Image]' : message?.contentDecoded ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
), ),
), ),
), 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<MessageTileCubit, MessageTileState>(
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)
),
),
),
),
],
),
), ),
], ),
), ),
); );
} }

View File

@ -1,7 +1,7 @@
/* /*
* @Author : Linloir * @Author : Linloir
* @Date : 2022-10-11 10:56:02 * @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 * @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 ( static Future<void> _updateDatabaseToVer2(Database db) async {
// userid integer not null, await db.transaction((txn) async {
// targetid integer not null, db.execute(
// contenttype text not null, '''
// content text not null, create table readhistory (
// timestamp int not null, userid int not null,
// md5encoded text primary key, targetid int not null,
// filemd5 text timestamp int not null,
// ); primary key (userid, targetid)
// create table users ( )
// userid integer primary key, '''
// username text not null, );
// avatar text });
// ); }
// create table files (
// filemd5 text primary key, static FutureOr<void> _onDatabaseUpgrade(Database db, int curVer, int newVer) async {
// dir text not null if(curVer == 1 && newVer == 2) {
// ); await _updateDatabaseToVer2(db);
// ''' }
// );
} }
static Future<LocalServiceRepository> create({ static Future<LocalServiceRepository> create({
@ -104,8 +113,9 @@ class LocalServiceRepository {
}) async { }) async {
var database = await openDatabase( var database = await openDatabase(
databaseFilePath, databaseFilePath,
version: 1, version: 2,
onCreate: _onDatabaseCreate onCreate: _onDatabaseCreate,
onUpgrade: _onDatabaseUpgrade,
); );
return LocalServiceRepository._internal(database: database); return LocalServiceRepository._internal(database: database);
} }
@ -457,4 +467,94 @@ class LocalServiceRepository {
var imageContent = await image.readAsBytes(); var imageContent = await image.readAsBytes();
return imageContent; return imageContent;
} }
Future<void> 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<int> fetchReadHistory({
required int userid,
required int targetid
}) async {
return await _database.transaction<int>((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<int> getUnreadCount({
required int userid,
required int targetid
}) async {
return await _database.transaction<int>((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;
});
}
} }

View File

@ -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 # 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 # 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. # of the product and file versions while build-number is used as the build suffix.
version: 2.0.1 version: 2.2.0
environment: environment:
sdk: '>=2.18.2 <3.0.0' sdk: '>=2.18.2 <3.0.0'