new chat widget

This commit is contained in:
Christien Rioux 2025-05-17 18:02:17 -04:00
parent 063eeb8d12
commit 1a9cca0667
44 changed files with 1904 additions and 981 deletions

View file

@ -4,11 +4,8 @@ import 'dart:typed_data';
import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart' as core;
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
@ -19,6 +16,7 @@ import '../../tools/tools.dart';
import '../models/chat_component_state.dart';
import '../models/message_state.dart';
import '../models/window_state.dart';
import '../views/chat_component_widget.dart';
import 'cubits.dart';
const metadataKeyIdentityPublicKey = 'identityPublicKey';
@ -39,15 +37,12 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
_contactListCubit = contactListCubit,
_conversationCubits = conversationCubits,
_messagesCubit = messagesCubit,
super(ChatComponentState(
chatKey: GlobalKey<ChatState>(),
scrollController: AutoScrollController(),
textEditingController: InputTextFieldController(),
super(const ChatComponentState(
localUser: null,
remoteUsers: const IMap.empty(),
historicalRemoteUsers: const IMap.empty(),
unknownUsers: const IMap.empty(),
messageWindow: const AsyncLoading(),
remoteUsers: IMap.empty(),
historicalRemoteUsers: IMap.empty(),
unknownUsers: IMap.empty(),
messageWindow: AsyncLoading(),
title: '',
)) {
// Immediate Init
@ -102,6 +97,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
await _accountRecordSubscription.cancel();
await _messagesSubscription.cancel();
await _conversationSubscriptions.values.map((v) => v.cancel()).wait;
await super.close();
}
@ -122,32 +118,15 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
}
// Send a message
void sendMessage(types.PartialText message) {
final text = message.text;
final replyId = (message.repliedMessage != null)
? base64UrlNoPadDecode(message.repliedMessage!.id)
void sendMessage(
{required String text,
String? replyToMessageId,
Timestamp? expiration,
int? viewLimit,
List<proto.Attachment>? attachments}) {
final replyId = (replyToMessageId != null)
? base64UrlNoPadDecode(replyToMessageId)
: null;
Timestamp? expiration;
int? viewLimit;
List<proto.Attachment>? attachments;
final metadata = message.metadata;
if (metadata != null) {
final expirationValue =
metadata[metadataKeyExpirationDuration] as TimestampDuration?;
if (expirationValue != null) {
expiration = Veilid.instance.now().offset(expirationValue);
}
final viewLimitValue = metadata[metadataKeyViewLimit] as int?;
if (viewLimitValue != null) {
viewLimit = viewLimitValue;
}
final attachmentsValue =
metadata[metadataKeyAttachments] as List<proto.Attachment>?;
if (attachmentsValue != null) {
attachments = attachmentsValue;
}
}
_addTextMessage(
text: text,
@ -172,9 +151,9 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
emit(state.copyWith(localUser: null));
return;
}
final localUser = types.User(
final localUser = core.User(
id: _localUserIdentityKey.toString(),
firstName: account.profile.name,
name: account.profile.name,
metadata: {metadataKeyIdentityPublicKey: _localUserIdentityKey});
emit(state.copyWith(localUser: localUser));
}
@ -199,11 +178,12 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
// Don't change user information on loading state
return;
}
final remoteUser =
_convertRemoteUser(remoteIdentityPublicKey, activeConversationState);
emit(_updateTitle(state.copyWith(
remoteUsers: state.remoteUsers.add(
remoteIdentityPublicKey,
_convertRemoteUser(
remoteIdentityPublicKey, activeConversationState)))));
remoteUsers: state.remoteUsers.add(remoteUser.id, remoteUser))));
}
static ChatComponentState _updateTitle(ChatComponentState currentState) {
@ -212,13 +192,13 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
}
if (currentState.remoteUsers.length == 1) {
final remoteUser = currentState.remoteUsers.values.first;
return currentState.copyWith(title: remoteUser.firstName ?? '<unnamed>');
return currentState.copyWith(title: remoteUser.name ?? '<unnamed>');
}
return currentState.copyWith(
title: '<group chat with ${currentState.remoteUsers.length} users>');
}
types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey,
core.User _convertRemoteUser(TypedKey remoteIdentityPublicKey,
ActiveConversationState activeConversationState) {
// See if we have a contact for this remote user
final contacts = _contactListCubit.state.state.asData?.value;
@ -227,25 +207,24 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
x.value.identityPublicKey.toVeilid() == remoteIdentityPublicKey);
if (contactIdx != -1) {
final contact = contacts[contactIdx].value;
return types.User(
return core.User(
id: remoteIdentityPublicKey.toString(),
firstName: contact.displayName,
name: contact.displayName,
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
}
}
return types.User(
return core.User(
id: remoteIdentityPublicKey.toString(),
firstName: activeConversationState.remoteConversation?.profile.name ??
name: activeConversationState.remoteConversation?.profile.name ??
'<unnamed>',
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
}
types.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) =>
types.User(
id: remoteIdentityPublicKey.toString(),
firstName: '<$remoteIdentityPublicKey>',
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
core.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) => core.User(
id: remoteIdentityPublicKey.toString(),
name: '<$remoteIdentityPublicKey>',
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
Future<void> _updateConversationSubscriptions() async {
// Get existing subscription keys and state
@ -267,16 +246,17 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
final activeConversationState = cc.state.asData?.value;
if (activeConversationState != null) {
currentRemoteUsersState = currentRemoteUsersState.add(
remoteIdentityPublicKey,
_convertRemoteUser(
remoteIdentityPublicKey, activeConversationState));
final remoteUser = _convertRemoteUser(
remoteIdentityPublicKey, activeConversationState);
currentRemoteUsersState =
currentRemoteUsersState.add(remoteUser.id, remoteUser);
}
}
// Purge remote users we didn't see in the cubit list any more
final cancels = <Future<void>>[];
for (final deadUser in existing) {
currentRemoteUsersState = currentRemoteUsersState.remove(deadUser);
currentRemoteUsersState =
currentRemoteUsersState.remove(deadUser.toString());
cancels.add(_conversationSubscriptions.remove(deadUser)!.cancel());
}
await cancels.wait;
@ -285,63 +265,76 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
emit(_updateTitle(state.copyWith(remoteUsers: currentRemoteUsersState)));
}
(ChatComponentState, types.Message?) _messageStateToChatMessage(
(ChatComponentState, core.Message?) _messageStateToChatMessage(
ChatComponentState currentState, MessageState message) {
final authorIdentityPublicKey = message.content.author.toVeilid();
late final types.User author;
final authorUserId = authorIdentityPublicKey.toString();
late final core.User author;
if (authorIdentityPublicKey == _localUserIdentityKey &&
currentState.localUser != null) {
author = currentState.localUser!;
} else {
final remoteUser = currentState.remoteUsers[authorIdentityPublicKey];
final remoteUser = currentState.remoteUsers[authorUserId];
if (remoteUser != null) {
author = remoteUser;
} else {
final historicalRemoteUser =
currentState.historicalRemoteUsers[authorIdentityPublicKey];
currentState.historicalRemoteUsers[authorUserId];
if (historicalRemoteUser != null) {
author = historicalRemoteUser;
} else {
final unknownRemoteUser =
currentState.unknownUsers[authorIdentityPublicKey];
final unknownRemoteUser = currentState.unknownUsers[authorUserId];
if (unknownRemoteUser != null) {
author = unknownRemoteUser;
} else {
final unknownUser = _convertUnknownUser(authorIdentityPublicKey);
currentState = currentState.copyWith(
unknownUsers: currentState.unknownUsers
.add(authorIdentityPublicKey, unknownUser));
unknownUsers:
currentState.unknownUsers.add(authorUserId, unknownUser));
author = unknownUser;
}
}
}
}
types.Status? status;
if (message.sendState != null) {
assert(author.id == _localUserIdentityKey.toString(),
'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;
}
}
// types.Status? status;
// if (message.sendState != null) {
// assert(author.id == _localUserIdentityKey.toString(),
// '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 reconciledAt = message.reconciledTimestamp == null
? null
: DateTime.fromMicrosecondsSinceEpoch(
message.reconciledTimestamp!.value.toInt());
// print('message seqid: ${message.seqId}');
switch (message.content.whichKind()) {
case proto.Message_Kind.text:
final contextText = message.content.text;
final textMessage = types.TextMessage(
author: author,
createdAt:
(message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(),
id: message.content.authorUniqueIdString,
text: contextText.text,
showStatus: status != null,
status: status);
final reconciledId = message.content.authorUniqueIdString;
final contentText = message.content.text;
final textMessage = core.TextMessage(
authorId: author.id,
createdAt: DateTime.fromMicrosecondsSinceEpoch(
message.sentTimestamp.value.toInt()),
sentAt: reconciledAt,
id: reconciledId,
text: '${contentText.text} (${message.seqId})',
//text: contentText.text,
metadata: {
kSeqId: message.seqId,
if (core.isOnlyEmoji(contentText.text)) 'isOnlyEmoji': true,
});
return (currentState, textMessage);
case proto.Message_Kind.secret:
case proto.Message_Kind.delete:
@ -375,7 +368,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
final messagesState = avMessagesState.asData!.value;
// Convert protobuf messages to chat messages
final chatMessages = <types.Message>[];
final chatMessages = <core.Message>[];
final tsSet = <String>{};
for (final message in messagesState.window) {
final (newState, chatMessage) =
@ -390,11 +383,11 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
// '\nChatMessages:\n$chatMessages'
);
} else {
chatMessages.insert(0, chatMessage);
chatMessages.add(chatMessage);
}
}
return currentState.copyWith(
messageWindow: AsyncValue.data(WindowState<types.Message>(
messageWindow: AsyncValue.data(WindowState<core.Message>(
window: chatMessages.toIList(),
length: messagesState.length,
windowTail: messagesState.windowTail,