more refactor and dhtrecord multiple-open support

This commit is contained in:
Christien Rioux 2024-02-24 22:27:59 -05:00
parent c4c7b264aa
commit e262b0f777
19 changed files with 782 additions and 419 deletions

View file

@ -147,7 +147,7 @@ class ContactInvitationListCubit
Future<void> deleteInvitation(
{required bool accepted,
required proto.ContactInvitationRecord contactInvitationRecord}) async {
required TypedKey contactRequestInboxRecordKey}) async {
final pool = DHTRecordPool.instance;
final accountRecordKey =
_activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;
@ -159,26 +159,25 @@ class ContactInvitationListCubit
if (item == null) {
throw Exception('Failed to get contact invitation record');
}
if (item.contactRequestInbox.recordKey ==
contactInvitationRecord.contactRequestInbox.recordKey) {
if (item.contactRequestInbox.recordKey.toVeilid() ==
contactRequestInboxRecordKey) {
await shortArray.tryRemoveItem(i);
break;
await (await pool.openOwned(item.contactRequestInbox.toVeilid(),
parent: accountRecordKey))
.scope((contactRequestInbox) async {
// Wipe out old invitation so it shows up as invalid
await contactRequestInbox.tryWriteBytes(Uint8List(0));
await contactRequestInbox.delete();
});
if (!accepted) {
await (await pool.openRead(item.localConversationRecordKey.toVeilid(),
parent: accountRecordKey))
.delete();
}
return;
}
}
await (await pool.openOwned(
contactInvitationRecord.contactRequestInbox.toVeilid(),
parent: accountRecordKey))
.scope((contactRequestInbox) async {
// Wipe out old invitation so it shows up as invalid
await contactRequestInbox.tryWriteBytes(Uint8List(0));
await contactRequestInbox.delete();
});
if (!accepted) {
await (await pool.openRead(
contactInvitationRecord.localConversationRecordKey.toVeilid(),
parent: accountRecordKey))
.delete();
}
}
Future<ValidContactInvitation?> validateInvitation(

View file

@ -10,7 +10,7 @@ import 'cubits.dart';
typedef WaitingInvitationsBlocMapState
= BlocMapState<TypedKey, AsyncValue<InvitationStatus>>;
// Map of contactInvitationListRecordKey to WaitingInvitationCubit
// Map of contactRequestInboxRecordKey to WaitingInvitationCubit
// Wraps a contact invitation cubit to watch for accept/reject
// Automatically follows the state of a ContactInvitationListCubit.
class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
@ -20,6 +20,7 @@ class WaitingInvitationsBlocMapCubit extends BlocMapCubit<TypedKey,
TypedKey, proto.ContactInvitationRecord> {
WaitingInvitationsBlocMapCubit(
{required this.activeAccountInfo, required this.account});
Future<void> addWaitingInvitation(
{required proto.ContactInvitationRecord
contactInvitationRecord}) async =>

View file

@ -53,7 +53,9 @@ class ContactInvitationItemWidget extends StatelessWidget {
context.read<ContactInvitationListCubit>();
await contactInvitationListCubit.deleteInvitation(
accepted: false,
contactInvitationRecord: contactInvitationRecord);
contactRequestInboxRecordKey: contactInvitationRecord
.contactRequestInbox.recordKey
.toVeilid());
},
backgroundColor: scale.tertiaryScale.background,
foregroundColor: scale.tertiaryScale.text,

View file

@ -1,8 +1,10 @@
import 'package:async_tools/async_tools.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../../account_manager/account_manager.dart';
import '../../../chat/chat.dart';
@ -13,84 +15,168 @@ import '../../../router/router.dart';
import '../../../tools/tools.dart';
class HomeAccountReadyShell extends StatefulWidget {
const HomeAccountReadyShell({required this.child, super.key});
@override
HomeAccountReadyShellState createState() => HomeAccountReadyShellState();
final Widget child;
}
class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
//
@override
void initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
// These must be valid already before making this widget,
// per the ShellRoute above it
factory HomeAccountReadyShell(
{required BuildContext context, required Widget child, Key? key}) {
// These must exist in order for the account to
// be considered 'ready' for this widget subtree
final activeLocalAccount = context.read<ActiveLocalAccountCubit>().state!;
final accountInfo =
AccountRepository.instance.getAccountInfo(activeLocalAccount);
final activeAccountInfo = accountInfo.activeAccountInfo!;
final routerCubit = context.read<RouterCubit>();
return Provider<ActiveAccountInfo>.value(
value: activeAccountInfo,
child: BlocProvider(
create: (context) =>
AccountRecordCubit(record: activeAccountInfo.accountRecord),
child: Builder(builder: (context) {
final account =
context.watch<AccountRecordCubit>().state.data?.value;
if (account == null) {
return waitingPage();
}
return MultiBlocProvider(providers: [
BlocProvider(
create: (context) => ContactInvitationListCubit(
activeAccountInfo: activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ContactListCubit(
activeAccountInfo: activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ChatListCubit(
activeAccountInfo: activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ActiveConversationsBlocMapCubit(
activeAccountInfo: activeAccountInfo,
contactListCubit: context.read<ContactListCubit>())
..follow(
initialInputState: const AsyncValue.loading(),
stream: context.read<ChatListCubit>().stream)),
BlocProvider(
create: (context) => ActiveConversationMessagesBlocMapCubit(
activeAccountInfo: activeAccountInfo,
)..follow(
initialInputState: IMap(),
stream: context
.read<ActiveConversationsBlocMapCubit>()
.stream)),
BlocProvider(
create: (context) => ActiveChatCubit(null)
..withStateListen((event) {
routerCubit.setHasActiveChat(event != null);
})),
BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit(
activeAccountInfo: activeAccountInfo, account: account)
..follow(
initialInputState: const AsyncValue.loading(),
stream: context
.read<ContactInvitationListCubit>()
.stream))
], child: widget.child);
})));
return HomeAccountReadyShell._(
activeLocalAccount: activeLocalAccount,
accountInfo: accountInfo,
activeAccountInfo: activeAccountInfo,
routerCubit: routerCubit,
key: key,
child: child);
}
const HomeAccountReadyShell._(
{required this.activeLocalAccount,
required this.accountInfo,
required this.activeAccountInfo,
required this.routerCubit,
required this.child,
super.key});
@override
HomeAccountReadyShellState createState() => HomeAccountReadyShellState();
final Widget child;
final TypedKey activeLocalAccount;
final AccountInfo accountInfo;
final ActiveAccountInfo activeAccountInfo;
final RouterCubit routerCubit;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty<TypedKey>(
'activeLocalAccount', activeLocalAccount))
..add(DiagnosticsProperty<AccountInfo>('accountInfo', accountInfo))
..add(DiagnosticsProperty<ActiveAccountInfo>(
'activeAccountInfo', activeAccountInfo))
..add(DiagnosticsProperty<RouterCubit>('routerCubit', routerCubit));
}
}
class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
final SingleStateProcessor<WaitingInvitationsBlocMapState>
_singleInvitationStatusProcessor = SingleStateProcessor();
@override
void initState() {
super.initState();
}
// Process all accepted or rejected invitations
void _invitationStatusListener(
BuildContext context, WaitingInvitationsBlocMapState state) {
_singleInvitationStatusProcessor.updateState(state,
closure: (newState) async {
final contactListCubit = context.read<ContactListCubit>();
final contactInvitationListCubit =
context.read<ContactInvitationListCubit>();
for (final entry in newState.entries) {
final contactRequestInboxRecordKey = entry.key;
final invStatus = entry.value.data?.value;
// Skip invitations that have not yet been accepted or rejected
if (invStatus == null) {
continue;
}
// Delete invitation and process the accepted or rejected contact
final acceptedContact = invStatus.acceptedContact;
if (acceptedContact != null) {
await contactInvitationListCubit.deleteInvitation(
accepted: true,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
// Accept
await contactListCubit.createContact(
remoteProfile: acceptedContact.remoteProfile,
remoteIdentity: acceptedContact.remoteIdentity,
remoteConversationRecordKey:
acceptedContact.remoteConversationRecordKey,
localConversationRecordKey:
acceptedContact.localConversationRecordKey,
);
} else {
// Reject
await contactInvitationListCubit.deleteInvitation(
accepted: false,
contactRequestInboxRecordKey: contactRequestInboxRecordKey);
}
}
});
}
@override
Widget build(BuildContext context) => Provider<ActiveAccountInfo>.value(
value: widget.activeAccountInfo,
child: BlocProvider(
create: (context) => AccountRecordCubit(
record: widget.activeAccountInfo.accountRecord),
child: Builder(builder: (context) {
final account =
context.watch<AccountRecordCubit>().state.data?.value;
if (account == null) {
return waitingPage();
}
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => ContactInvitationListCubit(
activeAccountInfo: widget.activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ContactListCubit(
activeAccountInfo: widget.activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ChatListCubit(
activeAccountInfo: widget.activeAccountInfo,
account: account)),
BlocProvider(
create: (context) => ActiveConversationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo,
contactListCubit: context.read<ContactListCubit>())
..follow(
initialInputState: const AsyncValue.loading(),
stream: context.read<ChatListCubit>().stream)),
BlocProvider(
create: (context) =>
ActiveConversationMessagesBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo,
)..follow(
initialInputState: IMap(),
stream: context
.read<ActiveConversationsBlocMapCubit>()
.stream)),
BlocProvider(
create: (context) => ActiveChatCubit(null)
..withStateListen((event) {
widget.routerCubit.setHasActiveChat(event != null);
})),
BlocProvider(
create: (context) => WaitingInvitationsBlocMapCubit(
activeAccountInfo: widget.activeAccountInfo,
account: account)
..follow(
initialInputState: const AsyncValue.loading(),
stream: context
.read<ContactInvitationListCubit>()
.stream))
],
child: MultiBlocListener(listeners: [
BlocListener<WaitingInvitationsBlocMapCubit,
WaitingInvitationsBlocMapState>(
listener: _invitationStatusListener,
)
], child: widget.child));
})));
}

View file

@ -10,12 +10,12 @@ import 'home_account_missing.dart';
import 'home_no_active.dart';
class HomeShell extends StatefulWidget {
const HomeShell({required this.child, super.key});
const HomeShell({required this.accountReadyBuilder, super.key});
@override
HomeShellState createState() => HomeShellState();
final Widget child;
final Builder accountReadyBuilder;
}
class HomeShellState extends State<HomeShell> {
@ -32,7 +32,7 @@ class HomeShellState extends State<HomeShell> {
super.dispose();
}
Widget buildWithLogin(BuildContext context, Widget child) {
Widget buildWithLogin(BuildContext context) {
final activeLocalAccount = context.watch<ActiveLocalAccountCubit>().state;
if (activeLocalAccount == null) {
@ -56,7 +56,7 @@ class HomeShellState extends State<HomeShell> {
child: BlocProvider(
create: (context) => AccountRecordCubit(
record: accountInfo.activeAccountInfo!.accountRecord),
child: child));
child: widget.accountReadyBuilder));
}
}
@ -72,6 +72,6 @@ class HomeShellState extends State<HomeShell> {
child: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
child: buildWithLogin(context, widget.child))));
child: buildWithLogin(context))));
}
}

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart';
import 'package:stream_transform/stream_transform.dart';
@ -68,8 +69,10 @@ class RouterCubit extends Cubit<RouterState> {
),
ShellRoute(
navigatorKey: _homeNavKey,
builder: (context, state, child) =>
HomeShell(child: HomeAccountReadyShell(child: child)),
builder: (context, state, child) => HomeShell(
accountReadyBuilder: Builder(
builder: (context) =>
HomeAccountReadyShell(context: context, child: child))),
routes: [
GoRoute(
path: '/home',

View file

@ -10,41 +10,20 @@ class AsyncTransformerCubit<T, S> extends Cubit<AsyncValue<T>> {
_subscription = input.stream.listen(_asyncTransform);
}
void _asyncTransform(AsyncValue<S> newInputState) {
// Use a singlefuture here to ensure we get dont lose any updates
// If the input stream gives us an update while we are
// still processing the last update, the most recent input state will
// be saved and processed eventually.
singleFuture(this, () async {
var newState = newInputState;
var done = false;
while (!done) {
// Emit the transformed state
try {
if (newState is AsyncLoading) {
return AsyncValue<T>.loading();
}
if (newState is AsyncError) {
final newStateError = newState as AsyncError<S>;
return AsyncValue<T>.error(
newStateError.error, newStateError.stackTrace);
}
_singleStateProcessor.updateState(newInputState, closure: (newState) async {
// Emit the transformed state
try {
if (newState is AsyncLoading<S>) {
emit(const AsyncValue.loading());
} else if (newState is AsyncError<S>) {
emit(AsyncValue.error(newState.error, newState.stackTrace));
} else {
final transformedState = await transform(newState.data!.value);
emit(transformedState);
} on Exception catch (e, st) {
emit(AsyncValue.error(e, st));
}
// See if there's another state change to process
final next = _nextInputState;
_nextInputState = null;
if (next != null) {
newState = next;
} else {
done = true;
}
} on Exception catch (e, st) {
emit(AsyncValue.error(e, st));
}
}, onBusy: () {
// Keep this state until we process again
_nextInputState = newInputState;
});
}
@ -56,7 +35,8 @@ class AsyncTransformerCubit<T, S> extends Cubit<AsyncValue<T>> {
}
Cubit<AsyncValue<S>> input;
AsyncValue<S>? _nextInputState;
final SingleStateProcessor<AsyncValue<S>> _singleStateProcessor =
SingleStateProcessor();
Future<AsyncValue<T>> Function(S) transform;
late final StreamSubscription<AsyncValue<S>> _subscription;
}

View file

@ -30,49 +30,29 @@ abstract mixin class StateFollower<S extends Object, K, V> {
Future<void> updateState(K key, V value);
void _updateFollow(S newInputState) {
// Use a singlefuture here to ensure we get dont lose any updates
// If the input stream gives us an update while we are
// still processing the last update, the most recent input state will
// be saved and processed eventually.
final newInputStateMap = getStateMap(newInputState);
singleFuture(this, () async {
var newStateMap = newInputStateMap;
var done = false;
while (!done) {
for (final k in _lastInputStateMap.keys) {
if (!newStateMap.containsKey(k)) {
// deleted
await removeFromState(k);
}
}
for (final newEntry in newStateMap.entries) {
final v = _lastInputStateMap.get(newEntry.key);
if (v == null || v != newEntry.value) {
// added or changed
await updateState(newEntry.key, newEntry.value);
}
}
// Keep this state map for the next time
_lastInputStateMap = newStateMap;
// See if there's another state change to process
final next = _nextInputStateMap;
_nextInputStateMap = null;
if (next != null) {
newStateMap = next;
} else {
done = true;
_singleStateProcessor.updateState(getStateMap(newInputState),
closure: (newStateMap) async {
for (final k in _lastInputStateMap.keys) {
if (!newStateMap.containsKey(k)) {
// deleted
await removeFromState(k);
}
}
}, onBusy: () {
// Keep this state until we process again
_nextInputStateMap = newInputStateMap;
for (final newEntry in newStateMap.entries) {
final v = _lastInputStateMap.get(newEntry.key);
if (v == null || v != newEntry.value) {
// added or changed
await updateState(newEntry.key, newEntry.value);
}
}
// Keep this state map for the next time
_lastInputStateMap = newStateMap;
});
}
late IMap<K, V> _lastInputStateMap;
IMap<K, V>? _nextInputStateMap;
final SingleStateProcessor<IMap<K, V>> _singleStateProcessor =
SingleStateProcessor();
late final StreamSubscription<S> _subscription;
}