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", "name": "Name",
"pronouns": "Pronouns", "pronouns": "Pronouns",
"remove_account": "Remove Account", "remove_account": "Remove Account",
"delete_identity": "Delete Identity", "destroy_account": "Destroy Account",
"remove_account_confirm": "Confirm Account Removal", "remove_account_confirm": "Confirm Account Removal",
"remove_account_description": "Remove account from this device only", "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", "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", "destroy_account_confirm": "Confirm Account Destruction",
"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!", "destroy_account_description": "Destroy account, removing it completely from all devices everywhere",
"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_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?", "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_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", "account_removed": "Account removed successfully",
"identity_deleted": "Identity deleted successfully" "account_destroyed": "Account destroyed successfully"
}, },
"show_recovery_key_page": { "show_recovery_key_page": {
"titlebar": "Save Recovery Key", "titlebar": "Save Recovery Key",
@ -103,7 +104,8 @@
}, },
"chat": { "chat": {
"start_a_conversation": "Start A Conversation", "start_a_conversation": "Start A Conversation",
"say_something": "Say Something" "say_something": "Say Something",
"message_too_long": "Message too long"
}, },
"create_invitation_dialog": { "create_invitation_dialog": {
"title": "Create Contact Invitation", "title": "Create Contact Invitation",

View File

@ -7,7 +7,7 @@ import '../../../proto/proto.dart' as proto;
import '../../tools/tools.dart'; import '../../tools/tools.dart';
import '../models/models.dart'; import '../models/models.dart';
const String veilidChatAccountKey = 'com.veilid.veilidchat'; const String veilidChatApplicationId = 'com.veilid.veilidchat';
enum AccountRepositoryChange { localAccounts, userLogins, activeLocalAccount } enum AccountRepositoryChange { localAccounts, userLogins, activeLocalAccount }
@ -194,6 +194,33 @@ class AccountRepository {
/// Recover an account with the master identity secret /// Recover an account with the master identity secret
/// Delete an account from all devices /// 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 { Future<void> switchToAccount(TypedKey? superIdentityRecordKey) async {
final activeLocalAccount = await _activeLocalAccount.get(); final activeLocalAccount = await _activeLocalAccount.get();
@ -231,7 +258,7 @@ class AccountRepository {
await superIdentity.currentInstance.addAccount( await superIdentity.currentInstance.addAccount(
superRecordKey: superIdentity.recordKey, superRecordKey: superIdentity.recordKey,
secretKey: identitySecret, secretKey: identitySecret,
accountKey: veilidChatAccountKey, applicationId: veilidChatApplicationId,
createAccountCallback: (parent) async { createAccountCallback: (parent) async {
// Make empty contact list // Make empty contact list
log.debug('Creating contacts list'); log.debug('Creating contacts list');
@ -305,7 +332,7 @@ class AccountRepository {
.readAccount( .readAccount(
superRecordKey: superIdentity.recordKey, superRecordKey: superIdentity.recordKey,
secretKey: identitySecret, secretKey: identitySecret,
accountKey: veilidChatAccountKey); applicationId: veilidChatApplicationId);
if (accountRecordInfoList.length > 1) { if (accountRecordInfoList.length > 1) {
throw IdentityException.limitExceeded; throw IdentityException.limitExceeded;
} else if (accountRecordInfoList.isEmpty) { } 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 { Future<void> _onSubmit(GlobalKey<FormBuilderState> formKey) async {
@ -234,13 +296,13 @@ class _EditAccountPageState extends State<EditAccountPage> {
Text(translate('edit_account_page.remove_account')) Text(translate('edit_account_page.remove_account'))
.paddingLTRB(0, 0, 4, 0) .paddingLTRB(0, 0, 4, 0)
])).paddingLTRB(0, 8, 0, 24), ])).paddingLTRB(0, 8, 0, 24),
Text(translate('edit_account_page.delete_identity_description')), Text(translate('edit_account_page.destroy_account_description')),
ElevatedButton( ElevatedButton(
onPressed: _onDeleteIdentity, onPressed: _onDestroyAccount,
child: Row(mainAxisSize: MainAxisSize.min, children: [ child: Row(mainAxisSize: MainAxisSize.min, children: [
const Icon(Icons.person_off, size: 16) const Icon(Icons.person_off, size: 16)
.paddingLTRB(0, 0, 4, 0), .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, 0, 4, 0)
])).paddingLTRB(0, 8, 0, 24) ])).paddingLTRB(0, 8, 0, 24)
]).paddingSymmetric(horizontal: 24, vertical: 8)) ]).paddingSymmetric(horizontal: 24, vertical: 8))

View File

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

View File

@ -97,11 +97,13 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
// Initialize everything // Initialize everything
Future<void> _init() async { Future<void> _init() async {
_unsentMessagesQueue = PersistentQueue<proto.Message>( _unsentMessagesQueue = PersistentQueue<proto.Message>(
table: 'SingleContactUnsentMessages', table: 'SingleContactUnsentMessages',
key: _remoteConversationRecordKey.toString(), key: _remoteConversationRecordKey.toString(),
fromBuffer: proto.Message.fromBuffer, fromBuffer: proto.Message.fromBuffer,
closure: _processUnsentMessages, closure: _processUnsentMessages,
); onError: (e, sp) {
log.error('Exception while processing unsent messages: $e\n$sp\n');
});
// Make crypto // Make crypto
await _initCrypto(); await _initCrypto();
@ -297,16 +299,23 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
proto.Message? previousMessage; proto.Message? previousMessage;
final processedMessages = messages.toList(); final processedMessages = messages.toList();
for (final message in processedMessages) { for (final message in processedMessages) {
await _processMessageToSend(message, previousMessage); try {
previousMessage = message; await _processMessageToSend(message, previousMessage);
previousMessage = message;
} on Exception catch (e) {
log.error('Exception processing unsent message: $e');
}
} }
// _sendingMessages = messages; // _sendingMessages = messages;
// _renderState(); // _renderState();
try {
await _sentMessagesCubit!.operateAppendEventual((writer) => await _sentMessagesCubit!.operateAppendEventual((writer) =>
writer.addAll(messages.map((m) => m.writeToBuffer()).toList())); writer.addAll(messages.map((m) => m.writeToBuffer()).toList()));
} on Exception catch (e) {
log.error('Exception appending unsent messages: $e');
}
// _sendingMessages = const IList.empty(); // _sendingMessages = const IList.empty();
} }
@ -403,6 +412,10 @@ class SingleContactMessagesCubit extends Cubit<SingleContactMessagesState> {
..author = _accountInfo.identityTypedPublicKey.toProto() ..author = _accountInfo.identityTypedPublicKey.toProto()
..timestamp = Veilid.instance.now().toInt64(); ..timestamp = Veilid.instance.now().toInt64();
if ((message.writeToBuffer().lengthInBytes + 256) > 4096) {
throw const FormatException('message is too long');
}
// Put in the queue // Put in the queue
_unsentMessagesQueue.addSync(message); _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:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' show Message, User; 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:freezed_annotation/freezed_annotation.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
@ -19,6 +20,8 @@ class ChatComponentState with _$ChatComponentState {
required GlobalKey<ChatState> chatKey, required GlobalKey<ChatState> chatKey,
// ScrollController for the chat // ScrollController for the chat
required AutoScrollController scrollController, required AutoScrollController scrollController,
// TextEditingController for the chat
required InputTextFieldController textEditingController,
// Local user // Local user
required User? localUser, required User? localUser,
// Active remote users // Active remote users

View File

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

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:async_tools/async_tools.dart'; 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_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart' as types; import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart';
import 'package:flutter_translate/flutter_translate.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart'; import '../../account_manager/account_manager.dart';
@ -154,6 +156,14 @@ class ChatComponentWidget extends StatelessWidget {
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
final chatTheme = makeChatTheme(scale, 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 // Get the enclosing chat component cubit that contains our state
// (created by ChatComponentWidget.builder()) // (created by ChatComponentWidget.builder())
@ -216,80 +226,125 @@ class ChatComponentWidget extends StatelessWidget {
), ),
Expanded( Expanded(
child: DecoratedBox( child: DecoratedBox(
decoration: const BoxDecoration(), decoration: const BoxDecoration(),
child: NotificationListener<ScrollNotification>( child: NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
if (chatComponentCubit.scrollOffset != 0) { 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; return false;
} },
child: ValueListenableBuilder(
valueListenable:
chatComponentState.textEditingController,
builder: (context, textEditingValue, __) {
final messageIsValid = utf8
.encode(textEditingValue.text)
.lengthInBytes <
2048;
if (!isFirstPage && return Chat(
notification.metrics.pixels <= key: chatComponentState.chatKey,
((notification.metrics.maxScrollExtent - theme: messageIsValid
notification ? chatTheme
.metrics.minScrollExtent) * : errorChatTheme,
(1.0 - onEndReachedThreshold) + messages: messageWindow.window.toList(),
notification.metrics.minScrollExtent)) { scrollToBottomOnSend: isFirstPage,
// scrollController:
final scrollOffset = (notification chatComponentState.scrollController,
.metrics.maxScrollExtent - inputOptions: InputOptions(
notification.metrics.minScrollExtent) * inputClearMode: messageIsValid
(1.0 - onEndReachedThreshold); ? InputClearMode.always
: InputClearMode.never,
chatComponentCubit.scrollOffset = scrollOffset; textEditingController:
chatComponentState
// .textEditingController),
singleFuture(chatComponentState.chatKey, // isLastPage: isLastPage,
() async { // onEndReached: () async {
await _handlePageForward(chatComponentCubit, // await _handlePageBackward(
messageWindow, notification); // chatComponentCubit, messageWindow);
}); // },
} else if (!isLastPage && //onEndReachedThreshold: onEndReachedThreshold,
notification.metrics.pixels >= //onAttachmentPressed: _handleAttachmentPressed,
((notification.metrics.maxScrollExtent - //onMessageTap: _handleMessageTap,
notification //onPreviewDataFetched: _handlePreviewDataFetched,
.metrics.minScrollExtent) * onSendPressed: (pt) {
onEndReachedThreshold + try {
notification.metrics.minScrollExtent)) { if (!messageIsValid) {
// showErrorToast(
final scrollOffset = -(notification context,
.metrics.maxScrollExtent - translate(
notification.metrics.minScrollExtent) * 'chat.message_too_long'));
(1.0 - onEndReachedThreshold); return;
}
chatComponentCubit.scrollOffset = scrollOffset; _handleSendPressed(
// chatComponentCubit, pt);
singleFuture(chatComponentState.chatKey, } on FormatException {
() async { showErrorToast(
await _handlePageBackward(chatComponentCubit, context,
messageWindow, notification); translate(
}); 'chat.message_too_long'));
} }
return false; },
}, listBottomWidget: messageIsValid
child: Chat( ? null
key: chatComponentState.chatKey, : Text(
theme: chatTheme, translate(
messages: messageWindow.window.toList(), 'chat.message_too_long'),
scrollToBottomOnSend: isFirstPage, style: TextStyle(
scrollController: color: scale
chatComponentState.scrollController, .errorScale.primary))
// isLastPage: isLastPage, .toCenter(),
// onEndReached: () async { //showUserAvatars: false,
// await _handlePageBackward( //showUserNames: true,
// chatComponentCubit, messageWindow); user: localUser,
// }, emptyState: const EmptyChatWidget());
//onEndReachedThreshold: onEndReachedThreshold, }))),
//onAttachmentPressed: _handleAttachmentPressed,
//onMessageTap: _handleMessageTap,
//onPreviewDataFetched: _handlePreviewDataFetched,
onSendPressed: (pt) =>
_handleSendPressed(chatComponentCubit, pt),
//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/material.dart';
import 'package:flutter_chat_ui/flutter_chat_ui.dart'; import 'package:flutter_chat_ui/flutter_chat_ui.dart';
@ -52,3 +54,398 @@ ChatTheme makeChatTheme(ScaleScheme scale, TextTheme textTheme) =>
color: Colors.white, color: Colors.white,
fontSize: 64, 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'), readError('identity could not be read'),
noAccount('no account record info'), noAccount('no account record info'),
limitExceeded('too many items for the limit'), 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); const IdentityException(this.message);
final String message; final String message;

View File

@ -68,12 +68,12 @@ class IdentityInstance with _$IdentityInstance {
return cs; return cs;
} }
/// Read the account record info for a specific accountKey from the identity /// Read the account record info for a specific applicationId from the
/// instance record using the identity instance secret key to decrypt /// identity instance record using the identity instance secret key to decrypt
Future<List<AccountRecordInfo>> readAccount( Future<List<AccountRecordInfo>> readAccount(
{required TypedKey superRecordKey, {required TypedKey superRecordKey,
required SecretKey secretKey, required SecretKey secretKey,
required String accountKey}) async { required String applicationId}) async {
// Read the identity key to get the account keys // Read the identity key to get the account keys
final pool = DHTRecordPool.instance; final pool = DHTRecordPool.instance;
@ -91,7 +91,7 @@ class IdentityInstance with _$IdentityInstance {
throw IdentityException.readError; throw IdentityException.readError;
} }
final accountRecords = IMapOfSets.from(identity.accountRecords); final accountRecords = IMapOfSets.from(identity.accountRecords);
final vcAccounts = accountRecords.get(accountKey); final vcAccounts = accountRecords.get(applicationId);
accountRecordInfo = vcAccounts.toList(); accountRecordInfo = vcAccounts.toList();
}); });
@ -104,7 +104,7 @@ class IdentityInstance with _$IdentityInstance {
Future<AccountRecordInfo> addAccount({ Future<AccountRecordInfo> addAccount({
required TypedKey superRecordKey, required TypedKey superRecordKey,
required SecretKey secretKey, required SecretKey secretKey,
required String accountKey, required String applicationId,
required Future<Uint8List> Function(TypedKey parent) createAccountCallback, required Future<Uint8List> Function(TypedKey parent) createAccountCallback,
int maxAccounts = 1, int maxAccounts = 1,
}) async { }) async {
@ -143,11 +143,12 @@ class IdentityInstance with _$IdentityInstance {
} }
final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords); final oldAccountRecords = IMapOfSets.from(oldIdentity.accountRecords);
if (oldAccountRecords.get(accountKey).length >= maxAccounts) { if (oldAccountRecords.get(applicationId).length >= maxAccounts) {
throw IdentityException.limitExceeded; throw IdentityException.limitExceeded;
} }
final accountRecords = final accountRecords = oldAccountRecords
oldAccountRecords.add(accountKey, newAccountRecordInfo).asIMap(); .add(applicationId, newAccountRecordInfo)
.asIMap();
return oldIdentity.copyWith(accountRecords: accountRecords); 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 // Internal implementation

View File

@ -15,12 +15,14 @@ class PersistentQueue<T extends GeneratedMessage>
required String key, required String key,
required T Function(Uint8List) fromBuffer, required T Function(Uint8List) fromBuffer,
required Future<void> Function(IList<T>) closure, required Future<void> Function(IList<T>) closure,
bool deleteOnClose = true}) bool deleteOnClose = true,
void Function(Object, StackTrace)? onError})
: _table = table, : _table = table,
_key = key, _key = key,
_fromBuffer = fromBuffer, _fromBuffer = fromBuffer,
_closure = closure, _closure = closure,
_deleteOnClose = deleteOnClose { _deleteOnClose = deleteOnClose,
_onError = onError {
_initWait.add(_init); _initWait.add(_init);
} }
@ -61,9 +63,17 @@ class PersistentQueue<T extends GeneratedMessage>
})); }));
// Load the queue if we have one // Load the queue if we have one
await _queueMutex.protect(() async { try {
_queue = await load() ?? await store(IList<T>.empty()); 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 { Future<void> _updateQueueInner(IList<T> newQueue) async {
@ -132,24 +142,32 @@ class PersistentQueue<T extends GeneratedMessage>
// } // }
Future<void> _process() async { Future<void> _process() async {
// Take a copy of the current queue try {
// (doesn't need queue mutex because this is a sync operation) // Take a copy of the current queue
final toProcess = _queue; // (doesn't need queue mutex because this is a sync operation)
final processCount = toProcess.length; final toProcess = _queue;
if (processCount == 0) { final processCount = toProcess.length;
return; 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; IList<T> get queue => _queue;
@ -190,4 +208,5 @@ class PersistentQueue<T extends GeneratedMessage>
final StreamController<Iterable<T>> _syncAddController = StreamController(); final StreamController<Iterable<T>> _syncAddController = StreamController();
final StreamController<void> _queueReady = StreamController(); final StreamController<void> _queueReady = StreamController();
final Future<void> Function(IList<T>) _closure; final Future<void> Function(IList<T>) _closure;
final void Function(Object, StackTrace)? _onError;
} }

View File

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