mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-07-23 22:51:00 -04:00
new chat widget
This commit is contained in:
parent
063eeb8d12
commit
1a9cca0667
44 changed files with 1904 additions and 981 deletions
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue