better message status support

This commit is contained in:
Christien Rioux 2024-04-17 21:31:26 -04:00
parent 4f02435964
commit 809f6d69bf
31 changed files with 1046 additions and 248 deletions

View file

@ -1,2 +1,3 @@
export 'cubits/cubits.dart';
export 'models/models.dart';
export 'views/views.dart';

View file

@ -1,20 +1,49 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:bloc_tools/bloc_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../proto/proto.dart' as proto;
import '../models/models.dart';
class _SingleContactMessageQueueEntry {
_SingleContactMessageQueueEntry({this.remoteMessages});
IList<proto.Message>? remoteMessages;
class RenderStateElement {
RenderStateElement(
{required this.message,
required this.isLocal,
this.reconciled = false,
this.reconciledOffline = false,
this.sent = false,
this.sentOffline = false});
MessageSendState? get sendState {
if (!isLocal) {
return null;
}
if (reconciled && sent) {
if (!reconciledOffline && !sentOffline) {
return MessageSendState.delivered;
}
return MessageSendState.sent;
}
if (sent && !sentOffline) {
return MessageSendState.sent;
}
return MessageSendState.sending;
}
proto.Message message;
bool isLocal;
bool reconciled;
bool reconciledOffline;
bool sent;
bool sentOffline;
}
typedef SingleContactMessagesState = AsyncValue<IList<proto.Message>>;
typedef SingleContactMessagesState = AsyncValue<IList<MessageState>>;
// Cubit that processes single-contact chats
// Builds the reconciled chat record from the local and remote conversation keys
@ -34,7 +63,14 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
_remoteConversationRecordKey = remoteConversationRecordKey,
_remoteMessagesRecordKey = remoteMessagesRecordKey,
_reconciledChatRecord = reconciledChatRecord,
_messagesUpdateQueue = StreamController(),
_unreconciledMessagesQueue = PersistentQueueCubit<proto.Message>(
table: 'SingleContactUnreconciledMessages',
key: remoteConversationRecordKey.toString(),
fromBuffer: proto.Message.fromBuffer),
_sendingMessagesQueue = PersistentQueueCubit<proto.Message>(
table: 'SingleContactSendingMessages',
key: remoteConversationRecordKey.toString(),
fromBuffer: proto.Message.fromBuffer),
super(const AsyncValue.loading()) {
// Async Init
_initWait.add(_init);
@ -44,13 +80,14 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
Future<void> close() async {
await _initWait();
await _messagesUpdateQueue.close();
await _localSubscription?.cancel();
await _remoteSubscription?.cancel();
await _reconciledChatSubscription?.cancel();
await _localMessagesCubit?.close();
await _remoteMessagesCubit?.close();
await _reconciledChatMessagesCubit?.close();
await _unreconciledMessagesQueue.close();
await _sendingMessagesQueue.close();
await _sentSubscription?.cancel();
await _rcvdSubscription?.cancel();
await _reconciledSubscription?.cancel();
await _sentMessagesCubit?.close();
await _rcvdMessagesCubit?.close();
await _reconciledMessagesCubit?.close();
await super.close();
}
@ -60,95 +97,137 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
await _initMessagesCrypto();
// Reconciled messages key
await _initReconciledChatMessages();
await _initReconciledMessagesCubit();
// Local messages key
await _initLocalMessages();
await _initSentMessagesCubit();
// Remote messages key
await _initRemoteMessages();
await _initRcvdMessagesCubit();
// Messages listener
// Unreconciled messages processing queue listener
Future.delayed(Duration.zero, () async {
await for (final entry in _messagesUpdateQueue.stream) {
await _updateMessagesStateAsync(entry);
await for (final entry in _unreconciledMessagesQueue.stream) {
final data = entry.asData;
if (data != null && data.value.isNotEmpty) {
// Process data using recoverable processing mechanism
await _unreconciledMessagesQueue.process((messages) async {
await _processUnreconciledMessages(data.value);
});
}
}
});
// Sending messages processing queue listener
Future.delayed(Duration.zero, () async {
await for (final entry in _sendingMessagesQueue.stream) {
final data = entry.asData;
if (data != null && data.value.isNotEmpty) {
// Process data using recoverable processing mechanism
await _sendingMessagesQueue.process((messages) async {
await _processSendingMessages(data.value);
});
}
}
});
}
// Make crypto
Future<void> _initMessagesCrypto() async {
_messagesCrypto = await _activeAccountInfo
.makeConversationCrypto(_remoteIdentityPublicKey);
}
// Open local messages key
Future<void> _initLocalMessages() async {
Future<void> _initSentMessagesCubit() async {
final writer = _activeAccountInfo.conversationWriter;
_localMessagesCubit = DHTShortArrayCubit(
_sentMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openWrite(
_localMessagesRecordKey, writer,
debugName:
'SingleContactMessagesCubit::_initLocalMessages::LocalMessages',
'SingleContactMessagesCubit::_initSentMessagesCubit::SentMessages',
parent: _localConversationRecordKey,
crypto: _messagesCrypto),
decodeElement: proto.Message.fromBuffer);
_sentSubscription =
_sentMessagesCubit!.stream.listen(_updateSentMessagesState);
_updateSentMessagesState(_sentMessagesCubit!.state);
}
// Open remote messages key
Future<void> _initRemoteMessages() async {
_remoteMessagesCubit = DHTShortArrayCubit(
Future<void> _initRcvdMessagesCubit() async {
_rcvdMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openRead(_remoteMessagesRecordKey,
debugName: 'SingleContactMessagesCubit::_initRemoteMessages::'
'RemoteMessages',
debugName: 'SingleContactMessagesCubit::_initRcvdMessagesCubit::'
'RcvdMessages',
parent: _remoteConversationRecordKey,
crypto: _messagesCrypto),
decodeElement: proto.Message.fromBuffer);
_remoteSubscription =
_remoteMessagesCubit!.stream.listen(_updateRemoteMessagesState);
_updateRemoteMessagesState(_remoteMessagesCubit!.state);
_rcvdSubscription =
_rcvdMessagesCubit!.stream.listen(_updateRcvdMessagesState);
_updateRcvdMessagesState(_rcvdMessagesCubit!.state);
}
// Open reconciled chat record key
Future<void> _initReconciledChatMessages() async {
Future<void> _initReconciledMessagesCubit() async {
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
_reconciledChatMessagesCubit = DHTShortArrayCubit(
_reconciledMessagesCubit = DHTShortArrayCubit(
open: () async => DHTShortArray.openOwned(_reconciledChatRecord,
debugName:
'SingleContactMessagesCubit::_initReconciledChatMessages::'
'ReconciledChat',
debugName: 'SingleContactMessagesCubit::_initReconciledMessages::'
'ReconciledMessages',
parent: accountRecordKey),
decodeElement: proto.Message.fromBuffer);
_reconciledChatSubscription =
_reconciledChatMessagesCubit!.stream.listen(_updateReconciledChatState);
_updateReconciledChatState(_reconciledChatMessagesCubit!.state);
_reconciledSubscription =
_reconciledMessagesCubit!.stream.listen(_updateReconciledMessagesState);
_updateReconciledMessagesState(_reconciledMessagesCubit!.state);
}
// Called when the remote messages list gets a change
void _updateRemoteMessagesState(
BlocBusyState<AsyncValue<IList<proto.Message>>> avmessages) {
void _updateRcvdMessagesState(
DHTShortArrayBusyState<proto.Message> avmessages) {
final remoteMessages = avmessages.state.asData?.value;
if (remoteMessages == null) {
return;
}
// Add remote messages updates to queue to process asynchronously
_messagesUpdateQueue
.add(_SingleContactMessageQueueEntry(remoteMessages: remoteMessages));
// Ignore offline state because remote messages are always fully delivered
// This may happen once per client but should be idempotent
_unreconciledMessagesQueue
.addAllSync(remoteMessages.map((x) => x.value).toIList());
// Update the view
_renderState();
}
// Called when the send messages list gets a change
// This will re-render when messages are sent from another machine
void _updateSentMessagesState(
DHTShortArrayBusyState<proto.Message> avmessages) {
final remoteMessages = avmessages.state.asData?.value;
if (remoteMessages == null) {
return;
}
// Don't reconcile, the sending machine will have already added
// to the reconciliation queue on that machine
// Update the view
_renderState();
}
// Called when the reconciled messages list gets a change
void _updateReconciledChatState(
BlocBusyState<AsyncValue<IList<proto.Message>>> avmessages) {
// When reconciled messages are updated, pass this
// directly to the messages cubit state
emit(avmessages.state);
// This can happen when multiple clients for the same identity are
// reading and reconciling the same remote chat
void _updateReconciledMessagesState(
DHTShortArrayBusyState<proto.Message> avmessages) {
// Update the view
_renderState();
}
Future<void> _mergeMessagesInner(
Future<void> _reconcileMessagesInner(
{required DHTShortArrayWrite reconciledMessagesWriter,
required IList<proto.Message> messages}) async {
// Ensure remoteMessages is sorted by timestamp
@ -209,29 +288,129 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
}
}
Future<void> _updateMessagesStateAsync(
_SingleContactMessageQueueEntry entry) async {
final reconciledChatMessagesCubit = _reconciledChatMessagesCubit!;
// Merge remote and local messages into the reconciled chat log
await reconciledChatMessagesCubit
// Async process to reconcile messages sent or received in the background
Future<void> _processUnreconciledMessages(
IList<proto.Message> messages) async {
await _reconciledMessagesCubit!
.operateWrite((reconciledMessagesWriter) async {
if (entry.remoteMessages != null) {
await _mergeMessagesInner(
reconciledMessagesWriter: reconciledMessagesWriter,
messages: entry.remoteMessages!);
}
await _reconcileMessagesInner(
reconciledMessagesWriter: reconciledMessagesWriter,
messages: messages);
});
}
Future<void> addMessage({required proto.Message message}) async {
await _initWait();
// Async process to send messages in the background
Future<void> _processSendingMessages(IList<proto.Message> messages) async {
for (final message in messages) {
await _sentMessagesCubit!.operateWriteEventual(
(writer) => writer.tryAddItem(message.writeToBuffer()));
}
}
await _reconciledChatMessagesCubit!.operateWrite((writer) =>
_mergeMessagesInner(
reconciledMessagesWriter: writer, messages: [message].toIList()));
await _localMessagesCubit!
.operateWrite((writer) => writer.tryAddItem(message.writeToBuffer()));
// Produce a state for this cubit from the input cubits and queues
void _renderState() {
// Get all reconciled messages
final reconciledMessages =
_reconciledMessagesCubit?.state.state.asData?.value;
// Get all sent messages
final sentMessages = _sentMessagesCubit?.state.state.asData?.value;
// Get all items in the unreconciled queue
final unreconciledMessages = _unreconciledMessagesQueue.state.asData?.value;
// Get all items in the unsent queue
final sendingMessages = _sendingMessagesQueue.state.asData?.value;
// If we aren't ready to render a state, say we're loading
if (reconciledMessages == null ||
sentMessages == null ||
unreconciledMessages == null ||
sendingMessages == null) {
emit(const AsyncLoading());
return;
}
// Generate state for each message
final sentMessagesMap =
IMap<Int64, DHTShortArrayElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.timestamp,
values: sentMessages,
);
final reconciledMessagesMap =
IMap<Int64, DHTShortArrayElementState<proto.Message>>.fromValues(
keyMapper: (x) => x.value.timestamp,
values: reconciledMessages,
);
final sendingMessagesMap = IMap<Int64, proto.Message>.fromValues(
keyMapper: (x) => x.timestamp,
values: sendingMessages,
);
final unreconciledMessagesMap = IMap<Int64, proto.Message>.fromValues(
keyMapper: (x) => x.timestamp,
values: unreconciledMessages,
);
final renderedElements = <Int64, RenderStateElement>{};
for (final m in reconciledMessagesMap.entries) {
renderedElements[m.key] = RenderStateElement(
message: m.value.value,
isLocal: m.value.value.author.toVeilid() != _remoteIdentityPublicKey,
reconciled: true,
reconciledOffline: m.value.isOffline);
}
for (final m in sentMessagesMap.entries) {
renderedElements.putIfAbsent(
m.key,
() => RenderStateElement(
message: m.value.value,
isLocal: true,
))
..sent = true
..sentOffline = m.value.isOffline;
}
for (final m in unreconciledMessagesMap.entries) {
renderedElements
.putIfAbsent(
m.key,
() => RenderStateElement(
message: m.value,
isLocal:
m.value.author.toVeilid() != _remoteIdentityPublicKey,
))
.reconciled = false;
}
for (final m in sendingMessagesMap.entries) {
renderedElements
.putIfAbsent(
m.key,
() => RenderStateElement(
message: m.value,
isLocal: true,
))
.sent = false;
}
// Render the state
final messageKeys = renderedElements.entries
.toIList()
.sort((x, y) => x.key.compareTo(y.key));
final renderedState = messageKeys
.map((x) => MessageState(
author: x.value.message.author.toVeilid(),
timestamp: Timestamp.fromInt64(x.key),
text: x.value.message.text,
sendState: x.value.sendState))
.toIList();
// Emit the rendered state
emit(AsyncValue.data(renderedState));
}
void addMessage({required proto.Message message}) {
_unreconciledMessagesQueue.addSync(message);
_sendingMessagesQueue.addSync(message);
// Update the view
_renderState();
}
final WaitSet _initWait = WaitSet();
@ -245,16 +424,15 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
late final DHTRecordCrypto _messagesCrypto;
DHTShortArrayCubit<proto.Message>? _localMessagesCubit;
DHTShortArrayCubit<proto.Message>? _remoteMessagesCubit;
DHTShortArrayCubit<proto.Message>? _reconciledChatMessagesCubit;
DHTShortArrayCubit<proto.Message>? _sentMessagesCubit;
DHTShortArrayCubit<proto.Message>? _rcvdMessagesCubit;
DHTShortArrayCubit<proto.Message>? _reconciledMessagesCubit;
final StreamController<_SingleContactMessageQueueEntry> _messagesUpdateQueue;
final PersistentQueueCubit<proto.Message> _unreconciledMessagesQueue;
final PersistentQueueCubit<proto.Message> _sendingMessagesQueue;
StreamSubscription<BlocBusyState<AsyncValue<IList<proto.Message>>>>?
_localSubscription;
StreamSubscription<BlocBusyState<AsyncValue<IList<proto.Message>>>>?
_remoteSubscription;
StreamSubscription<BlocBusyState<AsyncValue<IList<proto.Message>>>>?
_reconciledChatSubscription;
StreamSubscription<DHTShortArrayBusyState<proto.Message>>? _sentSubscription;
StreamSubscription<DHTShortArrayBusyState<proto.Message>>? _rcvdSubscription;
StreamSubscription<DHTShortArrayBusyState<proto.Message>>?
_reconciledSubscription;
}

View file

@ -0,0 +1,34 @@
import 'package:change_case/change_case.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:veilid_support/veilid_support.dart';
part 'message_state.freezed.dart';
part 'message_state.g.dart';
// Whether or not a message has been fully sent
enum MessageSendState {
// message is still being sent
sending,
// message issued has not reached the network
sent,
// message was sent and has reached the network
delivered;
factory MessageSendState.fromJson(dynamic j) =>
MessageSendState.values.byName((j as String).toCamelCase());
String toJson() => name.toPascalCase();
}
@freezed
class MessageState with _$MessageState {
const factory MessageState({
required TypedKey author,
required Timestamp timestamp,
required String text,
required MessageSendState? sendState,
}) = _MessageState;
factory MessageState.fromJson(dynamic json) =>
_$MessageStateFromJson(json as Map<String, dynamic>);
}

View file

@ -0,0 +1,229 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'message_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
MessageState _$MessageStateFromJson(Map<String, dynamic> json) {
return _MessageState.fromJson(json);
}
/// @nodoc
mixin _$MessageState {
Typed<FixedEncodedString43> get author => throw _privateConstructorUsedError;
Timestamp get timestamp => throw _privateConstructorUsedError;
String get text => throw _privateConstructorUsedError;
MessageSendState? get sendState => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$MessageStateCopyWith<MessageState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MessageStateCopyWith<$Res> {
factory $MessageStateCopyWith(
MessageState value, $Res Function(MessageState) then) =
_$MessageStateCopyWithImpl<$Res, MessageState>;
@useResult
$Res call(
{Typed<FixedEncodedString43> author,
Timestamp timestamp,
String text,
MessageSendState? sendState});
}
/// @nodoc
class _$MessageStateCopyWithImpl<$Res, $Val extends MessageState>
implements $MessageStateCopyWith<$Res> {
_$MessageStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? author = null,
Object? timestamp = null,
Object? text = null,
Object? sendState = freezed,
}) {
return _then(_value.copyWith(
author: null == author
? _value.author
: author // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
timestamp: null == timestamp
? _value.timestamp
: timestamp // ignore: cast_nullable_to_non_nullable
as Timestamp,
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
sendState: freezed == sendState
? _value.sendState
: sendState // ignore: cast_nullable_to_non_nullable
as MessageSendState?,
) as $Val);
}
}
/// @nodoc
abstract class _$$MessageStateImplCopyWith<$Res>
implements $MessageStateCopyWith<$Res> {
factory _$$MessageStateImplCopyWith(
_$MessageStateImpl value, $Res Function(_$MessageStateImpl) then) =
__$$MessageStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Typed<FixedEncodedString43> author,
Timestamp timestamp,
String text,
MessageSendState? sendState});
}
/// @nodoc
class __$$MessageStateImplCopyWithImpl<$Res>
extends _$MessageStateCopyWithImpl<$Res, _$MessageStateImpl>
implements _$$MessageStateImplCopyWith<$Res> {
__$$MessageStateImplCopyWithImpl(
_$MessageStateImpl _value, $Res Function(_$MessageStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? author = null,
Object? timestamp = null,
Object? text = null,
Object? sendState = freezed,
}) {
return _then(_$MessageStateImpl(
author: null == author
? _value.author
: author // ignore: cast_nullable_to_non_nullable
as Typed<FixedEncodedString43>,
timestamp: null == timestamp
? _value.timestamp
: timestamp // ignore: cast_nullable_to_non_nullable
as Timestamp,
text: null == text
? _value.text
: text // ignore: cast_nullable_to_non_nullable
as String,
sendState: freezed == sendState
? _value.sendState
: sendState // ignore: cast_nullable_to_non_nullable
as MessageSendState?,
));
}
}
/// @nodoc
@JsonSerializable()
class _$MessageStateImpl with DiagnosticableTreeMixin implements _MessageState {
const _$MessageStateImpl(
{required this.author,
required this.timestamp,
required this.text,
required this.sendState});
factory _$MessageStateImpl.fromJson(Map<String, dynamic> json) =>
_$$MessageStateImplFromJson(json);
@override
final Typed<FixedEncodedString43> author;
@override
final Timestamp timestamp;
@override
final String text;
@override
final MessageSendState? sendState;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'MessageState(author: $author, timestamp: $timestamp, text: $text, sendState: $sendState)';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'MessageState'))
..add(DiagnosticsProperty('author', author))
..add(DiagnosticsProperty('timestamp', timestamp))
..add(DiagnosticsProperty('text', text))
..add(DiagnosticsProperty('sendState', sendState));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MessageStateImpl &&
(identical(other.author, author) || other.author == author) &&
(identical(other.timestamp, timestamp) ||
other.timestamp == timestamp) &&
(identical(other.text, text) || other.text == text) &&
(identical(other.sendState, sendState) ||
other.sendState == sendState));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, author, timestamp, text, sendState);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$MessageStateImplCopyWith<_$MessageStateImpl> get copyWith =>
__$$MessageStateImplCopyWithImpl<_$MessageStateImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$MessageStateImplToJson(
this,
);
}
}
abstract class _MessageState implements MessageState {
const factory _MessageState(
{required final Typed<FixedEncodedString43> author,
required final Timestamp timestamp,
required final String text,
required final MessageSendState? sendState}) = _$MessageStateImpl;
factory _MessageState.fromJson(Map<String, dynamic> json) =
_$MessageStateImpl.fromJson;
@override
Typed<FixedEncodedString43> get author;
@override
Timestamp get timestamp;
@override
String get text;
@override
MessageSendState? get sendState;
@override
@JsonKey(ignore: true)
_$$MessageStateImplCopyWith<_$MessageStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'message_state.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$MessageStateImpl _$$MessageStateImplFromJson(Map<String, dynamic> json) =>
_$MessageStateImpl(
author: Typed<FixedEncodedString43>.fromJson(json['author']),
timestamp: Timestamp.fromJson(json['timestamp']),
text: json['text'] as String,
sendState: json['send_state'] == null
? null
: MessageSendState.fromJson(json['send_state']),
);
Map<String, dynamic> _$$MessageStateImplToJson(_$MessageStateImpl instance) =>
<String, dynamic>{
'author': instance.author.toJson(),
'timestamp': instance.timestamp.toJson(),
'text': instance.text,
'send_state': instance.sendState?.toJson(),
};

View file

@ -0,0 +1 @@
export 'message_state.dart';

View file

@ -1,5 +1,3 @@
import 'dart:async';
import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
@ -99,38 +97,52 @@ class ChatComponent extends StatelessWidget {
/////////////////////////////////////////////////////////////////////
types.Message messageToChatMessage(proto.Message message) {
final isLocal = message.author == _localUserIdentityKey.toProto();
types.Message messageToChatMessage(MessageState message) {
final isLocal = message.author == _localUserIdentityKey;
types.Status? status;
if (message.sendState != null) {
assert(isLocal, 'send state should only be on sent messages');
switch (message.sendState!) {
case MessageSendState.sending:
status = types.Status.sending;
case MessageSendState.sent:
status = types.Status.sent;
case MessageSendState.delivered:
status = types.Status.delivered;
}
}
final textMessage = types.TextMessage(
author: isLocal ? _localUser : _remoteUser,
createdAt: (message.timestamp ~/ 1000).toInt(),
id: message.timestamp.toString(),
text: message.text,
);
author: isLocal ? _localUser : _remoteUser,
createdAt: (message.timestamp.value ~/ BigInt.from(1000)).toInt(),
id: message.timestamp.toString(),
text: message.text,
showStatus: status != null,
status: status);
return textMessage;
}
Future<void> _addMessage(proto.Message message) async {
void _addMessage(proto.Message message) {
if (message.text.isEmpty) {
return;
}
await _messagesCubit.addMessage(message: message);
_messagesCubit.addMessage(message: message);
}
Future<void> _handleSendPressed(types.PartialText message) async {
void _handleSendPressed(types.PartialText message) {
final protoMessage = proto.Message()
..author = _localUserIdentityKey.toProto()
..timestamp = Veilid.instance.now().toInt64()
..text = message.text;
//..signature = signature;
await _addMessage(protoMessage);
_addMessage(protoMessage);
}
Future<void> _handleAttachmentPressed() async {
//
}
// void _handleAttachmentPressed() async {
// //
// }
@override
Widget build(BuildContext context) {
@ -195,10 +207,7 @@ class ChatComponent extends StatelessWidget {
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: (message) {
singleFuture(
this, () async => _handleSendPressed(message));
},
onSendPressed: _handleSendPressed,
//showUserAvatars: false,
//showUserNames: true,
user: _localUser,