busy handling

This commit is contained in:
Christien Rioux 2024-02-27 12:45:58 -05:00
parent 43b01c7555
commit c6f017b0d1
23 changed files with 307 additions and 179 deletions

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:bloc_tools/bloc_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
@ -61,9 +62,10 @@ class MessagesCubit extends Cubit<AsyncValue<IList<proto.Message>>> {
await super.close(); await super.close();
} }
void updateLocalMessagesState(AsyncValue<IList<proto.Message>> avmessages) { void updateLocalMessagesState(
BlocBusyState<AsyncValue<IList<proto.Message>>> avmessages) {
// Updated local messages from online just update the state immediately // Updated local messages from online just update the state immediately
emit(avmessages); emit(avmessages.state);
} }
Future<void> _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async { Future<void> _updateRemoteMessagesStateAsync(_MessageQueueEntry entry) async {
@ -97,16 +99,17 @@ class MessagesCubit extends Cubit<AsyncValue<IList<proto.Message>>> {
// Insert at this position // Insert at this position
if (!skip) { if (!skip) {
// Insert into dht backing array // Insert into dht backing array
await _localMessagesCubit!.shortArray await _localMessagesCubit!.operate((shortArray) =>
.tryInsertItem(pos, newMessage.writeToBuffer()); shortArray.tryInsertItem(pos, newMessage.writeToBuffer()));
// Insert into local copy as well for this operation // Insert into local copy as well for this operation
localMessages = localMessages.insert(pos, newMessage); localMessages = localMessages.insert(pos, newMessage);
} }
} }
} }
void updateRemoteMessagesState(AsyncValue<IList<proto.Message>> avmessages) { void updateRemoteMessagesState(
final remoteMessages = avmessages.data?.value; BlocBusyState<AsyncValue<IList<proto.Message>>> avmessages) {
final remoteMessages = avmessages.state.data?.value;
if (remoteMessages == null) { if (remoteMessages == null) {
return; return;
} }
@ -171,7 +174,8 @@ class MessagesCubit extends Cubit<AsyncValue<IList<proto.Message>>> {
} }
Future<void> addMessage({required proto.Message message}) async { Future<void> addMessage({required proto.Message message}) async {
await _localMessagesCubit!.shortArray.tryAddItem(message.writeToBuffer()); await _localMessagesCubit!.operate(
(shortArray) => shortArray.tryAddItem(message.writeToBuffer()));
} }
Future<DHTRecordCrypto> getMessagesCrypto() async { Future<DHTRecordCrypto> getMessagesCrypto() async {

View File

@ -48,7 +48,8 @@ class ChatComponent extends StatelessWidget {
if (accountRecordInfo == null) { if (accountRecordInfo == null) {
return debugPage('should always have an account record here'); return debugPage('should always have an account record here');
} }
final contactList = context.watch<ContactListCubit>().state.data?.value; final contactList =
context.watch<ContactListCubit>().state.state.data?.value;
if (contactList == null) { if (contactList == null) {
return debugPage('should always have a contact list here'); return debugPage('should always have a contact list here');
} }

View File

@ -36,7 +36,9 @@ typedef ActiveConversationsBlocMapState
// Automatically follows the state of a ChatListCubit. // Automatically follows the state of a ChatListCubit.
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey, class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<ActiveConversationState>, ActiveConversationCubit> AsyncValue<ActiveConversationState>, ActiveConversationCubit>
with StateFollower<AsyncValue<IList<proto.Chat>>, TypedKey, proto.Chat> { with
StateFollower<BlocBusyState<AsyncValue<IList<proto.Chat>>>, TypedKey,
proto.Chat> {
ActiveConversationsBlocMapCubit( ActiveConversationsBlocMapCubit(
{required ActiveAccountInfo activeAccountInfo, {required ActiveAccountInfo activeAccountInfo,
required ContactListCubit contactListCubit}) required ContactListCubit contactListCubit})
@ -73,8 +75,9 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
/// StateFollower ///////////////////////// /// StateFollower /////////////////////////
@override @override
IMap<TypedKey, proto.Chat> getStateMap(AsyncValue<IList<proto.Chat>> state) { IMap<TypedKey, proto.Chat> getStateMap(
final stateValue = state.data?.value; BlocBusyState<AsyncValue<IList<proto.Chat>>> state) {
final stateValue = state.state.data?.value;
if (stateValue == null) { if (stateValue == null) {
return IMap(); return IMap();
} }
@ -88,7 +91,7 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
@override @override
Future<void> updateState(TypedKey key, proto.Chat value) async { Future<void> updateState(TypedKey key, proto.Chat value) async {
final contactList = _contactListCubit.state.data?.value; final contactList = _contactListCubit.state.state.data?.value;
if (contactList == null) { if (contactList == null) {
await addState(key, const AsyncValue.loading()); await addState(key, const AsyncValue.loading());
return; return;

View File

@ -1,7 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:bloc_tools/bloc_tools.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';
@ -44,7 +42,9 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
// Add Chat to account's list // Add Chat to account's list
// if this fails, don't keep retrying, user can try again later // if this fails, don't keep retrying, user can try again later
if (await shortArray.tryAddItem(chat.writeToBuffer()) == false) { final added = await operate(
(shortArray) => shortArray.tryAddItem(chat.writeToBuffer()));
if (!added) {
throw Exception('Failed to add chat'); throw Exception('Failed to add chat');
} }
} }
@ -57,17 +57,18 @@ class ChatListCubit extends DHTShortArrayCubit<proto.Chat> {
// Remove Chat from account's list // Remove Chat from account's list
// if this fails, don't keep retrying, user can try again later // if this fails, don't keep retrying, user can try again later
await operate((shortArray) async {
for (var i = 0; i < shortArray.length; i++) { for (var i = 0; i < shortArray.length; i++) {
final cbuf = await shortArray.getItem(i); final cbuf = await shortArray.getItem(i);
if (cbuf == null) { if (cbuf == null) {
throw Exception('Failed to get chat'); throw Exception('Failed to get chat');
}
final c = proto.Chat.fromBuffer(cbuf);
if (c.remoteConversationKey == remoteConversationKey) {
await shortArray.tryRemoveItem(i);
return;
}
} }
final c = proto.Chat.fromBuffer(cbuf); });
if (c.remoteConversationKey == remoteConversationKey) {
await shortArray.tryRemoveItem(i);
return;
}
}
} }
} }

View File

@ -10,10 +10,15 @@ import '../../theme/theme.dart';
import '../chat_list.dart'; import '../chat_list.dart';
class ChatSingleContactItemWidget extends StatelessWidget { class ChatSingleContactItemWidget extends StatelessWidget {
const ChatSingleContactItemWidget({required proto.Contact contact, super.key}) const ChatSingleContactItemWidget({
: _contact = contact; required proto.Contact contact,
required bool disabled,
super.key,
}) : _contact = contact,
_disabled = disabled;
final proto.Contact _contact; final proto.Contact _contact;
final bool _disabled;
@override @override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
@ -43,12 +48,14 @@ class ChatSingleContactItemWidget extends StatelessWidget {
motion: const DrawerMotion(), motion: const DrawerMotion(),
children: [ children: [
SlidableAction( SlidableAction(
onPressed: (context) async { onPressed: _disabled
final chatListCubit = context.read<ChatListCubit>(); ? null
await chatListCubit.deleteChat( : (context) async {
remoteConversationRecordKey: final chatListCubit = context.read<ChatListCubit>();
remoteConversationRecordKey); await chatListCubit.deleteChat(
}, remoteConversationRecordKey:
remoteConversationRecordKey);
},
backgroundColor: scale.tertiaryScale.background, backgroundColor: scale.tertiaryScale.background,
foregroundColor: scale.tertiaryScale.text, foregroundColor: scale.tertiaryScale.text,
icon: Icons.delete, icon: Icons.delete,
@ -67,11 +74,14 @@ class ChatSingleContactItemWidget extends StatelessWidget {
// The child of the Slidable is what the user sees when the // The child of the Slidable is what the user sees when the
// component is not dragged. // component is not dragged.
child: ListTile( child: ListTile(
onTap: () { onTap: _disabled
singleFuture(activeChatCubit, () async { ? null
activeChatCubit.setActiveChat(remoteConversationRecordKey); : () {
}); singleFuture(activeChatCubit, () async {
}, activeChatCubit
.setActiveChat(remoteConversationRecordKey);
});
},
title: Text(_contact.editedProfile.name), title: Text(_contact.editedProfile.name),
/// xxx show last message here /// xxx show last message here

View File

@ -45,7 +45,8 @@ class ChatSingleContactListWidget extends StatelessWidget {
return const Text('...'); return const Text('...');
} }
return ChatSingleContactItemWidget( return ChatSingleContactItemWidget(
contact: contact); contact: contact,
disabled: contactListV.busy);
}, },
filter: (value) { filter: (value) {
final lowerValue = value.toLowerCase(); final lowerValue = value.toLowerCase();

View File

@ -138,9 +138,11 @@ class ContactInvitationListCubit
// Add ContactInvitationRecord to account's list // Add ContactInvitationRecord to account's list
// if this fails, don't keep retrying, user can try again later // if this fails, don't keep retrying, user can try again later
if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) { await operate((shortArray) async {
throw Exception('Failed to add contact invitation record'); if (await shortArray.tryAddItem(cinvrec.writeToBuffer()) == false) {
} throw Exception('Failed to add contact invitation record');
}
});
}); });
}); });
@ -155,31 +157,34 @@ class ContactInvitationListCubit
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; _activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
// Remove ContactInvitationRecord from account's list // Remove ContactInvitationRecord from account's list
for (var i = 0; i < shortArray.length; i++) { await operate((shortArray) async {
final item = await shortArray.getItemProtobuf( for (var i = 0; i < shortArray.length; i++) {
proto.ContactInvitationRecord.fromBuffer, i); final item = await shortArray.getItemProtobuf(
if (item == null) { proto.ContactInvitationRecord.fromBuffer, i);
throw Exception('Failed to get contact invitation record'); if (item == null) {
} throw Exception('Failed to get contact invitation record');
if (item.contactRequestInbox.recordKey.toVeilid() == }
contactRequestInboxRecordKey) { if (item.contactRequestInbox.recordKey.toVeilid() ==
await shortArray.tryRemoveItem(i); contactRequestInboxRecordKey) {
await shortArray.tryRemoveItem(i);
await (await pool.openOwned(item.contactRequestInbox.toVeilid(),
parent: accountRecordKey)) await (await pool.openOwned(item.contactRequestInbox.toVeilid(),
.scope((contactRequestInbox) async { parent: accountRecordKey))
// Wipe out old invitation so it shows up as invalid .scope((contactRequestInbox) async {
await contactRequestInbox.tryWriteBytes(Uint8List(0)); // Wipe out old invitation so it shows up as invalid
await contactRequestInbox.delete(); await contactRequestInbox.tryWriteBytes(Uint8List(0));
}); await contactRequestInbox.delete();
if (!accepted) { });
await (await pool.openRead(item.localConversationRecordKey.toVeilid(), if (!accepted) {
parent: accountRecordKey)) await (await pool.openRead(
.delete(); item.localConversationRecordKey.toVeilid(),
parent: accountRecordKey))
.delete();
}
return;
} }
return;
} }
} });
} }
Future<ValidContactInvitation?> validateInvitation( Future<ValidContactInvitation?> validateInvitation(
@ -205,7 +210,7 @@ class ContactInvitationListCubit
// inbox with our list of extant invitations // inbox with our list of extant invitations
// If we're chatting to ourselves, // If we're chatting to ourselves,
// we are validating an invitation we have created // we are validating an invitation we have created
final isSelf = state.data!.value.indexWhere((cir) => final isSelf = state.state.data!.value.indexWhere((cir) =>
cir.contactRequestInbox.recordKey.toVeilid() == cir.contactRequestInbox.recordKey.toVeilid() ==
contactRequestInboxKey) != contactRequestInboxKey) !=
-1; -1;

View File

@ -16,8 +16,10 @@ typedef WaitingInvitationsBlocMapState
class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey, class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
AsyncValue<InvitationStatus>, WaitingInvitationCubit> AsyncValue<InvitationStatus>, WaitingInvitationCubit>
with with
StateFollower<AsyncValue<IList<proto.ContactInvitationRecord>>, StateFollower<
TypedKey, proto.ContactInvitationRecord> { BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>>,
TypedKey,
proto.ContactInvitationRecord> {
WaitingInvitationsBlocMapCubit( WaitingInvitationsBlocMapCubit(
{required this.activeAccountInfo, required this.account}); {required this.activeAccountInfo, required this.account});
@ -37,8 +39,8 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
/// StateFollower ///////////////////////// /// StateFollower /////////////////////////
@override @override
IMap<TypedKey, proto.ContactInvitationRecord> getStateMap( IMap<TypedKey, proto.ContactInvitationRecord> getStateMap(
AsyncValue<IList<proto.ContactInvitationRecord>> state) { BlocBusyState<AsyncValue<IList<proto.ContactInvitationRecord>>> state) {
final stateValue = state.data?.value; final stateValue = state.state.data?.value;
if (stateValue == null) { if (stateValue == null) {
return IMap(); return IMap();
} }

View File

@ -9,15 +9,20 @@ import '../contact_invitation.dart';
class ContactInvitationItemWidget extends StatelessWidget { class ContactInvitationItemWidget extends StatelessWidget {
const ContactInvitationItemWidget( const ContactInvitationItemWidget(
{required this.contactInvitationRecord, super.key}); {required this.contactInvitationRecord,
required this.disabled,
super.key});
final proto.ContactInvitationRecord contactInvitationRecord; final proto.ContactInvitationRecord contactInvitationRecord;
final bool disabled;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<proto.ContactInvitationRecord>( properties
'contactInvitationRecord', contactInvitationRecord)); ..add(DiagnosticsProperty<proto.ContactInvitationRecord>(
'contactInvitationRecord', contactInvitationRecord))
..add(DiagnosticsProperty<bool>('disabled', disabled));
} }
@override @override

View File

@ -10,10 +10,12 @@ import 'contact_invitation_item_widget.dart';
class ContactInvitationListWidget extends StatefulWidget { class ContactInvitationListWidget extends StatefulWidget {
const ContactInvitationListWidget({ const ContactInvitationListWidget({
required this.contactInvitationRecordList, required this.contactInvitationRecordList,
required this.disabled,
super.key, super.key,
}); });
final IList<proto.ContactInvitationRecord> contactInvitationRecordList; final IList<proto.ContactInvitationRecord> contactInvitationRecordList;
final bool disabled;
@override @override
ContactInvitationListWidgetState createState() => ContactInvitationListWidgetState createState() =>
@ -21,8 +23,10 @@ class ContactInvitationListWidget extends StatefulWidget {
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(IterableProperty<proto.ContactInvitationRecord>( properties
'contactInvitationRecordList', contactInvitationRecordList)); ..add(IterableProperty<proto.ContactInvitationRecord>(
'contactInvitationRecordList', contactInvitationRecordList))
..add(DiagnosticsProperty<bool>('disabled', disabled));
} }
} }
@ -63,6 +67,7 @@ class ContactInvitationListWidgetState
return ContactInvitationItemWidget( return ContactInvitationItemWidget(
contactInvitationRecord: contactInvitationRecord:
widget.contactInvitationRecordList[index], widget.contactInvitationRecordList[index],
disabled: widget.disabled,
key: ObjectKey(widget.contactInvitationRecordList[index])) key: ObjectKey(widget.contactInvitationRecordList[index]))
.paddingLTRB(4, 2, 4, 2); .paddingLTRB(4, 2, 4, 2);
}, },

View File

@ -53,9 +53,11 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
// Add Contact to account's list // Add Contact to account's list
// if this fails, don't keep retrying, user can try again later // if this fails, don't keep retrying, user can try again later
if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) { await operate((shortArray) async {
throw Exception('Failed to add contact record'); if (await shortArray.tryAddItem(contact.writeToBuffer()) == false) {
} throw Exception('Failed to add contact record');
}
});
} }
Future<void> deleteContact({required proto.Contact contact}) async { Future<void> deleteContact({required proto.Contact contact}) async {
@ -67,34 +69,36 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
contact.remoteConversationRecordKey.toVeilid(); contact.remoteConversationRecordKey.toVeilid();
// Remove Contact from account's list // Remove Contact from account's list
for (var i = 0; i < shortArray.length; i++) { await operate((shortArray) async {
final item = for (var i = 0; i < shortArray.length; i++) {
await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i); final item =
if (item == null) { await shortArray.getItemProtobuf(proto.Contact.fromBuffer, i);
throw Exception('Failed to get contact'); if (item == null) {
throw Exception('Failed to get contact');
}
if (item.remoteConversationRecordKey ==
contact.remoteConversationRecordKey) {
await shortArray.tryRemoveItem(i);
break;
}
} }
if (item.remoteConversationRecordKey == try {
contact.remoteConversationRecordKey) { await (await pool.openRead(localConversationKey,
await shortArray.tryRemoveItem(i);
break;
}
}
try {
await (await pool.openRead(localConversationKey,
parent: accountRecordKey))
.delete();
} on Exception catch (e) {
log.debug('error removing local conversation record key: $e', e);
}
try {
if (localConversationKey != remoteConversationKey) {
await (await pool.openRead(remoteConversationKey,
parent: accountRecordKey)) parent: accountRecordKey))
.delete(); .delete();
} on Exception catch (e) {
log.debug('error removing local conversation record key: $e', e);
} }
} on Exception catch (e) { try {
log.debug('error removing remote conversation record key: $e', e); if (localConversationKey != remoteConversationKey) {
} await (await pool.openRead(remoteConversationKey,
parent: accountRecordKey))
.delete();
}
} on Exception catch (e) {
log.debug('error removing remote conversation record key: $e', e);
}
});
} }
// //

View File

@ -11,9 +11,11 @@ import '../../theme/theme.dart';
import '../contacts.dart'; import '../contacts.dart';
class ContactItemWidget extends StatelessWidget { class ContactItemWidget extends StatelessWidget {
const ContactItemWidget({required this.contact, super.key}); const ContactItemWidget(
{required this.contact, required this.disabled, super.key});
final proto.Contact contact; final proto.Contact contact;
final bool disabled;
@override @override
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
@ -41,17 +43,22 @@ class ContactItemWidget extends StatelessWidget {
motion: const DrawerMotion(), motion: const DrawerMotion(),
children: [ children: [
SlidableAction( SlidableAction(
onPressed: (context) async { onPressed: disabled || context.read<ChatListCubit>().isBusy
final contactListCubit = context.read<ContactListCubit>(); ? null
final chatListCubit = context.read<ChatListCubit>(); : (context) async {
final contactListCubit =
context.read<ContactListCubit>();
final chatListCubit = context.read<ChatListCubit>();
// Remove any chats for this contact // Remove any chats for this contact
await chatListCubit.deleteChat( await chatListCubit.deleteChat(
remoteConversationRecordKey: remoteConversationKey); remoteConversationRecordKey:
remoteConversationKey);
// Delete the contact itself // Delete the contact itself
await contactListCubit.deleteContact(contact: contact); await contactListCubit.deleteContact(
}, contact: contact);
},
backgroundColor: scale.tertiaryScale.background, backgroundColor: scale.tertiaryScale.background,
foregroundColor: scale.tertiaryScale.text, foregroundColor: scale.tertiaryScale.text,
icon: Icons.delete, icon: Icons.delete,
@ -70,17 +77,21 @@ class ContactItemWidget extends StatelessWidget {
// The child of the Slidable is what the user sees when the // The child of the Slidable is what the user sees when the
// component is not dragged. // component is not dragged.
child: ListTile( child: ListTile(
onTap: () async { onTap: disabled || context.read<ChatListCubit>().isBusy
// Start a chat ? null
final chatListCubit = context.read<ChatListCubit>(); : () async {
await chatListCubit.getOrCreateChatSingleContact( // Start a chat
remoteConversationRecordKey: remoteConversationKey); final chatListCubit = context.read<ChatListCubit>();
// Click over to chats await chatListCubit.getOrCreateChatSingleContact(
if (context.mounted) { remoteConversationRecordKey: remoteConversationKey);
await MainPager.of(context)?.pageController.animateToPage(1, // Click over to chats
duration: 250.ms, curve: Curves.easeInOut); if (context.mounted) {
} await MainPager.of(context)
}, ?.pageController
.animateToPage(1,
duration: 250.ms, curve: Curves.easeInOut);
}
},
title: Text(contact.editedProfile.name), title: Text(contact.editedProfile.name),
subtitle: (contact.editedProfile.pronouns.isNotEmpty) subtitle: (contact.editedProfile.pronouns.isNotEmpty)
? Text(contact.editedProfile.pronouns) ? Text(contact.editedProfile.pronouns)

View File

@ -12,13 +12,17 @@ import 'contact_item_widget.dart';
import 'empty_contact_list_widget.dart'; import 'empty_contact_list_widget.dart';
class ContactListWidget extends StatelessWidget { class ContactListWidget extends StatelessWidget {
const ContactListWidget({required this.contactList, super.key}); const ContactListWidget(
{required this.contactList, required this.disabled, super.key});
final IList<proto.Contact> contactList; final IList<proto.Contact> contactList;
final bool disabled;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.add(IterableProperty<proto.Contact>('contactList', contactList)); properties
..add(IterableProperty<proto.Contact>('contactList', contactList))
..add(DiagnosticsProperty<bool>('disabled', disabled));
} }
@override @override
@ -36,7 +40,8 @@ class ContactListWidget extends StatelessWidget {
? const EmptyContactListWidget() ? const EmptyContactListWidget()
: SearchableList<proto.Contact>( : SearchableList<proto.Contact>(
initialList: contactList.toList(), initialList: contactList.toList(),
builder: (l, i, c) => ContactItemWidget(contact: c), builder: (l, i, c) =>
ContactItemWidget(contact: c, disabled: disabled),
filter: (value) { filter: (value) {
final lowerValue = value.toLowerCase(); final lowerValue = value.toLowerCase();
return contactList return contactList

View File

@ -1,4 +1,5 @@
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:bloc_tools/bloc_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -75,8 +76,7 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
// Process all accepted or rejected invitations // Process all accepted or rejected invitations
void _invitationStatusListener( void _invitationStatusListener(
BuildContext context, WaitingInvitationsBlocMapState state) { BuildContext context, WaitingInvitationsBlocMapState state) {
_singleInvitationStatusProcessor.updateState(state, _singleInvitationStatusProcessor.updateState(state, (newState) async {
closure: (newState) async {
final contactListCubit = context.read<ContactListCubit>(); final contactListCubit = context.read<ContactListCubit>();
final contactInvitationListCubit = final contactInvitationListCubit =
context.read<ContactInvitationListCubit>(); context.read<ContactInvitationListCubit>();
@ -146,7 +146,8 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
activeAccountInfo: widget.activeAccountInfo, activeAccountInfo: widget.activeAccountInfo,
contactListCubit: context.read<ContactListCubit>()) contactListCubit: context.read<ContactListCubit>())
..follow( ..follow(
initialInputState: const AsyncValue.loading(), initialInputState:
const BlocBusyState(AsyncValue.loading()),
stream: context.read<ChatListCubit>().stream)), stream: context.read<ChatListCubit>().stream)),
BlocProvider( BlocProvider(
create: (context) => create: (context) =>
@ -167,7 +168,8 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
activeAccountInfo: widget.activeAccountInfo, activeAccountInfo: widget.activeAccountInfo,
account: account) account: account)
..follow( ..follow(
initialInputState: const AsyncValue.loading(), initialInputState:
const BlocBusyState(AsyncValue.loading()),
stream: context stream: context
.read<ContactInvitationListCubit>() .read<ContactInvitationListCubit>()
.stream)) .stream))

View File

@ -38,11 +38,14 @@ class AccountPageState extends State<AccountPage> {
final textTheme = theme.textTheme; final textTheme = theme.textTheme;
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
final cilState = context.watch<ContactInvitationListCubit>().state;
final cilBusy = cilState.busy;
final contactInvitationRecordList = final contactInvitationRecordList =
context.watch<ContactInvitationListCubit>().state.data?.value ?? cilState.state.data?.value ?? const IListConst([]);
const IListConst([]);
final contactList = context.watch<ContactListCubit>().state.data?.value ?? final ciState = context.watch<ContactListCubit>().state;
const IListConst([]); final ciBusy = ciState.busy;
final contactList = ciState.state.data?.value ?? const IListConst([]);
return SizedBox( return SizedBox(
child: Column(children: <Widget>[ child: Column(children: <Widget>[
@ -66,10 +69,11 @@ class AccountPageState extends State<AccountPage> {
initiallyExpanded: true, initiallyExpanded: true,
children: [ children: [
ContactInvitationListWidget( ContactInvitationListWidget(
contactInvitationRecordList: contactInvitationRecordList) contactInvitationRecordList: contactInvitationRecordList,
disabled: cilBusy)
], ],
).paddingLTRB(8, 0, 8, 8), ).paddingLTRB(8, 0, 8, 8),
ContactListWidget(contactList: contactList).expanded(), ContactListWidget(contactList: contactList, disabled: ciBusy).expanded(),
])); ]));
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:async_tools/async_tools.dart'; import 'package:async_tools/async_tools.dart';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:bloc_tools/bloc_tools.dart';
import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart'; import 'package:blurry_modal_progress_hud/blurry_modal_progress_hud.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -77,6 +78,17 @@ extension AsyncValueBuilderExt<T> on AsyncValue<T> {
data: (d) => debugPage('AsyncValue should not be data here')); data: (d) => debugPage('AsyncValue should not be data here'));
} }
extension BusyAsyncValueBuilderExt<T> on BlocBusyState<AsyncValue<T>> {
Widget builder(Widget Function(BuildContext, T) builder) =>
AbsorbPointer(absorbing: busy, child: state.builder(builder));
Widget buildNotData(
{Widget Function()? loading,
Widget Function(Object, StackTrace?)? error}) =>
AbsorbPointer(
absorbing: busy,
child: state.buildNotData(loading: loading, error: error));
}
class AsyncBlocBuilder<B extends StateStreamable<AsyncValue<S>>, S> class AsyncBlocBuilder<B extends StateStreamable<AsyncValue<S>>, S>
extends BlocBuilder<B, AsyncValue<S>> { extends BlocBuilder<B, AsyncValue<S>> {
AsyncBlocBuilder({ AsyncBlocBuilder({

View File

@ -6,3 +6,4 @@ export 'src/async_value.dart';
export 'src/serial_future.dart'; export 'src/serial_future.dart';
export 'src/single_future.dart'; export 'src/single_future.dart';
export 'src/single_state_processor.dart'; export 'src/single_state_processor.dart';
export 'src/single_stateless_processor.dart';

View File

@ -14,8 +14,7 @@ import '../async_tools.dart';
class SingleStateProcessor<State> { class SingleStateProcessor<State> {
SingleStateProcessor(); SingleStateProcessor();
void updateState(State newInputState, void updateState(State newInputState, Future<void> Function(State) closure) {
{required Future<void> Function(State) closure}) {
// Use a singlefuture here to ensure we get dont lose any updates // Use a singlefuture here to ensure we get dont lose any updates
// If the input stream gives us an update while we are // If the input stream gives us an update while we are
// still processing the last update, the most recent input state will // still processing the last update, the most recent input state will

View File

@ -0,0 +1,47 @@
import 'dart:async';
import '../async_tools.dart';
// Process a single stateless update at a time ensuring each request
// gets processed asynchronously, and continuously while update is requested.
//
// This is useful for processing updates asynchronously without waiting
// from a synchronous execution context
class SingleStatelessProcessor {
SingleStatelessProcessor();
void update(Future<void> Function() closure) {
singleFuture(this, () async {
do {
_more = false;
await closure();
// See if another update was requested
} while (_more);
}, onBusy: () {
// Keep this state until we process again
_more = true;
});
}
// Like update, but with a busy wrapper that clears once the updating is finished
void busyUpdate<T, S>(
Future<void> Function(Future<void> Function(void Function(S))) busy,
Future<void> Function(void Function(S)) closure) {
singleFuture(
this,
() async => busy((emit) async {
do {
_more = false;
await closure(emit);
// See if another update was requested
} while (_more);
}), onBusy: () {
// Keep this state until we process again
_more = true;
});
}
bool _more = false;
}

View File

@ -10,7 +10,7 @@ class AsyncTransformerCubit<T, S> extends Cubit<AsyncValue<T>> {
_subscription = input.stream.listen(_asyncTransform); _subscription = input.stream.listen(_asyncTransform);
} }
void _asyncTransform(AsyncValue<S> newInputState) { void _asyncTransform(AsyncValue<S> newInputState) {
_singleStateProcessor.updateState(newInputState, closure: (newState) async { _singleStateProcessor.updateState(newInputState, (newState) async {
// Emit the transformed state // Emit the transformed state
try { try {
if (newState is AsyncLoading<S>) { if (newState is AsyncLoading<S>) {

View File

@ -17,7 +17,7 @@ class BlocBusyState<S> extends Equatable {
} }
mixin BlocBusyWrapper<S> on BlocBase<BlocBusyState<S>> { mixin BlocBusyWrapper<S> on BlocBase<BlocBusyState<S>> {
Future<T> busy<T>(Future<T> Function(void Function(S) emit) closure) async => Future<T> busyValue<T>(Future<T> Function(void Function(S) emit) closure) =>
_mutex.protect(() async { _mutex.protect(() async {
void busyemit(S state) { void busyemit(S state) {
changedState = state; changedState = state;
@ -41,6 +41,27 @@ mixin BlocBusyWrapper<S> on BlocBase<BlocBusyState<S>> {
return out; return out;
}); });
Future<void> busy(Future<void> Function(void Function(S) emit) closure) =>
_mutex.protect(() async {
void busyemit(S state) {
changedState = state;
}
// Turn on busy state
emit(BlocBusyState._busy(state.state));
// Run the closure
await closure(busyemit);
// If the closure did one or more 'busy emits' then
// take the most recent one and emit it for real
final finalState = changedState;
if (finalState != null && finalState != state.state) {
emit(BlocBusyState._busy(finalState));
} else {
emit(BlocBusyState._busy(state.state));
}
});
void changeState(S state) { void changeState(S state) {
if (_mutex.isLocked) { if (_mutex.isLocked) {
changedState = state; changedState = state;
@ -49,6 +70,8 @@ mixin BlocBusyWrapper<S> on BlocBase<BlocBusyState<S>> {
} }
} }
bool get isBusy => _mutex.isLocked;
final Mutex _mutex = Mutex(); final Mutex _mutex = Mutex();
S? changedState; S? changedState;
} }

View File

@ -31,7 +31,7 @@ abstract mixin class StateFollower<S extends Object, K, V> {
void _updateFollow(S newInputState) { void _updateFollow(S newInputState) {
_singleStateProcessor.updateState(getStateMap(newInputState), _singleStateProcessor.updateState(getStateMap(newInputState),
closure: (newStateMap) async { (newStateMap) async {
for (final k in _lastInputStateMap.keys) { for (final k in _lastInputStateMap.keys) {
if (!newStateMap.containsKey(k)) { if (!newStateMap.containsKey(k)) {
// deleted // deleted

View File

@ -4,18 +4,19 @@ import 'package:async_tools/async_tools.dart';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:bloc_tools/bloc_tools.dart'; import 'package:bloc_tools/bloc_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:mutex/mutex.dart';
import '../../veilid_support.dart'; import '../../veilid_support.dart';
class DHTShortArrayCubit<T> extends Cubit<BlocBusyState<AsyncValue<IList<T>>>> typedef DHTShortArrayState<T> = AsyncValue<IList<T>>;
with BlocBusyWrapper<AsyncValue<IList<T>>> { typedef DHTShortArrayBusyState<T> = BlocBusyState<DHTShortArrayState<T>>;
class DHTShortArrayCubit<T> extends Cubit<DHTShortArrayBusyState<T>>
with BlocBusyWrapper<DHTShortArrayState<T>> {
DHTShortArrayCubit({ DHTShortArrayCubit({
required Future<DHTShortArray> Function() open, required Future<DHTShortArray> Function() open,
required T Function(List<int> data) decodeElement, required T Function(List<int> data) decodeElement,
}) : _decodeElement = decodeElement, }) : _decodeElement = decodeElement,
_wantsUpdate = false,
_isUpdating = false,
_wantsCloseRecord = false,
super(const BlocBusyState(AsyncValue.loading())) { super(const BlocBusyState(AsyncValue.loading())) {
Future.delayed(Duration.zero, () async { Future.delayed(Duration.zero, () async {
// Open DHT record // Open DHT record
@ -33,9 +34,6 @@ class DHTShortArrayCubit<T> extends Cubit<BlocBusyState<AsyncValue<IList<T>>>>
required T Function(List<int> data) decodeElement, required T Function(List<int> data) decodeElement,
}) : _shortArray = shortArray, }) : _shortArray = shortArray,
_decodeElement = decodeElement, _decodeElement = decodeElement,
_wantsUpdate = false,
_isUpdating = false,
_wantsCloseRecord = false,
super(const BlocBusyState(AsyncValue.loading())) { super(const BlocBusyState(AsyncValue.loading())) {
// Make initial state update // Make initial state update
_update(); _update();
@ -59,37 +57,21 @@ class DHTShortArrayCubit<T> extends Cubit<BlocBusyState<AsyncValue<IList<T>>>>
void _update() { void _update() {
// Run at most one background update process // Run at most one background update process
// Because this is async, we could get an update while we're
xxx convert to singleFuture with onBusy that sets wantsupdate // still processing the last one
_sspUpdate.busyUpdate<T, AsyncValue<IList<T>>>(busy, (emit) async {
_wantsUpdate = true;
if (_isUpdating) {
return;
}
_isUpdating = true;
Future.delayed(Duration.zero, () async {
// Keep updating until we don't want to update any more
// Because this is async, we could get an update while we're
// still processing the last one
try { try {
do { final initialState = await _getElementsInner();
_wantsUpdate = false; emit(AsyncValue.data(initialState));
try { } on Exception catch (e) {
final initialState = await _getElements(); emit(AsyncValue.error(e));
emit(AsyncValue.data(initialState));
} on Exception catch (e) {
emit(AsyncValue.error(e));
}
} while (_wantsUpdate);
} finally {
// Note that this update future has finished
_isUpdating = false;
} }
}); });
} }
// Get and decode the entire short array // Get and decode the entire short array
Future<IList<T>> _getElements() async { Future<IList<T>> _getElementsInner() async {
assert(isBusy, 'should only be called from a busy state');
var out = IList<T>(); var out = IList<T>();
for (var i = 0; i < _shortArray.length; i++) { for (var i = 0; i < _shortArray.length; i++) {
// Get the element bytes (throw if fails, array state is invalid) // Get the element bytes (throw if fails, array state is invalid)
@ -112,12 +94,13 @@ xxx convert to singleFuture with onBusy that sets wantsupdate
await super.close(); await super.close();
} }
DHTShortArray get shortArray => _shortArray; Future<R> operate<R>(Future<R> Function(DHTShortArray) closure) async =>
_operateMutex.protect(() async => closure(_shortArray));
final _operateMutex = Mutex();
late final DHTShortArray _shortArray; late final DHTShortArray _shortArray;
final T Function(List<int> data) _decodeElement; final T Function(List<int> data) _decodeElement;
StreamSubscription<void>? _subscription; StreamSubscription<void>? _subscription;
bool _wantsUpdate; bool _wantsCloseRecord = false;
bool _isUpdating; final _sspUpdate = SingleStatelessProcessor();
bool _wantsCloseRecord;
} }