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

@ -5,6 +5,7 @@
- Fixed issue with Android 'back' button exiting the app (#331) - Fixed issue with Android 'back' button exiting the app (#331)
- Deprecated accounts no longer crash application at startup - Deprecated accounts no longer crash application at startup
- Simplify SingleContactMessagesCubit and MessageReconciliation - Simplify SingleContactMessagesCubit and MessageReconciliation
- Update flutter_chat_ui to 2.0.0
## v0.4.7 ## ## v0.4.7 ##
- *Community Contributions* - *Community Contributions*

View file

@ -313,5 +313,9 @@
"info": "Info", "info": "Info",
"debug": "Debug", "debug": "Debug",
"trace": "Trace" "trace": "Trace"
},
"date_formatter": {
"just_now": "Just now",
"yesterday": "Yesterday"
} }
} }

0
flutter_01.png Normal file
View file

0
flutter_02.png Normal file
View file

0
flutter_03.png Normal file
View file

View file

@ -15,8 +15,9 @@ part 'local_account.freezed.dart';
// and the identitySecretKey optionally encrypted by an unlock code // and the identitySecretKey optionally encrypted by an unlock code
// This is the root of the account information tree for VeilidChat // This is the root of the account information tree for VeilidChat
// //
@freezed @Freezed(toJson: true)
sealed class LocalAccount with _$LocalAccount { abstract class LocalAccount with _$LocalAccount {
@JsonSerializable()
const factory LocalAccount({ const factory LocalAccount({
// The super identity key record for the account, // The super identity key record for the account,
// containing the publicKey in the currentIdentity // containing the publicKey in the currentIdentity

View file

@ -153,6 +153,7 @@ class _$LocalAccountCopyWithImpl<$Res> implements $LocalAccountCopyWith<$Res> {
} }
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _LocalAccount implements LocalAccount { class _LocalAccount implements LocalAccount {
const _LocalAccount( const _LocalAccount(
@ -162,8 +163,6 @@ class _LocalAccount implements LocalAccount {
required this.biometricsEnabled, required this.biometricsEnabled,
required this.hiddenAccount, required this.hiddenAccount,
required this.name}); required this.name});
factory _LocalAccount.fromJson(Map<String, dynamic> json) =>
_$LocalAccountFromJson(json);
// The super identity key record for the account, // The super identity key record for the account,
// containing the publicKey in the currentIdentity // containing the publicKey in the currentIdentity

View file

@ -8,8 +8,9 @@ part 'user_login.g.dart';
// Represents a currently logged in account // Represents a currently logged in account
// User logins are stored in the user_logins tablestore table // User logins are stored in the user_logins tablestore table
// indexed by the accountSuperIdentityRecordKey // indexed by the accountSuperIdentityRecordKey
@freezed @Freezed(toJson: true)
sealed class UserLogin with _$UserLogin { sealed class UserLogin with _$UserLogin {
@JsonSerializable()
const factory UserLogin({ const factory UserLogin({
// SuperIdentity record key for the user // SuperIdentity record key for the user
// used to index the local accounts table // used to index the local accounts table

View file

@ -124,6 +124,7 @@ class _$UserLoginCopyWithImpl<$Res> implements $UserLoginCopyWith<$Res> {
} }
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _UserLogin implements UserLogin { class _UserLogin implements UserLogin {
const _UserLogin( const _UserLogin(
@ -131,8 +132,6 @@ class _UserLogin implements UserLogin {
required this.identitySecret, required this.identitySecret,
required this.accountRecordInfo, required this.accountRecordInfo,
required this.lastActive}); required this.lastActive});
factory _UserLogin.fromJson(Map<String, dynamic> json) =>
_$UserLoginFromJson(json);
// SuperIdentity record key for the user // SuperIdentity record key for the user
// used to index the local accounts table // used to index the local accounts table

View file

@ -249,7 +249,6 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
final displayModalHUD = _isInAsyncCall; final displayModalHUD = _isInAsyncCall;
return StyledScaffold( return StyledScaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: Text(translate('edit_account_page.titlebar')), title: Text(translate('edit_account_page.titlebar')),
leading: Navigator.canPop(context) leading: Navigator.canPop(context)

View file

@ -163,7 +163,6 @@ class _ShowRecoveryKeyPageState extends WindowSetupState<ShowRecoveryKeyPage> {
final displayModalHUD = _isInAsyncCall; final displayModalHUD = _isInAsyncCall;
return StyledScaffold( return StyledScaffold(
// resizeToAvoidBottomInset: false,
appBar: DefaultAppBar( appBar: DefaultAppBar(
title: Text(translate('show_recovery_key_page.titlebar')), title: Text(translate('show_recovery_key_page.titlebar')),
actions: [ actions: [

View file

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

View file

@ -3,6 +3,8 @@ import 'dart:async';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:uuid/uuid.dart';
import 'package:uuid/v4.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
@ -11,9 +13,12 @@ import '../../tools/tools.dart';
import '../models/models.dart'; import '../models/models.dart';
import 'reconciliation/reconciliation.dart'; import 'reconciliation/reconciliation.dart';
const _sfSendMessageTag = 'sfSendMessageTag';
class RenderStateElement { class RenderStateElement {
RenderStateElement( RenderStateElement(
{required this.message, {required this.seqId,
required this.message,
required this.isLocal, required this.isLocal,
this.reconciledTimestamp, this.reconciledTimestamp,
this.sent = false, this.sent = false,
@ -36,6 +41,7 @@ class RenderStateElement {
return null; return null;
} }
int seqId;
proto.Message message; proto.Message message;
bool isLocal; bool isLocal;
Timestamp? reconciledTimestamp; Timestamp? reconciledTimestamp;
@ -71,6 +77,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
Future<void> close() async { Future<void> close() async {
await _initWait(); await _initWait();
await serialFutureClose((this, _sfSendMessageTag));
await _commandController.close(); await _commandController.close();
await _commandRunnerFut; await _commandRunnerFut;
await _unsentMessagesQueue.close(); await _unsentMessagesQueue.close();
@ -309,9 +317,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Async process to send messages in the background // Async process to send messages in the background
Future<void> _processUnsentMessages(IList<proto.Message> messages) async { Future<void> _processUnsentMessages(IList<proto.Message> messages) async {
// _sendingMessages = messages;
// _renderState();
try { try {
await _sentMessagesDHTLog!.operateAppendEventual((writer) async { await _sentMessagesDHTLog!.operateAppendEventual((writer) async {
// Get the previous message if we have one // Get the previous message if we have one
@ -337,8 +342,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
} on Exception catch (e, st) { } on Exception catch (e, st) {
log.error('Exception appending unsent messages: $e:\n$st\n'); log.error('Exception appending unsent messages: $e:\n$st\n');
} }
// _sendingMessages = const IList.empty();
} }
// Produce a state for this cubit from the input cubits and queues // Produce a state for this cubit from the input cubits and queues
@ -349,8 +352,9 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Get all sent messages that are still offline // Get all sent messages that are still offline
//final sentMessages = _sentMessagesDHTLog. //final sentMessages = _sentMessagesDHTLog.
//Get all items in the unsent queue
//final unsentMessages = _unsentMessagesQueue.queue; // Get all items in the unsent queue
final unsentMessages = _unsentMessagesQueue.queue;
// If we aren't ready to render a state, say we're loading // If we aren't ready to render a state, say we're loading
if (reconciledMessages == null) { if (reconciledMessages == null) {
@ -374,8 +378,19 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// values: unsentMessages, // values: unsentMessages,
// ); // );
// List of all rendered state elements that we will turn into
// message states
final renderedElements = <RenderStateElement>[]; final renderedElements = <RenderStateElement>[];
// Keep track of the ids we have rendered
// because there can be an overlap between the 'unsent messages'
// and the reconciled messages as the async state catches up
final renderedIds = <String>{}; final renderedIds = <String>{};
var seqId = (reconciledMessages.windowTail == 0
? reconciledMessages.length
: reconciledMessages.windowTail) -
reconciledMessages.windowElements.length;
for (final m in reconciledMessages.windowElements) { for (final m in reconciledMessages.windowElements) {
final isLocal = final isLocal =
m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey; m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey;
@ -387,33 +402,44 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
final sent = isLocal; final sent = isLocal;
final sentOffline = false; // final sentOffline = false; //
if (renderedIds.contains(m.content.authorUniqueIdString)) {
seqId++;
continue;
}
renderedElements.add(RenderStateElement( renderedElements.add(RenderStateElement(
seqId: seqId,
message: m.content, message: m.content,
isLocal: isLocal, isLocal: isLocal,
reconciledTimestamp: reconciledTimestamp, reconciledTimestamp: reconciledTimestamp,
sent: sent, sent: sent,
sentOffline: sentOffline, sentOffline: sentOffline,
)); ));
renderedIds.add(m.content.authorUniqueIdString); renderedIds.add(m.content.authorUniqueIdString);
seqId++;
} }
// Render in-flight messages at the bottom // Render in-flight messages at the bottom
// for (final m in _sendingMessages) { //
// for (final m in unsentMessages) {
// if (renderedIds.contains(m.authorUniqueIdString)) { // if (renderedIds.contains(m.authorUniqueIdString)) {
// seqId++;
// continue; // continue;
// } // }
// renderedElements.add(RenderStateElement( // renderedElements.add(RenderStateElement(
// seqId: seqId,
// message: m, // message: m,
// isLocal: true, // isLocal: true,
// sent: true, // sent: true,
// sentOffline: true, // sentOffline: true,
// )); // ));
// renderedIds.add(m.authorUniqueIdString);
// seqId++;
// } // }
// Render the state // Render the state
final messages = renderedElements final messages = renderedElements
.map((x) => MessageState( .map((x) => MessageState(
seqId: x.seqId,
content: x.message, content: x.message,
sentTimestamp: Timestamp.fromInt64(x.message.timestamp), sentTimestamp: Timestamp.fromInt64(x.message.timestamp),
reconciledTimestamp: x.reconciledTimestamp, reconciledTimestamp: x.reconciledTimestamp,
@ -431,20 +457,26 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
void _sendMessage({required proto.Message message}) { void _sendMessage({required proto.Message message}) {
// Add common fields // Add common fields
// id and signature will get set by _processMessageToSend // real id and signature will get set by _processMessageToSend
// temporary id set here is random and not 'valid' in the eyes
// of reconcilation, noting that reconciled timestamp is not yet set.
message message
..author = _accountInfo.identityTypedPublicKey.toProto() ..author = _accountInfo.identityTypedPublicKey.toProto()
..timestamp = Veilid.instance.now().toInt64(); ..timestamp = Veilid.instance.now().toInt64()
..id = Uuid.parse(_uuidGen.generate());
if ((message.writeToBuffer().lengthInBytes + 256) > 4096) { if ((message.writeToBuffer().lengthInBytes + 256) > 4096) {
throw const FormatException('message is too long'); throw const FormatException('message is too long');
} }
// Put in the queue // Put in the queue
_unsentMessagesQueue.addSync(message); serialFuture((this, _sfSendMessageTag), () async {
// Add the message to the persistent queue
await _unsentMessagesQueue.add(message);
// Update the view // Update the view
_renderState(); _renderState();
});
} }
Future<void> _commandRunner() async { Future<void> _commandRunner() async {
@ -487,7 +519,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
late final MessageReconciliation _reconciliation; late final MessageReconciliation _reconciliation;
late final PersistentQueue<proto.Message> _unsentMessagesQueue; late final PersistentQueue<proto.Message> _unsentMessagesQueue;
// IList<proto.Message> _sendingMessages = const IList.empty();
StreamSubscription<void>? _sentSubscription; StreamSubscription<void>? _sentSubscription;
StreamSubscription<void>? _rcvdSubscription; StreamSubscription<void>? _rcvdSubscription;
StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>? StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>?
@ -496,4 +527,5 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
late final Future<void> _commandRunnerFut; late final Future<void> _commandRunnerFut;
final _sspRemoteConversationRecordKey = SingleStateProcessor<TypedKey?>(); final _sspRemoteConversationRecordKey = SingleStateProcessor<TypedKey?>();
final _uuidGen = const UuidV4();
} }

View file

@ -1,12 +1,7 @@
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' show Message, User;
import 'package:flutter_chat_ui/flutter_chat_ui.dart'
show ChatState, InputTextFieldController;
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:veilid_support/veilid_support.dart';
import 'window_state.dart'; import 'window_state.dart';
@ -16,26 +11,16 @@ part 'chat_component_state.freezed.dart';
sealed class ChatComponentState with _$ChatComponentState { sealed class ChatComponentState with _$ChatComponentState {
const factory ChatComponentState( const factory ChatComponentState(
{ {
// GlobalKey for the chat
required GlobalKey<ChatState> chatKey,
// ScrollController for the chat
required AutoScrollController scrollController,
// TextEditingController for the chat
required InputTextFieldController textEditingController,
// Local user // Local user
required User? localUser, required User? localUser,
// Active remote users // Active remote users
required IMap<TypedKey, User> remoteUsers, required IMap<UserID, User> remoteUsers,
// Historical remote users // Historical remote users
required IMap<TypedKey, User> historicalRemoteUsers, required IMap<UserID, User> historicalRemoteUsers,
// Unknown users // Unknown users
required IMap<TypedKey, User> unknownUsers, required IMap<UserID, User> unknownUsers,
// Messages state // Messages state
required AsyncValue<WindowState<Message>> messageWindow, required AsyncValue<WindowState<Message>> messageWindow,
// Title of the chat // Title of the chat
required String title}) = _ChatComponentState; required String title}) = _ChatComponentState;
} }
extension ChatComponentStateExt on ChatComponentState {
//
}

View file

@ -15,15 +15,11 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$ChatComponentState { mixin _$ChatComponentState {
// GlobalKey for the chat // Local user
GlobalKey<ChatState> get chatKey; // ScrollController for the chat
AutoScrollController
get scrollController; // TextEditingController for the chat
InputTextFieldController get textEditingController; // Local user
User? get localUser; // Active remote users User? get localUser; // Active remote users
IMap<TypedKey, User> get remoteUsers; // Historical remote users IMap<UserID, User> get remoteUsers; // Historical remote users
IMap<TypedKey, User> get historicalRemoteUsers; // Unknown users IMap<UserID, User> get historicalRemoteUsers; // Unknown users
IMap<TypedKey, User> get unknownUsers; // Messages state IMap<UserID, User> get unknownUsers; // Messages state
AsyncValue<WindowState<Message>> get messageWindow; // Title of the chat AsyncValue<WindowState<Message>> get messageWindow; // Title of the chat
String get title; String get title;
@ -40,11 +36,6 @@ mixin _$ChatComponentState {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is ChatComponentState && other is ChatComponentState &&
(identical(other.chatKey, chatKey) || other.chatKey == chatKey) &&
(identical(other.scrollController, scrollController) ||
other.scrollController == scrollController) &&
(identical(other.textEditingController, textEditingController) ||
other.textEditingController == textEditingController) &&
(identical(other.localUser, localUser) || (identical(other.localUser, localUser) ||
other.localUser == localUser) && other.localUser == localUser) &&
(identical(other.remoteUsers, remoteUsers) || (identical(other.remoteUsers, remoteUsers) ||
@ -59,21 +50,12 @@ mixin _$ChatComponentState {
} }
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(runtimeType, localUser, remoteUsers,
runtimeType, historicalRemoteUsers, unknownUsers, messageWindow, title);
chatKey,
scrollController,
textEditingController,
localUser,
remoteUsers,
historicalRemoteUsers,
unknownUsers,
messageWindow,
title);
@override @override
String toString() { String toString() {
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; return 'ChatComponentState(localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)';
} }
} }
@ -84,16 +66,14 @@ abstract mixin class $ChatComponentStateCopyWith<$Res> {
_$ChatComponentStateCopyWithImpl; _$ChatComponentStateCopyWithImpl;
@useResult @useResult
$Res call( $Res call(
{GlobalKey<ChatState> chatKey, {User? localUser,
AutoScrollController scrollController, IMap<String, User> remoteUsers,
InputTextFieldController textEditingController, IMap<String, User> historicalRemoteUsers,
User? localUser, IMap<String, User> unknownUsers,
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
AsyncValue<WindowState<Message>> messageWindow, AsyncValue<WindowState<Message>> messageWindow,
String title}); String title});
$UserCopyWith<$Res>? get localUser;
$AsyncValueCopyWith<WindowState<Message>, $Res> get messageWindow; $AsyncValueCopyWith<WindowState<Message>, $Res> get messageWindow;
} }
@ -110,9 +90,6 @@ class _$ChatComponentStateCopyWithImpl<$Res>
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? chatKey = null,
Object? scrollController = null,
Object? textEditingController = null,
Object? localUser = freezed, Object? localUser = freezed,
Object? remoteUsers = null, Object? remoteUsers = null,
Object? historicalRemoteUsers = null, Object? historicalRemoteUsers = null,
@ -121,18 +98,6 @@ class _$ChatComponentStateCopyWithImpl<$Res>
Object? title = null, Object? title = null,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
chatKey: null == chatKey
? _self.chatKey
: chatKey // ignore: cast_nullable_to_non_nullable
as GlobalKey<ChatState>,
scrollController: null == scrollController
? _self.scrollController
: scrollController // ignore: cast_nullable_to_non_nullable
as AutoScrollController,
textEditingController: null == textEditingController
? _self.textEditingController
: textEditingController // ignore: cast_nullable_to_non_nullable
as InputTextFieldController,
localUser: freezed == localUser localUser: freezed == localUser
? _self.localUser ? _self.localUser
: localUser // ignore: cast_nullable_to_non_nullable : localUser // ignore: cast_nullable_to_non_nullable
@ -140,15 +105,15 @@ class _$ChatComponentStateCopyWithImpl<$Res>
remoteUsers: null == remoteUsers remoteUsers: null == remoteUsers
? _self.remoteUsers! ? _self.remoteUsers!
: remoteUsers // ignore: cast_nullable_to_non_nullable : remoteUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>, as IMap<String, User>,
historicalRemoteUsers: null == historicalRemoteUsers historicalRemoteUsers: null == historicalRemoteUsers
? _self.historicalRemoteUsers! ? _self.historicalRemoteUsers!
: historicalRemoteUsers // ignore: cast_nullable_to_non_nullable : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>, as IMap<String, User>,
unknownUsers: null == unknownUsers unknownUsers: null == unknownUsers
? _self.unknownUsers! ? _self.unknownUsers!
: unknownUsers // ignore: cast_nullable_to_non_nullable : unknownUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>, as IMap<String, User>,
messageWindow: null == messageWindow messageWindow: null == messageWindow
? _self.messageWindow ? _self.messageWindow
: messageWindow // ignore: cast_nullable_to_non_nullable : messageWindow // ignore: cast_nullable_to_non_nullable
@ -160,6 +125,20 @@ class _$ChatComponentStateCopyWithImpl<$Res>
)); ));
} }
/// Create a copy of ChatComponentState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserCopyWith<$Res>? get localUser {
if (_self.localUser == null) {
return null;
}
return $UserCopyWith<$Res>(_self.localUser!, (value) {
return _then(_self.copyWith(localUser: value));
});
}
/// Create a copy of ChatComponentState /// Create a copy of ChatComponentState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@ -176,37 +155,25 @@ class _$ChatComponentStateCopyWithImpl<$Res>
class _ChatComponentState implements ChatComponentState { class _ChatComponentState implements ChatComponentState {
const _ChatComponentState( const _ChatComponentState(
{required this.chatKey, {required this.localUser,
required this.scrollController,
required this.textEditingController,
required this.localUser,
required this.remoteUsers, required this.remoteUsers,
required this.historicalRemoteUsers, required this.historicalRemoteUsers,
required this.unknownUsers, required this.unknownUsers,
required this.messageWindow, required this.messageWindow,
required this.title}); required this.title});
// GlobalKey for the chat
@override
final GlobalKey<ChatState> chatKey;
// ScrollController for the chat
@override
final AutoScrollController scrollController;
// TextEditingController for the chat
@override
final InputTextFieldController textEditingController;
// Local user // Local user
@override @override
final User? localUser; final User? localUser;
// Active remote users // Active remote users
@override @override
final IMap<Typed<FixedEncodedString43>, User> remoteUsers; final IMap<String, User> remoteUsers;
// Historical remote users // Historical remote users
@override @override
final IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers; final IMap<String, User> historicalRemoteUsers;
// Unknown users // Unknown users
@override @override
final IMap<Typed<FixedEncodedString43>, User> unknownUsers; final IMap<String, User> unknownUsers;
// Messages state // Messages state
@override @override
final AsyncValue<WindowState<Message>> messageWindow; final AsyncValue<WindowState<Message>> messageWindow;
@ -227,11 +194,6 @@ class _ChatComponentState implements ChatComponentState {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _ChatComponentState && other is _ChatComponentState &&
(identical(other.chatKey, chatKey) || other.chatKey == chatKey) &&
(identical(other.scrollController, scrollController) ||
other.scrollController == scrollController) &&
(identical(other.textEditingController, textEditingController) ||
other.textEditingController == textEditingController) &&
(identical(other.localUser, localUser) || (identical(other.localUser, localUser) ||
other.localUser == localUser) && other.localUser == localUser) &&
(identical(other.remoteUsers, remoteUsers) || (identical(other.remoteUsers, remoteUsers) ||
@ -246,21 +208,12 @@ class _ChatComponentState implements ChatComponentState {
} }
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(runtimeType, localUser, remoteUsers,
runtimeType, historicalRemoteUsers, unknownUsers, messageWindow, title);
chatKey,
scrollController,
textEditingController,
localUser,
remoteUsers,
historicalRemoteUsers,
unknownUsers,
messageWindow,
title);
@override @override
String toString() { String toString() {
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)'; return 'ChatComponentState(localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)';
} }
} }
@ -273,16 +226,15 @@ abstract mixin class _$ChatComponentStateCopyWith<$Res>
@override @override
@useResult @useResult
$Res call( $Res call(
{GlobalKey<ChatState> chatKey, {User? localUser,
AutoScrollController scrollController, IMap<String, User> remoteUsers,
InputTextFieldController textEditingController, IMap<String, User> historicalRemoteUsers,
User? localUser, IMap<String, User> unknownUsers,
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
AsyncValue<WindowState<Message>> messageWindow, AsyncValue<WindowState<Message>> messageWindow,
String title}); String title});
@override
$UserCopyWith<$Res>? get localUser;
@override @override
$AsyncValueCopyWith<WindowState<Message>, $Res> get messageWindow; $AsyncValueCopyWith<WindowState<Message>, $Res> get messageWindow;
} }
@ -300,9 +252,6 @@ class __$ChatComponentStateCopyWithImpl<$Res>
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$Res call({ $Res call({
Object? chatKey = null,
Object? scrollController = null,
Object? textEditingController = null,
Object? localUser = freezed, Object? localUser = freezed,
Object? remoteUsers = null, Object? remoteUsers = null,
Object? historicalRemoteUsers = null, Object? historicalRemoteUsers = null,
@ -311,18 +260,6 @@ class __$ChatComponentStateCopyWithImpl<$Res>
Object? title = null, Object? title = null,
}) { }) {
return _then(_ChatComponentState( return _then(_ChatComponentState(
chatKey: null == chatKey
? _self.chatKey
: chatKey // ignore: cast_nullable_to_non_nullable
as GlobalKey<ChatState>,
scrollController: null == scrollController
? _self.scrollController
: scrollController // ignore: cast_nullable_to_non_nullable
as AutoScrollController,
textEditingController: null == textEditingController
? _self.textEditingController
: textEditingController // ignore: cast_nullable_to_non_nullable
as InputTextFieldController,
localUser: freezed == localUser localUser: freezed == localUser
? _self.localUser ? _self.localUser
: localUser // ignore: cast_nullable_to_non_nullable : localUser // ignore: cast_nullable_to_non_nullable
@ -330,15 +267,15 @@ class __$ChatComponentStateCopyWithImpl<$Res>
remoteUsers: null == remoteUsers remoteUsers: null == remoteUsers
? _self.remoteUsers ? _self.remoteUsers
: remoteUsers // ignore: cast_nullable_to_non_nullable : remoteUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>, as IMap<String, User>,
historicalRemoteUsers: null == historicalRemoteUsers historicalRemoteUsers: null == historicalRemoteUsers
? _self.historicalRemoteUsers ? _self.historicalRemoteUsers
: historicalRemoteUsers // ignore: cast_nullable_to_non_nullable : historicalRemoteUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>, as IMap<String, User>,
unknownUsers: null == unknownUsers unknownUsers: null == unknownUsers
? _self.unknownUsers ? _self.unknownUsers
: unknownUsers // ignore: cast_nullable_to_non_nullable : unknownUsers // ignore: cast_nullable_to_non_nullable
as IMap<Typed<FixedEncodedString43>, User>, as IMap<String, User>,
messageWindow: null == messageWindow messageWindow: null == messageWindow
? _self.messageWindow ? _self.messageWindow
: messageWindow // ignore: cast_nullable_to_non_nullable : messageWindow // ignore: cast_nullable_to_non_nullable
@ -350,6 +287,20 @@ class __$ChatComponentStateCopyWithImpl<$Res>
)); ));
} }
/// Create a copy of ChatComponentState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$UserCopyWith<$Res>? get localUser {
if (_self.localUser == null) {
return null;
}
return $UserCopyWith<$Res>(_self.localUser!, (value) {
return _then(_self.copyWith(localUser: value));
});
}
/// Create a copy of ChatComponentState /// Create a copy of ChatComponentState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override

View file

@ -25,7 +25,10 @@ enum MessageSendState {
@freezed @freezed
sealed class MessageState with _$MessageState { sealed class MessageState with _$MessageState {
@JsonSerializable()
const factory MessageState({ const factory MessageState({
// Sequence number of the message for display purposes
required int seqId,
// Content of the message // Content of the message
@JsonKey(fromJson: messageFromJson, toJson: messageToJson) @JsonKey(fromJson: messageFromJson, toJson: messageToJson)
required proto.Message content, required proto.Message content,

View file

@ -15,7 +15,8 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$MessageState implements DiagnosticableTreeMixin { mixin _$MessageState implements DiagnosticableTreeMixin {
// Content of the message // Sequence number of the message for display purposes
int get seqId; // Content of the message
@JsonKey(fromJson: messageFromJson, toJson: messageToJson) @JsonKey(fromJson: messageFromJson, toJson: messageToJson)
proto.Message get content; // Sent timestamp proto.Message get content; // Sent timestamp
Timestamp get sentTimestamp; // Reconciled timestamp Timestamp get sentTimestamp; // Reconciled timestamp
@ -37,6 +38,7 @@ mixin _$MessageState implements DiagnosticableTreeMixin {
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties properties
..add(DiagnosticsProperty('type', 'MessageState')) ..add(DiagnosticsProperty('type', 'MessageState'))
..add(DiagnosticsProperty('seqId', seqId))
..add(DiagnosticsProperty('content', content)) ..add(DiagnosticsProperty('content', content))
..add(DiagnosticsProperty('sentTimestamp', sentTimestamp)) ..add(DiagnosticsProperty('sentTimestamp', sentTimestamp))
..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp)) ..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp))
@ -48,6 +50,7 @@ mixin _$MessageState implements DiagnosticableTreeMixin {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is MessageState && other is MessageState &&
(identical(other.seqId, seqId) || other.seqId == seqId) &&
(identical(other.content, content) || other.content == content) && (identical(other.content, content) || other.content == content) &&
(identical(other.sentTimestamp, sentTimestamp) || (identical(other.sentTimestamp, sentTimestamp) ||
other.sentTimestamp == sentTimestamp) && other.sentTimestamp == sentTimestamp) &&
@ -59,12 +62,12 @@ mixin _$MessageState implements DiagnosticableTreeMixin {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(runtimeType, seqId, content, sentTimestamp,
runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); reconciledTimestamp, sendState);
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; return 'MessageState(seqId: $seqId, content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)';
} }
} }
@ -75,7 +78,8 @@ abstract mixin class $MessageStateCopyWith<$Res> {
_$MessageStateCopyWithImpl; _$MessageStateCopyWithImpl;
@useResult @useResult
$Res call( $Res call(
{@JsonKey(fromJson: messageFromJson, toJson: messageToJson) {int seqId,
@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
proto.Message content, proto.Message content,
Timestamp sentTimestamp, Timestamp sentTimestamp,
Timestamp? reconciledTimestamp, Timestamp? reconciledTimestamp,
@ -94,12 +98,17 @@ class _$MessageStateCopyWithImpl<$Res> implements $MessageStateCopyWith<$Res> {
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
@override @override
$Res call({ $Res call({
Object? seqId = null,
Object? content = null, Object? content = null,
Object? sentTimestamp = null, Object? sentTimestamp = null,
Object? reconciledTimestamp = freezed, Object? reconciledTimestamp = freezed,
Object? sendState = freezed, Object? sendState = freezed,
}) { }) {
return _then(_self.copyWith( return _then(_self.copyWith(
seqId: null == seqId
? _self.seqId
: seqId // ignore: cast_nullable_to_non_nullable
as int,
content: null == content content: null == content
? _self.content ? _self.content
: content // ignore: cast_nullable_to_non_nullable : content // ignore: cast_nullable_to_non_nullable
@ -121,10 +130,12 @@ class _$MessageStateCopyWithImpl<$Res> implements $MessageStateCopyWith<$Res> {
} }
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _MessageState with DiagnosticableTreeMixin implements MessageState { class _MessageState with DiagnosticableTreeMixin implements MessageState {
const _MessageState( const _MessageState(
{@JsonKey(fromJson: messageFromJson, toJson: messageToJson) {required this.seqId,
@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
required this.content, required this.content,
required this.sentTimestamp, required this.sentTimestamp,
required this.reconciledTimestamp, required this.reconciledTimestamp,
@ -132,6 +143,9 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState {
factory _MessageState.fromJson(Map<String, dynamic> json) => factory _MessageState.fromJson(Map<String, dynamic> json) =>
_$MessageStateFromJson(json); _$MessageStateFromJson(json);
// Sequence number of the message for display purposes
@override
final int seqId;
// Content of the message // Content of the message
@override @override
@JsonKey(fromJson: messageFromJson, toJson: messageToJson) @JsonKey(fromJson: messageFromJson, toJson: messageToJson)
@ -165,6 +179,7 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState {
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
properties properties
..add(DiagnosticsProperty('type', 'MessageState')) ..add(DiagnosticsProperty('type', 'MessageState'))
..add(DiagnosticsProperty('seqId', seqId))
..add(DiagnosticsProperty('content', content)) ..add(DiagnosticsProperty('content', content))
..add(DiagnosticsProperty('sentTimestamp', sentTimestamp)) ..add(DiagnosticsProperty('sentTimestamp', sentTimestamp))
..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp)) ..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp))
@ -176,6 +191,7 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState {
return identical(this, other) || return identical(this, other) ||
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _MessageState && other is _MessageState &&
(identical(other.seqId, seqId) || other.seqId == seqId) &&
(identical(other.content, content) || other.content == content) && (identical(other.content, content) || other.content == content) &&
(identical(other.sentTimestamp, sentTimestamp) || (identical(other.sentTimestamp, sentTimestamp) ||
other.sentTimestamp == sentTimestamp) && other.sentTimestamp == sentTimestamp) &&
@ -187,12 +203,12 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState {
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@override @override
int get hashCode => Object.hash( int get hashCode => Object.hash(runtimeType, seqId, content, sentTimestamp,
runtimeType, content, sentTimestamp, reconciledTimestamp, sendState); reconciledTimestamp, sendState);
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'MessageState(content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)'; return 'MessageState(seqId: $seqId, content: $content, sentTimestamp: $sentTimestamp, reconciledTimestamp: $reconciledTimestamp, sendState: $sendState)';
} }
} }
@ -205,7 +221,8 @@ abstract mixin class _$MessageStateCopyWith<$Res>
@override @override
@useResult @useResult
$Res call( $Res call(
{@JsonKey(fromJson: messageFromJson, toJson: messageToJson) {int seqId,
@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
proto.Message content, proto.Message content,
Timestamp sentTimestamp, Timestamp sentTimestamp,
Timestamp? reconciledTimestamp, Timestamp? reconciledTimestamp,
@ -225,12 +242,17 @@ class __$MessageStateCopyWithImpl<$Res>
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$Res call({ $Res call({
Object? seqId = null,
Object? content = null, Object? content = null,
Object? sentTimestamp = null, Object? sentTimestamp = null,
Object? reconciledTimestamp = freezed, Object? reconciledTimestamp = freezed,
Object? sendState = freezed, Object? sendState = freezed,
}) { }) {
return _then(_MessageState( return _then(_MessageState(
seqId: null == seqId
? _self.seqId
: seqId // ignore: cast_nullable_to_non_nullable
as int,
content: null == content content: null == content
? _self.content ? _self.content
: content // ignore: cast_nullable_to_non_nullable : content // ignore: cast_nullable_to_non_nullable

View file

@ -8,6 +8,7 @@ part of 'message_state.dart';
_MessageState _$MessageStateFromJson(Map<String, dynamic> json) => _MessageState _$MessageStateFromJson(Map<String, dynamic> json) =>
_MessageState( _MessageState(
seqId: (json['seq_id'] as num).toInt(),
content: messageFromJson(json['content'] as Map<String, dynamic>), content: messageFromJson(json['content'] as Map<String, dynamic>),
sentTimestamp: Timestamp.fromJson(json['sent_timestamp']), sentTimestamp: Timestamp.fromJson(json['sent_timestamp']),
reconciledTimestamp: json['reconciled_timestamp'] == null reconciledTimestamp: json['reconciled_timestamp'] == null
@ -20,6 +21,7 @@ _MessageState _$MessageStateFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$MessageStateToJson(_MessageState instance) => Map<String, dynamic> _$MessageStateToJson(_MessageState instance) =>
<String, dynamic>{ <String, dynamic>{
'seq_id': instance.seqId,
'content': messageToJson(instance.content), 'content': messageToJson(instance.content),
'sent_timestamp': instance.sentTimestamp.toJson(), 'sent_timestamp': instance.sentTimestamp.toJson(),
'reconciled_timestamp': instance.reconciledTimestamp?.toJson(), 'reconciled_timestamp': instance.reconciledTimestamp?.toJson(),

View file

@ -0,0 +1,2 @@
export 'vc_composer_widget.dart';
export 'vc_text_message_widget.dart';

View file

@ -0,0 +1,431 @@
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
// Typedefs need to come out
// ignore: implementation_imports
import 'package:flutter_chat_ui/src/utils/typedefs.dart';
import 'package:provider/provider.dart';
import '../../../theme/theme.dart';
import '../../chat.dart';
enum ShiftEnterAction { newline, send }
/// The message composer widget positioned at the bottom of the chat screen.
///
/// Includes a text input field, an optional attachment button,
/// and a send button.
class VcComposerWidget extends StatefulWidget {
/// Creates a message composer widget.
const VcComposerWidget({
super.key,
this.textEditingController,
this.left = 0,
this.right = 0,
this.top,
this.bottom = 0,
this.sigmaX = 20,
this.sigmaY = 20,
this.padding = const EdgeInsets.all(8),
this.attachmentIcon = const Icon(Icons.attachment),
this.sendIcon = const Icon(Icons.send),
this.gap = 8,
this.inputBorder,
this.filled,
this.topWidget,
this.handleSafeArea = true,
this.backgroundColor,
this.attachmentIconColor,
this.sendIconColor,
this.hintColor,
this.textColor,
this.inputFillColor,
this.hintText = 'Type a message',
this.keyboardAppearance,
this.autocorrect,
this.autofocus = false,
this.textCapitalization = TextCapitalization.sentences,
this.keyboardType,
this.textInputAction = TextInputAction.newline,
this.shiftEnterAction = ShiftEnterAction.send,
this.focusNode,
this.maxLength,
this.minLines = 1,
this.maxLines = 3,
});
/// Optional controller for the text input field.
final TextEditingController? textEditingController;
/// Optional left position.
final double? left;
/// Optional right position.
final double? right;
/// Optional top position.
final double? top;
/// Optional bottom position.
final double? bottom;
/// Optional X blur value for the background (if using glassmorphism).
final double? sigmaX;
/// Optional Y blur value for the background (if using glassmorphism).
final double? sigmaY;
/// Padding around the composer content.
final EdgeInsetsGeometry? padding;
/// Icon for the attachment button. Defaults to [Icons.attachment].
final Widget? attachmentIcon;
/// Icon for the send button. Defaults to [Icons.send].
final Widget? sendIcon;
/// Horizontal gap between elements (attachment icon, text field, send icon).
final double? gap;
/// Border style for the text input field.
final InputBorder? inputBorder;
/// Whether the text input field should be filled.
final bool? filled;
/// Optional widget to display above the main composer row.
final Widget? topWidget;
/// Whether to adjust padding for the bottom safe area.
final bool handleSafeArea;
/// Background color of the composer container.
final Color? backgroundColor;
/// Color of the attachment icon.
final Color? attachmentIconColor;
/// Color of the send icon.
final Color? sendIconColor;
/// Color of the hint text in the input field.
final Color? hintColor;
/// Color of the text entered in the input field.
final Color? textColor;
/// Fill color for the text input field when [filled] is true.
final Color? inputFillColor;
/// Placeholder text for the input field.
final String? hintText;
/// Appearance of the keyboard.
final Brightness? keyboardAppearance;
/// Whether to enable autocorrect for the input field.
final bool? autocorrect;
/// Whether the input field should autofocus.
final bool autofocus;
/// Capitalization behavior for the input field.
final TextCapitalization textCapitalization;
/// Type of keyboard to display.
final TextInputType? keyboardType;
/// Action button type for the keyboard (e.g., newline, send).
final TextInputAction textInputAction;
/// Action when shift-enter is pressed (e.g., newline, send).
final ShiftEnterAction shiftEnterAction;
/// Focus node for the text input field.
final FocusNode? focusNode;
/// Maximum character length for the input field.
final int? maxLength;
/// Minimum number of lines for the input field.
final int? minLines;
/// Maximum number of lines the input field can expand to.
final int? maxLines;
@override
State<VcComposerWidget> createState() => _VcComposerState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TextEditingController?>(
'textEditingController', textEditingController))
..add(DoubleProperty('left', left))
..add(DoubleProperty('right', right))
..add(DoubleProperty('top', top))
..add(DoubleProperty('bottom', bottom))
..add(DoubleProperty('sigmaX', sigmaX))
..add(DoubleProperty('sigmaY', sigmaY))
..add(DiagnosticsProperty<EdgeInsetsGeometry?>('padding', padding))
..add(DoubleProperty('gap', gap))
..add(DiagnosticsProperty<InputBorder?>('inputBorder', inputBorder))
..add(DiagnosticsProperty<bool?>('filled', filled))
..add(DiagnosticsProperty<bool>('handleSafeArea', handleSafeArea))
..add(ColorProperty('backgroundColor', backgroundColor))
..add(ColorProperty('attachmentIconColor', attachmentIconColor))
..add(ColorProperty('sendIconColor', sendIconColor))
..add(ColorProperty('hintColor', hintColor))
..add(ColorProperty('textColor', textColor))
..add(ColorProperty('inputFillColor', inputFillColor))
..add(StringProperty('hintText', hintText))
..add(EnumProperty<Brightness?>('keyboardAppearance', keyboardAppearance))
..add(DiagnosticsProperty<bool?>('autocorrect', autocorrect))
..add(DiagnosticsProperty<bool>('autofocus', autofocus))
..add(EnumProperty<TextCapitalization>(
'textCapitalization', textCapitalization))
..add(DiagnosticsProperty<TextInputType?>('keyboardType', keyboardType))
..add(EnumProperty<TextInputAction>('textInputAction', textInputAction))
..add(
EnumProperty<ShiftEnterAction>('shiftEnterAction', shiftEnterAction))
..add(DiagnosticsProperty<FocusNode?>('focusNode', focusNode))
..add(IntProperty('maxLength', maxLength))
..add(IntProperty('minLines', minLines))
..add(IntProperty('maxLines', maxLines));
}
}
class _VcComposerState extends State<VcComposerWidget> {
final _key = GlobalKey();
late final TextEditingController _textController;
late final FocusNode _focusNode;
late String _suffixText;
@override
void initState() {
super.initState();
_textController = widget.textEditingController ?? TextEditingController();
_focusNode = widget.focusNode ?? FocusNode();
_focusNode.onKeyEvent = _handleKeyEvent;
_updateSuffixText();
WidgetsBinding.instance.addPostFrameCallback((_) => _measure());
}
void _updateSuffixText() {
final utf8Length = utf8.encode(_textController.text).length;
_suffixText = '$utf8Length/${widget.maxLength}';
}
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
// Check for Shift+Enter
if (event is KeyDownEvent &&
event.logicalKey == LogicalKeyboardKey.enter &&
HardwareKeyboard.instance.isShiftPressed) {
if (widget.shiftEnterAction == ShiftEnterAction.send) {
_handleSubmitted(_textController.text);
return KeyEventResult.handled;
} else if (widget.shiftEnterAction == ShiftEnterAction.newline) {
final val = _textController.value;
final insertOffset = val.selection.extent.offset;
final messageWithNewLine =
'${_textController.text.substring(0, insertOffset)}\n'
'${_textController.text.substring(insertOffset)}';
_textController.value = TextEditingValue(
text: messageWithNewLine,
selection: TextSelection.fromPosition(
TextPosition(offset: insertOffset + 1),
),
);
return KeyEventResult.handled;
}
}
return KeyEventResult.ignored;
}
@override
void didUpdateWidget(covariant VcComposerWidget oldWidget) {
super.didUpdateWidget(oldWidget);
WidgetsBinding.instance.addPostFrameCallback((_) => _measure());
}
@override
void dispose() {
// Only try to dispose text controller if it's not provided, let
// user handle disposing it how they want.
if (widget.textEditingController == null) {
_textController.dispose();
}
if (widget.focusNode == null) {
_focusNode.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
final bottomSafeArea =
widget.handleSafeArea ? MediaQuery.of(context).padding.bottom : 0.0;
final onAttachmentTap = context.read<OnAttachmentTapCallback?>();
final theme = Theme.of(context);
final scaleTheme = theme.extension<ScaleTheme>()!;
final config = scaleTheme.config;
final scheme = scaleTheme.scheme;
final scale = scaleTheme.scheme.scale(ScaleKind.primary);
final textTheme = theme.textTheme;
final scaleChatTheme = scaleTheme.chatTheme();
final chatTheme = scaleChatTheme.chatTheme;
final suffixTextStyle =
textTheme.bodySmall!.copyWith(color: scale.subtleText);
return Positioned(
left: widget.left,
right: widget.right,
top: widget.top,
bottom: widget.bottom,
child: ClipRect(
child: DecoratedBox(
key: _key,
decoration: BoxDecoration(
border: config.preferBorders
? Border(top: BorderSide(color: scale.border, width: 2))
: null,
color: config.preferBorders
? scale.elementBackground
: scale.border),
child: Column(
children: [
if (widget.topWidget != null) widget.topWidget!,
Padding(
padding: widget.handleSafeArea
? (widget.padding?.add(
EdgeInsets.only(bottom: bottomSafeArea),
) ??
EdgeInsets.only(bottom: bottomSafeArea))
: (widget.padding ?? EdgeInsets.zero),
child: Row(
children: [
if (widget.attachmentIcon != null &&
onAttachmentTap != null)
IconButton(
icon: widget.attachmentIcon!,
color: widget.attachmentIconColor ??
chatTheme.colors.onSurface.withValues(alpha: 0.5),
onPressed: onAttachmentTap,
)
else
const SizedBox.shrink(),
SizedBox(width: widget.gap),
Expanded(
child: TextField(
controller: _textController,
decoration: InputDecoration(
filled: widget.filled ?? !config.preferBorders,
fillColor: widget.inputFillColor ??
scheme.primaryScale.subtleBackground,
isDense: true,
contentPadding:
const EdgeInsets.fromLTRB(8, 8, 8, 8),
disabledBorder: OutlineInputBorder(
borderSide: config.preferBorders
? BorderSide(
color: scheme.grayScale.border,
width: 2)
: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(
8 * config.borderRadiusScale))),
enabledBorder: OutlineInputBorder(
borderSide: config.preferBorders
? BorderSide(
color: scheme.primaryScale.border,
width: 2)
: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(
8 * config.borderRadiusScale))),
focusedBorder: OutlineInputBorder(
borderSide: config.preferBorders
? BorderSide(
color: scheme.primaryScale.border,
width: 2)
: BorderSide.none,
borderRadius: BorderRadius.all(Radius.circular(
8 * config.borderRadiusScale))),
hintText: widget.hintText,
hintStyle: chatTheme.typography.bodyMedium.copyWith(
color: widget.hintColor ??
chatTheme.colors.onSurface
.withValues(alpha: 0.5),
),
border: widget.inputBorder,
hoverColor: Colors.transparent,
suffix: Text(_suffixText, style: suffixTextStyle)),
onSubmitted: _handleSubmitted,
onChanged: (value) {
setState(_updateSuffixText);
},
textInputAction: widget.textInputAction,
keyboardAppearance: widget.keyboardAppearance,
autocorrect: widget.autocorrect ?? true,
autofocus: widget.autofocus,
textCapitalization: widget.textCapitalization,
keyboardType: widget.keyboardType,
focusNode: _focusNode,
//maxLength: widget.maxLength,
minLines: widget.minLines,
maxLines: widget.maxLines,
maxLengthEnforcement: MaxLengthEnforcement.none,
inputFormatters: [
Utf8LengthLimitingTextInputFormatter(
maxLength: widget.maxLength),
],
),
),
SizedBox(width: widget.gap),
if ((widget.sendIcon ?? scaleChatTheme.sendButtonIcon) !=
null)
IconButton(
icon:
(widget.sendIcon ?? scaleChatTheme.sendButtonIcon)!,
color: widget.sendIconColor,
onPressed: () => _handleSubmitted(_textController.text),
)
else
const SizedBox.shrink(),
],
),
),
],
),
),
),
);
}
void _measure() {
if (!mounted) {
return;
}
final renderBox = _key.currentContext?.findRenderObject() as RenderBox?;
if (renderBox != null) {
final height = renderBox.size.height;
final bottomSafeArea = MediaQuery.of(context).padding.bottom;
context.read<ComposerHeightNotifier>().setHeight(
// only set real height of the composer, ignoring safe area
widget.handleSafeArea ? height - bottomSafeArea : height,
);
}
}
void _handleSubmitted(String text) {
if (text.isNotEmpty) {
context.read<OnMessageSendCallback?>()?.call(text);
_textController.clear();
}
}
}

View file

@ -0,0 +1,269 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart';
import 'package:provider/provider.dart';
import '../../../theme/theme.dart';
import '../date_formatter.dart';
/// A widget that displays a text message.
class VcTextMessageWidget extends StatelessWidget {
/// Creates a widget to display a simple text message.
const VcTextMessageWidget({
required this.message,
required this.index,
this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
this.borderRadius,
this.onlyEmojiFontSize,
this.sentBackgroundColor,
this.receivedBackgroundColor,
this.sentTextStyle,
this.receivedTextStyle,
this.timeStyle,
this.showTime = true,
this.showStatus = true,
this.timeAndStatusPosition = TimeAndStatusPosition.end,
super.key,
});
/// The text message data model.
final TextMessage message;
/// The index of the message in the list.
final int index;
/// Padding around the message bubble content.
final EdgeInsetsGeometry? padding;
/// Border radius of the message bubble.
final BorderRadiusGeometry? borderRadius;
/// Font size for messages containing only emojis.
final double? onlyEmojiFontSize;
/// Background color for messages sent by the current user.
final Color? sentBackgroundColor;
/// Background color for messages received from other users.
final Color? receivedBackgroundColor;
/// Text style for messages sent by the current user.
final TextStyle? sentTextStyle;
/// Text style for messages received from other users.
final TextStyle? receivedTextStyle;
/// Text style for the message timestamp and status.
final TextStyle? timeStyle;
/// Whether to display the message timestamp.
final bool showTime;
/// Whether to display the message status (sent, delivered, seen)
/// for sent messages.
final bool showStatus;
/// Position of the timestamp and status indicator relative to the text.
final TimeAndStatusPosition timeAndStatusPosition;
bool get _isOnlyEmoji => message.metadata?['isOnlyEmoji'] == true;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scaleTheme = theme.extension<ScaleTheme>()!;
final config = scaleTheme.config;
final scheme = scaleTheme.scheme;
final scale = scaleTheme.scheme.scale(ScaleKind.primary);
final textTheme = theme.textTheme;
final scaleChatTheme = scaleTheme.chatTheme();
final chatTheme = scaleChatTheme.chatTheme;
final isSentByMe = context.watch<UserID>() == message.authorId;
final backgroundColor = _resolveBackgroundColor(isSentByMe, scaleChatTheme);
final textStyle = _resolveTextStyle(isSentByMe, scaleChatTheme);
final timeStyle = _resolveTimeStyle(isSentByMe, scaleChatTheme);
final emojiFontSize = onlyEmojiFontSize ?? scaleChatTheme.onlyEmojiFontSize;
final timeAndStatus = showTime || (isSentByMe && showStatus)
? TimeAndStatus(
time: message.time,
status: message.status,
showTime: showTime,
showStatus: isSentByMe && showStatus,
textStyle: timeStyle,
)
: null;
final textContent = Text(
message.text,
style: _isOnlyEmoji
? textStyle.copyWith(fontSize: emojiFontSize)
: textStyle,
);
return Container(
padding: _isOnlyEmoji
? EdgeInsets.symmetric(
horizontal: (padding?.horizontal ?? 0) / 2,
// vertical: 0,
)
: padding,
decoration: _isOnlyEmoji
? null
: BoxDecoration(
color: backgroundColor,
borderRadius: borderRadius ?? chatTheme.shape,
),
child: _buildContentBasedOnPosition(
context: context,
textContent: textContent,
timeAndStatus: timeAndStatus,
textStyle: textStyle,
),
);
}
Widget _buildContentBasedOnPosition({
required BuildContext context,
required Widget textContent,
TimeAndStatus? timeAndStatus,
TextStyle? textStyle,
}) {
if (timeAndStatus == null) {
return textContent;
}
switch (timeAndStatusPosition) {
case TimeAndStatusPosition.start:
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [textContent, timeAndStatus],
);
case TimeAndStatusPosition.inline:
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Flexible(child: textContent),
const SizedBox(width: 4),
timeAndStatus,
],
);
case TimeAndStatusPosition.end:
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
children: [textContent, timeAndStatus],
);
}
}
Color _resolveBackgroundColor(bool isSentByMe, ScaleChatTheme theme) {
if (isSentByMe) {
return sentBackgroundColor ?? theme.primaryColor;
}
return receivedBackgroundColor ?? theme.secondaryColor;
}
TextStyle _resolveTextStyle(bool isSentByMe, ScaleChatTheme theme) {
if (isSentByMe) {
return sentTextStyle ?? theme.sentMessageBodyTextStyle;
}
return receivedTextStyle ?? theme.receivedMessageBodyTextStyle;
}
TextStyle _resolveTimeStyle(bool isSentByMe, ScaleChatTheme theme) {
final ts = _resolveTextStyle(isSentByMe, theme);
return theme.timeStyle.copyWith(color: ts.color);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TextMessage>('message', message))
..add(IntProperty('index', index))
..add(DiagnosticsProperty<EdgeInsetsGeometry?>('padding', padding))
..add(DiagnosticsProperty<BorderRadiusGeometry?>(
'borderRadius', borderRadius))
..add(DoubleProperty('onlyEmojiFontSize', onlyEmojiFontSize))
..add(ColorProperty('sentBackgroundColor', sentBackgroundColor))
..add(ColorProperty('receivedBackgroundColor', receivedBackgroundColor))
..add(DiagnosticsProperty<TextStyle?>('sentTextStyle', sentTextStyle))
..add(DiagnosticsProperty<TextStyle?>(
'receivedTextStyle', receivedTextStyle))
..add(DiagnosticsProperty<TextStyle?>('timeStyle', timeStyle))
..add(DiagnosticsProperty<bool>('showTime', showTime))
..add(DiagnosticsProperty<bool>('showStatus', showStatus))
..add(EnumProperty<TimeAndStatusPosition>(
'timeAndStatusPosition', timeAndStatusPosition));
}
}
/// A widget to display the message timestamp and status indicator.
class TimeAndStatus extends StatelessWidget {
/// Creates a widget for displaying time and status.
const TimeAndStatus({
required this.time,
this.status,
this.showTime = true,
this.showStatus = true,
this.textStyle,
super.key,
});
/// The time the message was created.
final DateTime? time;
/// The status of the message.
final MessageStatus? status;
/// Whether to display the timestamp.
final bool showTime;
/// Whether to display the status indicator.
final bool showStatus;
/// The text style for the time and status.
final TextStyle? textStyle;
@override
Widget build(BuildContext context) {
final dformat = DateFormatter();
return Row(
spacing: 2,
mainAxisSize: MainAxisSize.min,
children: [
if (showTime && time != null)
Text(dformat.chatDateTimeFormat(time!.toLocal()), style: textStyle),
if (showStatus && status != null)
if (status == MessageStatus.sending)
SizedBox(
width: 6,
height: 6,
child: CircularProgressIndicator(
color: textStyle?.color,
strokeWidth: 2,
),
)
else
Icon(getIconForStatus(status!), color: textStyle?.color, size: 12),
],
);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<DateTime?>('time', time))
..add(EnumProperty<MessageStatus?>('status', status))
..add(DiagnosticsProperty<bool>('showTime', showTime))
..add(DiagnosticsProperty<bool>('showStatus', showStatus))
..add(DiagnosticsProperty<TextStyle?>('textStyle', textStyle));
}
}

View file

@ -1,11 +1,13 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_core/flutter_chat_core.dart' as core;
import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
@ -16,11 +18,15 @@ import '../../conversation/conversation.dart';
import '../../notifications/notifications.dart'; import '../../notifications/notifications.dart';
import '../../theme/theme.dart'; import '../../theme/theme.dart';
import '../chat.dart'; import '../chat.dart';
import 'chat_builders/chat_builders.dart';
const onEndReachedThreshold = 0.75; const onEndReachedThreshold = 0.75;
const _kScrollTag = 'kScrollTag';
const kSeqId = 'seqId';
const maxMessageLength = 2048;
class ChatComponentWidget extends StatelessWidget { class ChatComponentWidget extends StatefulWidget {
const ChatComponentWidget({ const ChatComponentWidget._({
required super.key, required super.key,
required TypedKey localConversationRecordKey, required TypedKey localConversationRecordKey,
required void Function() onCancel, required void Function() onCancel,
@ -29,10 +35,14 @@ class ChatComponentWidget extends StatelessWidget {
_onCancel = onCancel, _onCancel = onCancel,
_onClose = onClose; _onClose = onClose;
///////////////////////////////////////////////////////////////////// // Create a single-contact chat and its associated state
static Widget singleContact({
@override required BuildContext context,
Widget build(BuildContext context) { required TypedKey localConversationRecordKey,
required void Function() onCancel,
required void Function() onClose,
Key? key,
}) {
// Get the account info // Get the account info
final accountInfo = context.watch<AccountInfoCubit>().state; final accountInfo = context.watch<AccountInfoCubit>().state;
@ -45,19 +55,19 @@ class ChatComponentWidget extends StatelessWidget {
// Get the active conversation cubit // Get the active conversation cubit
final activeConversationCubit = context final activeConversationCubit = context
.select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>( .select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>(
(x) => x.tryOperateSync(_localConversationRecordKey, (x) => x.tryOperateSync(localConversationRecordKey,
closure: (cubit) => cubit)); closure: (cubit) => cubit));
if (activeConversationCubit == null) { if (activeConversationCubit == null) {
return waitingPage(onCancel: _onCancel); return waitingPage(onCancel: onCancel);
} }
// Get the messages cubit // Get the messages cubit
final messagesCubit = context.select<ActiveSingleContactChatBlocMapCubit, final messagesCubit = context.select<ActiveSingleContactChatBlocMapCubit,
SingleContactMessagesCubit?>( SingleContactMessagesCubit?>(
(x) => x.tryOperateSync(_localConversationRecordKey, (x) => x.tryOperateSync(localConversationRecordKey,
closure: (cubit) => cubit)); closure: (cubit) => cubit));
if (messagesCubit == null) { if (messagesCubit == null) {
return waitingPage(onCancel: _onCancel); return waitingPage(onCancel: onCancel);
} }
// Make chat component state // Make chat component state
@ -70,26 +80,65 @@ class ChatComponentWidget extends StatelessWidget {
activeConversationCubit: activeConversationCubit, activeConversationCubit: activeConversationCubit,
messagesCubit: messagesCubit, messagesCubit: messagesCubit,
), ),
child: Builder(builder: _buildChatComponent)); child: ChatComponentWidget._(
key: ValueKey(localConversationRecordKey),
localConversationRecordKey: localConversationRecordKey,
onCancel: onCancel,
onClose: onClose));
} }
///////////////////////////////////////////////////////////////////// @override
State<ChatComponentWidget> createState() => _ChatComponentWidgetState();
Widget _buildChatComponent(BuildContext context) { ////////////////////////////////////////////////////////////////////////////
final TypedKey _localConversationRecordKey;
final void Function() _onCancel;
final void Function() _onClose;
}
class _ChatComponentWidgetState extends State<ChatComponentWidget> {
////////////////////////////////////////////////////////////////////
@override
void initState() {
_chatController = core.InMemoryChatController();
_textEditingController = TextEditingController();
_scrollController = ScrollController();
_chatStateProcessor = SingleStateProcessor<ChatComponentState>();
final _chatComponentCubit = context.read<ChatComponentCubit>();
_chatStateProcessor.follow(_chatComponentCubit.stream,
_chatComponentCubit.state, _updateChatState);
super.initState();
}
@override
void dispose() {
unawaited(_chatStateProcessor.close());
_chatController.dispose();
_scrollController.dispose();
_textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final scaleScheme = theme.extension<ScaleScheme>()!; final scaleTheme = theme.extension<ScaleTheme>()!;
final scaleConfig = theme.extension<ScaleConfig>()!; final scale = scaleTheme.scheme.scale(ScaleKind.primary);
final scale = scaleScheme.scale(ScaleKind.primary);
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final chatTheme = makeChatTheme(scaleScheme, scaleConfig, textTheme); final scaleChatTheme = scaleTheme.chatTheme();
final errorChatTheme = (ChatThemeEditor(chatTheme) // final errorChatTheme = chatTheme.copyWith(color:)
..inputTextColor = scaleScheme.errorScale.primary // ..inputTextColor = scaleScheme.errorScale.primary
..sendButtonIcon = Image.asset( // ..sendButtonIcon = Image.asset(
'assets/icon-send.png', // 'assets/icon-send.png',
color: scaleScheme.errorScale.primary, // color: scaleScheme.errorScale.primary,
package: 'flutter_chat_ui', // package: 'flutter_chat_ui',
)) // ))
.commit(); // .commit();
// Get the enclosing chat component cubit that contains our state // Get the enclosing chat component cubit that contains our state
// (created by ChatComponentWidget.builder()) // (created by ChatComponentWidget.builder())
@ -110,9 +159,8 @@ class ChatComponentWidget extends StatelessWidget {
final title = chatComponentState.title; final title = chatComponentState.title;
if (chatComponentCubit.scrollOffset != 0) { if (chatComponentCubit.scrollOffset != 0) {
chatComponentState.scrollController.position.correctPixels( _scrollController.position.correctPixels(
chatComponentState.scrollController.position.pixels + _scrollController.position.pixels + chatComponentCubit.scrollOffset);
chatComponentCubit.scrollOffset);
chatComponentCubit.scrollOffset = 0; chatComponentCubit.scrollOffset = 0;
} }
@ -138,7 +186,7 @@ class ChatComponentWidget extends StatelessWidget {
IconButton( IconButton(
iconSize: 24, iconSize: 24,
icon: Icon(Icons.close, color: scale.borderText), icon: Icon(Icons.close, color: scale.borderText),
onPressed: _onClose) onPressed: widget._onClose)
.paddingLTRB(0, 0, 8, 0) .paddingLTRB(0, 0, 8, 0)
]), ]),
), ),
@ -164,7 +212,7 @@ class ChatComponentWidget extends StatelessWidget {
chatComponentCubit.scrollOffset = scrollOffset; chatComponentCubit.scrollOffset = scrollOffset;
// //
singleFuture(chatComponentState.chatKey, () async { singleFuture((chatComponentCubit, _kScrollTag), () async {
await _handlePageForward( await _handlePageForward(
chatComponentCubit, messageWindow, notification); chatComponentCubit, messageWindow, notification);
}); });
@ -182,7 +230,7 @@ class ChatComponentWidget extends StatelessWidget {
chatComponentCubit.scrollOffset = scrollOffset; chatComponentCubit.scrollOffset = scrollOffset;
// //
singleFuture(chatComponentState.chatKey, () async { singleFuture((chatComponentCubit, _kScrollTag), () async {
await _handlePageBackward( await _handlePageBackward(
chatComponentCubit, messageWindow, notification); chatComponentCubit, messageWindow, notification);
}); });
@ -190,82 +238,181 @@ class ChatComponentWidget extends StatelessWidget {
return false; return false;
}, },
child: ValueListenableBuilder( child: ValueListenableBuilder(
valueListenable: chatComponentState.textEditingController, valueListenable: _textEditingController,
builder: (context, textEditingValue, __) { builder: (context, textEditingValue, __) {
final messageIsValid = final messageIsValid =
utf8.encode(textEditingValue.text).lengthInBytes < _messageIsValid(textEditingValue.text);
2048; var sendIconColor = scaleTheme.config.preferBorders
? scale.border
: scale.borderText;
if (!messageIsValid ||
_textEditingController.text.isEmpty) {
sendIconColor = sendIconColor.withAlpha(128);
}
return Chat( return Chat(
key: chatComponentState.chatKey, currentUserId: localUser.id,
theme: messageIsValid ? chatTheme : errorChatTheme, resolveUser: (id) async {
messages: messageWindow.window.toList(), if (id == localUser.id) {
scrollToBottomOnSend: isFirstPage, return localUser;
scrollController: chatComponentState.scrollController,
inputOptions: InputOptions(
inputClearMode: messageIsValid
? InputClearMode.always
: InputClearMode.never,
textEditingController:
chatComponentState.textEditingController),
// isLastPage: isLastPage,
// onEndReached: () async {
// await _handlePageBackward(
// chatComponentCubit, messageWindow);
// },
//onEndReachedThreshold: onEndReachedThreshold,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
usePreviewData: false, //
onSendPressed: (pt) {
try {
if (!messageIsValid) {
context.read<NotificationsCubit>().error(
text: translate('chat.message_too_long'));
return;
}
_handleSendPressed(chatComponentCubit, pt);
} on FormatException {
context.read<NotificationsCubit>().error(
text: translate('chat.message_too_long'));
} }
return chatComponentState.remoteUsers.get(id);
}, },
listBottomWidget: messageIsValid chatController: _chatController,
onMessageSend: (text) =>
_handleSendPressed(chatComponentCubit, text),
theme: scaleChatTheme.chatTheme,
builders: core.Builders(
// Chat list builder
chatAnimatedListBuilder: (context, itemBuilder) =>
ChatAnimatedListReversed(
scrollController: _scrollController,
itemBuilder: itemBuilder),
// Text message builder
textMessageBuilder: (context, message, index) =>
VcTextMessageWidget(
message: message,
index: index,
// showTime: true,
// showStatus: true,
),
// Composer builder
composerBuilder: (ctx) => VcComposerWidget(
autofocus: true,
textInputAction: isAnyMobile
? TextInputAction.newline
: TextInputAction.send,
shiftEnterAction: isAnyMobile
? ShiftEnterAction.send
: ShiftEnterAction.newline,
textEditingController: _textEditingController,
maxLength: maxMessageLength,
keyboardType: TextInputType.multiline,
sendIconColor: sendIconColor,
topWidget: messageIsValid
? null ? null
: Text(translate('chat.message_too_long'), : Text(translate('chat.message_too_long'),
style: TextStyle( style: TextStyle(
color: color: scaleTheme
scaleScheme.errorScale.primary)) .scheme.errorScale.primary))
.toCenter(), .toCenter(),
//showUserAvatars: false, ),
//showUserNames: true, ),
user: localUser, timeFormat: core.DateFormat.jm(),
emptyState: const EmptyChatWidget()); );
}))).expanded(), }))).expanded(),
], ],
); );
} }
void _handleSendPressed( /////////////////////////////////////////////////////////////////////
ChatComponentCubit chatComponentCubit, types.PartialText message) {
final text = message.text;
bool _messageIsValid(String text) =>
utf8.encode(text).lengthInBytes < maxMessageLength;
Future<void> _updateChatState(ChatComponentState chatComponentState) async {
// Update message window state
final data = chatComponentState.messageWindow.asData;
if (data == null) {
await _chatController.setMessages([]);
return;
}
final windowState = data.value;
await _chatController.setMessages(windowState.window.toList());
// final newMessagesSet = windowState.window.toSet();
// final newMessagesById =
// Map.fromEntries(newMessagesSet.map((m) => MapEntry(m.id, m)));
// final newMessagesBySeqId = Map.fromEntries(
// newMessagesSet.map((m) => MapEntry(m.metadata![kSeqId], m)));
// final oldMessagesSet = _chatController.messages.toSet();
// if (oldMessagesSet.isEmpty) {
// await _chatController.setMessages(windowState.window.toList());
// return;
// }
// // See how many messages differ by equality (not identity)
// // If there are more than `replaceAllMessagesThreshold` differences
// // just replace the whole list of messages
// final diffs = newMessagesSet.diffAndIntersect(oldMessagesSet,
// diffThisMinusOther: true, diffOtherMinusThis: true);
// final addedMessages = diffs.diffThisMinusOther!;
// final removedMessages = diffs.diffOtherMinusThis!;
// final replaceAllPaginationLimit = windowState.windowCount / 3;
// if ((addedMessages.length >= replaceAllPaginationLimit) ||
// removedMessages.length >= replaceAllPaginationLimit) {
// await _chatController.setMessages(windowState.window.toList());
// return;
// }
// // Remove messages that are gone, and replace the ones that have changed
// for (final m in removedMessages) {
// final newm = newMessagesById[m.id];
// if (newm != null) {
// await _chatController.updateMessage(m, newm);
// } else {
// final newm = newMessagesBySeqId[m.metadata![kSeqId]];
// if (newm != null) {
// await _chatController.updateMessage(m, newm);
// addedMessages.remove(newm);
// } else {
// await _chatController.removeMessage(m);
// }
// }
// }
// // // Check for append
// if (addedMessages.isNotEmpty) {
// if (_chatController.messages.isNotEmpty &&
// (addedMessages.first.metadata![kSeqId] as int) >
// (_chatController.messages.reversed.last.metadata![kSeqId]
// as int)) {
// await _chatController.insertAllMessages(addedMessages.reversedView,
// index: 0);
// }
// // Check for prepend
// else if (_chatController.messages.isNotEmpty &&
// (addedMessages.last.metadata![kSeqId] as int) <
// (_chatController.messages.reversed.first.metadata![kSeqId]
// as int)) {
// await _chatController.insertAllMessages(
// addedMessages.reversedView,
// );
// }
// // Otherwise just replace
// // xxx could use a better algorithm here to merge added messages in
// else {
// await _chatController.setMessages(windowState.window.toList());
// }
// }
}
void _handleSendPressed(ChatComponentCubit chatComponentCubit, String text) {
if (text.startsWith('/')) { if (text.startsWith('/')) {
chatComponentCubit.runCommand(text); chatComponentCubit.runCommand(text);
return; return;
} }
chatComponentCubit.sendMessage(message); if (!_messageIsValid(text)) {
context
.read<NotificationsCubit>()
.error(text: translate('chat.message_too_long'));
return;
}
chatComponentCubit.sendMessage(text: text);
} }
// void _handleAttachmentPressed() async { // void _handleAttachmentPressed() async {
// //
// }
Future<void> _handlePageForward( Future<void> _handlePageForward(
ChatComponentCubit chatComponentCubit, ChatComponentCubit chatComponentCubit,
WindowState<types.Message> messageWindow, WindowState<core.Message> messageWindow,
ScrollNotification notification) async { ScrollNotification notification) async {
debugPrint( debugPrint(
'_handlePageForward: messagesState.length=${messageWindow.length} ' '_handlePageForward: messagesState.length=${messageWindow.length} '
@ -299,7 +446,7 @@ class ChatComponentWidget extends StatelessWidget {
Future<void> _handlePageBackward( Future<void> _handlePageBackward(
ChatComponentCubit chatComponentCubit, ChatComponentCubit chatComponentCubit,
WindowState<types.Message> messageWindow, WindowState<core.Message> messageWindow,
ScrollNotification notification, ScrollNotification notification,
) async { ) async {
debugPrint( debugPrint(
@ -335,8 +482,8 @@ class ChatComponentWidget extends StatelessWidget {
//chatComponentCubit.scrollOffset = 0; //chatComponentCubit.scrollOffset = 0;
} }
//////////////////////////////////////////////////////////////////////////// late final core.ChatController _chatController;
final TypedKey _localConversationRecordKey; late final TextEditingController _textEditingController;
final void Function() _onCancel; late final ScrollController _scrollController;
final void Function() _onClose; late final SingleStateProcessor<ChatComponentState> _chatStateProcessor;
} }

View file

@ -0,0 +1,41 @@
import 'package:flutter_translate/flutter_translate.dart';
import 'package:intl/intl.dart';
class DateFormatter {
DateFormatter();
String chatDateTimeFormat(DateTime dateTime) {
final now = DateTime.now();
final justNow = now.subtract(const Duration(minutes: 1));
final localDateTime = dateTime.toLocal();
if (!localDateTime.difference(justNow).isNegative) {
return translate('date_formatter.just_now');
}
final roughTimeString = DateFormat.jm().format(dateTime);
if (localDateTime.day == now.day &&
localDateTime.month == now.month &&
localDateTime.year == now.year) {
return roughTimeString;
}
final yesterday = now.subtract(const Duration(days: 1));
if (localDateTime.day == yesterday.day &&
localDateTime.month == now.month &&
localDateTime.year == now.year) {
return translate('date_formatter.yesterday');
}
if (now.difference(localDateTime).inDays < 4) {
final weekday = DateFormat(DateFormat.WEEKDAY).format(localDateTime);
return '$weekday, $roughTimeString';
}
return '${DateFormat.yMd().format(dateTime)}, $roughTimeString';
}
}

View file

@ -0,0 +1,54 @@
import 'dart:convert';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
Utf8LengthLimitingTextInputFormatter({this.maxLength})
: assert(maxLength != null || maxLength! >= 0, 'maxLength is invalid');
final int? maxLength;
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
if (maxLength != null && _bytesLength(newValue.text) > maxLength!) {
// If already at the maximum and tried to enter even more,
// keep the old value.
if (_bytesLength(oldValue.text) == maxLength) {
return oldValue;
}
return _truncate(newValue, maxLength!);
}
return newValue;
}
static TextEditingValue _truncate(TextEditingValue value, int maxLength) {
var newValue = '';
if (_bytesLength(value.text) > maxLength) {
var length = 0;
value.text.characters.takeWhile((char) {
final nbBytes = _bytesLength(char);
if (length + nbBytes <= maxLength) {
newValue += char;
length += nbBytes;
return true;
}
return false;
});
}
return TextEditingValue(
text: newValue,
selection: value.selection.copyWith(
baseOffset: min(value.selection.start, newValue.length),
extentOffset: min(value.selection.end, newValue.length),
),
);
}
static int _bytesLength(String value) => utf8.encode(value).length;
}

View file

@ -1,3 +1,4 @@
export 'chat_component_widget.dart'; export 'chat_component_widget.dart';
export 'empty_chat_widget.dart'; export 'empty_chat_widget.dart';
export 'no_conversation_widget.dart'; export 'no_conversation_widget.dart';
export 'utf8_length_limiting_text_input_formatter.dart';

View file

@ -17,8 +17,12 @@ class VeilidChatGlobalInit {
// Initialize Veilid // Initialize Veilid
Future<void> _initializeVeilid() async { Future<void> _initializeVeilid() async {
// Init Veilid // Init Veilid
try {
Veilid.instance.initializeVeilidCore( Veilid.instance.initializeVeilidCore(
await getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name)); await getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
} on VeilidAPIExceptionAlreadyInitialized {
log.debug('Already initialized, not reinitializing veilid-core');
}
// Veilid logging // Veilid logging
initVeilidLog(kIsDebugMode); initVeilidLog(kIsDebugMode);

View file

@ -16,6 +16,14 @@ class ReloadThemeIntent extends Intent {
const ReloadThemeIntent(); const ReloadThemeIntent();
} }
class ChangeBrightnessIntent extends Intent {
const ChangeBrightnessIntent();
}
class ChangeColorIntent extends Intent {
const ChangeColorIntent();
}
class AttachDetachIntent extends Intent { class AttachDetachIntent extends Intent {
const AttachDetachIntent(); const AttachDetachIntent();
} }
@ -49,6 +57,49 @@ class KeyboardShortcuts extends StatelessWidget {
}); });
} }
void changeBrightness(BuildContext context) {
singleFuture(this, () async {
final prefs = PreferencesRepository.instance.value;
final oldBrightness = prefs.themePreference.brightnessPreference;
final newBrightness = BrightnessPreference.values[
(oldBrightness.index + 1) % BrightnessPreference.values.length];
log.info('Changing brightness to $newBrightness');
final newPrefs = prefs.copyWith(
themePreference: prefs.themePreference
.copyWith(brightnessPreference: newBrightness));
await PreferencesRepository.instance.set(newPrefs);
if (context.mounted) {
ThemeSwitcher.of(context)
.changeTheme(theme: newPrefs.themePreference.themeData());
}
});
}
void changeColor(BuildContext context) {
singleFuture(this, () async {
final prefs = PreferencesRepository.instance.value;
final oldColor = prefs.themePreference.colorPreference;
final newColor = ColorPreference
.values[(oldColor.index + 1) % ColorPreference.values.length];
log.info('Changing color to $newColor');
final newPrefs = prefs.copyWith(
themePreference:
prefs.themePreference.copyWith(colorPreference: newColor));
await PreferencesRepository.instance.set(newPrefs);
if (context.mounted) {
ThemeSwitcher.of(context)
.changeTheme(theme: newPrefs.themePreference.themeData());
}
});
}
void _attachDetach(BuildContext context) { void _attachDetach(BuildContext context) {
singleFuture(this, () async { singleFuture(this, () async {
if (ProcessorRepository.instance.processorConnectionState.isAttached) { if (ProcessorRepository.instance.processorConnectionState.isAttached) {
@ -75,17 +126,34 @@ class KeyboardShortcuts extends StatelessWidget {
Widget build(BuildContext context) => ThemeSwitcher( Widget build(BuildContext context) => ThemeSwitcher(
builder: (context) => Shortcuts( builder: (context) => Shortcuts(
shortcuts: <LogicalKeySet, Intent>{ shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.keyR):
const ReloadThemeIntent(),
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD):
const AttachDetachIntent(),
LogicalKeySet( LogicalKeySet(
LogicalKeyboardKey.alt, LogicalKeyboardKey.backquote): LogicalKeyboardKey.alt,
const DeveloperPageIntent(), LogicalKeyboardKey.control,
LogicalKeyboardKey.keyR): const ReloadThemeIntent(),
LogicalKeySet(
LogicalKeyboardKey.alt,
LogicalKeyboardKey.control,
LogicalKeyboardKey.keyB): const ChangeBrightnessIntent(),
LogicalKeySet(
LogicalKeyboardKey.alt,
LogicalKeyboardKey.control,
LogicalKeyboardKey.keyC): const ChangeColorIntent(),
LogicalKeySet(
LogicalKeyboardKey.alt,
LogicalKeyboardKey.control,
LogicalKeyboardKey.keyD): const AttachDetachIntent(),
LogicalKeySet(
LogicalKeyboardKey.alt,
LogicalKeyboardKey.control,
LogicalKeyboardKey.backquote): const DeveloperPageIntent(),
}, },
child: Actions(actions: <Type, Action<Intent>>{ child: Actions(actions: <Type, Action<Intent>>{
ReloadThemeIntent: CallbackAction<ReloadThemeIntent>( ReloadThemeIntent: CallbackAction<ReloadThemeIntent>(
onInvoke: (intent) => reloadTheme(context)), onInvoke: (intent) => reloadTheme(context)),
ChangeBrightnessIntent: CallbackAction<ChangeBrightnessIntent>(
onInvoke: (intent) => changeBrightness(context)),
ChangeColorIntent: CallbackAction<ChangeColorIntent>(
onInvoke: (intent) => changeColor(context)),
AttachDetachIntent: CallbackAction<AttachDetachIntent>( AttachDetachIntent: CallbackAction<AttachDetachIntent>(
onInvoke: (intent) => _attachDetach(context)), onInvoke: (intent) => _attachDetach(context)),
DeveloperPageIntent: CallbackAction<DeveloperPageIntent>( DeveloperPageIntent: CallbackAction<DeveloperPageIntent>(

View file

@ -130,7 +130,11 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
if (activeChatLocalConversationKey == null) { if (activeChatLocalConversationKey == null) {
return const NoConversationWidget(); return const NoConversationWidget();
} }
return ChatComponentWidget( return Material(
color: Colors.transparent,
child: Builder(
builder: (context) => ChatComponentWidget.singleContact(
context: context,
localConversationRecordKey: activeChatLocalConversationKey, localConversationRecordKey: activeChatLocalConversationKey,
onCancel: () { onCancel: () {
activeChatCubit.setActiveChat(null); activeChatCubit.setActiveChat(null);
@ -138,7 +142,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
onClose: () { onClose: () {
activeChatCubit.setActiveChat(null); activeChatCubit.setActiveChat(null);
}, },
key: ValueKey(activeChatLocalConversationKey)); key: ValueKey(activeChatLocalConversationKey))));
} }
@override @override

View file

@ -5,6 +5,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.dart';
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart'; import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
import 'package:keyboard_avoider/keyboard_avoider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart'; import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -207,6 +208,8 @@ class HomeScreenState extends State<HomeScreen>
return DefaultTextStyle( return DefaultTextStyle(
style: theme.textTheme.bodySmall!, style: theme.textTheme.bodySmall!,
child: KeyboardAvoider(
curve: Curves.ease,
child: ZoomDrawer( child: ZoomDrawer(
controller: _zoomDrawerController, controller: _zoomDrawerController,
menuScreen: Builder(builder: (context) { menuScreen: Builder(builder: (context) {
@ -223,17 +226,14 @@ class HomeScreenState extends State<HomeScreen>
child: Builder(builder: _buildAccountPageView)), child: Builder(builder: _buildAccountPageView)),
borderRadius: 0, borderRadius: 0,
angle: 0, angle: 0,
//mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F),
openCurve: Curves.fastEaseInToSlowEaseOut, openCurve: Curves.fastEaseInToSlowEaseOut,
closeCurve: Curves.fastEaseInToSlowEaseOut, closeCurve: Curves.fastEaseInToSlowEaseOut,
// duration: const Duration(milliseconds: 250),
// reverseDuration: const Duration(milliseconds: 250),
menuScreenTapClose: canClose, menuScreenTapClose: canClose,
mainScreenTapClose: canClose, mainScreenTapClose: canClose,
disableDragGesture: !canClose, disableDragGesture: !canClose,
mainScreenScale: .25, mainScreenScale: .25,
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9), slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
)); )));
} }
//////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////

View file

@ -1,488 +0,0 @@
// ignore_for_file: always_put_required_named_parameters_first
import 'package:flutter/material.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'scale_theme/scale_scheme.dart';
ChatTheme makeChatTheme(
ScaleScheme scale, ScaleConfig scaleConfig, TextTheme textTheme) =>
DefaultChatTheme(
primaryColor: scaleConfig.preferBorders
? scale.primaryScale.calloutText
: scale.primaryScale.calloutBackground,
secondaryColor: scaleConfig.preferBorders
? scale.secondaryScale.calloutText
: scale.secondaryScale.calloutBackground,
backgroundColor:
scale.grayScale.appBackground.withAlpha(scaleConfig.wallpaperAlpha),
messageBorderRadius: scaleConfig.borderRadiusScale * 12,
bubbleBorderSide: scaleConfig.preferBorders
? BorderSide(
color: scale.primaryScale.calloutBackground,
width: 2,
)
: BorderSide(width: 2, color: Colors.black.withAlpha(96)),
sendButtonIcon: Image.asset(
'assets/icon-send.png',
color: scaleConfig.preferBorders
? scale.primaryScale.border
: scale.primaryScale.borderText,
package: 'flutter_chat_ui',
),
inputBackgroundColor: Colors.blue,
inputBorderRadius: BorderRadius.zero,
inputTextStyle: textTheme.bodyLarge!,
inputTextDecoration: InputDecoration(
filled: !scaleConfig.preferBorders,
fillColor: scale.primaryScale.subtleBackground,
isDense: true,
contentPadding: const EdgeInsets.fromLTRB(8, 8, 8, 8),
disabledBorder: OutlineInputBorder(
borderSide: scaleConfig.preferBorders
? BorderSide(color: scale.grayScale.border, width: 2)
: BorderSide.none,
borderRadius: BorderRadius.all(
Radius.circular(8 * scaleConfig.borderRadiusScale))),
enabledBorder: OutlineInputBorder(
borderSide: scaleConfig.preferBorders
? BorderSide(color: scale.primaryScale.border, width: 2)
: BorderSide.none,
borderRadius: BorderRadius.all(
Radius.circular(8 * scaleConfig.borderRadiusScale))),
focusedBorder: OutlineInputBorder(
borderSide: scaleConfig.preferBorders
? BorderSide(color: scale.primaryScale.border, width: 2)
: BorderSide.none,
borderRadius: BorderRadius.all(
Radius.circular(8 * scaleConfig.borderRadiusScale))),
),
inputContainerDecoration: BoxDecoration(
border: scaleConfig.preferBorders
? Border(
top: BorderSide(color: scale.primaryScale.border, width: 2))
: null,
color: scaleConfig.preferBorders
? scale.primaryScale.elementBackground
: scale.primaryScale.border),
inputPadding: const EdgeInsets.all(6),
inputTextColor: !scaleConfig.preferBorders
? scale.primaryScale.appText
: scale.primaryScale.border,
messageInsetsHorizontal: 12,
messageInsetsVertical: 8,
attachmentButtonIcon: const Icon(Icons.attach_file),
sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith(
color: scaleConfig.preferBorders
? scale.primaryScale.calloutBackground
: scale.primaryScale.calloutText,
),
sentEmojiMessageTextStyle: const TextStyle(
color: Colors.white,
fontSize: 64,
),
receivedMessageBodyTextStyle: textTheme.bodyLarge!.copyWith(
color: scaleConfig.preferBorders
? scale.secondaryScale.calloutBackground
: scale.secondaryScale.calloutText,
),
receivedEmojiMessageTextStyle: const TextStyle(
color: Colors.white,
fontSize: 64,
),
dateDividerTextStyle: textTheme.labelSmall!);
class EditedChatTheme extends ChatTheme {
const EditedChatTheme({
required super.attachmentButtonIcon,
required super.attachmentButtonMargin,
required super.backgroundColor,
super.bubbleMargin,
required super.dateDividerMargin,
required super.dateDividerTextStyle,
required super.deliveredIcon,
required super.documentIcon,
required super.emptyChatPlaceholderTextStyle,
required super.errorColor,
required super.errorIcon,
required super.inputBackgroundColor,
required super.inputSurfaceTintColor,
required super.inputElevation,
required super.inputBorderRadius,
super.inputContainerDecoration,
required super.inputMargin,
required super.inputPadding,
required super.inputTextColor,
super.inputTextCursorColor,
required super.inputTextDecoration,
required super.inputTextStyle,
required super.messageBorderRadius,
required super.messageInsetsHorizontal,
required super.messageInsetsVertical,
required super.messageMaxWidth,
required super.primaryColor,
required super.receivedEmojiMessageTextStyle,
super.receivedMessageBodyBoldTextStyle,
super.receivedMessageBodyCodeTextStyle,
super.receivedMessageBodyLinkTextStyle,
required super.receivedMessageBodyTextStyle,
required super.receivedMessageCaptionTextStyle,
required super.receivedMessageDocumentIconColor,
required super.receivedMessageLinkDescriptionTextStyle,
required super.receivedMessageLinkTitleTextStyle,
required super.secondaryColor,
required super.seenIcon,
required super.sendButtonIcon,
required super.sendButtonMargin,
required super.sendingIcon,
required super.sentEmojiMessageTextStyle,
super.sentMessageBodyBoldTextStyle,
super.sentMessageBodyCodeTextStyle,
super.sentMessageBodyLinkTextStyle,
required super.sentMessageBodyTextStyle,
required super.sentMessageCaptionTextStyle,
required super.sentMessageDocumentIconColor,
required super.sentMessageLinkDescriptionTextStyle,
required super.sentMessageLinkTitleTextStyle,
required super.statusIconPadding,
required super.systemMessageTheme,
required super.typingIndicatorTheme,
required super.unreadHeaderTheme,
required super.userAvatarImageBackgroundColor,
required super.userAvatarNameColors,
required super.userAvatarTextStyle,
required super.userNameTextStyle,
super.highlightMessageColor,
});
}
class ChatThemeEditor {
ChatThemeEditor(ChatTheme base)
: attachmentButtonIcon = base.attachmentButtonIcon,
attachmentButtonMargin = base.attachmentButtonMargin,
backgroundColor = base.backgroundColor,
bubbleMargin = base.bubbleMargin,
dateDividerMargin = base.dateDividerMargin,
dateDividerTextStyle = base.dateDividerTextStyle,
deliveredIcon = base.deliveredIcon,
documentIcon = base.documentIcon,
emptyChatPlaceholderTextStyle = base.emptyChatPlaceholderTextStyle,
errorColor = base.errorColor,
errorIcon = base.errorIcon,
inputBackgroundColor = base.inputBackgroundColor,
inputSurfaceTintColor = base.inputSurfaceTintColor,
inputElevation = base.inputElevation,
inputBorderRadius = base.inputBorderRadius,
inputContainerDecoration = base.inputContainerDecoration,
inputMargin = base.inputMargin,
inputPadding = base.inputPadding,
inputTextColor = base.inputTextColor,
inputTextCursorColor = base.inputTextCursorColor,
inputTextDecoration = base.inputTextDecoration,
inputTextStyle = base.inputTextStyle,
messageBorderRadius = base.messageBorderRadius,
messageInsetsHorizontal = base.messageInsetsHorizontal,
messageInsetsVertical = base.messageInsetsVertical,
messageMaxWidth = base.messageMaxWidth,
primaryColor = base.primaryColor,
receivedEmojiMessageTextStyle = base.receivedEmojiMessageTextStyle,
receivedMessageBodyBoldTextStyle =
base.receivedMessageBodyBoldTextStyle,
receivedMessageBodyCodeTextStyle =
base.receivedMessageBodyCodeTextStyle,
receivedMessageBodyLinkTextStyle =
base.receivedMessageBodyLinkTextStyle,
receivedMessageBodyTextStyle = base.receivedMessageBodyTextStyle,
receivedMessageCaptionTextStyle = base.receivedMessageCaptionTextStyle,
receivedMessageDocumentIconColor =
base.receivedMessageDocumentIconColor,
receivedMessageLinkDescriptionTextStyle =
base.receivedMessageLinkDescriptionTextStyle,
receivedMessageLinkTitleTextStyle =
base.receivedMessageLinkTitleTextStyle,
secondaryColor = base.secondaryColor,
seenIcon = base.seenIcon,
sendButtonIcon = base.sendButtonIcon,
sendButtonMargin = base.sendButtonMargin,
sendingIcon = base.sendingIcon,
sentEmojiMessageTextStyle = base.sentEmojiMessageTextStyle,
sentMessageBodyBoldTextStyle = base.sentMessageBodyBoldTextStyle,
sentMessageBodyCodeTextStyle = base.sentMessageBodyCodeTextStyle,
sentMessageBodyLinkTextStyle = base.sentMessageBodyLinkTextStyle,
sentMessageBodyTextStyle = base.sentMessageBodyTextStyle,
sentMessageCaptionTextStyle = base.sentMessageCaptionTextStyle,
sentMessageDocumentIconColor = base.sentMessageDocumentIconColor,
sentMessageLinkDescriptionTextStyle =
base.sentMessageLinkDescriptionTextStyle,
sentMessageLinkTitleTextStyle = base.sentMessageLinkTitleTextStyle,
statusIconPadding = base.statusIconPadding,
systemMessageTheme = base.systemMessageTheme,
typingIndicatorTheme = base.typingIndicatorTheme,
unreadHeaderTheme = base.unreadHeaderTheme,
userAvatarImageBackgroundColor = base.userAvatarImageBackgroundColor,
userAvatarNameColors = base.userAvatarNameColors,
userAvatarTextStyle = base.userAvatarTextStyle,
userNameTextStyle = base.userNameTextStyle,
highlightMessageColor = base.highlightMessageColor;
EditedChatTheme commit() => EditedChatTheme(
attachmentButtonIcon: attachmentButtonIcon,
attachmentButtonMargin: attachmentButtonMargin,
backgroundColor: backgroundColor,
bubbleMargin: bubbleMargin,
dateDividerMargin: dateDividerMargin,
dateDividerTextStyle: dateDividerTextStyle,
deliveredIcon: deliveredIcon,
documentIcon: documentIcon,
emptyChatPlaceholderTextStyle: emptyChatPlaceholderTextStyle,
errorColor: errorColor,
errorIcon: errorIcon,
inputBackgroundColor: inputBackgroundColor,
inputSurfaceTintColor: inputSurfaceTintColor,
inputElevation: inputElevation,
inputBorderRadius: inputBorderRadius,
inputContainerDecoration: inputContainerDecoration,
inputMargin: inputMargin,
inputPadding: inputPadding,
inputTextColor: inputTextColor,
inputTextCursorColor: inputTextCursorColor,
inputTextDecoration: inputTextDecoration,
inputTextStyle: inputTextStyle,
messageBorderRadius: messageBorderRadius,
messageInsetsHorizontal: messageInsetsHorizontal,
messageInsetsVertical: messageInsetsVertical,
messageMaxWidth: messageMaxWidth,
primaryColor: primaryColor,
receivedEmojiMessageTextStyle: receivedEmojiMessageTextStyle,
receivedMessageBodyBoldTextStyle: receivedMessageBodyBoldTextStyle,
receivedMessageBodyCodeTextStyle: receivedMessageBodyCodeTextStyle,
receivedMessageBodyLinkTextStyle: receivedMessageBodyLinkTextStyle,
receivedMessageBodyTextStyle: receivedMessageBodyTextStyle,
receivedMessageCaptionTextStyle: receivedMessageCaptionTextStyle,
receivedMessageDocumentIconColor: receivedMessageDocumentIconColor,
receivedMessageLinkDescriptionTextStyle:
receivedMessageLinkDescriptionTextStyle,
receivedMessageLinkTitleTextStyle: receivedMessageLinkTitleTextStyle,
secondaryColor: secondaryColor,
seenIcon: seenIcon,
sendButtonIcon: sendButtonIcon,
sendButtonMargin: sendButtonMargin,
sendingIcon: sendingIcon,
sentEmojiMessageTextStyle: sentEmojiMessageTextStyle,
sentMessageBodyBoldTextStyle: sentMessageBodyBoldTextStyle,
sentMessageBodyCodeTextStyle: sentMessageBodyCodeTextStyle,
sentMessageBodyLinkTextStyle: sentMessageBodyLinkTextStyle,
sentMessageBodyTextStyle: sentMessageBodyTextStyle,
sentMessageCaptionTextStyle: sentMessageCaptionTextStyle,
sentMessageDocumentIconColor: sentMessageDocumentIconColor,
sentMessageLinkDescriptionTextStyle:
sentMessageLinkDescriptionTextStyle,
sentMessageLinkTitleTextStyle: sentMessageLinkTitleTextStyle,
statusIconPadding: statusIconPadding,
systemMessageTheme: systemMessageTheme,
typingIndicatorTheme: typingIndicatorTheme,
unreadHeaderTheme: unreadHeaderTheme,
userAvatarImageBackgroundColor: userAvatarImageBackgroundColor,
userAvatarNameColors: userAvatarNameColors,
userAvatarTextStyle: userAvatarTextStyle,
userNameTextStyle: userNameTextStyle,
highlightMessageColor: highlightMessageColor,
);
/////////////////////////////////////////////////////////////////////////////
/// Icon for select attachment button.
Widget? attachmentButtonIcon;
/// Margin of attachment button.
EdgeInsets? attachmentButtonMargin;
/// Used as a background color of a chat widget.
Color backgroundColor;
// Margin around the message bubble.
EdgeInsetsGeometry? bubbleMargin;
/// Margin around date dividers.
EdgeInsets dateDividerMargin;
/// Text style of the date dividers.
TextStyle dateDividerTextStyle;
/// Icon for message's `delivered` status. For the best look use size of 16.
Widget? deliveredIcon;
/// Icon inside file message.
Widget? documentIcon;
/// Text style of the empty chat placeholder.
TextStyle emptyChatPlaceholderTextStyle;
/// Color to indicate something bad happened (usually - shades of red).
Color errorColor;
/// Icon for message's `error` status. For the best look use size of 16.
Widget? errorIcon;
/// Color of the bottom bar where text field is.
Color inputBackgroundColor;
/// Surface Tint Color of the bottom bar where text field is.
Color inputSurfaceTintColor;
double inputElevation;
/// Top border radius of the bottom bar where text field is.
BorderRadius inputBorderRadius;
/// Decoration of the container wrapping the text field.
Decoration? inputContainerDecoration;
/// Outer insets of the bottom bar where text field is.
EdgeInsets inputMargin;
/// Inner insets of the bottom bar where text field is.
EdgeInsets inputPadding;
/// Color of the text field's text and attachment/send buttons.
Color inputTextColor;
/// Color of the text field's cursor.
Color? inputTextCursorColor;
/// Decoration of the input text field.
InputDecoration inputTextDecoration;
/// Text style of the message input. To change the color use [inputTextColor].
TextStyle inputTextStyle;
/// Border radius of message container.
double messageBorderRadius;
/// Horizontal message bubble insets.
double messageInsetsHorizontal;
/// Vertical message bubble insets.
double messageInsetsVertical;
/// Message bubble max width. set to [double.infinity] adaptive screen.
double messageMaxWidth;
/// Primary color of the chat used as a background of sent messages
/// and statuses.
Color primaryColor;
/// Text style used for displaying emojis on text messages.
TextStyle receivedEmojiMessageTextStyle;
/// Body text style used for displaying bold text on received text messages.
/// Default to a bold version of [receivedMessageBodyTextStyle].
TextStyle? receivedMessageBodyBoldTextStyle;
/// Body text style used for displaying code text on received text messages.
/// Defaults to a mono version of [receivedMessageBodyTextStyle].
TextStyle? receivedMessageBodyCodeTextStyle;
/// Text style used for displaying link text on received text messages.
/// Defaults to [receivedMessageBodyTextStyle].
TextStyle? receivedMessageBodyLinkTextStyle;
/// Body text style used for displaying text on different types
/// of received messages.
TextStyle receivedMessageBodyTextStyle;
/// Caption text style used for displaying secondary info (e.g. file size) on
/// different types of received messages.
TextStyle receivedMessageCaptionTextStyle;
/// Color of the document icon on received messages. Has no effect when
/// [documentIcon] is used.
Color receivedMessageDocumentIconColor;
/// Text style used for displaying link description on received messages.
TextStyle receivedMessageLinkDescriptionTextStyle;
/// Text style used for displaying link title on received messages.
TextStyle receivedMessageLinkTitleTextStyle;
/// Secondary color, used as a background of received messages.
Color secondaryColor;
/// Icon for message's `seen` status. For the best look use size of 16.
Widget? seenIcon;
/// Icon for send button.
Widget? sendButtonIcon;
/// Margin of send button.
EdgeInsets? sendButtonMargin;
/// Icon for message's `sending` status. For the best look use size of 10.
Widget? sendingIcon;
/// Text style used for displaying emojis on text messages.
TextStyle sentEmojiMessageTextStyle;
/// Body text style used for displaying bold text on sent text messages.
/// Defaults to a bold version of [sentMessageBodyTextStyle].
TextStyle? sentMessageBodyBoldTextStyle;
/// Body text style used for displaying code text on sent text messages.
/// Defaults to a mono version of [sentMessageBodyTextStyle].
TextStyle? sentMessageBodyCodeTextStyle;
/// Text style used for displaying link text on sent text messages.
/// Defaults to [sentMessageBodyTextStyle].
TextStyle? sentMessageBodyLinkTextStyle;
/// Body text style used for displaying text on different types
/// of sent messages.
TextStyle sentMessageBodyTextStyle;
/// Caption text style used for displaying secondary info (e.g. file size) on
/// different types of sent messages.
TextStyle sentMessageCaptionTextStyle;
/// Color of the document icon on sent messages. Has no effect when
/// [documentIcon] is used.
Color sentMessageDocumentIconColor;
/// Text style used for displaying link description on sent messages.
TextStyle sentMessageLinkDescriptionTextStyle;
/// Text style used for displaying link title on sent messages.
TextStyle sentMessageLinkTitleTextStyle;
/// Padding around status icons.
EdgeInsets statusIconPadding;
/// Theme for the system message. Will not have an effect if a custom builder
/// is provided.
SystemMessageTheme systemMessageTheme;
/// Theme for typing indicator. See [TypingIndicator].
TypingIndicatorTheme typingIndicatorTheme;
/// Theme for the unread header.
UnreadHeaderTheme unreadHeaderTheme;
/// Color used as a background for user avatar if an image is provided.
/// Visible if the image has some transparent parts.
Color userAvatarImageBackgroundColor;
/// Colors used as backgrounds for user avatars with no image and so,
/// corresponding user names.
/// Calculated based on a user ID, so unique across the whole app.
List<Color> userAvatarNameColors;
/// Text style used for displaying initials on user avatar if no
/// image is provided.
TextStyle userAvatarTextStyle;
/// User names text style. Color will be overwritten
/// with [userAvatarNameColors].
TextStyle userNameTextStyle;
/// Color used as background of message row on highligth.
Color? highlightMessageColor;
}

View file

@ -1,4 +1,3 @@
export 'chat_theme.dart';
export 'radix_generator.dart'; export 'radix_generator.dart';
export 'scale_theme/scale_theme.dart'; export 'scale_theme/scale_theme.dart';
export 'theme_preference.dart'; export 'theme_preference.dart';

View file

@ -0,0 +1,369 @@
import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_chat_core/flutter_chat_core.dart' as core;
import 'scale_theme.dart';
class ScaleChatTheme {
ScaleChatTheme({
// Default chat theme
required this.chatTheme,
// Customization fields (from v1 of flutter chat ui)
required this.attachmentButtonIcon,
// required this.attachmentButtonMargin,
required this.backgroundColor,
required this.bubbleBorderSide,
// required this.dateDividerMargin,
// required this.chatContentMargin,
required this.dateDividerTextStyle,
// required this.deliveredIcon,
// required this.documentIcon,
// required this.emptyChatPlaceholderTextStyle,
// required this.errorColor,
// required this.errorIcon,
required this.inputBackgroundColor,
// required this.inputSurfaceTintColor,
// required this.inputElevation,
required this.inputBorderRadius,
// required this.inputMargin,
required this.inputPadding,
required this.inputTextColor,
required this.inputTextStyle,
required this.messageBorderRadius,
required this.messageInsetsHorizontal,
required this.messageInsetsVertical,
// required this.messageMaxWidth,
required this.primaryColor,
required this.receivedEmojiMessageTextStyle,
required this.receivedMessageBodyTextStyle,
// required this.receivedMessageCaptionTextStyle,
// required this.receivedMessageDocumentIconColor,
// required this.receivedMessageLinkDescriptionTextStyle,
// required this.receivedMessageLinkTitleTextStyle,
required this.secondaryColor,
// required this.seenIcon,
required this.sendButtonIcon,
// required this.sendButtonMargin,
// required this.sendingIcon,
required this.onlyEmojiFontSize,
required this.timeStyle,
required this.sentMessageBodyTextStyle,
// required this.sentMessageCaptionTextStyle,
// required this.sentMessageDocumentIconColor,
// required this.sentMessageLinkDescriptionTextStyle,
// required this.sentMessageLinkTitleTextStyle,
// required this.statusIconPadding,
// required this.userAvatarImageBackgroundColor,
// required this.userAvatarNameColors,
// required this.userAvatarTextStyle,
// required this.userNameTextStyle,
// required this.bubbleMargin,
required this.inputContainerDecoration,
// required this.inputTextCursorColor,
// required this.receivedMessageBodyBoldTextStyle,
// required this.receivedMessageBodyCodeTextStyle,
// required this.receivedMessageBodyLinkTextStyle,
// required this.sentMessageBodyBoldTextStyle,
// required this.sentMessageBodyCodeTextStyle,
// required this.sentMessageBodyLinkTextStyle,
// required this.highlightMessageColor,
});
final core.ChatTheme chatTheme;
/// Icon for select attachment button.
final Widget? attachmentButtonIcon;
/// Margin of attachment button.
// final EdgeInsets? attachmentButtonMargin;
/// Used as a background color of a chat widget.
final Color backgroundColor;
// Margin around the message bubble.
// final EdgeInsetsGeometry? bubbleMargin;
/// Border for chat bubbles
final BorderSide bubbleBorderSide;
/// Margin around date dividers.
// final EdgeInsets dateDividerMargin;
/// Margin inside chat area.
// final EdgeInsets chatContentMargin;
/// Text style of the date dividers.
final TextStyle dateDividerTextStyle;
/// Icon for message's `delivered` status. For the best look use size of 16.
// final Widget? deliveredIcon;
/// Icon inside file message.
// final Widget? documentIcon;
/// Text style of the empty chat placeholder.
// final TextStyle emptyChatPlaceholderTextStyle;
/// Color to indicate something bad happened (usually - shades of red).
// final Color errorColor;
/// Icon for message's `error` status. For the best look use size of 16.
// final Widget? errorIcon;
/// Color of the bottom bar where text field is.
final Color inputBackgroundColor;
/// Surface Tint Color of the bottom bar where text field is.
// final Color inputSurfaceTintColor;
/// Elevation to use for input material
// final double inputElevation;
/// Top border radius of the bottom bar where text field is.
final BorderRadius inputBorderRadius;
/// Decoration of the container wrapping the text field.
final Decoration? inputContainerDecoration;
/// Outer insets of the bottom bar where text field is.
// final EdgeInsets inputMargin;
/// Inner insets of the bottom bar where text field is.
final EdgeInsets inputPadding;
/// Color of the text field's text and attachment/send buttons.
final Color inputTextColor;
/// Color of the text field's cursor.
// final Color? inputTextCursorColor;
/// Text style of the message input. To change the color use [inputTextColor].
final TextStyle inputTextStyle;
/// Border radius of message container.
final double messageBorderRadius;
/// Horizontal message bubble insets.
final double messageInsetsHorizontal;
/// Vertical message bubble insets.
final double messageInsetsVertical;
/// Message bubble max width. set to [double.infinity] adaptive screen.
// final double messageMaxWidth;
/// Primary color of the chat used as a background of sent messages
/// and statuses.
final Color primaryColor;
/// Text style used for displaying emojis on text messages.
final TextStyle receivedEmojiMessageTextStyle;
/// Body text style used for displaying bold text on received text messages.
// Default to a bold version of [receivedMessageBodyTextStyle].
// final TextStyle? receivedMessageBodyBoldTextStyle;
/// Body text style used for displaying code text on received text messages.
// Defaults to a mono version of [receivedMessageBodyTextStyle].
// final TextStyle? receivedMessageBodyCodeTextStyle;
/// Text style used for displaying link text on received text messages.
// Defaults to [receivedMessageBodyTextStyle].
// final TextStyle? receivedMessageBodyLinkTextStyle;
/// Body text style used for displaying text on different types
/// of received messages.
final TextStyle receivedMessageBodyTextStyle;
/// Caption text style used for displaying secondary info (e.g. file size) on
/// different types of received messages.
// final TextStyle receivedMessageCaptionTextStyle;
/// Color of the document icon on received messages. Has no effect when
// [documentIcon] is used.
// final Color receivedMessageDocumentIconColor;
/// Text style used for displaying link description on received messages.
// final TextStyle receivedMessageLinkDescriptionTextStyle;
/// Text style used for displaying link title on received messages.
// final TextStyle receivedMessageLinkTitleTextStyle;
/// Secondary color, used as a background of received messages.
final Color secondaryColor;
/// Icon for message's `seen` status. For the best look use size of 16.
// final Widget? seenIcon;
/// Icon for send button.
final Widget? sendButtonIcon;
/// Margin of send button.
// final EdgeInsets? sendButtonMargin;
/// Icon for message's `sending` status. For the best look use size of 10.
// final Widget? sendingIcon;
/// Text size for displaying emojis on text messages.
final double onlyEmojiFontSize;
/// Text style used for time and status
final TextStyle timeStyle;
/// Body text style used for displaying bold text on sent text messages.
/// Defaults to a bold version of [sentMessageBodyTextStyle].
// final TextStyle? sentMessageBodyBoldTextStyle;
/// Body text style used for displaying code text on sent text messages.
/// Defaults to a mono version of [sentMessageBodyTextStyle].
// final TextStyle? sentMessageBodyCodeTextStyle;
/// Text style used for displaying link text on sent text messages.
/// Defaults to [sentMessageBodyTextStyle].
// final TextStyle? sentMessageBodyLinkTextStyle;
/// Body text style used for displaying text on different types
/// of sent messages.
final TextStyle sentMessageBodyTextStyle;
/// Caption text style used for displaying secondary info (e.g. file size) on
/// different types of sent messages.
// final TextStyle sentMessageCaptionTextStyle;
/// Color of the document icon on sent messages. Has no effect when
// [documentIcon] is used.
// final Color sentMessageDocumentIconColor;
/// Text style used for displaying link description on sent messages.
// final TextStyle sentMessageLinkDescriptionTextStyle;
/// Text style used for displaying link title on sent messages.
// final TextStyle sentMessageLinkTitleTextStyle;
/// Padding around status icons.
// final EdgeInsets statusIconPadding;
/// Color used as a background for user avatar if an image is provided.
/// Visible if the image has some transparent parts.
// final Color userAvatarImageBackgroundColor;
/// Colors used as backgrounds for user avatars with no image and so,
/// corresponding user names.
/// Calculated based on a user ID, so unique across the whole app.
// final List<Color> userAvatarNameColors;
/// Text style used for displaying initials on user avatar if no
/// image is provided.
// final TextStyle userAvatarTextStyle;
/// User names text style. Color will be overwritten with
// [userAvatarNameColors].
// final TextStyle userNameTextStyle;
/// Color used as background of message row on highlight.
// final Color? highlightMessageColor;
}
extension ScaleChatThemeExt on ScaleTheme {
ScaleChatTheme chatTheme() {
// 'brightness' is not actually used by ChatColors.fromThemeData,
// or ChatTypography.fromThemeData so just say 'light' here
final themeData = toThemeData(Brightness.light);
final typography = core.ChatTypography.fromThemeData(themeData);
final surfaceContainer = config.preferBorders
? scheme.secondaryScale.calloutText
: scheme.secondaryScale.calloutBackground;
final colors = core.ChatColors(
// Primary color, often used for sent messages and accents.
primary: config.preferBorders
? scheme.primaryScale.calloutText
: scheme.primaryScale.calloutBackground,
// Color for text and icons displayed on top of [primary].
onPrimary: scheme.primaryScale.primaryText,
// The main background color of the chat screen.
surface:
scheme.grayScale.appBackground.withAlpha(config.wallpaperAlpha),
// Color for text and icons displayed on top of [surface].
onSurface: scheme.primaryScale.appText,
// Background color for elements like received messages.
surfaceContainer: surfaceContainer,
// A slightly lighter/darker variant of [surfaceContainer].
surfaceContainerLow: surfaceContainer.darken(25),
// A slightly lighter/darker variant of [surfaceContainer].
surfaceContainerHigh: surfaceContainer.lighten(25));
final chatTheme = core.ChatTheme(
colors: colors,
typography: typography,
shape:
BorderRadius.all(Radius.circular(config.borderRadiusScale * 12)));
return ScaleChatTheme(
chatTheme: chatTheme,
primaryColor: config.preferBorders
? scheme.primaryScale.calloutText
: scheme.primaryScale.calloutBackground,
secondaryColor: config.preferBorders
? scheme.secondaryScale.calloutText
: scheme.secondaryScale.calloutBackground,
backgroundColor:
scheme.grayScale.appBackground.withAlpha(config.wallpaperAlpha),
messageBorderRadius: config.borderRadiusScale * 12,
bubbleBorderSide: config.preferBorders
? BorderSide(
color: scheme.primaryScale.calloutBackground,
width: 2,
)
: BorderSide(width: 2, color: Colors.black.withAlpha(96)),
sendButtonIcon: Image.asset(
'assets/icon-send.png',
color: config.preferBorders
? scheme.primaryScale.border
: scheme.primaryScale.borderText,
package: 'flutter_chat_ui',
),
inputBackgroundColor: Colors.blue,
inputBorderRadius: BorderRadius.zero,
inputTextStyle: textTheme.bodyLarge!,
inputContainerDecoration: BoxDecoration(
border: config.preferBorders
? Border(
top:
BorderSide(color: scheme.primaryScale.border, width: 2))
: null,
color: config.preferBorders
? scheme.primaryScale.elementBackground
: scheme.primaryScale.border),
inputPadding: const EdgeInsets.all(6),
inputTextColor: !config.preferBorders
? scheme.primaryScale.appText
: scheme.primaryScale.border,
messageInsetsHorizontal: 12,
messageInsetsVertical: 8,
attachmentButtonIcon: const Icon(Icons.attach_file),
sentMessageBodyTextStyle: textTheme.bodyLarge!.copyWith(
color: config.preferBorders
? scheme.primaryScale.calloutBackground
: scheme.primaryScale.calloutText,
),
onlyEmojiFontSize: 64,
timeStyle: textTheme.bodySmall!.copyWith(fontSize: 9),
receivedMessageBodyTextStyle: textTheme.bodyLarge!.copyWith(
color: config.preferBorders
? scheme.secondaryScale.calloutBackground
: scheme.secondaryScale.calloutText,
),
receivedEmojiMessageTextStyle: const TextStyle(
color: Colors.white,
fontSize: 64,
),
dateDividerTextStyle: textTheme.labelSmall!);
}
}

View file

@ -83,6 +83,7 @@ class ScaleColor {
calloutBackground: calloutBackground ?? this.calloutBackground, calloutBackground: calloutBackground ?? this.calloutBackground,
calloutText: calloutText ?? this.calloutText); calloutText: calloutText ?? this.calloutText);
// Use static method
// ignore: prefer_constructors_over_static_methods // ignore: prefer_constructors_over_static_methods
static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor( static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor(
appBackground: Color.lerp(a.appBackground, b.appBackground, t) ?? appBackground: Color.lerp(a.appBackground, b.appBackground, t) ??

View file

@ -4,6 +4,7 @@ import 'scale_input_decorator_theme.dart';
import 'scale_scheme.dart'; import 'scale_scheme.dart';
export 'scale_app_bar_theme.dart'; export 'scale_app_bar_theme.dart';
export 'scale_chat_theme.dart';
export 'scale_color.dart'; export 'scale_color.dart';
export 'scale_input_decorator_theme.dart'; export 'scale_input_decorator_theme.dart';
export 'scale_scheme.dart'; export 'scale_scheme.dart';

View file

@ -1,13 +1,24 @@
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
bool get isAndroid => !kIsWeb && Platform.isAndroid; final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
bool get isiOS => !kIsWeb && Platform.isIOS; final isiOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
bool get isWeb => kIsWeb; final isMobile = !kIsWeb &&
bool get isDesktop => (defaultTargetPlatform == TargetPlatform.iOS ||
!isWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS); defaultTargetPlatform == TargetPlatform.android);
final isDesktop = !kIsWeb &&
!(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.android);
const isWeb = kIsWeb;
final isWebMobile = kIsWeb &&
(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.android);
final isWebDesktop = kIsWeb &&
!(defaultTargetPlatform == TargetPlatform.iOS ||
defaultTargetPlatform == TargetPlatform.android);
final isAnyMobile = isMobile || isWebMobile;
const kMobileWidthCutoff = 500.0; const kMobileWidthCutoff = 500.0;

View file

@ -128,7 +128,7 @@ Future<void> showErrorStacktraceModal(
await showErrorModal( await showErrorModal(
context: context, context: context,
title: translate('toast.error'), title: translate('toast.error'),
text: 'Error: {e}\n StackTrace: {st}', text: 'Error: $error\n StackTrace: $stackTrace',
); );
} }

View file

@ -165,5 +165,8 @@ void initLoggy() {
registerVeilidProtoToDebug(); registerVeilidProtoToDebug();
registerVeilidDHTProtoToDebug(); registerVeilidDHTProtoToDebug();
registerVeilidchatProtoToDebug(); registerVeilidchatProtoToDebug();
if (kIsDebugMode) {
Bloc.observer = const StateLogger(); Bloc.observer = const StateLogger();
}
} }

View file

@ -6,14 +6,17 @@ import 'package:veilid_support/veilid_support.dart';
import 'loggy.dart'; import 'loggy.dart';
const Map<String, LogLevel> _blocChangeLogLevels = { const Map<String, LogLevel> _blocChangeLogLevels = {
'ConnectionStateCubit': LogLevel.off, 'RouterCubit': LogLevel.debug,
'ActiveSingleContactChatBlocMapCubit': LogLevel.off, 'PerAccountCollectionBlocMapCubit': LogLevel.debug,
'ActiveConversationsBlocMapCubit': LogLevel.off, 'PerAccountCollectionCubit': LogLevel.debug,
'PersistentQueueCubit<Message>': LogLevel.off, 'ActiveChatCubit': LogLevel.debug,
'TableDBArrayProtobufCubit<ReconciledMessage>': LogLevel.off, 'AccountRecordCubit': LogLevel.debug,
'DHTLogCubit<Message>': LogLevel.off, 'ContactListCubit': LogLevel.debug,
'SingleContactMessagesCubit': LogLevel.off, 'ContactInvitationListCubit': LogLevel.debug,
'ChatComponentCubit': LogLevel.off, 'ChatListCubit': LogLevel.debug,
'PreferencesCubit': LogLevel.debug,
'ConversationCubit': LogLevel.debug,
'DefaultDHTRecordCubit<Conversation>': LogLevel.debug,
}; };
const Map<String, LogLevel> _blocCreateCloseLogLevels = {}; const Map<String, LogLevel> _blocCreateCloseLogLevels = {};
@ -40,7 +43,7 @@ class StateLogger extends BlocObserver {
@override @override
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) { void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
super.onChange(bloc, change); super.onChange(bloc, change);
_checkLogLevel(_blocChangeLogLevels, LogLevel.debug, bloc, (logLevel) { _checkLogLevel(_blocChangeLogLevels, LogLevel.off, bloc, (logLevel) {
const encoder = JsonEncoder.withIndent(' ', DynamicDebug.toDebug); const encoder = JsonEncoder.withIndent(' ', DynamicDebug.toDebug);
log.log( log.log(
logLevel, logLevel,

View file

@ -23,6 +23,7 @@ part 'super_identity.g.dart';
/// Encryption: None /// Encryption: None
@freezed @freezed
sealed class SuperIdentity with _$SuperIdentity { sealed class SuperIdentity with _$SuperIdentity {
@JsonSerializable()
const factory SuperIdentity({ const factory SuperIdentity({
/// Public DHT record storing this structure for account recovery /// Public DHT record storing this structure for account recovery
/// changing this can migrate/forward the SuperIdentity to a new DHT record /// changing this can migrate/forward the SuperIdentity to a new DHT record

View file

@ -169,6 +169,7 @@ class _$SuperIdentityCopyWithImpl<$Res>
} }
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _SuperIdentity extends SuperIdentity { class _SuperIdentity extends SuperIdentity {
const _SuperIdentity( const _SuperIdentity(

View file

@ -4,8 +4,10 @@ import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:veilid/veilid.dart'; import 'package:veilid/veilid.dart';
// Allowed to pull sentinel value
// ignore: do_not_use_environment // ignore: do_not_use_environment
const bool kIsReleaseMode = bool.fromEnvironment('dart.vm.product'); const bool kIsReleaseMode = bool.fromEnvironment('dart.vm.product');
// Allowed to pull sentinel value
// ignore: do_not_use_environment // ignore: do_not_use_environment
const bool kIsProfileMode = bool.fromEnvironment('dart.vm.profile'); const bool kIsProfileMode = bool.fromEnvironment('dart.vm.profile');
const bool kIsDebugMode = !kIsReleaseMode && !kIsProfileMode; const bool kIsDebugMode = !kIsReleaseMode && !kIsProfileMode;
@ -13,18 +15,21 @@ const bool kIsDebugMode = !kIsReleaseMode && !kIsProfileMode;
Future<Map<String, dynamic>> getDefaultVeilidPlatformConfig( Future<Map<String, dynamic>> getDefaultVeilidPlatformConfig(
bool isWeb, String appName) async { bool isWeb, String appName) async {
final ignoreLogTargetsStr = final ignoreLogTargetsStr =
// Allowed to change settings
// ignore: do_not_use_environment // ignore: do_not_use_environment
const String.fromEnvironment('IGNORE_LOG_TARGETS').trim(); const String.fromEnvironment('IGNORE_LOG_TARGETS').trim();
final ignoreLogTargets = ignoreLogTargetsStr.isEmpty final ignoreLogTargets = ignoreLogTargetsStr.isEmpty
? <String>[] ? <String>[]
: ignoreLogTargetsStr.split(',').map((e) => e.trim()).toList(); : ignoreLogTargetsStr.split(',').map((e) => e.trim()).toList();
// Allowed to change settings
// ignore: do_not_use_environment // ignore: do_not_use_environment
var flamePathStr = const String.fromEnvironment('FLAME').trim(); var flamePathStr = const String.fromEnvironment('FLAME').trim();
if (flamePathStr == '1') { if (flamePathStr == '1') {
flamePathStr = p.join( flamePathStr = p.join(
(await getApplicationSupportDirectory()).absolute.path, (await getApplicationSupportDirectory()).absolute.path,
'$appName.folded'); '$appName.folded');
// Allowed for debugging
// ignore: avoid_print // ignore: avoid_print
print('Flame data logged to $flamePathStr'); print('Flame data logged to $flamePathStr');
} }
@ -73,30 +78,37 @@ Future<VeilidConfig> getVeilidConfig(bool isWeb, String programName) async {
var config = await getDefaultVeilidConfig( var config = await getDefaultVeilidConfig(
isWeb: isWeb, isWeb: isWeb,
programName: programName, programName: programName,
// Allowed to change settings
// ignore: avoid_redundant_argument_values, do_not_use_environment // ignore: avoid_redundant_argument_values, do_not_use_environment
namespace: const String.fromEnvironment('NAMESPACE'), namespace: const String.fromEnvironment('NAMESPACE'),
// Allowed to change settings
// ignore: avoid_redundant_argument_values, do_not_use_environment // ignore: avoid_redundant_argument_values, do_not_use_environment
bootstrap: const String.fromEnvironment('BOOTSTRAP'), bootstrap: const String.fromEnvironment('BOOTSTRAP'),
// Allowed to change settings
// ignore: avoid_redundant_argument_values, do_not_use_environment // ignore: avoid_redundant_argument_values, do_not_use_environment
networkKeyPassword: const String.fromEnvironment('NETWORK_KEY'), networkKeyPassword: const String.fromEnvironment('NETWORK_KEY'),
); );
// Allowed to change settings
// ignore: do_not_use_environment // ignore: do_not_use_environment
if (const String.fromEnvironment('DELETE_TABLE_STORE') == '1') { if (const String.fromEnvironment('DELETE_TABLE_STORE') == '1') {
config = config =
config.copyWith(tableStore: config.tableStore.copyWith(delete: true)); config.copyWith(tableStore: config.tableStore.copyWith(delete: true));
} }
// Allowed to change settings
// ignore: do_not_use_environment // ignore: do_not_use_environment
if (const String.fromEnvironment('DELETE_PROTECTED_STORE') == '1') { if (const String.fromEnvironment('DELETE_PROTECTED_STORE') == '1') {
config = config.copyWith( config = config.copyWith(
protectedStore: config.protectedStore.copyWith(delete: true)); protectedStore: config.protectedStore.copyWith(delete: true));
} }
// Allowed to change settings
// ignore: do_not_use_environment // ignore: do_not_use_environment
if (const String.fromEnvironment('DELETE_BLOCK_STORE') == '1') { if (const String.fromEnvironment('DELETE_BLOCK_STORE') == '1') {
config = config =
config.copyWith(blockStore: config.blockStore.copyWith(delete: true)); config.copyWith(blockStore: config.blockStore.copyWith(delete: true));
} }
// Allowed to change settings
// ignore: do_not_use_environment // ignore: do_not_use_environment
const envNetwork = String.fromEnvironment('NETWORK'); const envNetwork = String.fromEnvironment('NETWORK');
if (envNetwork.isNotEmpty) { if (envNetwork.isNotEmpty) {
@ -111,7 +123,8 @@ Future<VeilidConfig> getVeilidConfig(bool isWeb, String programName) async {
return config.copyWith( return config.copyWith(
capabilities: capabilities:
// XXX: Remove DHTV and DHTW when we get background sync implemented // XXX: Remove DHTV and DHTW after DHT widening (and maybe remote
// rehydration?)
const VeilidConfigCapabilities(disable: ['DHTV', 'DHTW', 'TUNL']), const VeilidConfigCapabilities(disable: ['DHTV', 'DHTW', 'TUNL']),
protectedStore: protectedStore:
// XXX: Linux often does not have a secret storage mechanism installed // XXX: Linux often does not have a secret storage mechanism installed

View file

@ -24,7 +24,7 @@ class TableDBArrayProtobufStateData<T extends GeneratedMessage>
final IList<T> windowElements; final IList<T> windowElements;
// The length of the entire array // The length of the entire array
final int length; final int length;
// One past the end of the last element // One past the end of the last element (modulo length, can be zero)
final int windowTail; final int windowTail;
// The total number of elements to try to keep in 'elements' // The total number of elements to try to keep in 'elements'
final int windowCount; final int windowCount;

View file

@ -393,6 +393,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.2"
cross_cache:
dependency: transitive
description:
name: cross_cache
sha256: "007d0340c19d4d201192a3335c4034f4b79eae5ea53f90b69eeb5d239d9fbd1d"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cross_file: cross_file:
dependency: transitive dependency: transitive
description: description:
@ -542,23 +550,20 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.4.1" version: "3.4.1"
flutter_chat_types: flutter_chat_core:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_chat_types path: "../flutter_chat_ui/packages/flutter_chat_core"
sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9 relative: true
url: "https://pub.dev" source: path
source: hosted version: "2.1.2"
version: "3.6.2"
flutter_chat_ui: flutter_chat_ui:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "../flutter_chat_ui/packages/flutter_chat_ui"
ref: main relative: true
resolved-ref: d4b9d507d10f5d640156cacfd754f661f8c0f4c1 source: path
url: "https://gitlab.com/veilid/flutter-chat-ui.git" version: "2.1.3"
source: git
version: "1.6.14"
flutter_form_builder: flutter_form_builder:
dependency: "direct main" dependency: "direct main"
description: description:
@ -575,22 +580,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.21.2" version: "0.21.2"
flutter_link_previewer:
dependency: transitive
description:
name: flutter_link_previewer
sha256: "007069e60f42419fb59872beb7a3cc3ea21e9f1bdff5d40239f376fa62ca9f20"
url: "https://pub.dev"
source: hosted
version: "3.2.2"
flutter_linkify:
dependency: transitive
description:
name: flutter_linkify
sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -604,14 +593,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.5" version: "2.4.5"
flutter_parsed_text:
dependency: transitive
description:
name: flutter_parsed_text
sha256: "529cf5793b7acdf16ee0f97b158d0d4ba0bf06e7121ef180abe1a5b59e32c1e2"
url: "https://pub.dev"
source: hosted
version: "2.2.1"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -801,6 +782,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
idb_shim:
dependency: transitive
description:
name: idb_shim
sha256: d3dae2085f2dcc9d05b851331fddb66d57d3447ff800de9676b396795436e135
url: "https://pub.dev"
source: hosted
version: "2.6.5+1"
image: image:
dependency: "direct main" dependency: "direct main"
description: description:
@ -857,14 +846,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.9.4" version: "6.9.4"
linkify: keyboard_avoider:
dependency: transitive dependency: "direct main"
description: description:
name: linkify name: keyboard_avoider
sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" sha256: d2917bd52c6612bf8d1ff97f74049ddf3592a81d44e814f0e7b07dcfd245b75c
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "0.2.0"
lint_hard: lint_hard:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1073,14 +1062,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.0" version: "6.1.0"
photo_view:
dependency: transitive
description:
name: photo_view
sha256: "1fc3d970a91295fbd1364296575f854c9863f225505c28c46e0a03e48960c75e"
url: "https://pub.dev"
source: hosted
version: "0.15.0"
pinput: pinput:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1157,10 +1138,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: provider name: provider
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.2" version: "6.1.5"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@ -1305,6 +1286,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
scrollview_observer:
dependency: transitive
description:
name: scrollview_observer
sha256: "437c930927c5a3240ed2d40398f99d96eaca58f861817ff44f6d0c60113bcf9d"
url: "https://pub.dev"
source: hosted
version: "1.26.0"
searchable_listview: searchable_listview:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1314,6 +1303,14 @@ packages:
url: "https://gitlab.com/veilid/Searchable-Listview.git" url: "https://gitlab.com/veilid/Searchable-Listview.git"
source: git source: git
version: "2.16.0" version: "2.16.0"
sembast:
dependency: transitive
description:
name: sembast
sha256: d3f0d0ba501a5f1fd7d6c8532ee01385977c8a069c334635dae390d059ae3d6d
url: "https://pub.dev"
source: hosted
version: "3.8.5"
share_plus: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1774,7 +1771,7 @@ packages:
path: "../veilid/veilid-flutter" path: "../veilid/veilid-flutter"
relative: true relative: true
source: path source: path
version: "0.4.4" version: "0.4.6"
veilid_support: veilid_support:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1782,14 +1779,6 @@ packages:
relative: true relative: true
source: path source: path
version: "1.0.2+0" version: "1.0.2+0"
visibility_detector:
dependency: transitive
description:
name: visibility_detector
sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420
url: "https://pub.dev"
source: hosted
version: "0.4.0+2"
watcher: watcher:
dependency: transitive dependency: transitive
description: description:

View file

@ -1,11 +1,11 @@
name: veilidchat name: veilidchat
description: VeilidChat description: VeilidChat
publish_to: 'none' publish_to: "none"
version: 0.4.7+20 version: 0.4.7+20
environment: environment:
sdk: '>=3.2.0 <4.0.0' sdk: ">=3.2.0 <4.0.0"
flutter: '>=3.22.1' flutter: ">=3.22.1"
dependencies: dependencies:
accordion: ^2.6.0 accordion: ^2.6.0
@ -37,11 +37,16 @@ dependencies:
sdk: flutter sdk: flutter
flutter_animate: ^4.5.2 flutter_animate: ^4.5.2
flutter_bloc: ^9.1.0 flutter_bloc: ^9.1.0
flutter_chat_types: ^3.6.2 flutter_chat_core:
git:
url: https://gitlab.com/veilid/flutter-chat-ui.git
path: packages/flutter_chat_core
ref: veilidchat
flutter_chat_ui: flutter_chat_ui:
git: git:
url: https://gitlab.com/veilid/flutter-chat-ui.git url: https://gitlab.com/veilid/flutter-chat-ui.git
ref: main path: packages/flutter_chat_ui
ref: veilidchat
flutter_form_builder: ^10.0.1 flutter_form_builder: ^10.0.1
flutter_hooks: ^0.21.2 flutter_hooks: ^0.21.2
flutter_localizations: flutter_localizations:
@ -59,6 +64,7 @@ dependencies:
image: ^4.5.3 image: ^4.5.3
intl: ^0.19.0 intl: ^0.19.0
json_annotation: ^4.9.0 json_annotation: ^4.9.0
keyboard_avoider: ^0.2.0
loggy: ^2.0.3 loggy: ^2.0.3
meta: ^1.16.0 meta: ^1.16.0
mobile_scanner: ^6.0.7 mobile_scanner: ^6.0.7
@ -110,15 +116,17 @@ dependencies:
xterm: ^4.0.0 xterm: ^4.0.0
zxing2: ^0.2.3 zxing2: ^0.2.3
# dependency_overrides: dependency_overrides:
# async_tools: # async_tools:
# path: ../dart_async_tools # path: ../dart_async_tools
# bloc_advanced_tools: # bloc_advanced_tools:
# path: ../bloc_advanced_tools # path: ../bloc_advanced_tools
# searchable_listview: # searchable_listview:
# path: ../Searchable-Listview # path: ../Searchable-Listview
# flutter_chat_ui: flutter_chat_core:
# path: ../flutter_chat_ui path: ../flutter_chat_ui/packages/flutter_chat_core
flutter_chat_ui:
path: ../flutter_chat_ui/packages/flutter_chat_ui
dev_dependencies: dev_dependencies:
build_runner: ^2.4.15 build_runner: ^2.4.15
@ -131,20 +139,20 @@ flutter_native_splash:
color: "#8588D0" color: "#8588D0"
icons_launcher: icons_launcher:
image_path: 'assets/launcher/icon.png' image_path: "assets/launcher/icon.png"
platforms: platforms:
android: android:
enable: true enable: true
adaptive_background_color: '#ffffff' adaptive_background_color: "#ffffff"
adaptive_foreground_image: 'assets/launcher/icon.png' adaptive_foreground_image: "assets/launcher/icon.png"
adaptive_round_image: 'assets/launcher/icon.png' adaptive_round_image: "assets/launcher/icon.png"
ios: ios:
enable: true enable: true
web: web:
enable: true enable: true
macos: macos:
enable: true enable: true
image_path: 'assets/launcher/macos_icon.png' image_path: "assets/launcher/macos_icon.png"
windows: windows:
enable: true enable: true
linux: linux: