mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-07-21 21:58:46 -04:00
new chat widget
This commit is contained in:
parent
063eeb8d12
commit
1a9cca0667
44 changed files with 1904 additions and 981 deletions
|
@ -5,6 +5,7 @@
|
|||
- Fixed issue with Android 'back' button exiting the app (#331)
|
||||
- Deprecated accounts no longer crash application at startup
|
||||
- Simplify SingleContactMessagesCubit and MessageReconciliation
|
||||
- Update flutter_chat_ui to 2.0.0
|
||||
|
||||
## v0.4.7 ##
|
||||
- *Community Contributions*
|
||||
|
|
|
@ -313,5 +313,9 @@
|
|||
"info": "Info",
|
||||
"debug": "Debug",
|
||||
"trace": "Trace"
|
||||
},
|
||||
"date_formatter": {
|
||||
"just_now": "Just now",
|
||||
"yesterday": "Yesterday"
|
||||
}
|
||||
}
|
0
flutter_01.png
Normal file
0
flutter_01.png
Normal file
0
flutter_02.png
Normal file
0
flutter_02.png
Normal file
0
flutter_03.png
Normal file
0
flutter_03.png
Normal file
|
@ -15,8 +15,9 @@ part 'local_account.freezed.dart';
|
|||
// and the identitySecretKey optionally encrypted by an unlock code
|
||||
// This is the root of the account information tree for VeilidChat
|
||||
//
|
||||
@freezed
|
||||
sealed class LocalAccount with _$LocalAccount {
|
||||
@Freezed(toJson: true)
|
||||
abstract class LocalAccount with _$LocalAccount {
|
||||
@JsonSerializable()
|
||||
const factory LocalAccount({
|
||||
// The super identity key record for the account,
|
||||
// containing the publicKey in the currentIdentity
|
||||
|
|
|
@ -153,6 +153,7 @@ class _$LocalAccountCopyWithImpl<$Res> implements $LocalAccountCopyWith<$Res> {
|
|||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
class _LocalAccount implements LocalAccount {
|
||||
const _LocalAccount(
|
||||
|
@ -162,8 +163,6 @@ class _LocalAccount implements LocalAccount {
|
|||
required this.biometricsEnabled,
|
||||
required this.hiddenAccount,
|
||||
required this.name});
|
||||
factory _LocalAccount.fromJson(Map<String, dynamic> json) =>
|
||||
_$LocalAccountFromJson(json);
|
||||
|
||||
// The super identity key record for the account,
|
||||
// containing the publicKey in the currentIdentity
|
||||
|
|
|
@ -8,8 +8,9 @@ part 'user_login.g.dart';
|
|||
// Represents a currently logged in account
|
||||
// User logins are stored in the user_logins tablestore table
|
||||
// indexed by the accountSuperIdentityRecordKey
|
||||
@freezed
|
||||
@Freezed(toJson: true)
|
||||
sealed class UserLogin with _$UserLogin {
|
||||
@JsonSerializable()
|
||||
const factory UserLogin({
|
||||
// SuperIdentity record key for the user
|
||||
// used to index the local accounts table
|
||||
|
|
|
@ -124,6 +124,7 @@ class _$UserLoginCopyWithImpl<$Res> implements $UserLoginCopyWith<$Res> {
|
|||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
class _UserLogin implements UserLogin {
|
||||
const _UserLogin(
|
||||
|
@ -131,8 +132,6 @@ class _UserLogin implements UserLogin {
|
|||
required this.identitySecret,
|
||||
required this.accountRecordInfo,
|
||||
required this.lastActive});
|
||||
factory _UserLogin.fromJson(Map<String, dynamic> json) =>
|
||||
_$UserLoginFromJson(json);
|
||||
|
||||
// SuperIdentity record key for the user
|
||||
// used to index the local accounts table
|
||||
|
|
|
@ -249,7 +249,6 @@ class _EditAccountPageState extends WindowSetupState<EditAccountPage> {
|
|||
final displayModalHUD = _isInAsyncCall;
|
||||
|
||||
return StyledScaffold(
|
||||
// resizeToAvoidBottomInset: false,
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(translate('edit_account_page.titlebar')),
|
||||
leading: Navigator.canPop(context)
|
||||
|
|
|
@ -163,7 +163,6 @@ class _ShowRecoveryKeyPageState extends WindowSetupState<ShowRecoveryKeyPage> {
|
|||
final displayModalHUD = _isInAsyncCall;
|
||||
|
||||
return StyledScaffold(
|
||||
// resizeToAvoidBottomInset: false,
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(translate('show_recovery_key_page.titlebar')),
|
||||
actions: [
|
||||
|
|
|
@ -4,11 +4,8 @@ import 'dart:typed_data';
|
|||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
import 'package:flutter_chat_core/flutter_chat_core.dart' as core;
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
|
@ -19,6 +16,7 @@ import '../../tools/tools.dart';
|
|||
import '../models/chat_component_state.dart';
|
||||
import '../models/message_state.dart';
|
||||
import '../models/window_state.dart';
|
||||
import '../views/chat_component_widget.dart';
|
||||
import 'cubits.dart';
|
||||
|
||||
const metadataKeyIdentityPublicKey = 'identityPublicKey';
|
||||
|
@ -39,15 +37,12 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
_contactListCubit = contactListCubit,
|
||||
_conversationCubits = conversationCubits,
|
||||
_messagesCubit = messagesCubit,
|
||||
super(ChatComponentState(
|
||||
chatKey: GlobalKey<ChatState>(),
|
||||
scrollController: AutoScrollController(),
|
||||
textEditingController: InputTextFieldController(),
|
||||
super(const ChatComponentState(
|
||||
localUser: null,
|
||||
remoteUsers: const IMap.empty(),
|
||||
historicalRemoteUsers: const IMap.empty(),
|
||||
unknownUsers: const IMap.empty(),
|
||||
messageWindow: const AsyncLoading(),
|
||||
remoteUsers: IMap.empty(),
|
||||
historicalRemoteUsers: IMap.empty(),
|
||||
unknownUsers: IMap.empty(),
|
||||
messageWindow: AsyncLoading(),
|
||||
title: '',
|
||||
)) {
|
||||
// Immediate Init
|
||||
|
@ -102,6 +97,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
await _accountRecordSubscription.cancel();
|
||||
await _messagesSubscription.cancel();
|
||||
await _conversationSubscriptions.values.map((v) => v.cancel()).wait;
|
||||
|
||||
await super.close();
|
||||
}
|
||||
|
||||
|
@ -122,32 +118,15 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
}
|
||||
|
||||
// Send a message
|
||||
void sendMessage(types.PartialText message) {
|
||||
final text = message.text;
|
||||
|
||||
final replyId = (message.repliedMessage != null)
|
||||
? base64UrlNoPadDecode(message.repliedMessage!.id)
|
||||
void sendMessage(
|
||||
{required String text,
|
||||
String? replyToMessageId,
|
||||
Timestamp? expiration,
|
||||
int? viewLimit,
|
||||
List<proto.Attachment>? attachments}) {
|
||||
final replyId = (replyToMessageId != null)
|
||||
? base64UrlNoPadDecode(replyToMessageId)
|
||||
: null;
|
||||
Timestamp? expiration;
|
||||
int? viewLimit;
|
||||
List<proto.Attachment>? attachments;
|
||||
final metadata = message.metadata;
|
||||
if (metadata != null) {
|
||||
final expirationValue =
|
||||
metadata[metadataKeyExpirationDuration] as TimestampDuration?;
|
||||
if (expirationValue != null) {
|
||||
expiration = Veilid.instance.now().offset(expirationValue);
|
||||
}
|
||||
final viewLimitValue = metadata[metadataKeyViewLimit] as int?;
|
||||
if (viewLimitValue != null) {
|
||||
viewLimit = viewLimitValue;
|
||||
}
|
||||
final attachmentsValue =
|
||||
metadata[metadataKeyAttachments] as List<proto.Attachment>?;
|
||||
if (attachmentsValue != null) {
|
||||
attachments = attachmentsValue;
|
||||
}
|
||||
}
|
||||
|
||||
_addTextMessage(
|
||||
text: text,
|
||||
|
@ -172,9 +151,9 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
emit(state.copyWith(localUser: null));
|
||||
return;
|
||||
}
|
||||
final localUser = types.User(
|
||||
final localUser = core.User(
|
||||
id: _localUserIdentityKey.toString(),
|
||||
firstName: account.profile.name,
|
||||
name: account.profile.name,
|
||||
metadata: {metadataKeyIdentityPublicKey: _localUserIdentityKey});
|
||||
emit(state.copyWith(localUser: localUser));
|
||||
}
|
||||
|
@ -199,11 +178,12 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
// Don't change user information on loading state
|
||||
return;
|
||||
}
|
||||
|
||||
final remoteUser =
|
||||
_convertRemoteUser(remoteIdentityPublicKey, activeConversationState);
|
||||
|
||||
emit(_updateTitle(state.copyWith(
|
||||
remoteUsers: state.remoteUsers.add(
|
||||
remoteIdentityPublicKey,
|
||||
_convertRemoteUser(
|
||||
remoteIdentityPublicKey, activeConversationState)))));
|
||||
remoteUsers: state.remoteUsers.add(remoteUser.id, remoteUser))));
|
||||
}
|
||||
|
||||
static ChatComponentState _updateTitle(ChatComponentState currentState) {
|
||||
|
@ -212,13 +192,13 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
}
|
||||
if (currentState.remoteUsers.length == 1) {
|
||||
final remoteUser = currentState.remoteUsers.values.first;
|
||||
return currentState.copyWith(title: remoteUser.firstName ?? '<unnamed>');
|
||||
return currentState.copyWith(title: remoteUser.name ?? '<unnamed>');
|
||||
}
|
||||
return currentState.copyWith(
|
||||
title: '<group chat with ${currentState.remoteUsers.length} users>');
|
||||
}
|
||||
|
||||
types.User _convertRemoteUser(TypedKey remoteIdentityPublicKey,
|
||||
core.User _convertRemoteUser(TypedKey remoteIdentityPublicKey,
|
||||
ActiveConversationState activeConversationState) {
|
||||
// See if we have a contact for this remote user
|
||||
final contacts = _contactListCubit.state.state.asData?.value;
|
||||
|
@ -227,24 +207,23 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
x.value.identityPublicKey.toVeilid() == remoteIdentityPublicKey);
|
||||
if (contactIdx != -1) {
|
||||
final contact = contacts[contactIdx].value;
|
||||
return types.User(
|
||||
return core.User(
|
||||
id: remoteIdentityPublicKey.toString(),
|
||||
firstName: contact.displayName,
|
||||
name: contact.displayName,
|
||||
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
|
||||
}
|
||||
}
|
||||
|
||||
return types.User(
|
||||
return core.User(
|
||||
id: remoteIdentityPublicKey.toString(),
|
||||
firstName: activeConversationState.remoteConversation?.profile.name ??
|
||||
name: activeConversationState.remoteConversation?.profile.name ??
|
||||
'<unnamed>',
|
||||
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
|
||||
}
|
||||
|
||||
types.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) =>
|
||||
types.User(
|
||||
core.User _convertUnknownUser(TypedKey remoteIdentityPublicKey) => core.User(
|
||||
id: remoteIdentityPublicKey.toString(),
|
||||
firstName: '<$remoteIdentityPublicKey>',
|
||||
name: '<$remoteIdentityPublicKey>',
|
||||
metadata: {metadataKeyIdentityPublicKey: remoteIdentityPublicKey});
|
||||
|
||||
Future<void> _updateConversationSubscriptions() async {
|
||||
|
@ -267,16 +246,17 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
|
||||
final activeConversationState = cc.state.asData?.value;
|
||||
if (activeConversationState != null) {
|
||||
currentRemoteUsersState = currentRemoteUsersState.add(
|
||||
remoteIdentityPublicKey,
|
||||
_convertRemoteUser(
|
||||
remoteIdentityPublicKey, activeConversationState));
|
||||
final remoteUser = _convertRemoteUser(
|
||||
remoteIdentityPublicKey, activeConversationState);
|
||||
currentRemoteUsersState =
|
||||
currentRemoteUsersState.add(remoteUser.id, remoteUser);
|
||||
}
|
||||
}
|
||||
// Purge remote users we didn't see in the cubit list any more
|
||||
final cancels = <Future<void>>[];
|
||||
for (final deadUser in existing) {
|
||||
currentRemoteUsersState = currentRemoteUsersState.remove(deadUser);
|
||||
currentRemoteUsersState =
|
||||
currentRemoteUsersState.remove(deadUser.toString());
|
||||
cancels.add(_conversationSubscriptions.remove(deadUser)!.cancel());
|
||||
}
|
||||
await cancels.wait;
|
||||
|
@ -285,63 +265,76 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
emit(_updateTitle(state.copyWith(remoteUsers: currentRemoteUsersState)));
|
||||
}
|
||||
|
||||
(ChatComponentState, types.Message?) _messageStateToChatMessage(
|
||||
(ChatComponentState, core.Message?) _messageStateToChatMessage(
|
||||
ChatComponentState currentState, MessageState message) {
|
||||
final authorIdentityPublicKey = message.content.author.toVeilid();
|
||||
late final types.User author;
|
||||
final authorUserId = authorIdentityPublicKey.toString();
|
||||
|
||||
late final core.User author;
|
||||
if (authorIdentityPublicKey == _localUserIdentityKey &&
|
||||
currentState.localUser != null) {
|
||||
author = currentState.localUser!;
|
||||
} else {
|
||||
final remoteUser = currentState.remoteUsers[authorIdentityPublicKey];
|
||||
final remoteUser = currentState.remoteUsers[authorUserId];
|
||||
if (remoteUser != null) {
|
||||
author = remoteUser;
|
||||
} else {
|
||||
final historicalRemoteUser =
|
||||
currentState.historicalRemoteUsers[authorIdentityPublicKey];
|
||||
currentState.historicalRemoteUsers[authorUserId];
|
||||
if (historicalRemoteUser != null) {
|
||||
author = historicalRemoteUser;
|
||||
} else {
|
||||
final unknownRemoteUser =
|
||||
currentState.unknownUsers[authorIdentityPublicKey];
|
||||
final unknownRemoteUser = currentState.unknownUsers[authorUserId];
|
||||
if (unknownRemoteUser != null) {
|
||||
author = unknownRemoteUser;
|
||||
} else {
|
||||
final unknownUser = _convertUnknownUser(authorIdentityPublicKey);
|
||||
currentState = currentState.copyWith(
|
||||
unknownUsers: currentState.unknownUsers
|
||||
.add(authorIdentityPublicKey, unknownUser));
|
||||
unknownUsers:
|
||||
currentState.unknownUsers.add(authorUserId, unknownUser));
|
||||
author = unknownUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
types.Status? status;
|
||||
if (message.sendState != null) {
|
||||
assert(author.id == _localUserIdentityKey.toString(),
|
||||
'send state should only be on sent messages');
|
||||
switch (message.sendState!) {
|
||||
case MessageSendState.sending:
|
||||
status = types.Status.sending;
|
||||
case MessageSendState.sent:
|
||||
status = types.Status.sent;
|
||||
case MessageSendState.delivered:
|
||||
status = types.Status.delivered;
|
||||
}
|
||||
}
|
||||
// types.Status? status;
|
||||
// if (message.sendState != null) {
|
||||
// assert(author.id == _localUserIdentityKey.toString(),
|
||||
// 'send state should only be on sent messages');
|
||||
// switch (message.sendState!) {
|
||||
// case MessageSendState.sending:
|
||||
// status = types.Status.sending;
|
||||
// case MessageSendState.sent:
|
||||
// status = types.Status.sent;
|
||||
// case MessageSendState.delivered:
|
||||
// status = types.Status.delivered;
|
||||
// }
|
||||
// }
|
||||
|
||||
final reconciledAt = message.reconciledTimestamp == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(
|
||||
message.reconciledTimestamp!.value.toInt());
|
||||
|
||||
// print('message seqid: ${message.seqId}');
|
||||
|
||||
switch (message.content.whichKind()) {
|
||||
case proto.Message_Kind.text:
|
||||
final contextText = message.content.text;
|
||||
final textMessage = types.TextMessage(
|
||||
author: author,
|
||||
createdAt:
|
||||
(message.sentTimestamp.value ~/ BigInt.from(1000)).toInt(),
|
||||
id: message.content.authorUniqueIdString,
|
||||
text: contextText.text,
|
||||
showStatus: status != null,
|
||||
status: status);
|
||||
final reconciledId = message.content.authorUniqueIdString;
|
||||
final contentText = message.content.text;
|
||||
final textMessage = core.TextMessage(
|
||||
authorId: author.id,
|
||||
createdAt: DateTime.fromMicrosecondsSinceEpoch(
|
||||
message.sentTimestamp.value.toInt()),
|
||||
sentAt: reconciledAt,
|
||||
id: reconciledId,
|
||||
text: '${contentText.text} (${message.seqId})',
|
||||
//text: contentText.text,
|
||||
metadata: {
|
||||
kSeqId: message.seqId,
|
||||
if (core.isOnlyEmoji(contentText.text)) 'isOnlyEmoji': true,
|
||||
});
|
||||
return (currentState, textMessage);
|
||||
case proto.Message_Kind.secret:
|
||||
case proto.Message_Kind.delete:
|
||||
|
@ -375,7 +368,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
final messagesState = avMessagesState.asData!.value;
|
||||
|
||||
// Convert protobuf messages to chat messages
|
||||
final chatMessages = <types.Message>[];
|
||||
final chatMessages = <core.Message>[];
|
||||
final tsSet = <String>{};
|
||||
for (final message in messagesState.window) {
|
||||
final (newState, chatMessage) =
|
||||
|
@ -390,11 +383,11 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
|
|||
// '\nChatMessages:\n$chatMessages'
|
||||
);
|
||||
} else {
|
||||
chatMessages.insert(0, chatMessage);
|
||||
chatMessages.add(chatMessage);
|
||||
}
|
||||
}
|
||||
return currentState.copyWith(
|
||||
messageWindow: AsyncValue.data(WindowState<types.Message>(
|
||||
messageWindow: AsyncValue.data(WindowState<core.Message>(
|
||||
window: chatMessages.toIList(),
|
||||
length: messagesState.length,
|
||||
windowTail: messagesState.windowTail,
|
||||
|
|
|
@ -3,6 +3,8 @@ import 'dart:async';
|
|||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.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 '../../account_manager/account_manager.dart';
|
||||
|
@ -11,9 +13,12 @@ import '../../tools/tools.dart';
|
|||
import '../models/models.dart';
|
||||
import 'reconciliation/reconciliation.dart';
|
||||
|
||||
const _sfSendMessageTag = 'sfSendMessageTag';
|
||||
|
||||
class RenderStateElement {
|
||||
RenderStateElement(
|
||||
{required this.message,
|
||||
{required this.seqId,
|
||||
required this.message,
|
||||
required this.isLocal,
|
||||
this.reconciledTimestamp,
|
||||
this.sent = false,
|
||||
|
@ -36,6 +41,7 @@ class RenderStateElement {
|
|||
return null;
|
||||
}
|
||||
|
||||
int seqId;
|
||||
proto.Message message;
|
||||
bool isLocal;
|
||||
Timestamp? reconciledTimestamp;
|
||||
|
@ -71,6 +77,8 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
Future<void> close() async {
|
||||
await _initWait();
|
||||
|
||||
await serialFutureClose((this, _sfSendMessageTag));
|
||||
|
||||
await _commandController.close();
|
||||
await _commandRunnerFut;
|
||||
await _unsentMessagesQueue.close();
|
||||
|
@ -309,9 +317,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
|
||||
// Async process to send messages in the background
|
||||
Future<void> _processUnsentMessages(IList<proto.Message> messages) async {
|
||||
// _sendingMessages = messages;
|
||||
|
||||
// _renderState();
|
||||
try {
|
||||
await _sentMessagesDHTLog!.operateAppendEventual((writer) async {
|
||||
// Get the previous message if we have one
|
||||
|
@ -337,8 +342,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
} on Exception catch (e, st) {
|
||||
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
|
||||
|
@ -349,8 +352,9 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
|
||||
// Get all sent messages that are still offline
|
||||
//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 (reconciledMessages == null) {
|
||||
|
@ -374,8 +378,19 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
// values: unsentMessages,
|
||||
// );
|
||||
|
||||
// List of all rendered state elements that we will turn into
|
||||
// message states
|
||||
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>{};
|
||||
|
||||
var seqId = (reconciledMessages.windowTail == 0
|
||||
? reconciledMessages.length
|
||||
: reconciledMessages.windowTail) -
|
||||
reconciledMessages.windowElements.length;
|
||||
for (final m in reconciledMessages.windowElements) {
|
||||
final isLocal =
|
||||
m.content.author.toVeilid() == _accountInfo.identityTypedPublicKey;
|
||||
|
@ -387,33 +402,44 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
final sent = isLocal;
|
||||
final sentOffline = false; //
|
||||
|
||||
if (renderedIds.contains(m.content.authorUniqueIdString)) {
|
||||
seqId++;
|
||||
continue;
|
||||
}
|
||||
renderedElements.add(RenderStateElement(
|
||||
seqId: seqId,
|
||||
message: m.content,
|
||||
isLocal: isLocal,
|
||||
reconciledTimestamp: reconciledTimestamp,
|
||||
sent: sent,
|
||||
sentOffline: sentOffline,
|
||||
));
|
||||
|
||||
renderedIds.add(m.content.authorUniqueIdString);
|
||||
seqId++;
|
||||
}
|
||||
|
||||
// Render in-flight messages at the bottom
|
||||
// for (final m in _sendingMessages) {
|
||||
//
|
||||
// for (final m in unsentMessages) {
|
||||
// if (renderedIds.contains(m.authorUniqueIdString)) {
|
||||
// seqId++;
|
||||
// continue;
|
||||
// }
|
||||
// renderedElements.add(RenderStateElement(
|
||||
// seqId: seqId,
|
||||
// message: m,
|
||||
// isLocal: true,
|
||||
// sent: true,
|
||||
// sentOffline: true,
|
||||
// ));
|
||||
// renderedIds.add(m.authorUniqueIdString);
|
||||
// seqId++;
|
||||
// }
|
||||
|
||||
// Render the state
|
||||
final messages = renderedElements
|
||||
.map((x) => MessageState(
|
||||
seqId: x.seqId,
|
||||
content: x.message,
|
||||
sentTimestamp: Timestamp.fromInt64(x.message.timestamp),
|
||||
reconciledTimestamp: x.reconciledTimestamp,
|
||||
|
@ -431,20 +457,26 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
|
||||
void _sendMessage({required proto.Message message}) {
|
||||
// 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
|
||||
..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) {
|
||||
throw const FormatException('message is too long');
|
||||
}
|
||||
|
||||
// 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
|
||||
_renderState();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _commandRunner() async {
|
||||
|
@ -487,7 +519,6 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
late final MessageReconciliation _reconciliation;
|
||||
|
||||
late final PersistentQueue<proto.Message> _unsentMessagesQueue;
|
||||
// IList<proto.Message> _sendingMessages = const IList.empty();
|
||||
StreamSubscription<void>? _sentSubscription;
|
||||
StreamSubscription<void>? _rcvdSubscription;
|
||||
StreamSubscription<TableDBArrayProtobufBusyState<proto.ReconciledMessage>>?
|
||||
|
@ -496,4 +527,5 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
|
|||
late final Future<void> _commandRunnerFut;
|
||||
|
||||
final _sspRemoteConversationRecordKey = SingleStateProcessor<TypedKey?>();
|
||||
final _uuidGen = const UuidV4();
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
import 'package:flutter/material.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:flutter_chat_core/flutter_chat_core.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';
|
||||
|
||||
|
@ -16,26 +11,16 @@ part 'chat_component_state.freezed.dart';
|
|||
sealed class ChatComponentState with _$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
|
||||
required User? localUser,
|
||||
// Active remote users
|
||||
required IMap<TypedKey, User> remoteUsers,
|
||||
required IMap<UserID, User> remoteUsers,
|
||||
// Historical remote users
|
||||
required IMap<TypedKey, User> historicalRemoteUsers,
|
||||
required IMap<UserID, User> historicalRemoteUsers,
|
||||
// Unknown users
|
||||
required IMap<TypedKey, User> unknownUsers,
|
||||
required IMap<UserID, User> unknownUsers,
|
||||
// Messages state
|
||||
required AsyncValue<WindowState<Message>> messageWindow,
|
||||
// Title of the chat
|
||||
required String title}) = _ChatComponentState;
|
||||
}
|
||||
|
||||
extension ChatComponentStateExt on ChatComponentState {
|
||||
//
|
||||
}
|
||||
|
|
|
@ -15,15 +15,11 @@ T _$identity<T>(T value) => value;
|
|||
|
||||
/// @nodoc
|
||||
mixin _$ChatComponentState {
|
||||
// GlobalKey for the chat
|
||||
GlobalKey<ChatState> get chatKey; // ScrollController for the chat
|
||||
AutoScrollController
|
||||
get scrollController; // TextEditingController for the chat
|
||||
InputTextFieldController get textEditingController; // Local user
|
||||
// Local user
|
||||
User? get localUser; // Active remote users
|
||||
IMap<TypedKey, User> get remoteUsers; // Historical remote users
|
||||
IMap<TypedKey, User> get historicalRemoteUsers; // Unknown users
|
||||
IMap<TypedKey, User> get unknownUsers; // Messages state
|
||||
IMap<UserID, User> get remoteUsers; // Historical remote users
|
||||
IMap<UserID, User> get historicalRemoteUsers; // Unknown users
|
||||
IMap<UserID, User> get unknownUsers; // Messages state
|
||||
AsyncValue<WindowState<Message>> get messageWindow; // Title of the chat
|
||||
String get title;
|
||||
|
||||
|
@ -40,11 +36,6 @@ mixin _$ChatComponentState {
|
|||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
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) ||
|
||||
other.localUser == localUser) &&
|
||||
(identical(other.remoteUsers, remoteUsers) ||
|
||||
|
@ -59,21 +50,12 @@ mixin _$ChatComponentState {
|
|||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
chatKey,
|
||||
scrollController,
|
||||
textEditingController,
|
||||
localUser,
|
||||
remoteUsers,
|
||||
historicalRemoteUsers,
|
||||
unknownUsers,
|
||||
messageWindow,
|
||||
title);
|
||||
int get hashCode => Object.hash(runtimeType, localUser, remoteUsers,
|
||||
historicalRemoteUsers, unknownUsers, messageWindow, title);
|
||||
|
||||
@override
|
||||
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;
|
||||
@useResult
|
||||
$Res call(
|
||||
{GlobalKey<ChatState> chatKey,
|
||||
AutoScrollController scrollController,
|
||||
InputTextFieldController textEditingController,
|
||||
User? localUser,
|
||||
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
||||
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
|
||||
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
|
||||
{User? localUser,
|
||||
IMap<String, User> remoteUsers,
|
||||
IMap<String, User> historicalRemoteUsers,
|
||||
IMap<String, User> unknownUsers,
|
||||
AsyncValue<WindowState<Message>> messageWindow,
|
||||
String title});
|
||||
|
||||
$UserCopyWith<$Res>? get localUser;
|
||||
$AsyncValueCopyWith<WindowState<Message>, $Res> get messageWindow;
|
||||
}
|
||||
|
||||
|
@ -110,9 +90,6 @@ class _$ChatComponentStateCopyWithImpl<$Res>
|
|||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? chatKey = null,
|
||||
Object? scrollController = null,
|
||||
Object? textEditingController = null,
|
||||
Object? localUser = freezed,
|
||||
Object? remoteUsers = null,
|
||||
Object? historicalRemoteUsers = null,
|
||||
|
@ -121,18 +98,6 @@ class _$ChatComponentStateCopyWithImpl<$Res>
|
|||
Object? title = null,
|
||||
}) {
|
||||
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
|
||||
? _self.localUser
|
||||
: localUser // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -140,15 +105,15 @@ class _$ChatComponentStateCopyWithImpl<$Res>
|
|||
remoteUsers: null == remoteUsers
|
||||
? _self.remoteUsers!
|
||||
: remoteUsers // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<Typed<FixedEncodedString43>, User>,
|
||||
as IMap<String, User>,
|
||||
historicalRemoteUsers: null == historicalRemoteUsers
|
||||
? _self.historicalRemoteUsers!
|
||||
: historicalRemoteUsers // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<Typed<FixedEncodedString43>, User>,
|
||||
as IMap<String, User>,
|
||||
unknownUsers: null == unknownUsers
|
||||
? _self.unknownUsers!
|
||||
: unknownUsers // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<Typed<FixedEncodedString43>, User>,
|
||||
as IMap<String, User>,
|
||||
messageWindow: null == messageWindow
|
||||
? _self.messageWindow
|
||||
: 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
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
@ -176,37 +155,25 @@ class _$ChatComponentStateCopyWithImpl<$Res>
|
|||
|
||||
class _ChatComponentState implements ChatComponentState {
|
||||
const _ChatComponentState(
|
||||
{required this.chatKey,
|
||||
required this.scrollController,
|
||||
required this.textEditingController,
|
||||
required this.localUser,
|
||||
{required this.localUser,
|
||||
required this.remoteUsers,
|
||||
required this.historicalRemoteUsers,
|
||||
required this.unknownUsers,
|
||||
required this.messageWindow,
|
||||
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
|
||||
@override
|
||||
final User? localUser;
|
||||
// Active remote users
|
||||
@override
|
||||
final IMap<Typed<FixedEncodedString43>, User> remoteUsers;
|
||||
final IMap<String, User> remoteUsers;
|
||||
// Historical remote users
|
||||
@override
|
||||
final IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers;
|
||||
final IMap<String, User> historicalRemoteUsers;
|
||||
// Unknown users
|
||||
@override
|
||||
final IMap<Typed<FixedEncodedString43>, User> unknownUsers;
|
||||
final IMap<String, User> unknownUsers;
|
||||
// Messages state
|
||||
@override
|
||||
final AsyncValue<WindowState<Message>> messageWindow;
|
||||
|
@ -227,11 +194,6 @@ class _ChatComponentState implements ChatComponentState {
|
|||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
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) ||
|
||||
other.localUser == localUser) &&
|
||||
(identical(other.remoteUsers, remoteUsers) ||
|
||||
|
@ -246,21 +208,12 @@ class _ChatComponentState implements ChatComponentState {
|
|||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
chatKey,
|
||||
scrollController,
|
||||
textEditingController,
|
||||
localUser,
|
||||
remoteUsers,
|
||||
historicalRemoteUsers,
|
||||
unknownUsers,
|
||||
messageWindow,
|
||||
title);
|
||||
int get hashCode => Object.hash(runtimeType, localUser, remoteUsers,
|
||||
historicalRemoteUsers, unknownUsers, messageWindow, title);
|
||||
|
||||
@override
|
||||
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
|
||||
@useResult
|
||||
$Res call(
|
||||
{GlobalKey<ChatState> chatKey,
|
||||
AutoScrollController scrollController,
|
||||
InputTextFieldController textEditingController,
|
||||
User? localUser,
|
||||
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
|
||||
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
|
||||
IMap<Typed<FixedEncodedString43>, User> unknownUsers,
|
||||
{User? localUser,
|
||||
IMap<String, User> remoteUsers,
|
||||
IMap<String, User> historicalRemoteUsers,
|
||||
IMap<String, User> unknownUsers,
|
||||
AsyncValue<WindowState<Message>> messageWindow,
|
||||
String title});
|
||||
|
||||
@override
|
||||
$UserCopyWith<$Res>? get localUser;
|
||||
@override
|
||||
$AsyncValueCopyWith<WindowState<Message>, $Res> get messageWindow;
|
||||
}
|
||||
|
@ -300,9 +252,6 @@ class __$ChatComponentStateCopyWithImpl<$Res>
|
|||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? chatKey = null,
|
||||
Object? scrollController = null,
|
||||
Object? textEditingController = null,
|
||||
Object? localUser = freezed,
|
||||
Object? remoteUsers = null,
|
||||
Object? historicalRemoteUsers = null,
|
||||
|
@ -311,18 +260,6 @@ class __$ChatComponentStateCopyWithImpl<$Res>
|
|||
Object? title = null,
|
||||
}) {
|
||||
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
|
||||
? _self.localUser
|
||||
: localUser // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -330,15 +267,15 @@ class __$ChatComponentStateCopyWithImpl<$Res>
|
|||
remoteUsers: null == remoteUsers
|
||||
? _self.remoteUsers
|
||||
: remoteUsers // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<Typed<FixedEncodedString43>, User>,
|
||||
as IMap<String, User>,
|
||||
historicalRemoteUsers: null == historicalRemoteUsers
|
||||
? _self.historicalRemoteUsers
|
||||
: historicalRemoteUsers // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<Typed<FixedEncodedString43>, User>,
|
||||
as IMap<String, User>,
|
||||
unknownUsers: null == unknownUsers
|
||||
? _self.unknownUsers
|
||||
: unknownUsers // ignore: cast_nullable_to_non_nullable
|
||||
as IMap<Typed<FixedEncodedString43>, User>,
|
||||
as IMap<String, User>,
|
||||
messageWindow: null == messageWindow
|
||||
? _self.messageWindow
|
||||
: 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
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
|
|
|
@ -25,7 +25,10 @@ enum MessageSendState {
|
|||
|
||||
@freezed
|
||||
sealed class MessageState with _$MessageState {
|
||||
@JsonSerializable()
|
||||
const factory MessageState({
|
||||
// Sequence number of the message for display purposes
|
||||
required int seqId,
|
||||
// Content of the message
|
||||
@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
|
||||
required proto.Message content,
|
||||
|
|
|
@ -15,7 +15,8 @@ T _$identity<T>(T value) => value;
|
|||
|
||||
/// @nodoc
|
||||
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)
|
||||
proto.Message get content; // Sent timestamp
|
||||
Timestamp get sentTimestamp; // Reconciled timestamp
|
||||
|
@ -37,6 +38,7 @@ mixin _$MessageState implements DiagnosticableTreeMixin {
|
|||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'MessageState'))
|
||||
..add(DiagnosticsProperty('seqId', seqId))
|
||||
..add(DiagnosticsProperty('content', content))
|
||||
..add(DiagnosticsProperty('sentTimestamp', sentTimestamp))
|
||||
..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp))
|
||||
|
@ -48,6 +50,7 @@ mixin _$MessageState implements DiagnosticableTreeMixin {
|
|||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is MessageState &&
|
||||
(identical(other.seqId, seqId) || other.seqId == seqId) &&
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.sentTimestamp, sentTimestamp) ||
|
||||
other.sentTimestamp == sentTimestamp) &&
|
||||
|
@ -59,12 +62,12 @@ mixin _$MessageState implements DiagnosticableTreeMixin {
|
|||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, content, sentTimestamp, reconciledTimestamp, sendState);
|
||||
int get hashCode => Object.hash(runtimeType, seqId, content, sentTimestamp,
|
||||
reconciledTimestamp, sendState);
|
||||
|
||||
@override
|
||||
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;
|
||||
@useResult
|
||||
$Res call(
|
||||
{@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
|
||||
{int seqId,
|
||||
@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
|
||||
proto.Message content,
|
||||
Timestamp sentTimestamp,
|
||||
Timestamp? reconciledTimestamp,
|
||||
|
@ -94,12 +98,17 @@ class _$MessageStateCopyWithImpl<$Res> implements $MessageStateCopyWith<$Res> {
|
|||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? seqId = null,
|
||||
Object? content = null,
|
||||
Object? sentTimestamp = null,
|
||||
Object? reconciledTimestamp = freezed,
|
||||
Object? sendState = freezed,
|
||||
}) {
|
||||
return _then(_self.copyWith(
|
||||
seqId: null == seqId
|
||||
? _self.seqId
|
||||
: seqId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
content: null == content
|
||||
? _self.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
|
@ -121,10 +130,12 @@ class _$MessageStateCopyWithImpl<$Res> implements $MessageStateCopyWith<$Res> {
|
|||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
class _MessageState with DiagnosticableTreeMixin implements MessageState {
|
||||
const _MessageState(
|
||||
{@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
|
||||
{required this.seqId,
|
||||
@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
|
||||
required this.content,
|
||||
required this.sentTimestamp,
|
||||
required this.reconciledTimestamp,
|
||||
|
@ -132,6 +143,9 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState {
|
|||
factory _MessageState.fromJson(Map<String, dynamic> json) =>
|
||||
_$MessageStateFromJson(json);
|
||||
|
||||
// Sequence number of the message for display purposes
|
||||
@override
|
||||
final int seqId;
|
||||
// Content of the message
|
||||
@override
|
||||
@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
|
||||
|
@ -165,6 +179,7 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState {
|
|||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'MessageState'))
|
||||
..add(DiagnosticsProperty('seqId', seqId))
|
||||
..add(DiagnosticsProperty('content', content))
|
||||
..add(DiagnosticsProperty('sentTimestamp', sentTimestamp))
|
||||
..add(DiagnosticsProperty('reconciledTimestamp', reconciledTimestamp))
|
||||
|
@ -176,6 +191,7 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState {
|
|||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _MessageState &&
|
||||
(identical(other.seqId, seqId) || other.seqId == seqId) &&
|
||||
(identical(other.content, content) || other.content == content) &&
|
||||
(identical(other.sentTimestamp, sentTimestamp) ||
|
||||
other.sentTimestamp == sentTimestamp) &&
|
||||
|
@ -187,12 +203,12 @@ class _MessageState with DiagnosticableTreeMixin implements MessageState {
|
|||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, content, sentTimestamp, reconciledTimestamp, sendState);
|
||||
int get hashCode => Object.hash(runtimeType, seqId, content, sentTimestamp,
|
||||
reconciledTimestamp, sendState);
|
||||
|
||||
@override
|
||||
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
|
||||
@useResult
|
||||
$Res call(
|
||||
{@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
|
||||
{int seqId,
|
||||
@JsonKey(fromJson: messageFromJson, toJson: messageToJson)
|
||||
proto.Message content,
|
||||
Timestamp sentTimestamp,
|
||||
Timestamp? reconciledTimestamp,
|
||||
|
@ -225,12 +242,17 @@ class __$MessageStateCopyWithImpl<$Res>
|
|||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$Res call({
|
||||
Object? seqId = null,
|
||||
Object? content = null,
|
||||
Object? sentTimestamp = null,
|
||||
Object? reconciledTimestamp = freezed,
|
||||
Object? sendState = freezed,
|
||||
}) {
|
||||
return _then(_MessageState(
|
||||
seqId: null == seqId
|
||||
? _self.seqId
|
||||
: seqId // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
content: null == content
|
||||
? _self.content
|
||||
: content // ignore: cast_nullable_to_non_nullable
|
||||
|
|
|
@ -8,6 +8,7 @@ part of 'message_state.dart';
|
|||
|
||||
_MessageState _$MessageStateFromJson(Map<String, dynamic> json) =>
|
||||
_MessageState(
|
||||
seqId: (json['seq_id'] as num).toInt(),
|
||||
content: messageFromJson(json['content'] as Map<String, dynamic>),
|
||||
sentTimestamp: Timestamp.fromJson(json['sent_timestamp']),
|
||||
reconciledTimestamp: json['reconciled_timestamp'] == null
|
||||
|
@ -20,6 +21,7 @@ _MessageState _$MessageStateFromJson(Map<String, dynamic> json) =>
|
|||
|
||||
Map<String, dynamic> _$MessageStateToJson(_MessageState instance) =>
|
||||
<String, dynamic>{
|
||||
'seq_id': instance.seqId,
|
||||
'content': messageToJson(instance.content),
|
||||
'sent_timestamp': instance.sentTimestamp.toJson(),
|
||||
'reconciled_timestamp': instance.reconciledTimestamp?.toJson(),
|
||||
|
|
2
lib/chat/views/chat_builders/chat_builders.dart
Normal file
2
lib/chat/views/chat_builders/chat_builders.dart
Normal file
|
@ -0,0 +1,2 @@
|
|||
export 'vc_composer_widget.dart';
|
||||
export 'vc_text_message_widget.dart';
|
431
lib/chat/views/chat_builders/vc_composer_widget.dart
Normal file
431
lib/chat/views/chat_builders/vc_composer_widget.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
269
lib/chat/views/chat_builders/vc_text_message_widget.dart
Normal file
269
lib/chat/views/chat_builders/vc_text_message_widget.dart
Normal 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));
|
||||
}
|
||||
}
|
|
@ -1,11 +1,13 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:async_tools/async_tools.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_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_translate/flutter_translate.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
@ -16,11 +18,15 @@ import '../../conversation/conversation.dart';
|
|||
import '../../notifications/notifications.dart';
|
||||
import '../../theme/theme.dart';
|
||||
import '../chat.dart';
|
||||
import 'chat_builders/chat_builders.dart';
|
||||
|
||||
const onEndReachedThreshold = 0.75;
|
||||
const _kScrollTag = 'kScrollTag';
|
||||
const kSeqId = 'seqId';
|
||||
const maxMessageLength = 2048;
|
||||
|
||||
class ChatComponentWidget extends StatelessWidget {
|
||||
const ChatComponentWidget({
|
||||
class ChatComponentWidget extends StatefulWidget {
|
||||
const ChatComponentWidget._({
|
||||
required super.key,
|
||||
required TypedKey localConversationRecordKey,
|
||||
required void Function() onCancel,
|
||||
|
@ -29,10 +35,14 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
_onCancel = onCancel,
|
||||
_onClose = onClose;
|
||||
|
||||
/////////////////////////////////////////////////////////////////////
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Create a single-contact chat and its associated state
|
||||
static Widget singleContact({
|
||||
required BuildContext context,
|
||||
required TypedKey localConversationRecordKey,
|
||||
required void Function() onCancel,
|
||||
required void Function() onClose,
|
||||
Key? key,
|
||||
}) {
|
||||
// Get the account info
|
||||
final accountInfo = context.watch<AccountInfoCubit>().state;
|
||||
|
||||
|
@ -45,19 +55,19 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
// Get the active conversation cubit
|
||||
final activeConversationCubit = context
|
||||
.select<ActiveConversationsBlocMapCubit, ActiveConversationCubit?>(
|
||||
(x) => x.tryOperateSync(_localConversationRecordKey,
|
||||
(x) => x.tryOperateSync(localConversationRecordKey,
|
||||
closure: (cubit) => cubit));
|
||||
if (activeConversationCubit == null) {
|
||||
return waitingPage(onCancel: _onCancel);
|
||||
return waitingPage(onCancel: onCancel);
|
||||
}
|
||||
|
||||
// Get the messages cubit
|
||||
final messagesCubit = context.select<ActiveSingleContactChatBlocMapCubit,
|
||||
SingleContactMessagesCubit?>(
|
||||
(x) => x.tryOperateSync(_localConversationRecordKey,
|
||||
(x) => x.tryOperateSync(localConversationRecordKey,
|
||||
closure: (cubit) => cubit));
|
||||
if (messagesCubit == null) {
|
||||
return waitingPage(onCancel: _onCancel);
|
||||
return waitingPage(onCancel: onCancel);
|
||||
}
|
||||
|
||||
// Make chat component state
|
||||
|
@ -70,26 +80,65 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
activeConversationCubit: activeConversationCubit,
|
||||
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 scaleScheme = theme.extension<ScaleScheme>()!;
|
||||
final scaleConfig = theme.extension<ScaleConfig>()!;
|
||||
final scale = scaleScheme.scale(ScaleKind.primary);
|
||||
final scaleTheme = theme.extension<ScaleTheme>()!;
|
||||
final scale = scaleTheme.scheme.scale(ScaleKind.primary);
|
||||
final textTheme = theme.textTheme;
|
||||
final chatTheme = makeChatTheme(scaleScheme, scaleConfig, textTheme);
|
||||
final errorChatTheme = (ChatThemeEditor(chatTheme)
|
||||
..inputTextColor = scaleScheme.errorScale.primary
|
||||
..sendButtonIcon = Image.asset(
|
||||
'assets/icon-send.png',
|
||||
color: scaleScheme.errorScale.primary,
|
||||
package: 'flutter_chat_ui',
|
||||
))
|
||||
.commit();
|
||||
final scaleChatTheme = scaleTheme.chatTheme();
|
||||
// final errorChatTheme = chatTheme.copyWith(color:)
|
||||
// ..inputTextColor = scaleScheme.errorScale.primary
|
||||
// ..sendButtonIcon = Image.asset(
|
||||
// 'assets/icon-send.png',
|
||||
// color: scaleScheme.errorScale.primary,
|
||||
// package: 'flutter_chat_ui',
|
||||
// ))
|
||||
// .commit();
|
||||
|
||||
// Get the enclosing chat component cubit that contains our state
|
||||
// (created by ChatComponentWidget.builder())
|
||||
|
@ -110,9 +159,8 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
final title = chatComponentState.title;
|
||||
|
||||
if (chatComponentCubit.scrollOffset != 0) {
|
||||
chatComponentState.scrollController.position.correctPixels(
|
||||
chatComponentState.scrollController.position.pixels +
|
||||
chatComponentCubit.scrollOffset);
|
||||
_scrollController.position.correctPixels(
|
||||
_scrollController.position.pixels + chatComponentCubit.scrollOffset);
|
||||
|
||||
chatComponentCubit.scrollOffset = 0;
|
||||
}
|
||||
|
@ -138,7 +186,7 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
IconButton(
|
||||
iconSize: 24,
|
||||
icon: Icon(Icons.close, color: scale.borderText),
|
||||
onPressed: _onClose)
|
||||
onPressed: widget._onClose)
|
||||
.paddingLTRB(0, 0, 8, 0)
|
||||
]),
|
||||
),
|
||||
|
@ -164,7 +212,7 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
chatComponentCubit.scrollOffset = scrollOffset;
|
||||
|
||||
//
|
||||
singleFuture(chatComponentState.chatKey, () async {
|
||||
singleFuture((chatComponentCubit, _kScrollTag), () async {
|
||||
await _handlePageForward(
|
||||
chatComponentCubit, messageWindow, notification);
|
||||
});
|
||||
|
@ -182,7 +230,7 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
|
||||
chatComponentCubit.scrollOffset = scrollOffset;
|
||||
//
|
||||
singleFuture(chatComponentState.chatKey, () async {
|
||||
singleFuture((chatComponentCubit, _kScrollTag), () async {
|
||||
await _handlePageBackward(
|
||||
chatComponentCubit, messageWindow, notification);
|
||||
});
|
||||
|
@ -190,82 +238,181 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
return false;
|
||||
},
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: chatComponentState.textEditingController,
|
||||
valueListenable: _textEditingController,
|
||||
builder: (context, textEditingValue, __) {
|
||||
final messageIsValid =
|
||||
utf8.encode(textEditingValue.text).lengthInBytes <
|
||||
2048;
|
||||
_messageIsValid(textEditingValue.text);
|
||||
var sendIconColor = scaleTheme.config.preferBorders
|
||||
? scale.border
|
||||
: scale.borderText;
|
||||
|
||||
if (!messageIsValid ||
|
||||
_textEditingController.text.isEmpty) {
|
||||
sendIconColor = sendIconColor.withAlpha(128);
|
||||
}
|
||||
|
||||
return Chat(
|
||||
key: chatComponentState.chatKey,
|
||||
theme: messageIsValid ? chatTheme : errorChatTheme,
|
||||
messages: messageWindow.window.toList(),
|
||||
scrollToBottomOnSend: isFirstPage,
|
||||
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'));
|
||||
currentUserId: localUser.id,
|
||||
resolveUser: (id) async {
|
||||
if (id == localUser.id) {
|
||||
return localUser;
|
||||
}
|
||||
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
|
||||
: Text(translate('chat.message_too_long'),
|
||||
style: TextStyle(
|
||||
color:
|
||||
scaleScheme.errorScale.primary))
|
||||
color: scaleTheme
|
||||
.scheme.errorScale.primary))
|
||||
.toCenter(),
|
||||
//showUserAvatars: false,
|
||||
//showUserNames: true,
|
||||
user: localUser,
|
||||
emptyState: const EmptyChatWidget());
|
||||
),
|
||||
),
|
||||
timeFormat: core.DateFormat.jm(),
|
||||
);
|
||||
}))).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('/')) {
|
||||
chatComponentCubit.runCommand(text);
|
||||
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 {
|
||||
// //
|
||||
// }
|
||||
|
||||
Future<void> _handlePageForward(
|
||||
ChatComponentCubit chatComponentCubit,
|
||||
WindowState<types.Message> messageWindow,
|
||||
WindowState<core.Message> messageWindow,
|
||||
ScrollNotification notification) async {
|
||||
debugPrint(
|
||||
'_handlePageForward: messagesState.length=${messageWindow.length} '
|
||||
|
@ -299,7 +446,7 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
|
||||
Future<void> _handlePageBackward(
|
||||
ChatComponentCubit chatComponentCubit,
|
||||
WindowState<types.Message> messageWindow,
|
||||
WindowState<core.Message> messageWindow,
|
||||
ScrollNotification notification,
|
||||
) async {
|
||||
debugPrint(
|
||||
|
@ -335,8 +482,8 @@ class ChatComponentWidget extends StatelessWidget {
|
|||
//chatComponentCubit.scrollOffset = 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
final TypedKey _localConversationRecordKey;
|
||||
final void Function() _onCancel;
|
||||
final void Function() _onClose;
|
||||
late final core.ChatController _chatController;
|
||||
late final TextEditingController _textEditingController;
|
||||
late final ScrollController _scrollController;
|
||||
late final SingleStateProcessor<ChatComponentState> _chatStateProcessor;
|
||||
}
|
||||
|
|
41
lib/chat/views/date_formatter.dart
Normal file
41
lib/chat/views/date_formatter.dart
Normal 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';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export 'chat_component_widget.dart';
|
||||
export 'empty_chat_widget.dart';
|
||||
export 'no_conversation_widget.dart';
|
||||
export 'utf8_length_limiting_text_input_formatter.dart';
|
||||
|
|
|
@ -17,8 +17,12 @@ class VeilidChatGlobalInit {
|
|||
// Initialize Veilid
|
||||
Future<void> _initializeVeilid() async {
|
||||
// Init Veilid
|
||||
try {
|
||||
Veilid.instance.initializeVeilidCore(
|
||||
await getDefaultVeilidPlatformConfig(kIsWeb, VeilidChatApp.name));
|
||||
} on VeilidAPIExceptionAlreadyInitialized {
|
||||
log.debug('Already initialized, not reinitializing veilid-core');
|
||||
}
|
||||
|
||||
// Veilid logging
|
||||
initVeilidLog(kIsDebugMode);
|
||||
|
|
|
@ -16,6 +16,14 @@ class ReloadThemeIntent extends Intent {
|
|||
const ReloadThemeIntent();
|
||||
}
|
||||
|
||||
class ChangeBrightnessIntent extends Intent {
|
||||
const ChangeBrightnessIntent();
|
||||
}
|
||||
|
||||
class ChangeColorIntent extends Intent {
|
||||
const ChangeColorIntent();
|
||||
}
|
||||
|
||||
class AttachDetachIntent extends Intent {
|
||||
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) {
|
||||
singleFuture(this, () async {
|
||||
if (ProcessorRepository.instance.processorConnectionState.isAttached) {
|
||||
|
@ -75,17 +126,34 @@ class KeyboardShortcuts extends StatelessWidget {
|
|||
Widget build(BuildContext context) => ThemeSwitcher(
|
||||
builder: (context) => Shortcuts(
|
||||
shortcuts: <LogicalKeySet, Intent>{
|
||||
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.keyR):
|
||||
const ReloadThemeIntent(),
|
||||
LogicalKeySet(LogicalKeyboardKey.alt, LogicalKeyboardKey.keyD):
|
||||
const AttachDetachIntent(),
|
||||
LogicalKeySet(
|
||||
LogicalKeyboardKey.alt, LogicalKeyboardKey.backquote):
|
||||
const DeveloperPageIntent(),
|
||||
LogicalKeyboardKey.alt,
|
||||
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>>{
|
||||
ReloadThemeIntent: CallbackAction<ReloadThemeIntent>(
|
||||
onInvoke: (intent) => reloadTheme(context)),
|
||||
ChangeBrightnessIntent: CallbackAction<ChangeBrightnessIntent>(
|
||||
onInvoke: (intent) => changeBrightness(context)),
|
||||
ChangeColorIntent: CallbackAction<ChangeColorIntent>(
|
||||
onInvoke: (intent) => changeColor(context)),
|
||||
AttachDetachIntent: CallbackAction<AttachDetachIntent>(
|
||||
onInvoke: (intent) => _attachDetach(context)),
|
||||
DeveloperPageIntent: CallbackAction<DeveloperPageIntent>(
|
||||
|
|
|
@ -130,7 +130,11 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
|
|||
if (activeChatLocalConversationKey == null) {
|
||||
return const NoConversationWidget();
|
||||
}
|
||||
return ChatComponentWidget(
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Builder(
|
||||
builder: (context) => ChatComponentWidget.singleContact(
|
||||
context: context,
|
||||
localConversationRecordKey: activeChatLocalConversationKey,
|
||||
onCancel: () {
|
||||
activeChatCubit.setActiveChat(null);
|
||||
|
@ -138,7 +142,7 @@ class _HomeAccountReadyState extends State<HomeAccountReady> {
|
|||
onClose: () {
|
||||
activeChatCubit.setActiveChat(null);
|
||||
},
|
||||
key: ValueKey(activeChatLocalConversationKey));
|
||||
key: ValueKey(activeChatLocalConversationKey))));
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:flutter/gestures.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
|
||||
import 'package:keyboard_avoider/keyboard_avoider.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:transitioned_indexed_stack/transitioned_indexed_stack.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
@ -207,6 +208,8 @@ class HomeScreenState extends State<HomeScreen>
|
|||
|
||||
return DefaultTextStyle(
|
||||
style: theme.textTheme.bodySmall!,
|
||||
child: KeyboardAvoider(
|
||||
curve: Curves.ease,
|
||||
child: ZoomDrawer(
|
||||
controller: _zoomDrawerController,
|
||||
menuScreen: Builder(builder: (context) {
|
||||
|
@ -223,17 +226,14 @@ class HomeScreenState extends State<HomeScreen>
|
|||
child: Builder(builder: _buildAccountPageView)),
|
||||
borderRadius: 0,
|
||||
angle: 0,
|
||||
//mainScreenOverlayColor: theme.shadowColor.withAlpha(0x2F),
|
||||
openCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
closeCurve: Curves.fastEaseInToSlowEaseOut,
|
||||
// duration: const Duration(milliseconds: 250),
|
||||
// reverseDuration: const Duration(milliseconds: 250),
|
||||
menuScreenTapClose: canClose,
|
||||
mainScreenTapClose: canClose,
|
||||
disableDragGesture: !canClose,
|
||||
mainScreenScale: .25,
|
||||
slideWidth: min(360, MediaQuery.of(context).size.width * 0.9),
|
||||
));
|
||||
)));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
export 'chat_theme.dart';
|
||||
export 'radix_generator.dart';
|
||||
export 'scale_theme/scale_theme.dart';
|
||||
export 'theme_preference.dart';
|
||||
|
|
369
lib/theme/models/scale_theme/scale_chat_theme.dart
Normal file
369
lib/theme/models/scale_theme/scale_chat_theme.dart
Normal 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!);
|
||||
}
|
||||
}
|
|
@ -83,6 +83,7 @@ class ScaleColor {
|
|||
calloutBackground: calloutBackground ?? this.calloutBackground,
|
||||
calloutText: calloutText ?? this.calloutText);
|
||||
|
||||
// Use static method
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ScaleColor lerp(ScaleColor a, ScaleColor b, double t) => ScaleColor(
|
||||
appBackground: Color.lerp(a.appBackground, b.appBackground, t) ??
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'scale_input_decorator_theme.dart';
|
|||
import 'scale_scheme.dart';
|
||||
|
||||
export 'scale_app_bar_theme.dart';
|
||||
export 'scale_chat_theme.dart';
|
||||
export 'scale_color.dart';
|
||||
export 'scale_input_decorator_theme.dart';
|
||||
export 'scale_scheme.dart';
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
bool get isAndroid => !kIsWeb && Platform.isAndroid;
|
||||
bool get isiOS => !kIsWeb && Platform.isIOS;
|
||||
bool get isWeb => kIsWeb;
|
||||
bool get isDesktop =>
|
||||
!isWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS);
|
||||
final isAndroid = !kIsWeb && defaultTargetPlatform == TargetPlatform.android;
|
||||
final isiOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS;
|
||||
final isMobile = !kIsWeb &&
|
||||
(defaultTargetPlatform == TargetPlatform.iOS ||
|
||||
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;
|
||||
|
||||
|
|
|
@ -128,7 +128,7 @@ Future<void> showErrorStacktraceModal(
|
|||
await showErrorModal(
|
||||
context: context,
|
||||
title: translate('toast.error'),
|
||||
text: 'Error: {e}\n StackTrace: {st}',
|
||||
text: 'Error: $error\n StackTrace: $stackTrace',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -165,5 +165,8 @@ void initLoggy() {
|
|||
registerVeilidProtoToDebug();
|
||||
registerVeilidDHTProtoToDebug();
|
||||
registerVeilidchatProtoToDebug();
|
||||
|
||||
if (kIsDebugMode) {
|
||||
Bloc.observer = const StateLogger();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,17 @@ import 'package:veilid_support/veilid_support.dart';
|
|||
import 'loggy.dart';
|
||||
|
||||
const Map<String, LogLevel> _blocChangeLogLevels = {
|
||||
'ConnectionStateCubit': LogLevel.off,
|
||||
'ActiveSingleContactChatBlocMapCubit': LogLevel.off,
|
||||
'ActiveConversationsBlocMapCubit': LogLevel.off,
|
||||
'PersistentQueueCubit<Message>': LogLevel.off,
|
||||
'TableDBArrayProtobufCubit<ReconciledMessage>': LogLevel.off,
|
||||
'DHTLogCubit<Message>': LogLevel.off,
|
||||
'SingleContactMessagesCubit': LogLevel.off,
|
||||
'ChatComponentCubit': LogLevel.off,
|
||||
'RouterCubit': LogLevel.debug,
|
||||
'PerAccountCollectionBlocMapCubit': LogLevel.debug,
|
||||
'PerAccountCollectionCubit': LogLevel.debug,
|
||||
'ActiveChatCubit': LogLevel.debug,
|
||||
'AccountRecordCubit': LogLevel.debug,
|
||||
'ContactListCubit': LogLevel.debug,
|
||||
'ContactInvitationListCubit': LogLevel.debug,
|
||||
'ChatListCubit': LogLevel.debug,
|
||||
'PreferencesCubit': LogLevel.debug,
|
||||
'ConversationCubit': LogLevel.debug,
|
||||
'DefaultDHTRecordCubit<Conversation>': LogLevel.debug,
|
||||
};
|
||||
|
||||
const Map<String, LogLevel> _blocCreateCloseLogLevels = {};
|
||||
|
@ -40,7 +43,7 @@ class StateLogger extends BlocObserver {
|
|||
@override
|
||||
void onChange(BlocBase<dynamic> bloc, Change<dynamic> change) {
|
||||
super.onChange(bloc, change);
|
||||
_checkLogLevel(_blocChangeLogLevels, LogLevel.debug, bloc, (logLevel) {
|
||||
_checkLogLevel(_blocChangeLogLevels, LogLevel.off, bloc, (logLevel) {
|
||||
const encoder = JsonEncoder.withIndent(' ', DynamicDebug.toDebug);
|
||||
log.log(
|
||||
logLevel,
|
||||
|
|
|
@ -23,6 +23,7 @@ part 'super_identity.g.dart';
|
|||
/// Encryption: None
|
||||
@freezed
|
||||
sealed class SuperIdentity with _$SuperIdentity {
|
||||
@JsonSerializable()
|
||||
const factory SuperIdentity({
|
||||
/// Public DHT record storing this structure for account recovery
|
||||
/// changing this can migrate/forward the SuperIdentity to a new DHT record
|
||||
|
|
|
@ -169,6 +169,7 @@ class _$SuperIdentityCopyWithImpl<$Res>
|
|||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
@JsonSerializable()
|
||||
class _SuperIdentity extends SuperIdentity {
|
||||
const _SuperIdentity(
|
||||
|
|
|
@ -4,8 +4,10 @@ import 'package:path/path.dart' as p;
|
|||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:veilid/veilid.dart';
|
||||
|
||||
// Allowed to pull sentinel value
|
||||
// ignore: do_not_use_environment
|
||||
const bool kIsReleaseMode = bool.fromEnvironment('dart.vm.product');
|
||||
// Allowed to pull sentinel value
|
||||
// ignore: do_not_use_environment
|
||||
const bool kIsProfileMode = bool.fromEnvironment('dart.vm.profile');
|
||||
const bool kIsDebugMode = !kIsReleaseMode && !kIsProfileMode;
|
||||
|
@ -13,18 +15,21 @@ const bool kIsDebugMode = !kIsReleaseMode && !kIsProfileMode;
|
|||
Future<Map<String, dynamic>> getDefaultVeilidPlatformConfig(
|
||||
bool isWeb, String appName) async {
|
||||
final ignoreLogTargetsStr =
|
||||
// Allowed to change settings
|
||||
// ignore: do_not_use_environment
|
||||
const String.fromEnvironment('IGNORE_LOG_TARGETS').trim();
|
||||
final ignoreLogTargets = ignoreLogTargetsStr.isEmpty
|
||||
? <String>[]
|
||||
: ignoreLogTargetsStr.split(',').map((e) => e.trim()).toList();
|
||||
|
||||
// Allowed to change settings
|
||||
// ignore: do_not_use_environment
|
||||
var flamePathStr = const String.fromEnvironment('FLAME').trim();
|
||||
if (flamePathStr == '1') {
|
||||
flamePathStr = p.join(
|
||||
(await getApplicationSupportDirectory()).absolute.path,
|
||||
'$appName.folded');
|
||||
// Allowed for debugging
|
||||
// ignore: avoid_print
|
||||
print('Flame data logged to $flamePathStr');
|
||||
}
|
||||
|
@ -73,30 +78,37 @@ Future<VeilidConfig> getVeilidConfig(bool isWeb, String programName) async {
|
|||
var config = await getDefaultVeilidConfig(
|
||||
isWeb: isWeb,
|
||||
programName: programName,
|
||||
// Allowed to change settings
|
||||
// ignore: avoid_redundant_argument_values, do_not_use_environment
|
||||
namespace: const String.fromEnvironment('NAMESPACE'),
|
||||
// Allowed to change settings
|
||||
// ignore: avoid_redundant_argument_values, do_not_use_environment
|
||||
bootstrap: const String.fromEnvironment('BOOTSTRAP'),
|
||||
// Allowed to change settings
|
||||
// ignore: avoid_redundant_argument_values, do_not_use_environment
|
||||
networkKeyPassword: const String.fromEnvironment('NETWORK_KEY'),
|
||||
);
|
||||
|
||||
// Allowed to change settings
|
||||
// ignore: do_not_use_environment
|
||||
if (const String.fromEnvironment('DELETE_TABLE_STORE') == '1') {
|
||||
config =
|
||||
config.copyWith(tableStore: config.tableStore.copyWith(delete: true));
|
||||
}
|
||||
// Allowed to change settings
|
||||
// ignore: do_not_use_environment
|
||||
if (const String.fromEnvironment('DELETE_PROTECTED_STORE') == '1') {
|
||||
config = config.copyWith(
|
||||
protectedStore: config.protectedStore.copyWith(delete: true));
|
||||
}
|
||||
// Allowed to change settings
|
||||
// ignore: do_not_use_environment
|
||||
if (const String.fromEnvironment('DELETE_BLOCK_STORE') == '1') {
|
||||
config =
|
||||
config.copyWith(blockStore: config.blockStore.copyWith(delete: true));
|
||||
}
|
||||
|
||||
// Allowed to change settings
|
||||
// ignore: do_not_use_environment
|
||||
const envNetwork = String.fromEnvironment('NETWORK');
|
||||
if (envNetwork.isNotEmpty) {
|
||||
|
@ -111,7 +123,8 @@ Future<VeilidConfig> getVeilidConfig(bool isWeb, String programName) async {
|
|||
|
||||
return config.copyWith(
|
||||
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']),
|
||||
protectedStore:
|
||||
// XXX: Linux often does not have a secret storage mechanism installed
|
||||
|
|
|
@ -24,7 +24,7 @@ class TableDBArrayProtobufStateData<T extends GeneratedMessage>
|
|||
final IList<T> windowElements;
|
||||
// The length of the entire array
|
||||
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;
|
||||
// The total number of elements to try to keep in 'elements'
|
||||
final int windowCount;
|
||||
|
|
109
pubspec.lock
109
pubspec.lock
|
@ -393,6 +393,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -542,23 +550,20 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
flutter_chat_types:
|
||||
flutter_chat_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_chat_types
|
||||
sha256: e285b588f6d19d907feb1f6d912deaf22e223656769c34093b64e1c59b094fb9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.2"
|
||||
path: "../flutter_chat_ui/packages/flutter_chat_core"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.1.2"
|
||||
flutter_chat_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "."
|
||||
ref: main
|
||||
resolved-ref: d4b9d507d10f5d640156cacfd754f661f8c0f4c1
|
||||
url: "https://gitlab.com/veilid/flutter-chat-ui.git"
|
||||
source: git
|
||||
version: "1.6.14"
|
||||
path: "../flutter_chat_ui/packages/flutter_chat_ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "2.1.3"
|
||||
flutter_form_builder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -575,22 +580,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
@ -604,14 +593,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -801,6 +782,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -857,14 +846,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.9.4"
|
||||
linkify:
|
||||
dependency: transitive
|
||||
keyboard_avoider:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: linkify
|
||||
sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832"
|
||||
name: keyboard_avoider
|
||||
sha256: d2917bd52c6612bf8d1ff97f74049ddf3592a81d44e814f0e7b07dcfd245b75c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
version: "0.2.0"
|
||||
lint_hard:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -1073,14 +1062,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1157,10 +1138,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: provider
|
||||
sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
version: "6.1.5"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -1305,6 +1286,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1314,6 +1303,14 @@ packages:
|
|||
url: "https://gitlab.com/veilid/Searchable-Listview.git"
|
||||
source: git
|
||||
version: "2.16.0"
|
||||
sembast:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sembast
|
||||
sha256: d3f0d0ba501a5f1fd7d6c8532ee01385977c8a069c334635dae390d059ae3d6d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.8.5"
|
||||
share_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1774,7 +1771,7 @@ packages:
|
|||
path: "../veilid/veilid-flutter"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.4.4"
|
||||
version: "0.4.6"
|
||||
veilid_support:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -1782,14 +1779,6 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
46
pubspec.yaml
46
pubspec.yaml
|
@ -1,11 +1,11 @@
|
|||
name: veilidchat
|
||||
description: VeilidChat
|
||||
publish_to: 'none'
|
||||
publish_to: "none"
|
||||
version: 0.4.7+20
|
||||
|
||||
environment:
|
||||
sdk: '>=3.2.0 <4.0.0'
|
||||
flutter: '>=3.22.1'
|
||||
sdk: ">=3.2.0 <4.0.0"
|
||||
flutter: ">=3.22.1"
|
||||
|
||||
dependencies:
|
||||
accordion: ^2.6.0
|
||||
|
@ -37,11 +37,16 @@ dependencies:
|
|||
sdk: flutter
|
||||
flutter_animate: ^4.5.2
|
||||
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:
|
||||
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_hooks: ^0.21.2
|
||||
flutter_localizations:
|
||||
|
@ -59,6 +64,7 @@ dependencies:
|
|||
image: ^4.5.3
|
||||
intl: ^0.19.0
|
||||
json_annotation: ^4.9.0
|
||||
keyboard_avoider: ^0.2.0
|
||||
loggy: ^2.0.3
|
||||
meta: ^1.16.0
|
||||
mobile_scanner: ^6.0.7
|
||||
|
@ -110,15 +116,17 @@ dependencies:
|
|||
xterm: ^4.0.0
|
||||
zxing2: ^0.2.3
|
||||
|
||||
# dependency_overrides:
|
||||
# async_tools:
|
||||
# path: ../dart_async_tools
|
||||
# bloc_advanced_tools:
|
||||
# path: ../bloc_advanced_tools
|
||||
# searchable_listview:
|
||||
# path: ../Searchable-Listview
|
||||
# flutter_chat_ui:
|
||||
# path: ../flutter_chat_ui
|
||||
dependency_overrides:
|
||||
# async_tools:
|
||||
# path: ../dart_async_tools
|
||||
# bloc_advanced_tools:
|
||||
# path: ../bloc_advanced_tools
|
||||
# searchable_listview:
|
||||
# path: ../Searchable-Listview
|
||||
flutter_chat_core:
|
||||
path: ../flutter_chat_ui/packages/flutter_chat_core
|
||||
flutter_chat_ui:
|
||||
path: ../flutter_chat_ui/packages/flutter_chat_ui
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.15
|
||||
|
@ -131,20 +139,20 @@ flutter_native_splash:
|
|||
color: "#8588D0"
|
||||
|
||||
icons_launcher:
|
||||
image_path: 'assets/launcher/icon.png'
|
||||
image_path: "assets/launcher/icon.png"
|
||||
platforms:
|
||||
android:
|
||||
enable: true
|
||||
adaptive_background_color: '#ffffff'
|
||||
adaptive_foreground_image: 'assets/launcher/icon.png'
|
||||
adaptive_round_image: 'assets/launcher/icon.png'
|
||||
adaptive_background_color: "#ffffff"
|
||||
adaptive_foreground_image: "assets/launcher/icon.png"
|
||||
adaptive_round_image: "assets/launcher/icon.png"
|
||||
ios:
|
||||
enable: true
|
||||
web:
|
||||
enable: true
|
||||
macos:
|
||||
enable: true
|
||||
image_path: 'assets/launcher/macos_icon.png'
|
||||
image_path: "assets/launcher/macos_icon.png"
|
||||
windows:
|
||||
enable: true
|
||||
linux:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue