message length limit

This commit is contained in:
Christien Rioux 2024-07-04 23:09:37 -04:00
parent 9dfb8c3f71
commit 94988718e8
13 changed files with 792 additions and 132 deletions

View File

@ -36,18 +36,19 @@
"name": "Name",
"pronouns": "Pronouns",
"remove_account": "Remove Account",
"delete_identity": "Delete Identity",
"destroy_account": "Destroy Account",
"remove_account_confirm": "Confirm Account Removal",
"remove_account_description": "Remove account from this device only",
"remove_account_confirm_message": " • Your account will be removed from this device ONLY\n • Your identity will remain recoverable with the recovery key\n • Your messages and contacts will remain available on other devices\n",
"delete_identity_description": "Delete identity from all devices everywhere",
"delete_identity_confirm_message": "This action is PERMANENT, and your identity will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!",
"delete_identity_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n",
"destroy_account_confirm": "Confirm Account Destruction",
"destroy_account_description": "Destroy account, removing it completely from all devices everywhere",
"destroy_account_confirm_message": "This action is PERMANENT, and your VeilidChat account will no longer be recoverable with the recovery key. Restoring from backups will not recover your account!",
"destroy_account_confirm_message_details": "You will lose access to:\n • Your entire message history\n • Your contacts\n • This will not remove your messages you have sent from other people's devices\n",
"confirm_are_you_sure": "Are you sure you want to do this?",
"failed_to_remove": "Failed to remove account.\n\nTry again when you have a more stable network connection.",
"failed_to_delete": "Failed to delete identity.\n\nTry again when you have a more stable network connection.",
"failed_to_destroy": "Failed to destroy account.\n\nTry again when you have a more stable network connection.",
"account_removed": "Account removed successfully",
"identity_deleted": "Identity deleted successfully"
"account_destroyed": "Account destroyed successfully"
},
"show_recovery_key_page": {
"titlebar": "Save Recovery Key",
@ -103,7 +104,8 @@
},
"chat": {
"start_a_conversation": "Start A Conversation",
"say_something": "Say Something"
"say_something": "Say Something",
"message_too_long": "Message too long"
},
"create_invitation_dialog": {
"title": "Create Contact Invitation",

View File

@ -7,7 +7,7 @@ import '../../../proto/proto.dart' as proto;
import '../../tools/tools.dart';
import '../models/models.dart';
const String veilidChatAccountKey = 'com.veilid.veilidchat';
const String veilidChatApplicationId = 'com.veilid.veilidchat';
enum AccountRepositoryChange { localAccounts, userLogins, activeLocalAccount }
@ -194,6 +194,33 @@ class AccountRepository {
/// Recover an account with the master identity secret
/// Delete an account from all devices
Future<bool> destroyAccount(TypedKey superIdentityRecordKey,
OwnedDHTRecordPointer accountRecord) async {
// Get which local account we want to fetch the profile for
final localAccount = fetchLocalAccount(superIdentityRecordKey);
if (localAccount == null) {
return false;
}
// See if we've logged into this account or if it is locked
final userLogin = fetchUserLogin(superIdentityRecordKey);
if (userLogin == null) {
return false;
}
final success = await localAccount.superIdentity.currentInstance
.removeAccount(
superRecordKey: localAccount.superIdentity.recordKey,
secretKey: userLogin.identitySecret.value,
applicationId: veilidChatApplicationId,
removeAccountCallback: (accountRecordInfos) async =>
accountRecordInfos.singleOrNull);
if (!success) {
return false;
}
return deleteLocalAccount(superIdentityRecordKey, accountRecord);
}
Future<void> switchToAccount(TypedKey? superIdentityRecordKey) async {
final activeLocalAccount = await _activeLocalAccount.get();
@ -231,7 +258,7 @@ class AccountRepository {
await superIdentity.currentInstance.addAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
accountKey: veilidChatAccountKey,
applicationId: veilidChatApplicationId,
createAccountCallback: (parent) async {
// Make empty contact list
log.debug('Creating contacts list');
@ -305,7 +332,7 @@ class AccountRepository {
.readAccount(
superRecordKey: superIdentity.recordKey,
secretKey: identitySecret,
accountKey: veilidChatAccountKey);
applicationId: veilidChatApplicationId);
if (accountRecordInfoList.length > 1) {
throw IdentityException.limitExceeded;
} else if (accountRecordInfoList.isEmpty) {

View File

@ -134,8 +134,70 @@ class _EditAccountPageState extends State<EditAccountPage> {
}
}
Future<void> _onDeleteIdentity() async {
//
Future<void> _onDestroyAccount() async {
final confirmed = await StyledDialog.show<bool>(
context: context,
title: translate('edit_account_page.destroy_account_confirm'),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Text(translate('edit_account_page.destroy_account_confirm_message'))
.paddingLTRB(24, 24, 24, 0),
Text(translate(
'edit_account_page.destroy_account_confirm_message_details'))
.paddingLTRB(24, 24, 24, 0),
Text(translate('edit_account_page.confirm_are_you_sure'))
.paddingAll(8),
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.cancel, size: 16).paddingLTRB(0, 0, 4, 0),
Text(translate('button.no_cancel')).paddingLTRB(0, 0, 4, 0)
])),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.check, size: 16).paddingLTRB(0, 0, 4, 0),
Text(translate('button.yes_proceed')).paddingLTRB(0, 0, 4, 0)
]))
]).paddingAll(24)
]));
if (confirmed != null && confirmed && mounted) {
// dismiss the keyboard by unfocusing the textfield
FocusScope.of(context).unfocus();
try {
setState(() {
_isInAsyncCall = true;
});
try {
final success = await AccountRepository.instance.destroyAccount(
widget.superIdentityRecordKey, widget.accountRecord);
if (success && mounted) {
showInfoToast(
context, translate('edit_account_page.account_destroyed'));
GoRouterHelper(context).pop();
} else if (mounted) {
showErrorToast(
context, translate('edit_account_page.failed_to_destroy'));
}
} finally {
if (mounted) {
setState(() {
_isInAsyncCall = false;
});
}
}
} on Exception catch (e) {
if (mounted) {
await showErrorModal(
context, translate('new_account_page.error'), 'Exception: $e');
}
}
}
}
Future<void> _onSubmit(GlobalKey<FormBuilderState> formKey) async {
@ -234,13 +296,13 @@ class _EditAccountPageState extends State<EditAccountPage> {
Text(translate('edit_account_page.remove_account'))
.paddingLTRB(0, 0, 4, 0)
])).paddingLTRB(0, 8, 0, 24),
Text(translate('edit_account_page.delete_identity_description')),
Text(translate('edit_account_page.destroy_account_description')),
ElevatedButton(
onPressed: _onDeleteIdentity,
onPressed: _onDestroyAccount,
child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.person_off, size: 16)
.paddingLTRB(0, 0, 4, 0),
Text(translate('edit_account_page.delete_identity'))
Text(translate('edit_account_page.destroy_account'))
.paddingLTRB(0, 0, 4, 0)
])).paddingLTRB(0, 8, 0, 24)
]).paddingSymmetric(horizontal: 24, vertical: 8))

View File

@ -42,6 +42,7 @@ class ChatComponentCubit extends Cubit<ChatComponentState> {
super(ChatComponentState(
chatKey: GlobalKey<ChatState>(),
scrollController: AutoScrollController(),
textEditingController: InputTextFieldController(),
localUser: null,
remoteUsers: const IMap.empty(),
historicalRemoteUsers: const IMap.empty(),

View File

@ -97,11 +97,13 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Initialize everything
Future<void> _init() async {
_unsentMessagesQueue = PersistentQueue<proto.Message>(
table: 'SingleContactUnsentMessages',
key: _remoteConversationRecordKey.toString(),
fromBuffer: proto.Message.fromBuffer,
closure: _processUnsentMessages,
);
table: 'SingleContactUnsentMessages',
key: _remoteConversationRecordKey.toString(),
fromBuffer: proto.Message.fromBuffer,
closure: _processUnsentMessages,
onError: (e, sp) {
log.error('Exception while processing unsent messages: $e\n$sp\n');
});
// Make crypto
await _initCrypto();
@ -297,16 +299,23 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
proto.Message? previousMessage;
final processedMessages = messages.toList();
for (final message in processedMessages) {
await _processMessageToSend(message, previousMessage);
previousMessage = message;
try {
await _processMessageToSend(message, previousMessage);
previousMessage = message;
} on Exception catch (e) {
log.error('Exception processing unsent message: $e');
}
}
// _sendingMessages = messages;
// _renderState();
await _sentMessagesCubit!.operateAppendEventual((writer) =>
writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
try {
await _sentMessagesCubit!.operateAppendEventual((writer) =>
writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
} on Exception catch (e) {
log.error('Exception appending unsent messages: $e');
}
// _sendingMessages = const IList.empty();
}
@ -403,6 +412,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
..author = _accountInfo.identityTypedPublicKey.toProto()
..timestamp = Veilid.instance.now().toInt64();
if ((message.writeToBuffer().lengthInBytes + 256) > 4096) {
throw const FormatException('message is too long');
}
// Put in the queue
_unsentMessagesQueue.addSync(message);

View File

@ -2,7 +2,8 @@ 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;
import 'package:flutter_chat_ui/flutter_chat_ui.dart'
show ChatState, InputTextFieldController;
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:veilid_support/veilid_support.dart';
@ -19,6 +20,8 @@ class ChatComponentState with _$ChatComponentState {
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

View File

@ -20,6 +20,8 @@ mixin _$ChatComponentState {
GlobalKey<ChatState> get chatKey =>
throw _privateConstructorUsedError; // ScrollController for the chat
AutoScrollController get scrollController =>
throw _privateConstructorUsedError; // TextEditingController for the chat
InputTextFieldController get textEditingController =>
throw _privateConstructorUsedError; // Local user
User? get localUser =>
throw _privateConstructorUsedError; // Active remote users
@ -47,6 +49,7 @@ abstract class $ChatComponentStateCopyWith<$Res> {
$Res call(
{GlobalKey<ChatState> chatKey,
AutoScrollController scrollController,
InputTextFieldController textEditingController,
User? localUser,
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
@ -72,6 +75,7 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
$Res call({
Object? chatKey = null,
Object? scrollController = null,
Object? textEditingController = null,
Object? localUser = freezed,
Object? remoteUsers = null,
Object? historicalRemoteUsers = null,
@ -88,6 +92,10 @@ class _$ChatComponentStateCopyWithImpl<$Res, $Val extends ChatComponentState>
? _value.scrollController
: scrollController // ignore: cast_nullable_to_non_nullable
as AutoScrollController,
textEditingController: null == textEditingController
? _value.textEditingController
: textEditingController // ignore: cast_nullable_to_non_nullable
as InputTextFieldController,
localUser: freezed == localUser
? _value.localUser
: localUser // ignore: cast_nullable_to_non_nullable
@ -136,6 +144,7 @@ abstract class _$$ChatComponentStateImplCopyWith<$Res>
$Res call(
{GlobalKey<ChatState> chatKey,
AutoScrollController scrollController,
InputTextFieldController textEditingController,
User? localUser,
IMap<Typed<FixedEncodedString43>, User> remoteUsers,
IMap<Typed<FixedEncodedString43>, User> historicalRemoteUsers,
@ -160,6 +169,7 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
$Res call({
Object? chatKey = null,
Object? scrollController = null,
Object? textEditingController = null,
Object? localUser = freezed,
Object? remoteUsers = null,
Object? historicalRemoteUsers = null,
@ -176,6 +186,10 @@ class __$$ChatComponentStateImplCopyWithImpl<$Res>
? _value.scrollController
: scrollController // ignore: cast_nullable_to_non_nullable
as AutoScrollController,
textEditingController: null == textEditingController
? _value.textEditingController
: textEditingController // ignore: cast_nullable_to_non_nullable
as InputTextFieldController,
localUser: freezed == localUser
? _value.localUser
: localUser // ignore: cast_nullable_to_non_nullable
@ -210,6 +224,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
const _$ChatComponentStateImpl(
{required this.chatKey,
required this.scrollController,
required this.textEditingController,
required this.localUser,
required this.remoteUsers,
required this.historicalRemoteUsers,
@ -223,6 +238,9 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
// ScrollController for the chat
@override
final AutoScrollController scrollController;
// TextEditingController for the chat
@override
final InputTextFieldController textEditingController;
// Local user
@override
final User? localUser;
@ -244,7 +262,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
@override
String toString() {
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)';
return 'ChatComponentState(chatKey: $chatKey, scrollController: $scrollController, textEditingController: $textEditingController, localUser: $localUser, remoteUsers: $remoteUsers, historicalRemoteUsers: $historicalRemoteUsers, unknownUsers: $unknownUsers, messageWindow: $messageWindow, title: $title)';
}
@override
@ -255,6 +273,8 @@ class _$ChatComponentStateImpl implements _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) ||
@ -273,6 +293,7 @@ class _$ChatComponentStateImpl implements _ChatComponentState {
runtimeType,
chatKey,
scrollController,
textEditingController,
localUser,
remoteUsers,
historicalRemoteUsers,
@ -292,6 +313,7 @@ abstract class _ChatComponentState implements ChatComponentState {
const factory _ChatComponentState(
{required final GlobalKey<ChatState> chatKey,
required final AutoScrollController scrollController,
required final InputTextFieldController textEditingController,
required final User? localUser,
required final IMap<Typed<FixedEncodedString43>, User> remoteUsers,
required final IMap<Typed<FixedEncodedString43>, User>
@ -304,6 +326,8 @@ abstract class _ChatComponentState implements ChatComponentState {
GlobalKey<ChatState> get chatKey;
@override // ScrollController for the chat
AutoScrollController get scrollController;
@override // TextEditingController for the chat
InputTextFieldController get textEditingController;
@override // Local user
User? get localUser;
@override // Active remote users

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:math';
import 'package:async_tools/async_tools.dart';
@ -6,6 +7,7 @@ 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_ui/flutter_chat_ui.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
@ -154,6 +156,14 @@ class ChatComponentWidget extends StatelessWidget {
final scale = theme.extension<ScaleScheme>()!;
final textTheme = Theme.of(context).textTheme;
final chatTheme = makeChatTheme(scale, textTheme);
final errorChatTheme = (ChatThemeEditor(chatTheme)
..inputTextColor = scale.errorScale.primary
..sendButtonIcon = Image.asset(
'assets/icon-send.png',
color: scale.errorScale.primary,
package: 'flutter_chat_ui',
))
.commit();
// Get the enclosing chat component cubit that contains our state
// (created by ChatComponentWidget.builder())
@ -216,80 +226,125 @@ class ChatComponentWidget extends StatelessWidget {
),
Expanded(
child: DecoratedBox(
decoration: const BoxDecoration(),
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (chatComponentCubit.scrollOffset != 0) {
decoration: const BoxDecoration(),
child: NotificationListener<ScrollNotification>(
onNotification: (notification) {
if (chatComponentCubit.scrollOffset != 0) {
return false;
}
if (!isFirstPage &&
notification.metrics.pixels <=
((notification.metrics.maxScrollExtent -
notification.metrics
.minScrollExtent) *
(1.0 - onEndReachedThreshold) +
notification
.metrics.minScrollExtent)) {
//
final scrollOffset = (notification
.metrics.maxScrollExtent -
notification.metrics.minScrollExtent) *
(1.0 - onEndReachedThreshold);
chatComponentCubit.scrollOffset = scrollOffset;
//
singleFuture(chatComponentState.chatKey,
() async {
await _handlePageForward(chatComponentCubit,
messageWindow, notification);
});
} else if (!isLastPage &&
notification.metrics.pixels >=
((notification.metrics.maxScrollExtent -
notification.metrics
.minScrollExtent) *
onEndReachedThreshold +
notification
.metrics.minScrollExtent)) {
//
final scrollOffset = -(notification
.metrics.maxScrollExtent -
notification.metrics.minScrollExtent) *
(1.0 - onEndReachedThreshold);
chatComponentCubit.scrollOffset = scrollOffset;
//
singleFuture(chatComponentState.chatKey,
() async {
await _handlePageBackward(chatComponentCubit,
messageWindow, notification);
});
}
return false;
}
},
child: ValueListenableBuilder(
valueListenable:
chatComponentState.textEditingController,
builder: (context, textEditingValue, __) {
final messageIsValid = utf8
.encode(textEditingValue.text)
.lengthInBytes <
2048;
if (!isFirstPage &&
notification.metrics.pixels <=
((notification.metrics.maxScrollExtent -
notification
.metrics.minScrollExtent) *
(1.0 - onEndReachedThreshold) +
notification.metrics.minScrollExtent)) {
//
final scrollOffset = (notification
.metrics.maxScrollExtent -
notification.metrics.minScrollExtent) *
(1.0 - onEndReachedThreshold);
chatComponentCubit.scrollOffset = scrollOffset;
//
singleFuture(chatComponentState.chatKey,
() async {
await _handlePageForward(chatComponentCubit,
messageWindow, notification);
});
} else if (!isLastPage &&
notification.metrics.pixels >=
((notification.metrics.maxScrollExtent -
notification
.metrics.minScrollExtent) *
onEndReachedThreshold +
notification.metrics.minScrollExtent)) {
//
final scrollOffset = -(notification
.metrics.maxScrollExtent -
notification.metrics.minScrollExtent) *
(1.0 - onEndReachedThreshold);
chatComponentCubit.scrollOffset = scrollOffset;
//
singleFuture(chatComponentState.chatKey,
() async {
await _handlePageBackward(chatComponentCubit,
messageWindow, notification);
});
}
return false;
},
child: Chat(
key: chatComponentState.chatKey,
theme: chatTheme,
messages: messageWindow.window.toList(),
scrollToBottomOnSend: isFirstPage,
scrollController:
chatComponentState.scrollController,
// isLastPage: isLastPage,
// onEndReached: () async {
// await _handlePageBackward(
// chatComponentCubit, messageWindow);
// },
//onEndReachedThreshold: onEndReachedThreshold,
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: (pt) =>
_handleSendPressed(chatComponentCubit, pt),
//showUserAvatars: false,
//showUserNames: true,
user: localUser,
emptyState: const EmptyChatWidget())),
),
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,
onSendPressed: (pt) {
try {
if (!messageIsValid) {
showErrorToast(
context,
translate(
'chat.message_too_long'));
return;
}
_handleSendPressed(
chatComponentCubit, pt);
} on FormatException {
showErrorToast(
context,
translate(
'chat.message_too_long'));
}
},
listBottomWidget: messageIsValid
? null
: Text(
translate(
'chat.message_too_long'),
style: TextStyle(
color: scale
.errorScale.primary))
.toCenter(),
//showUserAvatars: false,
//showUserNames: true,
user: localUser,
emptyState: const EmptyChatWidget());
}))),
),
],
),

View File

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

View File

@ -3,7 +3,8 @@ enum IdentityException implements Exception {
readError('identity could not be read'),
noAccount('no account record info'),
limitExceeded('too many items for the limit'),
invalid('identity is corrupted or secret is invalid');
invalid('identity is corrupted or secret is invalid'),
cancelled('account operation cancelled');
const IdentityException(this.message);
final String message;

View File

@ -68,12 +68,12 @@ class IdentityInstance with _$IdentityInstance {
return cs;
}
/// Read the account record info for a specific accountKey from the identity
/// instance record using the identity instance secret key to decrypt
/// Read the account record info for a specific applicationId from the
/// identity instance record using the identity instance secret key to decrypt
Future<List<AccountRecordInfo>> readAccount(
{required TypedKey superRecordKey,
required SecretKey secretKey,
required String accountKey}) async {
required String applicationId}) async {
// Read the identity key to get the account keys
final pool = DHTRecordPool.instance;
@ -91,7 +91,7 @@ class IdentityInstance with _$IdentityInstance {
throw IdentityException.readError;
}
final accountRecords = IMapOfSets.from(identity.accountRecords);
final vcAccounts = accountRecords.get(accountKey);
final vcAccounts = accountRecords.get(applicationId);
accountRecordInfo = vcAccounts.toList();
});
@ -104,7 +104,7 @@ class IdentityInstance with _$IdentityInstance {
Future<AccountRecordInfo> addAccount({
required TypedKey superRecordKey,
required SecretKey secretKey,
required String accountKey,
required String applicationId,
required Future<Uint8List> Function(TypedKey parent) createAccountCallback,
int maxAccounts = 1,
}) async {
@ -143,11 +143,12 @@ class IdentityInstance with _$IdentityInstance {
}
final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords);
if (oldAccountRecords.get(accountKey).length >= maxAccounts) {
if (oldAccountRecords.get(applicationId).length >= maxAccounts) {
throw IdentityException.limitExceeded;
}
final accountRecords =
oldAccountRecords.add(accountKey, newAccountRecordInfo).asIMap();
final accountRecords = oldAccountRecords
.add(applicationId, newAccountRecordInfo)
.asIMap();
return oldIdentity.copyWith(accountRecords: accountRecords);
});
@ -156,6 +157,61 @@ class IdentityInstance with _$IdentityInstance {
});
}
/// Removes an Account associated with super identity from the identity
/// instance record. 'removeAccountCallback' returns the account to be
/// removed from the list passed to it.
Future<bool> removeAccount({
required TypedKey superRecordKey,
required SecretKey secretKey,
required String applicationId,
required Future<AccountRecordInfo?> Function(
List<AccountRecordInfo> accountRecordInfos)
removeAccountCallback,
}) async {
final pool = DHTRecordPool.instance;
/////// Add account with profile to DHT
// Open identity key for writing
veilidLoggy.debug('Opening identity record');
return (await pool.openRecordWrite(recordKey, writer(secretKey),
debugName: 'IdentityInstance::addAccount::IdentityRecord',
parent: superRecordKey))
.scope((identityRec) async {
try {
// Update identity key to remove account
veilidLoggy.debug('Updating identity to remove account');
await identityRec.eventualUpdateJson(Identity.fromJson,
(oldIdentity) async {
if (oldIdentity == null) {
throw IdentityException.readError;
}
final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords);
// Get list of accounts associated with the application
final vcAccounts = oldAccountRecords.get(applicationId);
final accountRecordInfos = vcAccounts.toList();
// Call the callback to return what account to remove
final toRemove = await removeAccountCallback(accountRecordInfos);
if (toRemove == null) {
throw IdentityException.cancelled;
}
final newAccountRecords =
oldAccountRecords.remove(applicationId, toRemove).asIMap();
return oldIdentity.copyWith(accountRecords: newAccountRecords);
});
} on IdentityException catch (e) {
if (e == IdentityException.cancelled) {
return false;
}
rethrow;
}
return true;
});
}
////////////////////////////////////////////////////////////////////////////
// Internal implementation

View File

@ -15,12 +15,14 @@ class PersistentQueue<T extends GeneratedMessage>
required String key,
required T Function(Uint8List) fromBuffer,
required Future<void> Function(IList<T>) closure,
bool deleteOnClose = true})
bool deleteOnClose = true,
void Function(Object, StackTrace)? onError})
: _table = table,
_key = key,
_fromBuffer = fromBuffer,
_closure = closure,
_deleteOnClose = deleteOnClose {
_deleteOnClose = deleteOnClose,
_onError = onError {
_initWait.add(_init);
}
@ -61,9 +63,17 @@ class PersistentQueue<T extends GeneratedMessage>
}));
// Load the queue if we have one
await _queueMutex.protect(() async {
_queue = await load() ?? await store(IList<T>.empty());
});
try {
await _queueMutex.protect(() async {
_queue = await load() ?? await store(IList<T>.empty());
});
} on Exception catch (e, st) {
if (_onError != null) {
_onError(e, st);
} else {
rethrow;
}
}
}
Future<void> _updateQueueInner(IList<T> newQueue) async {
@ -132,24 +142,32 @@ class PersistentQueue<T extends GeneratedMessage>
// }
Future<void> _process() async {
// Take a copy of the current queue
// (doesn't need queue mutex because this is a sync operation)
final toProcess = _queue;
final processCount = toProcess.length;
if (processCount == 0) {
return;
try {
// Take a copy of the current queue
// (doesn't need queue mutex because this is a sync operation)
final toProcess = _queue;
final processCount = toProcess.length;
if (processCount == 0) {
return;
}
// Run the processing closure
await _closure(toProcess);
// If there was no exception, remove the processed items
await _queueMutex.protect(() async {
// Get the queue from the state again as items could
// have been added during processing
final newQueue = _queue.skip(processCount).toIList();
await _updateQueueInner(newQueue);
});
} on Exception catch (e, sp) {
if (_onError != null) {
_onError(e, sp);
} else {
rethrow;
}
}
// Run the processing closure
await _closure(toProcess);
// If there was no exception, remove the processed items
await _queueMutex.protect(() async {
// Get the queue from the state again as items could
// have been added during processing
final newQueue = _queue.skip(processCount).toIList();
await _updateQueueInner(newQueue);
});
}
IList<T> get queue => _queue;
@ -190,4 +208,5 @@ class PersistentQueue<T extends GeneratedMessage>
final StreamController<Iterable<T>> _syncAddController = StreamController();
final StreamController<void> _queueReady = StreamController();
final Future<void> Function(IList<T>) _closure;
final void Function(Object, StackTrace)? _onError;
}

View File

@ -483,10 +483,10 @@ packages:
description:
path: "."
ref: main
resolved-ref: "6712d897e25041de38fb53ec06dc7a12cc6bff7d"
resolved-ref: "0d8ac2fcafe24eba1adff9290a9ccd41f7718480"
url: "https://gitlab.com/veilid/flutter-chat-ui.git"
source: git
version: "1.6.13"
version: "1.6.14"
flutter_form_builder:
dependency: "direct main"
description: