mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-07-21 13:48:43 -04:00
more refactor and dhtrecord multiple-open support
This commit is contained in:
parent
c4c7b264aa
commit
e262b0f777
19 changed files with 782 additions and 419 deletions
|
@ -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(
|
||||
|
|
|
@ -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 =>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
})));
|
||||
}
|
||||
|
|
|
@ -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))));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue