Получение и отправка текста, статусы сообщения
This commit is contained in:
@@ -83,6 +83,11 @@
|
|||||||
"registrationFailed": "Registration failed",
|
"registrationFailed": "Registration failed",
|
||||||
"online": "online",
|
"online": "online",
|
||||||
"lastSeen": "last seen {date} at {time}",
|
"lastSeen": "last seen {date} at {time}",
|
||||||
|
"lastSeenRelative": "last seen {hours}h {minutes}m ago",
|
||||||
|
"lastSeenRelativeMinutes": "last seen {minutes}m ago",
|
||||||
|
"lastSeenJustNow": "last seen just now",
|
||||||
|
"lastSeenYesterday": "last seen yesterday at {time}",
|
||||||
|
"lastSeenDayBeforeYesterday": "last seen day before yesterday at {time}",
|
||||||
"noMessages": "No messages yet",
|
"noMessages": "No messages yet",
|
||||||
"messageHint": "Message"
|
"messageHint": "Message"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,11 @@
|
|||||||
"registrationFailed": "Ошибка регистрации",
|
"registrationFailed": "Ошибка регистрации",
|
||||||
"online": "в сети",
|
"online": "в сети",
|
||||||
"lastSeen": "был(а) в сети {date} в {time}",
|
"lastSeen": "был(а) в сети {date} в {time}",
|
||||||
|
"lastSeenRelative": "был(а) {hours} ч. {minutes} мин. назад",
|
||||||
|
"lastSeenRelativeMinutes": "был(а) {minutes} мин. назад",
|
||||||
|
"lastSeenJustNow": "был(а) только что",
|
||||||
|
"lastSeenYesterday": "был(а) вчера в {time}",
|
||||||
|
"lastSeenDayBeforeYesterday": "был(а) позавчера в {time}",
|
||||||
"noMessages": "Сообщений пока нет",
|
"noMessages": "Сообщений пока нет",
|
||||||
"messageHint": "Сообщение"
|
"messageHint": "Сообщение"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,14 @@ class SignalRService {
|
|||||||
final SharedPreferences prefs;
|
final SharedPreferences prefs;
|
||||||
HubConnection? _hubConnection;
|
HubConnection? _hubConnection;
|
||||||
final _messageController = StreamController<Message>.broadcast();
|
final _messageController = StreamController<Message>.broadcast();
|
||||||
|
final _typingController = StreamController<Map<String, dynamic>>.broadcast();
|
||||||
|
final _statusController = StreamController<Map<String, dynamic>>.broadcast();
|
||||||
|
final _readReceiptController = StreamController<Map<String, dynamic>>.broadcast();
|
||||||
|
|
||||||
Stream<Message> get messages => _messageController.stream;
|
Stream<Message> get messages => _messageController.stream;
|
||||||
|
Stream<Map<String, dynamic>> get typingUpdates => _typingController.stream;
|
||||||
|
Stream<Map<String, dynamic>> get statusUpdates => _statusController.stream;
|
||||||
|
Stream<Map<String, dynamic>> get readReceipts => _readReceiptController.stream;
|
||||||
|
|
||||||
SignalRService(this.prefs);
|
SignalRService(this.prefs);
|
||||||
|
|
||||||
@@ -54,13 +60,46 @@ class SignalRService {
|
|||||||
print('[SignalR] Reconnected! ID: $connectionId');
|
print('[SignalR] Reconnected! ID: $connectionId');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle multiple possible event names to be safe
|
// Event names verified from backend source code (MessageNotifier.cs)
|
||||||
_hubConnection?.on('ReceiveMessage', _handleReceiveMessage);
|
_hubConnection?.on('new_message', _handleReceiveMessage);
|
||||||
_hubConnection?.on('receiveMessage', _handleReceiveMessage);
|
_hubConnection?.on('new_message_received', _handleReceiveMessage); // Optional fallback
|
||||||
_hubConnection?.on('UserTyping', _handleUserTyping);
|
|
||||||
_hubConnection?.on('user_typing', _handleUserTyping);
|
_hubConnection?.on('user_typing', _handleUserTyping);
|
||||||
_hubConnection?.on('UserStoppedTyping', _handleUserStoppedTyping);
|
|
||||||
_hubConnection?.on('user_stopped_typing', _handleUserStoppedTyping);
|
_hubConnection?.on('user_stopped_typing', _handleUserStoppedTyping);
|
||||||
|
_hubConnection?.on('user_online', (args) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[SignalR] user_online: $args');
|
||||||
|
if (args != null && args.isNotEmpty) {
|
||||||
|
final data = args[0] as Map<String, dynamic>;
|
||||||
|
_statusController.add({'userId': data['userId'] ?? data['UserId'], 'isOnline': true});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_hubConnection?.on('user_offline', (args) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[SignalR] user_offline: $args');
|
||||||
|
if (args != null && args.isNotEmpty) {
|
||||||
|
final data = args[0] as Map<String, dynamic>;
|
||||||
|
_statusController.add({
|
||||||
|
'userId': data['userId'] ?? data['UserId'],
|
||||||
|
'isOnline': false,
|
||||||
|
'lastSeen': data['lastSeen'] ?? data['LastSeen']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_hubConnection?.on('messages_read', (args) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[SignalR] messages_read: $args');
|
||||||
|
if (args != null && args.isNotEmpty) {
|
||||||
|
final data = args[0] as Map<String, dynamic>;
|
||||||
|
_readReceiptController.add({
|
||||||
|
'chatId': data['chatId'] ?? data['ChatId'],
|
||||||
|
'userId': data['userId'] ?? data['UserId'],
|
||||||
|
'lastReadMessageId': data['lastReadMessageId'] ?? data['LastReadMessageId'],
|
||||||
|
'lastReadSequenceId': data['lastReadSequenceId'] ?? data['LastReadSequenceId'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
@@ -74,8 +113,17 @@ class SignalRService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final _typingController = StreamController<Map<String, dynamic>>.broadcast();
|
Future<void> markMessagesAsRead(String chatId, String lastReadMessageId, int lastReadSequenceId) async {
|
||||||
Stream<Map<String, dynamic>> get typingUpdates => _typingController.stream;
|
// ignore: avoid_print
|
||||||
|
print('[SignalR] markMessagesAsRead: chatId=$chatId, msgId=$lastReadMessageId, seqId=$lastReadSequenceId');
|
||||||
|
await _hubConnection?.invoke('read_messages', args: [
|
||||||
|
{
|
||||||
|
'ChatId': chatId,
|
||||||
|
'LastReadMessageId': lastReadMessageId,
|
||||||
|
'LastReadSequenceId': lastReadSequenceId,
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
void _handleUserTyping(List<dynamic>? arguments) {
|
void _handleUserTyping(List<dynamic>? arguments) {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
@@ -109,15 +157,18 @@ class SignalRService {
|
|||||||
if (arguments != null && arguments.isNotEmpty) {
|
if (arguments != null && arguments.isNotEmpty) {
|
||||||
final data = arguments[0] as Map<String, dynamic>;
|
final data = arguments[0] as Map<String, dynamic>;
|
||||||
final message = Message(
|
final message = Message(
|
||||||
id: data['id'] ?? '',
|
id: (data['id'] ?? data['Id'] ?? '').toString(),
|
||||||
chatId: data['chatId'] ?? '',
|
chatId: (data['chatId'] ?? data['ChatId'] ?? '').toString(),
|
||||||
senderId: data['userId'] ?? (data['senderId'] ?? ''),
|
senderId: (data['senderId'] ?? data['SenderId'] ?? data['userId'] ?? data['UserId'] ?? '').toString(),
|
||||||
content: data['content'] ?? '',
|
content: data['content'] ?? data['Content'] ?? '',
|
||||||
messageType: data['type']?.toString().toLowerCase() ?? 'text',
|
messageType: (data['type'] ?? data['Type'] ?? 'text').toString().toLowerCase(),
|
||||||
createdAt: _parseDateTime(data['createdAt']),
|
sequenceId: int.tryParse((data['sequenceId'] ?? data['SequenceId'] ?? '0').toString()) ?? 0,
|
||||||
updatedAt: _parseDateTime(data['updatedAt']),
|
createdAt: _parseDateTime(data['createdAt'] ?? data['CreatedAt']),
|
||||||
isRead: data['isRead'] ?? false,
|
updatedAt: _parseDateTime(data['updatedAt'] ?? data['UpdatedAt']),
|
||||||
media: data['media'] is Map<String, dynamic> ? data['media'] as Map<String, dynamic> : null,
|
status: (data['isRead'] ?? data['IsRead'] ?? false) ? MessageStatus.read : MessageStatus.delivered,
|
||||||
|
media: (data['media'] ?? data['Media']) is Map<String, dynamic>
|
||||||
|
? (data['media'] ?? data['Media']) as Map<String, dynamic>
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
_messageController.add(message);
|
_messageController.add(message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,9 +194,10 @@ class ChatRemoteDataSourceImpl implements ChatRemoteDataSource {
|
|||||||
senderId: json['userId'] ?? (json['senderId'] ?? ''),
|
senderId: json['userId'] ?? (json['senderId'] ?? ''),
|
||||||
content: json['content'] ?? '',
|
content: json['content'] ?? '',
|
||||||
messageType: json['type']?.toString().toLowerCase() ?? 'text',
|
messageType: json['type']?.toString().toLowerCase() ?? 'text',
|
||||||
|
sequenceId: int.tryParse(json['sequenceId']?.toString() ?? '0') ?? 0,
|
||||||
createdAt: _parseDateTime(json['createdAt']),
|
createdAt: _parseDateTime(json['createdAt']),
|
||||||
updatedAt: _parseDateTime(json['updatedAt']),
|
updatedAt: _parseDateTime(json['updatedAt']),
|
||||||
isRead: json['isRead'] ?? false,
|
status: (json['isRead'] ?? false) ? MessageStatus.read : MessageStatus.delivered,
|
||||||
media: json['media'] is Map<String, dynamic> ? json['media'] as Map<String, dynamic> : null,
|
media: json['media'] is Map<String, dynamic> ? json['media'] as Map<String, dynamic> : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,8 +62,12 @@ class LastMessageModel {
|
|||||||
late String id;
|
late String id;
|
||||||
late String content;
|
late String content;
|
||||||
late String messageType;
|
late String messageType;
|
||||||
|
late int sequenceId;
|
||||||
DateTime? createdAt;
|
DateTime? createdAt;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.name)
|
||||||
|
MessageStatus status = MessageStatus.delivered;
|
||||||
|
|
||||||
Message toEntity() {
|
Message toEntity() {
|
||||||
return Message(
|
return Message(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -71,7 +75,9 @@ class LastMessageModel {
|
|||||||
senderId: '',
|
senderId: '',
|
||||||
content: content,
|
content: content,
|
||||||
messageType: messageType,
|
messageType: messageType,
|
||||||
|
sequenceId: sequenceId,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
|
status: status,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +86,9 @@ class LastMessageModel {
|
|||||||
..id = message.id
|
..id = message.id
|
||||||
..content = message.content
|
..content = message.content
|
||||||
..messageType = message.messageType
|
..messageType = message.messageType
|
||||||
..createdAt = message.createdAt;
|
..sequenceId = message.sequenceId
|
||||||
|
..createdAt = message.createdAt
|
||||||
|
..status = message.status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1663,6 +1663,17 @@ const LastMessageModelSchema = Schema(
|
|||||||
id: 3,
|
id: 3,
|
||||||
name: r'messageType',
|
name: r'messageType',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
|
),
|
||||||
|
r'sequenceId': PropertySchema(
|
||||||
|
id: 4,
|
||||||
|
name: r'sequenceId',
|
||||||
|
type: IsarType.long,
|
||||||
|
),
|
||||||
|
r'status': PropertySchema(
|
||||||
|
id: 5,
|
||||||
|
name: r'status',
|
||||||
|
type: IsarType.string,
|
||||||
|
enumMap: _LastMessageModelstatusEnumValueMap,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
estimateSize: _lastMessageModelEstimateSize,
|
estimateSize: _lastMessageModelEstimateSize,
|
||||||
@@ -1680,6 +1691,7 @@ int _lastMessageModelEstimateSize(
|
|||||||
bytesCount += 3 + object.content.length * 3;
|
bytesCount += 3 + object.content.length * 3;
|
||||||
bytesCount += 3 + object.id.length * 3;
|
bytesCount += 3 + object.id.length * 3;
|
||||||
bytesCount += 3 + object.messageType.length * 3;
|
bytesCount += 3 + object.messageType.length * 3;
|
||||||
|
bytesCount += 3 + object.status.name.length * 3;
|
||||||
return bytesCount;
|
return bytesCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1693,6 +1705,8 @@ void _lastMessageModelSerialize(
|
|||||||
writer.writeDateTime(offsets[1], object.createdAt);
|
writer.writeDateTime(offsets[1], object.createdAt);
|
||||||
writer.writeString(offsets[2], object.id);
|
writer.writeString(offsets[2], object.id);
|
||||||
writer.writeString(offsets[3], object.messageType);
|
writer.writeString(offsets[3], object.messageType);
|
||||||
|
writer.writeLong(offsets[4], object.sequenceId);
|
||||||
|
writer.writeString(offsets[5], object.status.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
LastMessageModel _lastMessageModelDeserialize(
|
LastMessageModel _lastMessageModelDeserialize(
|
||||||
@@ -1706,6 +1720,10 @@ LastMessageModel _lastMessageModelDeserialize(
|
|||||||
object.createdAt = reader.readDateTimeOrNull(offsets[1]);
|
object.createdAt = reader.readDateTimeOrNull(offsets[1]);
|
||||||
object.id = reader.readString(offsets[2]);
|
object.id = reader.readString(offsets[2]);
|
||||||
object.messageType = reader.readString(offsets[3]);
|
object.messageType = reader.readString(offsets[3]);
|
||||||
|
object.sequenceId = reader.readLong(offsets[4]);
|
||||||
|
object.status = _LastMessageModelstatusValueEnumMap[
|
||||||
|
reader.readStringOrNull(offsets[5])] ??
|
||||||
|
MessageStatus.sending;
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1724,11 +1742,30 @@ P _lastMessageModelDeserializeProp<P>(
|
|||||||
return (reader.readString(offset)) as P;
|
return (reader.readString(offset)) as P;
|
||||||
case 3:
|
case 3:
|
||||||
return (reader.readString(offset)) as P;
|
return (reader.readString(offset)) as P;
|
||||||
|
case 4:
|
||||||
|
return (reader.readLong(offset)) as P;
|
||||||
|
case 5:
|
||||||
|
return (_LastMessageModelstatusValueEnumMap[
|
||||||
|
reader.readStringOrNull(offset)] ??
|
||||||
|
MessageStatus.sending) as P;
|
||||||
default:
|
default:
|
||||||
throw IsarError('Unknown property with id $propertyId');
|
throw IsarError('Unknown property with id $propertyId');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _LastMessageModelstatusEnumValueMap = {
|
||||||
|
r'sending': r'sending',
|
||||||
|
r'delivered': r'delivered',
|
||||||
|
r'read': r'read',
|
||||||
|
r'error': r'error',
|
||||||
|
};
|
||||||
|
const _LastMessageModelstatusValueEnumMap = {
|
||||||
|
r'sending': MessageStatus.sending,
|
||||||
|
r'delivered': MessageStatus.delivered,
|
||||||
|
r'read': MessageStatus.read,
|
||||||
|
r'error': MessageStatus.error,
|
||||||
|
};
|
||||||
|
|
||||||
extension LastMessageModelQueryFilter
|
extension LastMessageModelQueryFilter
|
||||||
on QueryBuilder<LastMessageModel, LastMessageModel, QFilterCondition> {
|
on QueryBuilder<LastMessageModel, LastMessageModel, QFilterCondition> {
|
||||||
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
@@ -2212,6 +2249,198 @@ extension LastMessageModelQueryFilter
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
sequenceIdEqualTo(int value) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'sequenceId',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
sequenceIdGreaterThan(
|
||||||
|
int value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'sequenceId',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
sequenceIdLessThan(
|
||||||
|
int value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'sequenceId',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
sequenceIdBetween(
|
||||||
|
int lower,
|
||||||
|
int upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'sequenceId',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusEqualTo(
|
||||||
|
MessageStatus value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusGreaterThan(
|
||||||
|
MessageStatus value, {
|
||||||
|
bool include = false,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusLessThan(
|
||||||
|
MessageStatus value, {
|
||||||
|
bool include = false,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusBetween(
|
||||||
|
MessageStatus lower,
|
||||||
|
MessageStatus upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'status',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusStartsWith(
|
||||||
|
String value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.startsWith(
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusEndsWith(
|
||||||
|
String value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.endsWith(
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusContains(String value, {bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.contains(
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusMatches(String pattern, {bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.matches(
|
||||||
|
property: r'status',
|
||||||
|
wildcard: pattern,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusIsEmpty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'status',
|
||||||
|
value: '',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<LastMessageModel, LastMessageModel, QAfterFilterCondition>
|
||||||
|
statusIsNotEmpty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
property: r'status',
|
||||||
|
value: '',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LastMessageModelQueryObject
|
extension LastMessageModelQueryObject
|
||||||
|
|||||||
@@ -16,12 +16,15 @@ class MessageModel {
|
|||||||
late String senderId;
|
late String senderId;
|
||||||
late String content;
|
late String content;
|
||||||
late String messageType;
|
late String messageType;
|
||||||
|
late int sequenceId;
|
||||||
|
|
||||||
@Index()
|
@Index()
|
||||||
DateTime? createdAt;
|
DateTime? createdAt;
|
||||||
|
|
||||||
DateTime? updatedAt;
|
DateTime? updatedAt;
|
||||||
bool isRead = false;
|
|
||||||
|
@Enumerated(EnumType.name)
|
||||||
|
MessageStatus status = MessageStatus.delivered;
|
||||||
|
|
||||||
// For simplicity, store media as a JSON string or ignore for now
|
// For simplicity, store media as a JSON string or ignore for now
|
||||||
String? mediaJson;
|
String? mediaJson;
|
||||||
@@ -33,9 +36,10 @@ class MessageModel {
|
|||||||
senderId: senderId,
|
senderId: senderId,
|
||||||
content: content,
|
content: content,
|
||||||
messageType: messageType,
|
messageType: messageType,
|
||||||
|
sequenceId: sequenceId,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
isRead: isRead,
|
status: status,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +50,9 @@ class MessageModel {
|
|||||||
..senderId = message.senderId
|
..senderId = message.senderId
|
||||||
..content = message.content
|
..content = message.content
|
||||||
..messageType = message.messageType
|
..messageType = message.messageType
|
||||||
|
..sequenceId = message.sequenceId
|
||||||
..createdAt = message.createdAt
|
..createdAt = message.createdAt
|
||||||
..updatedAt = message.updatedAt
|
..updatedAt = message.updatedAt
|
||||||
..isRead = message.isRead ?? false;
|
..status = message.status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,28 +37,34 @@ const MessageModelSchema = CollectionSchema(
|
|||||||
name: r'id',
|
name: r'id',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'isRead': PropertySchema(
|
|
||||||
id: 4,
|
|
||||||
name: r'isRead',
|
|
||||||
type: IsarType.bool,
|
|
||||||
),
|
|
||||||
r'mediaJson': PropertySchema(
|
r'mediaJson': PropertySchema(
|
||||||
id: 5,
|
id: 4,
|
||||||
name: r'mediaJson',
|
name: r'mediaJson',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'messageType': PropertySchema(
|
r'messageType': PropertySchema(
|
||||||
id: 6,
|
id: 5,
|
||||||
name: r'messageType',
|
name: r'messageType',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'senderId': PropertySchema(
|
r'senderId': PropertySchema(
|
||||||
id: 7,
|
id: 6,
|
||||||
name: r'senderId',
|
name: r'senderId',
|
||||||
type: IsarType.string,
|
type: IsarType.string,
|
||||||
),
|
),
|
||||||
r'updatedAt': PropertySchema(
|
r'sequenceId': PropertySchema(
|
||||||
|
id: 7,
|
||||||
|
name: r'sequenceId',
|
||||||
|
type: IsarType.long,
|
||||||
|
),
|
||||||
|
r'status': PropertySchema(
|
||||||
id: 8,
|
id: 8,
|
||||||
|
name: r'status',
|
||||||
|
type: IsarType.string,
|
||||||
|
enumMap: _MessageModelstatusEnumValueMap,
|
||||||
|
),
|
||||||
|
r'updatedAt': PropertySchema(
|
||||||
|
id: 9,
|
||||||
name: r'updatedAt',
|
name: r'updatedAt',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
)
|
)
|
||||||
@@ -134,6 +140,7 @@ int _messageModelEstimateSize(
|
|||||||
}
|
}
|
||||||
bytesCount += 3 + object.messageType.length * 3;
|
bytesCount += 3 + object.messageType.length * 3;
|
||||||
bytesCount += 3 + object.senderId.length * 3;
|
bytesCount += 3 + object.senderId.length * 3;
|
||||||
|
bytesCount += 3 + object.status.name.length * 3;
|
||||||
return bytesCount;
|
return bytesCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,11 +154,12 @@ void _messageModelSerialize(
|
|||||||
writer.writeString(offsets[1], object.content);
|
writer.writeString(offsets[1], object.content);
|
||||||
writer.writeDateTime(offsets[2], object.createdAt);
|
writer.writeDateTime(offsets[2], object.createdAt);
|
||||||
writer.writeString(offsets[3], object.id);
|
writer.writeString(offsets[3], object.id);
|
||||||
writer.writeBool(offsets[4], object.isRead);
|
writer.writeString(offsets[4], object.mediaJson);
|
||||||
writer.writeString(offsets[5], object.mediaJson);
|
writer.writeString(offsets[5], object.messageType);
|
||||||
writer.writeString(offsets[6], object.messageType);
|
writer.writeString(offsets[6], object.senderId);
|
||||||
writer.writeString(offsets[7], object.senderId);
|
writer.writeLong(offsets[7], object.sequenceId);
|
||||||
writer.writeDateTime(offsets[8], object.updatedAt);
|
writer.writeString(offsets[8], object.status.name);
|
||||||
|
writer.writeDateTime(offsets[9], object.updatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageModel _messageModelDeserialize(
|
MessageModel _messageModelDeserialize(
|
||||||
@@ -165,12 +173,15 @@ MessageModel _messageModelDeserialize(
|
|||||||
object.content = reader.readString(offsets[1]);
|
object.content = reader.readString(offsets[1]);
|
||||||
object.createdAt = reader.readDateTimeOrNull(offsets[2]);
|
object.createdAt = reader.readDateTimeOrNull(offsets[2]);
|
||||||
object.id = reader.readString(offsets[3]);
|
object.id = reader.readString(offsets[3]);
|
||||||
object.isRead = reader.readBool(offsets[4]);
|
|
||||||
object.isarId = id;
|
object.isarId = id;
|
||||||
object.mediaJson = reader.readStringOrNull(offsets[5]);
|
object.mediaJson = reader.readStringOrNull(offsets[4]);
|
||||||
object.messageType = reader.readString(offsets[6]);
|
object.messageType = reader.readString(offsets[5]);
|
||||||
object.senderId = reader.readString(offsets[7]);
|
object.senderId = reader.readString(offsets[6]);
|
||||||
object.updatedAt = reader.readDateTimeOrNull(offsets[8]);
|
object.sequenceId = reader.readLong(offsets[7]);
|
||||||
|
object.status =
|
||||||
|
_MessageModelstatusValueEnumMap[reader.readStringOrNull(offsets[8])] ??
|
||||||
|
MessageStatus.sending;
|
||||||
|
object.updatedAt = reader.readDateTimeOrNull(offsets[9]);
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,20 +201,37 @@ P _messageModelDeserializeProp<P>(
|
|||||||
case 3:
|
case 3:
|
||||||
return (reader.readString(offset)) as P;
|
return (reader.readString(offset)) as P;
|
||||||
case 4:
|
case 4:
|
||||||
return (reader.readBool(offset)) as P;
|
|
||||||
case 5:
|
|
||||||
return (reader.readStringOrNull(offset)) as P;
|
return (reader.readStringOrNull(offset)) as P;
|
||||||
|
case 5:
|
||||||
|
return (reader.readString(offset)) as P;
|
||||||
case 6:
|
case 6:
|
||||||
return (reader.readString(offset)) as P;
|
return (reader.readString(offset)) as P;
|
||||||
case 7:
|
case 7:
|
||||||
return (reader.readString(offset)) as P;
|
return (reader.readLong(offset)) as P;
|
||||||
case 8:
|
case 8:
|
||||||
|
return (_MessageModelstatusValueEnumMap[
|
||||||
|
reader.readStringOrNull(offset)] ??
|
||||||
|
MessageStatus.sending) as P;
|
||||||
|
case 9:
|
||||||
return (reader.readDateTimeOrNull(offset)) as P;
|
return (reader.readDateTimeOrNull(offset)) as P;
|
||||||
default:
|
default:
|
||||||
throw IsarError('Unknown property with id $propertyId');
|
throw IsarError('Unknown property with id $propertyId');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _MessageModelstatusEnumValueMap = {
|
||||||
|
r'sending': r'sending',
|
||||||
|
r'delivered': r'delivered',
|
||||||
|
r'read': r'read',
|
||||||
|
r'error': r'error',
|
||||||
|
};
|
||||||
|
const _MessageModelstatusValueEnumMap = {
|
||||||
|
r'sending': MessageStatus.sending,
|
||||||
|
r'delivered': MessageStatus.delivered,
|
||||||
|
r'read': MessageStatus.read,
|
||||||
|
r'error': MessageStatus.error,
|
||||||
|
};
|
||||||
|
|
||||||
Id _messageModelGetId(MessageModel object) {
|
Id _messageModelGetId(MessageModel object) {
|
||||||
return object.isarId;
|
return object.isarId;
|
||||||
}
|
}
|
||||||
@@ -1041,16 +1069,6 @@ extension MessageModelQueryFilter
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> isReadEqualTo(
|
|
||||||
bool value) {
|
|
||||||
return QueryBuilder.apply(this, (query) {
|
|
||||||
return query.addFilterCondition(FilterCondition.equalTo(
|
|
||||||
property: r'isRead',
|
|
||||||
value: value,
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> isarIdEqualTo(
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> isarIdEqualTo(
|
||||||
Id value) {
|
Id value) {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
@@ -1532,6 +1550,197 @@ extension MessageModelQueryFilter
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
sequenceIdEqualTo(int value) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'sequenceId',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
sequenceIdGreaterThan(
|
||||||
|
int value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'sequenceId',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
sequenceIdLessThan(
|
||||||
|
int value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'sequenceId',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
sequenceIdBetween(
|
||||||
|
int lower,
|
||||||
|
int upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'sequenceId',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> statusEqualTo(
|
||||||
|
MessageStatus value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
statusGreaterThan(
|
||||||
|
MessageStatus value, {
|
||||||
|
bool include = false,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
statusLessThan(
|
||||||
|
MessageStatus value, {
|
||||||
|
bool include = false,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> statusBetween(
|
||||||
|
MessageStatus lower,
|
||||||
|
MessageStatus upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'status',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
statusStartsWith(
|
||||||
|
String value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.startsWith(
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
statusEndsWith(
|
||||||
|
String value, {
|
||||||
|
bool caseSensitive = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.endsWith(
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
statusContains(String value, {bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.contains(
|
||||||
|
property: r'status',
|
||||||
|
value: value,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition> statusMatches(
|
||||||
|
String pattern,
|
||||||
|
{bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.matches(
|
||||||
|
property: r'status',
|
||||||
|
wildcard: pattern,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
statusIsEmpty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'status',
|
||||||
|
value: '',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
|
statusIsNotEmpty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
property: r'status',
|
||||||
|
value: '',
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
QueryBuilder<MessageModel, MessageModel, QAfterFilterCondition>
|
||||||
updatedAtIsNull() {
|
updatedAtIsNull() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
@@ -1663,18 +1872,6 @@ extension MessageModelQuerySortBy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByIsRead() {
|
|
||||||
return QueryBuilder.apply(this, (query) {
|
|
||||||
return query.addSortBy(r'isRead', Sort.asc);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByIsReadDesc() {
|
|
||||||
return QueryBuilder.apply(this, (query) {
|
|
||||||
return query.addSortBy(r'isRead', Sort.desc);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByMediaJson() {
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByMediaJson() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'mediaJson', Sort.asc);
|
return query.addSortBy(r'mediaJson', Sort.asc);
|
||||||
@@ -1712,6 +1909,31 @@ extension MessageModelQuerySortBy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortBySequenceId() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'sequenceId', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy>
|
||||||
|
sortBySequenceIdDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'sequenceId', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByStatus() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'status', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByStatusDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'status', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByUpdatedAt() {
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> sortByUpdatedAt() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'updatedAt', Sort.asc);
|
return query.addSortBy(r'updatedAt', Sort.asc);
|
||||||
@@ -1775,18 +1997,6 @@ extension MessageModelQuerySortThenBy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByIsRead() {
|
|
||||||
return QueryBuilder.apply(this, (query) {
|
|
||||||
return query.addSortBy(r'isRead', Sort.asc);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByIsReadDesc() {
|
|
||||||
return QueryBuilder.apply(this, (query) {
|
|
||||||
return query.addSortBy(r'isRead', Sort.desc);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByIsarId() {
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByIsarId() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'isarId', Sort.asc);
|
return query.addSortBy(r'isarId', Sort.asc);
|
||||||
@@ -1836,6 +2046,31 @@ extension MessageModelQuerySortThenBy
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenBySequenceId() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'sequenceId', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy>
|
||||||
|
thenBySequenceIdDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'sequenceId', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByStatus() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'status', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByStatusDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'status', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByUpdatedAt() {
|
QueryBuilder<MessageModel, MessageModel, QAfterSortBy> thenByUpdatedAt() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'updatedAt', Sort.asc);
|
return query.addSortBy(r'updatedAt', Sort.asc);
|
||||||
@@ -1878,12 +2113,6 @@ extension MessageModelQueryWhereDistinct
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByIsRead() {
|
|
||||||
return QueryBuilder.apply(this, (query) {
|
|
||||||
return query.addDistinctBy(r'isRead');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByMediaJson(
|
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByMediaJson(
|
||||||
{bool caseSensitive = true}) {
|
{bool caseSensitive = true}) {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
@@ -1905,6 +2134,19 @@ extension MessageModelQueryWhereDistinct
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctBySequenceId() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addDistinctBy(r'sequenceId');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByStatus(
|
||||||
|
{bool caseSensitive = true}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addDistinctBy(r'status', caseSensitive: caseSensitive);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByUpdatedAt() {
|
QueryBuilder<MessageModel, MessageModel, QDistinct> distinctByUpdatedAt() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addDistinctBy(r'updatedAt');
|
return query.addDistinctBy(r'updatedAt');
|
||||||
@@ -1944,12 +2186,6 @@ extension MessageModelQueryProperty
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, bool, QQueryOperations> isReadProperty() {
|
|
||||||
return QueryBuilder.apply(this, (query) {
|
|
||||||
return query.addPropertyName(r'isRead');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryBuilder<MessageModel, String?, QQueryOperations> mediaJsonProperty() {
|
QueryBuilder<MessageModel, String?, QQueryOperations> mediaJsonProperty() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addPropertyName(r'mediaJson');
|
return query.addPropertyName(r'mediaJson');
|
||||||
@@ -1968,6 +2204,18 @@ extension MessageModelQueryProperty
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, int, QQueryOperations> sequenceIdProperty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addPropertyName(r'sequenceId');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<MessageModel, MessageStatus, QQueryOperations> statusProperty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addPropertyName(r'status');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<MessageModel, DateTime?, QQueryOperations> updatedAtProperty() {
|
QueryBuilder<MessageModel, DateTime?, QQueryOperations> updatedAtProperty() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addPropertyName(r'updatedAt');
|
return query.addPropertyName(r'updatedAt');
|
||||||
|
|||||||
@@ -24,9 +24,19 @@ class ChatRepositoryImpl implements ChatRepository {
|
|||||||
@override
|
@override
|
||||||
Stream<Map<String, dynamic>> get typingStream => signalRService.typingUpdates;
|
Stream<Map<String, dynamic>> get typingStream => signalRService.typingUpdates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<String, dynamic>> get statusStream => signalRService.statusUpdates;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<String, dynamic>> get readReceiptStream => signalRService.readReceipts;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> sendTypingStatus(String chatId, bool isTyping) => signalRService.sendTypingStatus(chatId, isTyping);
|
Future<void> sendTypingStatus(String chatId, bool isTyping) => signalRService.sendTypingStatus(chatId, isTyping);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> markAsRead(String chatId, String lastReadMessageId, int lastReadSequenceId) =>
|
||||||
|
signalRService.markMessagesAsRead(chatId, lastReadMessageId, lastReadSequenceId);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> initSignalR() => signalRService.init();
|
Future<void> initSignalR() => signalRService.init();
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
|||||||
part 'message.freezed.dart';
|
part 'message.freezed.dart';
|
||||||
// part 'message.g.dart';
|
// part 'message.g.dart';
|
||||||
|
|
||||||
|
enum MessageStatus { sending, delivered, read, error }
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class Message with _$Message {
|
class Message with _$Message {
|
||||||
const factory Message({
|
const factory Message({
|
||||||
@@ -10,12 +12,11 @@ class Message with _$Message {
|
|||||||
required String chatId,
|
required String chatId,
|
||||||
required String senderId,
|
required String senderId,
|
||||||
required String content,
|
required String content,
|
||||||
required String messageType, // 'text', 'image', 'video', 'audio', 'file'
|
required String messageType,
|
||||||
|
required int sequenceId,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
bool? isRead,
|
@Default(MessageStatus.delivered) MessageStatus status,
|
||||||
Map<String, dynamic>? media,
|
Map<String, dynamic>? media,
|
||||||
}) = _Message;
|
}) = _Message;
|
||||||
|
|
||||||
// factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(json);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ mixin _$Message {
|
|||||||
String get chatId => throw _privateConstructorUsedError;
|
String get chatId => throw _privateConstructorUsedError;
|
||||||
String get senderId => throw _privateConstructorUsedError;
|
String get senderId => throw _privateConstructorUsedError;
|
||||||
String get content => throw _privateConstructorUsedError;
|
String get content => throw _privateConstructorUsedError;
|
||||||
String get messageType =>
|
String get messageType => throw _privateConstructorUsedError;
|
||||||
throw _privateConstructorUsedError; // 'text', 'image', 'video', 'audio', 'file'
|
int get sequenceId => throw _privateConstructorUsedError;
|
||||||
DateTime? get createdAt => throw _privateConstructorUsedError;
|
DateTime? get createdAt => throw _privateConstructorUsedError;
|
||||||
DateTime? get updatedAt => throw _privateConstructorUsedError;
|
DateTime? get updatedAt => throw _privateConstructorUsedError;
|
||||||
bool? get isRead => throw _privateConstructorUsedError;
|
MessageStatus get status => throw _privateConstructorUsedError;
|
||||||
Map<String, dynamic>? get media => throw _privateConstructorUsedError;
|
Map<String, dynamic>? get media => throw _privateConstructorUsedError;
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@@ -42,9 +42,10 @@ abstract class $MessageCopyWith<$Res> {
|
|||||||
String senderId,
|
String senderId,
|
||||||
String content,
|
String content,
|
||||||
String messageType,
|
String messageType,
|
||||||
|
int sequenceId,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
bool? isRead,
|
MessageStatus status,
|
||||||
Map<String, dynamic>? media});
|
Map<String, dynamic>? media});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,9 +67,10 @@ class _$MessageCopyWithImpl<$Res, $Val extends Message>
|
|||||||
Object? senderId = null,
|
Object? senderId = null,
|
||||||
Object? content = null,
|
Object? content = null,
|
||||||
Object? messageType = null,
|
Object? messageType = null,
|
||||||
|
Object? sequenceId = null,
|
||||||
Object? createdAt = freezed,
|
Object? createdAt = freezed,
|
||||||
Object? updatedAt = freezed,
|
Object? updatedAt = freezed,
|
||||||
Object? isRead = freezed,
|
Object? status = null,
|
||||||
Object? media = freezed,
|
Object? media = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_value.copyWith(
|
return _then(_value.copyWith(
|
||||||
@@ -92,6 +94,10 @@ class _$MessageCopyWithImpl<$Res, $Val extends Message>
|
|||||||
? _value.messageType
|
? _value.messageType
|
||||||
: messageType // ignore: cast_nullable_to_non_nullable
|
: messageType // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
sequenceId: null == sequenceId
|
||||||
|
? _value.sequenceId
|
||||||
|
: sequenceId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
createdAt: freezed == createdAt
|
createdAt: freezed == createdAt
|
||||||
? _value.createdAt
|
? _value.createdAt
|
||||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -100,10 +106,10 @@ class _$MessageCopyWithImpl<$Res, $Val extends Message>
|
|||||||
? _value.updatedAt
|
? _value.updatedAt
|
||||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
isRead: freezed == isRead
|
status: null == status
|
||||||
? _value.isRead
|
? _value.status
|
||||||
: isRead // ignore: cast_nullable_to_non_nullable
|
: status // ignore: cast_nullable_to_non_nullable
|
||||||
as bool?,
|
as MessageStatus,
|
||||||
media: freezed == media
|
media: freezed == media
|
||||||
? _value.media
|
? _value.media
|
||||||
: media // ignore: cast_nullable_to_non_nullable
|
: media // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -125,9 +131,10 @@ abstract class _$$MessageImplCopyWith<$Res> implements $MessageCopyWith<$Res> {
|
|||||||
String senderId,
|
String senderId,
|
||||||
String content,
|
String content,
|
||||||
String messageType,
|
String messageType,
|
||||||
|
int sequenceId,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
bool? isRead,
|
MessageStatus status,
|
||||||
Map<String, dynamic>? media});
|
Map<String, dynamic>? media});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,9 +154,10 @@ class __$$MessageImplCopyWithImpl<$Res>
|
|||||||
Object? senderId = null,
|
Object? senderId = null,
|
||||||
Object? content = null,
|
Object? content = null,
|
||||||
Object? messageType = null,
|
Object? messageType = null,
|
||||||
|
Object? sequenceId = null,
|
||||||
Object? createdAt = freezed,
|
Object? createdAt = freezed,
|
||||||
Object? updatedAt = freezed,
|
Object? updatedAt = freezed,
|
||||||
Object? isRead = freezed,
|
Object? status = null,
|
||||||
Object? media = freezed,
|
Object? media = freezed,
|
||||||
}) {
|
}) {
|
||||||
return _then(_$MessageImpl(
|
return _then(_$MessageImpl(
|
||||||
@@ -173,6 +181,10 @@ class __$$MessageImplCopyWithImpl<$Res>
|
|||||||
? _value.messageType
|
? _value.messageType
|
||||||
: messageType // ignore: cast_nullable_to_non_nullable
|
: messageType // ignore: cast_nullable_to_non_nullable
|
||||||
as String,
|
as String,
|
||||||
|
sequenceId: null == sequenceId
|
||||||
|
? _value.sequenceId
|
||||||
|
: sequenceId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int,
|
||||||
createdAt: freezed == createdAt
|
createdAt: freezed == createdAt
|
||||||
? _value.createdAt
|
? _value.createdAt
|
||||||
: createdAt // ignore: cast_nullable_to_non_nullable
|
: createdAt // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -181,10 +193,10 @@ class __$$MessageImplCopyWithImpl<$Res>
|
|||||||
? _value.updatedAt
|
? _value.updatedAt
|
||||||
: updatedAt // ignore: cast_nullable_to_non_nullable
|
: updatedAt // ignore: cast_nullable_to_non_nullable
|
||||||
as DateTime?,
|
as DateTime?,
|
||||||
isRead: freezed == isRead
|
status: null == status
|
||||||
? _value.isRead
|
? _value.status
|
||||||
: isRead // ignore: cast_nullable_to_non_nullable
|
: status // ignore: cast_nullable_to_non_nullable
|
||||||
as bool?,
|
as MessageStatus,
|
||||||
media: freezed == media
|
media: freezed == media
|
||||||
? _value._media
|
? _value._media
|
||||||
: media // ignore: cast_nullable_to_non_nullable
|
: media // ignore: cast_nullable_to_non_nullable
|
||||||
@@ -202,9 +214,10 @@ class _$MessageImpl implements _Message {
|
|||||||
required this.senderId,
|
required this.senderId,
|
||||||
required this.content,
|
required this.content,
|
||||||
required this.messageType,
|
required this.messageType,
|
||||||
|
required this.sequenceId,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
this.isRead,
|
this.status = MessageStatus.delivered,
|
||||||
final Map<String, dynamic>? media})
|
final Map<String, dynamic>? media})
|
||||||
: _media = media;
|
: _media = media;
|
||||||
|
|
||||||
@@ -218,13 +231,15 @@ class _$MessageImpl implements _Message {
|
|||||||
final String content;
|
final String content;
|
||||||
@override
|
@override
|
||||||
final String messageType;
|
final String messageType;
|
||||||
// 'text', 'image', 'video', 'audio', 'file'
|
@override
|
||||||
|
final int sequenceId;
|
||||||
@override
|
@override
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
@override
|
@override
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
@override
|
@override
|
||||||
final bool? isRead;
|
@JsonKey()
|
||||||
|
final MessageStatus status;
|
||||||
final Map<String, dynamic>? _media;
|
final Map<String, dynamic>? _media;
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic>? get media {
|
Map<String, dynamic>? get media {
|
||||||
@@ -237,7 +252,7 @@ class _$MessageImpl implements _Message {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Message(id: $id, chatId: $chatId, senderId: $senderId, content: $content, messageType: $messageType, createdAt: $createdAt, updatedAt: $updatedAt, isRead: $isRead, media: $media)';
|
return 'Message(id: $id, chatId: $chatId, senderId: $senderId, content: $content, messageType: $messageType, sequenceId: $sequenceId, createdAt: $createdAt, updatedAt: $updatedAt, status: $status, media: $media)';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -252,11 +267,13 @@ class _$MessageImpl implements _Message {
|
|||||||
(identical(other.content, content) || other.content == content) &&
|
(identical(other.content, content) || other.content == content) &&
|
||||||
(identical(other.messageType, messageType) ||
|
(identical(other.messageType, messageType) ||
|
||||||
other.messageType == messageType) &&
|
other.messageType == messageType) &&
|
||||||
|
(identical(other.sequenceId, sequenceId) ||
|
||||||
|
other.sequenceId == sequenceId) &&
|
||||||
(identical(other.createdAt, createdAt) ||
|
(identical(other.createdAt, createdAt) ||
|
||||||
other.createdAt == createdAt) &&
|
other.createdAt == createdAt) &&
|
||||||
(identical(other.updatedAt, updatedAt) ||
|
(identical(other.updatedAt, updatedAt) ||
|
||||||
other.updatedAt == updatedAt) &&
|
other.updatedAt == updatedAt) &&
|
||||||
(identical(other.isRead, isRead) || other.isRead == isRead) &&
|
(identical(other.status, status) || other.status == status) &&
|
||||||
const DeepCollectionEquality().equals(other._media, _media));
|
const DeepCollectionEquality().equals(other._media, _media));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,9 +285,10 @@ class _$MessageImpl implements _Message {
|
|||||||
senderId,
|
senderId,
|
||||||
content,
|
content,
|
||||||
messageType,
|
messageType,
|
||||||
|
sequenceId,
|
||||||
createdAt,
|
createdAt,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
isRead,
|
status,
|
||||||
const DeepCollectionEquality().hash(_media));
|
const DeepCollectionEquality().hash(_media));
|
||||||
|
|
||||||
@JsonKey(ignore: true)
|
@JsonKey(ignore: true)
|
||||||
@@ -287,9 +305,10 @@ abstract class _Message implements Message {
|
|||||||
required final String senderId,
|
required final String senderId,
|
||||||
required final String content,
|
required final String content,
|
||||||
required final String messageType,
|
required final String messageType,
|
||||||
|
required final int sequenceId,
|
||||||
final DateTime? createdAt,
|
final DateTime? createdAt,
|
||||||
final DateTime? updatedAt,
|
final DateTime? updatedAt,
|
||||||
final bool? isRead,
|
final MessageStatus status,
|
||||||
final Map<String, dynamic>? media}) = _$MessageImpl;
|
final Map<String, dynamic>? media}) = _$MessageImpl;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -302,12 +321,14 @@ abstract class _Message implements Message {
|
|||||||
String get content;
|
String get content;
|
||||||
@override
|
@override
|
||||||
String get messageType;
|
String get messageType;
|
||||||
@override // 'text', 'image', 'video', 'audio', 'file'
|
@override
|
||||||
|
int get sequenceId;
|
||||||
|
@override
|
||||||
DateTime? get createdAt;
|
DateTime? get createdAt;
|
||||||
@override
|
@override
|
||||||
DateTime? get updatedAt;
|
DateTime? get updatedAt;
|
||||||
@override
|
@override
|
||||||
bool? get isRead;
|
MessageStatus get status;
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic>? get media;
|
Map<String, dynamic>? get media;
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ abstract class ChatRepository {
|
|||||||
|
|
||||||
Stream<Message> get messageStream;
|
Stream<Message> get messageStream;
|
||||||
Stream<Map<String, dynamic>> get typingStream;
|
Stream<Map<String, dynamic>> get typingStream;
|
||||||
|
Stream<Map<String, dynamic>> get statusStream;
|
||||||
|
Stream<Map<String, dynamic>> get readReceiptStream;
|
||||||
Future<void> sendTypingStatus(String chatId, bool isTyping);
|
Future<void> sendTypingStatus(String chatId, bool isTyping);
|
||||||
|
Future<void> markAsRead(String chatId, String lastReadMessageId, int lastReadSequenceId);
|
||||||
Future<void> initSignalR();
|
Future<void> initSignalR();
|
||||||
Future<void> disposeSignalR();
|
Future<void> disposeSignalR();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
final ChatRepository chatRepository;
|
final ChatRepository chatRepository;
|
||||||
StreamSubscription? _messageSubscription;
|
StreamSubscription? _messageSubscription;
|
||||||
StreamSubscription? _typingSubscription;
|
StreamSubscription? _typingSubscription;
|
||||||
|
StreamSubscription? _statusSubscription;
|
||||||
|
StreamSubscription? _readReceiptSubscription;
|
||||||
|
Timer? _refreshTimer;
|
||||||
|
Timer? _retryTimer;
|
||||||
|
String? _currentUserId;
|
||||||
|
final Set<String> _sendingIds = {};
|
||||||
|
|
||||||
ChatBloc({required this.chatRepository}) : super(const ChatState.initial()) {
|
ChatBloc({required this.chatRepository}) : super(const ChatState.initial()) {
|
||||||
on<ChatEventStarted>(_onStarted);
|
on<ChatEventStarted>(_onStarted);
|
||||||
@@ -20,7 +26,13 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
on<ChatEventMessageSent>(_onMessageSent);
|
on<ChatEventMessageSent>(_onMessageSent);
|
||||||
on<ChatEventFavoritesRequested>(_onFavoritesRequested);
|
on<ChatEventFavoritesRequested>(_onFavoritesRequested);
|
||||||
on<ChatEventTypingUpdated>(_onTypingUpdated);
|
on<ChatEventTypingUpdated>(_onTypingUpdated);
|
||||||
|
on<ChatEventUserStatusChanged>(_onUserStatusChanged);
|
||||||
|
on<ChatEventRefreshPresence>(_onRefreshPresence);
|
||||||
on<ChatEventSendTypingStatus>(_onSendTypingStatus);
|
on<ChatEventSendTypingStatus>(_onSendTypingStatus);
|
||||||
|
on<ChatEventMarkAsRead>(_onMarkAsRead);
|
||||||
|
on<ChatEventReadReceiptReceived>(_onReadReceiptReceived);
|
||||||
|
on<ChatEventMessageStatusUpdated>(_onMessageStatusUpdated);
|
||||||
|
on<ChatEventMessageReplaced>(_onMessageReplaced);
|
||||||
on<ChatEventCacheCleared>(_onCacheCleared);
|
on<ChatEventCacheCleared>(_onCacheCleared);
|
||||||
|
|
||||||
_messageSubscription = chatRepository.messageStream.listen((message) {
|
_messageSubscription = chatRepository.messageStream.listen((message) {
|
||||||
@@ -30,9 +42,37 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
_typingSubscription = chatRepository.typingStream.listen((data) {
|
_typingSubscription = chatRepository.typingStream.listen((data) {
|
||||||
add(ChatEvent.typingUpdated(data['chatId'], data['userId'], data['isTyping']));
|
add(ChatEvent.typingUpdated(data['chatId'], data['userId'], data['isTyping']));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_statusSubscription = chatRepository.statusStream.listen((data) {
|
||||||
|
add(ChatEvent.userStatusChanged(
|
||||||
|
data['userId'],
|
||||||
|
data['isOnline'],
|
||||||
|
lastSeen: data['lastSeen'] != null ? DateTime.tryParse(data['lastSeen'].toString()) : null,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
_readReceiptSubscription = chatRepository.readReceiptStream.listen((data) {
|
||||||
|
add(ChatEvent.readReceiptReceived(
|
||||||
|
chatId: data['chatId'],
|
||||||
|
userId: data['userId'],
|
||||||
|
lastReadMessageId: data['lastReadMessageId'],
|
||||||
|
lastReadSequenceId: int.tryParse(data['lastReadSequenceId'].toString()) ?? 0,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh presence status every minute to update "X minutes ago"
|
||||||
|
_refreshTimer = Timer.periodic(const Duration(minutes: 1), (_) {
|
||||||
|
add(const ChatEvent.refreshPresence());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Retry sending messages every 10 seconds
|
||||||
|
_retryTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
||||||
|
_retryPendingMessages();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onStarted(ChatEventStarted event, Emitter<ChatState> emit) async {
|
Future<void> _onStarted(ChatEventStarted event, Emitter<ChatState> emit) async {
|
||||||
|
_currentUserId = event.userId;
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('[DEBUG] ChatBloc._onStarted for user: ${event.userId}');
|
print('[DEBUG] ChatBloc._onStarted for user: ${event.userId}');
|
||||||
// Start SignalR in background
|
// Start SignalR in background
|
||||||
@@ -129,12 +169,39 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
orElse: () => [],
|
orElse: () => [],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check for duplicates
|
final List<Message> mutableList = List<Message>.from(currentMessages);
|
||||||
if (currentMessages.any((m) => m.id == message.id)) return;
|
|
||||||
|
|
||||||
final updatedMessages = List<Message>.from(currentMessages)..insert(0, message);
|
// Check if this is a message from me that should replace a temp one
|
||||||
// Ensure sorting if needed (usually messages are already in order from stream)
|
if (message.senderId == _currentUserId) {
|
||||||
|
// Look for a temp message with same content and type
|
||||||
|
final tempIdx = mutableList.indexWhere((m) =>
|
||||||
|
m.id.startsWith('temp_') &&
|
||||||
|
m.messageType.toLowerCase() == message.messageType.toLowerCase() &&
|
||||||
|
(m.content.trim() == message.content.trim() || (m.content.isEmpty && message.content.isEmpty)));
|
||||||
|
|
||||||
|
if (tempIdx != -1) {
|
||||||
|
// Replace temp with real one
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Smart replacement (SignalR): replacing ${mutableList[tempIdx].id} with ${message.id} (Type: ${message.messageType})');
|
||||||
|
mutableList[tempIdx] = message;
|
||||||
|
emit(ChatState.messagesLoaded(currentChat, mutableList, typingUsers: state.typingUsers));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no temp message found, check if we already have this message by ID (prevent double add)
|
||||||
|
if (mutableList.any((m) => m.id == message.id)) {
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Self-message already in list, skipping: ${message.id}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] Self-message received but no matching temp found: ${message.id} (Content: "${message.content}", Type: ${message.messageType})');
|
||||||
|
} else {
|
||||||
|
// From someone else, check for duplicates
|
||||||
|
if (mutableList.any((m) => m.id == message.id)) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedMessages = mutableList..insert(0, message);
|
||||||
emit(ChatState.messagesLoaded(currentChat, updatedMessages, typingUsers: state.typingUsers));
|
emit(ChatState.messagesLoaded(currentChat, updatedMessages, typingUsers: state.typingUsers));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,12 +213,80 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onMessageSent(ChatEventMessageSent event, Emitter<ChatState> emit) async {
|
Future<void> _onMessageSent(ChatEventMessageSent event, Emitter<ChatState> emit) async {
|
||||||
|
final currentChat = state.maybeMap(
|
||||||
|
chatSelected: (s) => s.chat,
|
||||||
|
messagesLoaded: (s) => s.chat,
|
||||||
|
orElse: () => null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (currentChat == null) return;
|
||||||
|
final userId = _currentUserId ?? '';
|
||||||
|
|
||||||
|
final List<Message> currentMessages = state.maybeMap(
|
||||||
|
chatSelected: (s) => s.messages,
|
||||||
|
messagesLoaded: (s) => s.messages,
|
||||||
|
orElse: () => [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create optimistic "sending" message
|
||||||
|
final tempId = 'temp_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
final tempMessage = Message(
|
||||||
|
id: tempId,
|
||||||
|
chatId: event.chatId,
|
||||||
|
senderId: userId,
|
||||||
|
content: event.content,
|
||||||
|
messageType: 'text',
|
||||||
|
sequenceId: -1, // Temporary
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
status: MessageStatus.sending,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedMessages = List<Message>.from(currentMessages)..insert(0, tempMessage);
|
||||||
|
emit(ChatState.messagesLoaded(currentChat, updatedMessages, typingUsers: state.typingUsers));
|
||||||
|
|
||||||
final result = await chatRepository.sendMessage(event.chatId, event.content);
|
final result = await chatRepository.sendMessage(event.chatId, event.content);
|
||||||
|
|
||||||
result.fold(
|
result.fold(
|
||||||
(error) => emit(ChatState.error(error.message ?? 'Failed to send message', typingUsers: state.typingUsers)),
|
(error) {
|
||||||
(_) {
|
// ignore: avoid_print
|
||||||
// Optimistically or via refresh
|
print('[DEBUG] sendMessage failed for $tempId: ${error.toString()}');
|
||||||
add(ChatEvent.messagesRequested(event.chatId));
|
final isNetworkError = error.maybeMap(
|
||||||
|
network: (_) => true,
|
||||||
|
orElse: () => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNetworkError) {
|
||||||
|
// Keep as sending, retry timer will pick it up
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<Message> currentList = state.maybeMap(
|
||||||
|
messagesLoaded: (s) => s.messages,
|
||||||
|
orElse: () => updatedMessages,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Message> mutableList = List<Message>.from(currentList);
|
||||||
|
final idx = mutableList.indexWhere((m) => m.id == tempId);
|
||||||
|
if (idx != -1) {
|
||||||
|
mutableList[idx] = mutableList[idx].copyWith(status: MessageStatus.error);
|
||||||
|
emit(ChatState.messagesLoaded(currentChat, mutableList, typingUsers: state.typingUsers));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(sentMessage) {
|
||||||
|
// Replace temp message with the actual one from server
|
||||||
|
// ignore: avoid_print
|
||||||
|
print('[DEBUG] sendMessage success for $tempId, replacing with ${sentMessage.id}');
|
||||||
|
final List<Message> currentList = state.maybeMap(
|
||||||
|
messagesLoaded: (s) => s.messages,
|
||||||
|
orElse: () => updatedMessages,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<Message> mutableList = List<Message>.from(currentList);
|
||||||
|
final idx = mutableList.indexWhere((m) => m.id == tempId);
|
||||||
|
if (idx != -1) {
|
||||||
|
mutableList[idx] = sentMessage;
|
||||||
|
emit(ChatState.messagesLoaded(currentChat, mutableList, typingUsers: state.typingUsers));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -196,10 +331,98 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onUserStatusChanged(ChatEventUserStatusChanged event, Emitter<ChatState> emit) async {
|
||||||
|
state.mapOrNull(
|
||||||
|
chatsLoaded: (s) {
|
||||||
|
final updatedChats = s.chats.map((chat) {
|
||||||
|
final participants = chat.participants;
|
||||||
|
if (participants != null && participants.any((p) => p.id == event.userId)) {
|
||||||
|
final updatedParticipants = participants.map((p) {
|
||||||
|
if (p.id == event.userId) {
|
||||||
|
return p.copyWith(isOnline: event.isOnline, lastSeen: event.lastSeen ?? p.lastSeen);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).toList();
|
||||||
|
return chat.copyWith(participants: updatedParticipants);
|
||||||
|
}
|
||||||
|
return chat;
|
||||||
|
}).toList();
|
||||||
|
emit(s.copyWith(chats: updatedChats));
|
||||||
|
},
|
||||||
|
chatSelected: (s) {
|
||||||
|
if (s.chat.participants?.any((p) => p.id == event.userId) ?? false) {
|
||||||
|
final updatedParticipants = s.chat.participants!.map((p) {
|
||||||
|
if (p.id == event.userId) {
|
||||||
|
return p.copyWith(isOnline: event.isOnline, lastSeen: event.lastSeen ?? p.lastSeen);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).toList();
|
||||||
|
final updatedChat = s.chat.copyWith(participants: updatedParticipants);
|
||||||
|
emit(s.copyWith(chat: updatedChat));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messagesLoaded: (s) {
|
||||||
|
if (s.chat.participants?.any((p) => p.id == event.userId) ?? false) {
|
||||||
|
final updatedParticipants = s.chat.participants!.map((p) {
|
||||||
|
if (p.id == event.userId) {
|
||||||
|
return p.copyWith(isOnline: event.isOnline, lastSeen: event.lastSeen ?? p.lastSeen);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}).toList();
|
||||||
|
final updatedChat = s.chat.copyWith(participants: updatedParticipants);
|
||||||
|
emit(s.copyWith(chat: updatedChat));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onRefreshPresence(ChatEventRefreshPresence event, Emitter<ChatState> emit) async {
|
||||||
|
// Re-emit the current state with a new timestamp to trigger UI rebuild
|
||||||
|
state.mapOrNull(
|
||||||
|
initial: (s) => emit(s.copyWith(lastUpdate: DateTime.now())),
|
||||||
|
loading: (s) => emit(s.copyWith(lastUpdate: DateTime.now())),
|
||||||
|
chatsLoaded: (s) => emit(s.copyWith(lastUpdate: DateTime.now())),
|
||||||
|
chatSelected: (s) => emit(s.copyWith(lastUpdate: DateTime.now())),
|
||||||
|
messagesLoaded: (s) => emit(s.copyWith(lastUpdate: DateTime.now())),
|
||||||
|
error: (s) => emit(s.copyWith(lastUpdate: DateTime.now())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onSendTypingStatus(ChatEventSendTypingStatus event, Emitter<ChatState> emit) async {
|
Future<void> _onSendTypingStatus(ChatEventSendTypingStatus event, Emitter<ChatState> emit) async {
|
||||||
await chatRepository.sendTypingStatus(event.chatId, event.isTyping);
|
await chatRepository.sendTypingStatus(event.chatId, event.isTyping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onMarkAsRead(ChatEventMarkAsRead event, Emitter<ChatState> emit) async {
|
||||||
|
await chatRepository.markAsRead(event.chatId, event.messageId, event.sequenceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onReadReceiptReceived(ChatEventReadReceiptReceived event, Emitter<ChatState> emit) async {
|
||||||
|
state.mapOrNull(
|
||||||
|
messagesLoaded: (s) {
|
||||||
|
if (s.chat.id == event.chatId) {
|
||||||
|
final updatedMessages = s.messages.map((m) {
|
||||||
|
if (m.sequenceId <= event.lastReadSequenceId && m.status != MessageStatus.read) {
|
||||||
|
return m.copyWith(status: MessageStatus.read);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}).toList();
|
||||||
|
emit(s.copyWith(messages: updatedMessages));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
chatSelected: (s) {
|
||||||
|
if (s.chat.id == event.chatId) {
|
||||||
|
final updatedMessages = s.messages.map((m) {
|
||||||
|
if (m.sequenceId <= event.lastReadSequenceId && m.status != MessageStatus.read) {
|
||||||
|
return m.copyWith(status: MessageStatus.read);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}).toList();
|
||||||
|
emit(s.copyWith(messages: updatedMessages));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _onCacheCleared(ChatEventCacheCleared event, Emitter<ChatState> emit) async {
|
Future<void> _onCacheCleared(ChatEventCacheCleared event, Emitter<ChatState> emit) async {
|
||||||
// ignore: avoid_print
|
// ignore: avoid_print
|
||||||
print('[DEBUG] ChatBloc._onCacheCleared START');
|
print('[DEBUG] ChatBloc._onCacheCleared START');
|
||||||
@@ -209,10 +432,83 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
|||||||
emit(const ChatState.initial());
|
emit(const ChatState.initial());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _retryPendingMessages() {
|
||||||
|
state.mapOrNull(
|
||||||
|
messagesLoaded: (s) {
|
||||||
|
final pending = s.messages.where((m) => m.status == MessageStatus.sending && m.id.startsWith('temp_')).toList();
|
||||||
|
for (final msg in pending) {
|
||||||
|
if (!_sendingIds.contains(msg.id)) {
|
||||||
|
_retrySendMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _retrySendMessage(Message tempMessage) async {
|
||||||
|
_sendingIds.add(tempMessage.id);
|
||||||
|
final result = await chatRepository.sendMessage(tempMessage.chatId, tempMessage.content);
|
||||||
|
_sendingIds.remove(tempMessage.id);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(error) {
|
||||||
|
final isNetworkError = error.maybeMap(
|
||||||
|
network: (_) => true,
|
||||||
|
orElse: () => false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isNetworkError) {
|
||||||
|
_updateMessageStatus(tempMessage.id, MessageStatus.error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(sentMessage) {
|
||||||
|
_replaceTempMessage(tempMessage.id, sentMessage);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateMessageStatus(String id, MessageStatus status) {
|
||||||
|
add(ChatEvent.messageStatusUpdated(id, status));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _replaceTempMessage(String tempId, Message realMessage) {
|
||||||
|
add(ChatEvent.messageReplaced(tempId, realMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onMessageStatusUpdated(ChatEventMessageStatusUpdated event, Emitter<ChatState> emit) async {
|
||||||
|
state.mapOrNull(
|
||||||
|
messagesLoaded: (s) {
|
||||||
|
final List<Message> mutableList = List<Message>.from(s.messages);
|
||||||
|
final idx = mutableList.indexWhere((m) => m.id == event.messageId);
|
||||||
|
if (idx != -1) {
|
||||||
|
mutableList[idx] = mutableList[idx].copyWith(status: event.status);
|
||||||
|
emit(s.copyWith(messages: mutableList));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onMessageReplaced(ChatEventMessageReplaced event, Emitter<ChatState> emit) async {
|
||||||
|
state.mapOrNull(
|
||||||
|
messagesLoaded: (s) {
|
||||||
|
final List<Message> mutableList = List<Message>.from(s.messages);
|
||||||
|
final idx = mutableList.indexWhere((m) => m.id == event.tempId);
|
||||||
|
if (idx != -1) {
|
||||||
|
mutableList[idx] = event.realMessage;
|
||||||
|
emit(s.copyWith(messages: mutableList));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> close() {
|
Future<void> close() {
|
||||||
_messageSubscription?.cancel();
|
_messageSubscription?.cancel();
|
||||||
_typingSubscription?.cancel();
|
_typingSubscription?.cancel();
|
||||||
|
_statusSubscription?.cancel();
|
||||||
|
_readReceiptSubscription?.cancel();
|
||||||
|
_refreshTimer?.cancel();
|
||||||
|
_retryTimer?.cancel();
|
||||||
chatRepository.disposeSignalR();
|
chatRepository.disposeSignalR();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ class ChatEvent with _$ChatEvent {
|
|||||||
const factory ChatEvent.messageSent(String chatId, String content) = ChatEventMessageSent;
|
const factory ChatEvent.messageSent(String chatId, String content) = ChatEventMessageSent;
|
||||||
const factory ChatEvent.favoritesRequested() = ChatEventFavoritesRequested;
|
const factory ChatEvent.favoritesRequested() = ChatEventFavoritesRequested;
|
||||||
const factory ChatEvent.typingUpdated(String chatId, String userId, bool isTyping) = ChatEventTypingUpdated;
|
const factory ChatEvent.typingUpdated(String chatId, String userId, bool isTyping) = ChatEventTypingUpdated;
|
||||||
|
const factory ChatEvent.userStatusChanged(String userId, bool isOnline, {DateTime? lastSeen}) = ChatEventUserStatusChanged;
|
||||||
|
const factory ChatEvent.refreshPresence() = ChatEventRefreshPresence;
|
||||||
const factory ChatEvent.sendTypingStatus(String chatId, bool isTyping) = ChatEventSendTypingStatus;
|
const factory ChatEvent.sendTypingStatus(String chatId, bool isTyping) = ChatEventSendTypingStatus;
|
||||||
|
const factory ChatEvent.markAsRead(String chatId, String messageId, int sequenceId) = ChatEventMarkAsRead;
|
||||||
|
const factory ChatEvent.readReceiptReceived({
|
||||||
|
required String chatId,
|
||||||
|
required String userId,
|
||||||
|
required String lastReadMessageId,
|
||||||
|
required int lastReadSequenceId,
|
||||||
|
}) = ChatEventReadReceiptReceived;
|
||||||
|
const factory ChatEvent.messageStatusUpdated(String messageId, MessageStatus status) = ChatEventMessageStatusUpdated;
|
||||||
|
const factory ChatEvent.messageReplaced(String tempId, Message realMessage) = ChatEventMessageReplaced;
|
||||||
const factory ChatEvent.cacheCleared() = ChatEventCacheCleared;
|
const factory ChatEvent.cacheCleared() = ChatEventCacheCleared;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,10 +6,10 @@ part 'chat_state.freezed.dart';
|
|||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class ChatState with _$ChatState {
|
class ChatState with _$ChatState {
|
||||||
const factory ChatState.initial({@Default({}) Map<String, Set<String>> typingUsers}) = ChatInitial;
|
const factory ChatState.initial({@Default({}) Map<String, Set<String>> typingUsers, DateTime? lastUpdate}) = ChatInitial;
|
||||||
const factory ChatState.loading({@Default({}) Map<String, Set<String>> typingUsers}) = ChatLoading;
|
const factory ChatState.loading({@Default({}) Map<String, Set<String>> typingUsers, DateTime? lastUpdate}) = ChatLoading;
|
||||||
const factory ChatState.chatsLoaded(List<Chat> chats, {@Default({}) Map<String, Set<String>> typingUsers}) = _ChatsLoaded;
|
const factory ChatState.chatsLoaded(List<Chat> chats, {@Default({}) Map<String, Set<String>> typingUsers, DateTime? lastUpdate}) = _ChatsLoaded;
|
||||||
const factory ChatState.chatSelected(Chat chat, List<Message> messages, {@Default({}) Map<String, Set<String>> typingUsers}) = _ChatSelected;
|
const factory ChatState.chatSelected(Chat chat, List<Message> messages, {@Default({}) Map<String, Set<String>> typingUsers, DateTime? lastUpdate}) = _ChatSelected;
|
||||||
const factory ChatState.messagesLoaded(Chat chat, List<Message> messages, {@Default({}) Map<String, Set<String>> typingUsers}) = _MessagesLoaded;
|
const factory ChatState.messagesLoaded(Chat chat, List<Message> messages, {@Default({}) Map<String, Set<String>> typingUsers, DateTime? lastUpdate}) = _MessagesLoaded;
|
||||||
const factory ChatState.error(String message, {@Default({}) Map<String, Set<String>> typingUsers}) = ChatError;
|
const factory ChatState.error(String message, {@Default({}) Map<String, Set<String>> typingUsers, DateTime? lastUpdate}) = ChatError;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -41,8 +41,8 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
|
|||||||
title: BlocBuilder<ChatBloc, ChatState>(
|
title: BlocBuilder<ChatBloc, ChatState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final currentChat = state.maybeWhen(
|
final currentChat = state.maybeWhen(
|
||||||
chatSelected: (chat, _, __) => chat,
|
chatSelected: (chat, messages, typingUsers, lastUpdate) => chat,
|
||||||
messagesLoaded: (chat, _, __) => chat,
|
messagesLoaded: (chat, messages, typingUsers, lastUpdate) => chat,
|
||||||
orElse: () => widget.chat,
|
orElse: () => widget.chat,
|
||||||
);
|
);
|
||||||
return Column(
|
return Column(
|
||||||
@@ -65,10 +65,10 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
|
|||||||
child: BlocBuilder<ChatBloc, ChatState>(
|
child: BlocBuilder<ChatBloc, ChatState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return state.maybeWhen(
|
return state.maybeWhen(
|
||||||
chatSelected: (_, messages, __) => _buildMessageList(messages),
|
chatSelected: (chat, messages, typingUsers, lastUpdate) => _buildMessageList(messages),
|
||||||
messagesLoaded: (_, messages, __) => _buildMessageList(messages),
|
messagesLoaded: (chat, messages, typingUsers, lastUpdate) => _buildMessageList(messages),
|
||||||
loading: (_) => const Center(child: CircularProgressIndicator()),
|
loading: (typingUsers, lastUpdate) => const Center(child: CircularProgressIndicator()),
|
||||||
error: (msg, _) => Center(child: Text('Ошибка: $msg')),
|
error: (msg, typingUsers, lastUpdate) => Center(child: Text('Ошибка: $msg')),
|
||||||
orElse: () => const Center(child: Text('Начните общение')),
|
orElse: () => const Center(child: Text('Начните общение')),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -124,17 +124,46 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
} else if (otherMember.lastSeen != null) {
|
} else if (otherMember.lastSeen != null) {
|
||||||
final lastSeen = otherMember.lastSeen!.toLocal();
|
|
||||||
final timeStr = DateFormat.Hm(locale).format(lastSeen);
|
|
||||||
final dateStr = _formatDate(lastSeen, locale);
|
|
||||||
return Text(
|
return Text(
|
||||||
l10n.lastSeen(dateStr, timeStr),
|
_formatLastSeen(otherMember.lastSeen!, l10n, locale),
|
||||||
style: const TextStyle(fontSize: 12, color: Colors.white70, fontWeight: FontWeight.normal),
|
style: const TextStyle(fontSize: 12, color: Colors.white70, fontWeight: FontWeight.normal),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatLastSeen(DateTime utcDate, AppLocalizations l10n, String locale) {
|
||||||
|
final lastSeen = utcDate.toLocal();
|
||||||
|
final now = DateTime.now();
|
||||||
|
final difference = now.difference(lastSeen);
|
||||||
|
final timeStr = DateFormat.Hm(locale).format(lastSeen);
|
||||||
|
|
||||||
|
// Case 1: Same day (within 24h of NOW, but specifically checking calendar day is better for "yesterday")
|
||||||
|
// Let's use difference for "relative" and calendar days for "yesterday/day before"
|
||||||
|
|
||||||
|
final today = DateTime(now.year, now.month, now.day);
|
||||||
|
final seenDay = DateTime(lastSeen.year, lastSeen.month, lastSeen.day);
|
||||||
|
final dayDiff = today.difference(seenDay).inDays;
|
||||||
|
|
||||||
|
if (dayDiff == 0) {
|
||||||
|
// Same calendar day
|
||||||
|
if (difference.inHours > 0) {
|
||||||
|
return l10n.lastSeenRelative(difference.inHours, difference.inMinutes % 60);
|
||||||
|
} else if (difference.inMinutes > 0) {
|
||||||
|
return l10n.lastSeenRelativeMinutes(difference.inMinutes);
|
||||||
|
} else {
|
||||||
|
return l10n.lastSeenJustNow;
|
||||||
|
}
|
||||||
|
} else if (dayDiff == 1) {
|
||||||
|
return l10n.lastSeenYesterday(timeStr);
|
||||||
|
} else if (dayDiff == 2) {
|
||||||
|
return l10n.lastSeenDayBeforeYesterday(timeStr);
|
||||||
|
} else {
|
||||||
|
final dateStr = DateFormat.MMMMd(locale).format(lastSeen);
|
||||||
|
return l10n.lastSeen(dateStr, timeStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String _formatDate(DateTime date, String locale) {
|
String _formatDate(DateTime date, String locale) {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final localDate = date.toLocal();
|
final localDate = date.toLocal();
|
||||||
@@ -152,6 +181,20 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
|
|||||||
return Center(child: Text(l10n.noMessages));
|
return Center(child: Text(l10n.noMessages));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark as read logic
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final unreadFromOthers = messages.where((m) =>
|
||||||
|
m.senderId != widget.currentUserId &&
|
||||||
|
m.status != MessageStatus.read
|
||||||
|
).toList();
|
||||||
|
|
||||||
|
if (unreadFromOthers.isNotEmpty) {
|
||||||
|
// Find the message with the highest sequenceId to mark everything up to it as read
|
||||||
|
final latest = unreadFromOthers.reduce((a, b) => a.sequenceId > b.sequenceId ? a : b);
|
||||||
|
context.read<ChatBloc>().add(ChatEvent.markAsRead(latest.chatId, latest.id, latest.sequenceId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
final List<dynamic> items = [];
|
final List<dynamic> items = [];
|
||||||
for (int i = 0; i < messages.length; i++) {
|
for (int i = 0; i < messages.length; i++) {
|
||||||
final message = messages[i];
|
final message = messages[i];
|
||||||
@@ -233,6 +276,9 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
|
|||||||
style: const TextStyle(color: Colors.black87, fontSize: 16),
|
style: const TextStyle(color: Colors.black87, fontSize: 16),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
if (message.createdAt != null)
|
if (message.createdAt != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat.Hm(locale).format(message.createdAt!.toLocal()),
|
DateFormat.Hm(locale).format(message.createdAt!.toLocal()),
|
||||||
@@ -241,6 +287,12 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
|
|||||||
color: Colors.black45,
|
color: Colors.black45,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (isMe) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
_buildStatusIcon(message),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -249,6 +301,19 @@ class _ChatDetailPageState extends State<ChatDetailPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusIcon(Message message) {
|
||||||
|
switch (message.status) {
|
||||||
|
case MessageStatus.sending:
|
||||||
|
return const Icon(Icons.access_time, size: 12, color: Colors.black45);
|
||||||
|
case MessageStatus.delivered:
|
||||||
|
return const Icon(Icons.check, size: 12, color: Colors.black45);
|
||||||
|
case MessageStatus.read:
|
||||||
|
return const Icon(Icons.done_all, size: 12, color: Colors.blue);
|
||||||
|
case MessageStatus.error:
|
||||||
|
return const Icon(Icons.error_outline, size: 12, color: Colors.red);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildMessageInput(Chat chat) {
|
Widget _buildMessageInput(Chat chat) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
|
|||||||
@@ -49,19 +49,12 @@ class _ChatsPageState extends State<ChatsPage> {
|
|||||||
body: BlocConsumer<ChatBloc, ChatState>(
|
body: BlocConsumer<ChatBloc, ChatState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
state.whenOrNull(
|
state.whenOrNull(
|
||||||
chatSelected: (chat, _, __) {
|
chatSelected: (chat, messages, typingUsers, lastUpdate) {
|
||||||
if (_isNavigating) return;
|
if (_isNavigating) return;
|
||||||
|
|
||||||
// Extra safety: check if we are already on chat_detail
|
// Only navigate if we are currently on the ChatsPage
|
||||||
bool isAlreadyOnDetail = false;
|
final isCurrent = ModalRoute.of(context)?.isCurrent ?? false;
|
||||||
Navigator.popUntil(context, (route) {
|
if (!isCurrent) return;
|
||||||
if (route.settings.name == 'chat_detail') {
|
|
||||||
isAlreadyOnDetail = true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isAlreadyOnDetail) return;
|
|
||||||
|
|
||||||
_isNavigating = true;
|
_isNavigating = true;
|
||||||
|
|
||||||
@@ -91,9 +84,9 @@ class _ChatsPageState extends State<ChatsPage> {
|
|||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return state.maybeWhen(
|
return state.maybeWhen(
|
||||||
loading: (_) => const Center(child: CircularProgressIndicator()),
|
loading: (typingUsers, lastUpdate) => const Center(child: CircularProgressIndicator()),
|
||||||
chatsLoaded: (chats, _) => _buildChatList(chats, l10n),
|
chatsLoaded: (chats, typingUsers, lastUpdate) => _buildChatList(chats, l10n),
|
||||||
error: (message, _) => Center(child: Text('${l10n.error}: $message')),
|
error: (message, typingUsers, lastUpdate) => Center(child: Text('${l10n.error}: $message')),
|
||||||
orElse: () => const Center(child: CircularProgressIndicator()),
|
orElse: () => const Center(child: CircularProgressIndicator()),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -596,6 +596,36 @@ abstract class AppLocalizations {
|
|||||||
/// **'был(а) в сети {date} в {time}'**
|
/// **'был(а) в сети {date} в {time}'**
|
||||||
String lastSeen(Object date, Object time);
|
String lastSeen(Object date, Object time);
|
||||||
|
|
||||||
|
/// No description provided for @lastSeenRelative.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'был(а) {hours} ч. {minutes} мин. назад'**
|
||||||
|
String lastSeenRelative(Object hours, Object minutes);
|
||||||
|
|
||||||
|
/// No description provided for @lastSeenRelativeMinutes.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'был(а) {minutes} мин. назад'**
|
||||||
|
String lastSeenRelativeMinutes(Object minutes);
|
||||||
|
|
||||||
|
/// No description provided for @lastSeenJustNow.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'был(а) только что'**
|
||||||
|
String get lastSeenJustNow;
|
||||||
|
|
||||||
|
/// No description provided for @lastSeenYesterday.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'был(а) вчера в {time}'**
|
||||||
|
String lastSeenYesterday(Object time);
|
||||||
|
|
||||||
|
/// No description provided for @lastSeenDayBeforeYesterday.
|
||||||
|
///
|
||||||
|
/// In ru, this message translates to:
|
||||||
|
/// **'был(а) позавчера в {time}'**
|
||||||
|
String lastSeenDayBeforeYesterday(Object time);
|
||||||
|
|
||||||
/// No description provided for @noMessages.
|
/// No description provided for @noMessages.
|
||||||
///
|
///
|
||||||
/// In ru, this message translates to:
|
/// In ru, this message translates to:
|
||||||
|
|||||||
@@ -260,6 +260,29 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
return 'last seen $date at $time';
|
return 'last seen $date at $time';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeenRelative(Object hours, Object minutes) {
|
||||||
|
return 'last seen ${hours}h ${minutes}m ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeenRelativeMinutes(Object minutes) {
|
||||||
|
return 'last seen ${minutes}m ago';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSeenJustNow => 'last seen just now';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeenYesterday(Object time) {
|
||||||
|
return 'last seen yesterday at $time';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeenDayBeforeYesterday(Object time) {
|
||||||
|
return 'last seen day before yesterday at $time';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noMessages => 'No messages yet';
|
String get noMessages => 'No messages yet';
|
||||||
|
|
||||||
|
|||||||
@@ -260,6 +260,29 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
return 'был(а) в сети $date в $time';
|
return 'был(а) в сети $date в $time';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeenRelative(Object hours, Object minutes) {
|
||||||
|
return 'был(а) $hours ч. $minutes мин. назад';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeenRelativeMinutes(Object minutes) {
|
||||||
|
return 'был(а) $minutes мин. назад';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get lastSeenJustNow => 'был(а) только что';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeenYesterday(Object time) {
|
||||||
|
return 'был(а) вчера в $time';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String lastSeenDayBeforeYesterday(Object time) {
|
||||||
|
return 'был(а) позавчера в $time';
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noMessages => 'Сообщений пока нет';
|
String get noMessages => 'Сообщений пока нет';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user