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
* @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<ChatState> {
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(

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/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';

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/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';

View File

@ -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';

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
* @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,
);

View File

@ -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<Object> get props => [message?.contentmd5 ?? '', targetUser];
}

View File

@ -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,12 +29,31 @@ class MessageTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IntrinsicHeight(
return BlocProvider<MessageTileCubit>(
key: ValueKey(userID),
create: (context) {
return MessageTileCubit(
tcpRepository: context.read<TCPRepository>(),
localServiceRepository: context.read<LocalServiceRepository>(),
targetID: userID
);
},
child: Builder(
key: ValueKey(userID),
builder: (context) => IntrinsicHeight(
child: Stack(
fit: StackFit.expand,
children: [
InkWell(
onTap: () {
if(message != null) {
context.read<LocalServiceRepository>().setReadHistory(
userid: message!.recieverID == userID ? message!.senderID : message!.recieverID,
targetid: userID,
timestamp: message!.timeStamp
);
}
context.read<MessageTileCubit>().clearUnread();
Navigator.of(context).push(ChatPage.route(
userRepository: context.read<UserRepository>(),
localServiceRepository: context.read<LocalServiceRepository>(),
@ -119,11 +140,15 @@ class MessageTile extends StatelessWidget {
],
),
),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if(message != null)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 0
padding: const EdgeInsets.only(
top: 8.0,
bottom: 8.0
),
child: Align(
alignment: Alignment.topCenter,
@ -134,12 +159,39 @@ class MessageTile extends StatelessWidget {
),
),
),
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,
),
),
);
}
),
],
),
],
),
),
],
),
),
),
);
}

View File

@ -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<void> _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<void> _onDatabaseUpgrade(Database db, int curVer, int newVer) async {
if(curVer == 1 && newVer == 2) {
await _updateDatabaseToVer2(db);
}
}
static Future<LocalServiceRepository> 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<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
# 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'