mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-01-23 05:51:06 -05:00
checkpoint
This commit is contained in:
parent
56d65442f4
commit
751022e743
@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:protobuf/protobuf.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
@ -7,6 +8,11 @@ import '../account_manager.dart';
|
|||||||
|
|
||||||
typedef AccountRecordState = proto.Account;
|
typedef AccountRecordState = proto.Account;
|
||||||
|
|
||||||
|
/// The saved state of a VeilidChat Account on the DHT
|
||||||
|
/// Used to synchronize status, profile, and options for a specific account
|
||||||
|
/// across multiple clients. This DHT record is the 'source of truth' for an
|
||||||
|
/// account and is privately encrypted with an owned recrod from the 'userLogin'
|
||||||
|
/// tabledb-local storage, encrypted by the unlock code for the account.
|
||||||
class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
||||||
AccountRecordCubit(
|
AccountRecordCubit(
|
||||||
{required AccountRepository accountRepository,
|
{required AccountRepository accountRepository,
|
||||||
@ -35,4 +41,16 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
|||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Public Interface
|
||||||
|
|
||||||
|
Future<void> updateProfile(proto.Profile profile) async {
|
||||||
|
await record.eventualUpdateProtobuf(proto.Account.fromBuffer, (old) async {
|
||||||
|
if (old == null || old.profile == profile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return old.deepCopy()..profile = profile;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,8 @@ import '../../account_manager/account_manager.dart';
|
|||||||
typedef AccountRecordsBlocMapState
|
typedef AccountRecordsBlocMapState
|
||||||
= BlocMapState<TypedKey, AsyncValue<AccountRecordState>>;
|
= BlocMapState<TypedKey, AsyncValue<AccountRecordState>>;
|
||||||
|
|
||||||
// Map of the logged in user accounts to their account information
|
/// Map of the logged in user accounts to their AccountRecordCubit
|
||||||
|
/// Ensures there is an single account record cubit for each logged in account
|
||||||
class AccountRecordsBlocMapCubit extends BlocMapCubit<TypedKey,
|
class AccountRecordsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||||
AsyncValue<AccountRecordState>, AccountRecordCubit>
|
AsyncValue<AccountRecordState>, AccountRecordCubit>
|
||||||
with StateMapFollower<UserLoginsState, TypedKey, UserLogin> {
|
with StateMapFollower<UserLoginsState, TypedKey, UserLogin> {
|
||||||
|
@ -8,8 +8,6 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../contacts/cubits/contact_list_cubit.dart';
|
|
||||||
import '../../conversation/conversation.dart';
|
|
||||||
import '../../layout/default_app_bar.dart';
|
import '../../layout/default_app_bar.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../theme/theme.dart';
|
import '../../theme/theme.dart';
|
||||||
@ -41,7 +39,6 @@ class EditAccountPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _EditAccountPageState extends State<EditAccountPage> {
|
class _EditAccountPageState extends State<EditAccountPage> {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
|
||||||
bool _isInAsyncCall = false;
|
bool _isInAsyncCall = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,24 +55,37 @@ class _EditAccountPageState extends State<EditAccountPage> {
|
|||||||
{required Future<void> Function(GlobalKey<FormBuilderState>)
|
{required Future<void> Function(GlobalKey<FormBuilderState>)
|
||||||
onSubmit}) =>
|
onSubmit}) =>
|
||||||
EditProfileForm(
|
EditProfileForm(
|
||||||
header: translate('edit_account_page.header'),
|
header: translate('edit_account_page.header'),
|
||||||
instructions: translate('edit_account_page.instructions'),
|
instructions: translate('edit_account_page.instructions'),
|
||||||
submitText: translate('edit_account_page.update'),
|
submitText: translate('edit_account_page.update'),
|
||||||
submitDisabledText: translate('button.waiting_for_network'),
|
submitDisabledText: translate('button.waiting_for_network'),
|
||||||
onSubmit: onSubmit);
|
onSubmit: onSubmit,
|
||||||
|
initialValueCallback: (key) => switch (key) {
|
||||||
|
EditProfileForm.formFieldName => widget.existingProfile.name,
|
||||||
|
EditProfileForm.formFieldPronouns => widget.existingProfile.pronouns,
|
||||||
|
String() => throw UnimplementedError(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final displayModalHUD = _isInAsyncCall;
|
final displayModalHUD = _isInAsyncCall;
|
||||||
final accountRecordCubit = context.read<AccountRecordCubit>();
|
final accountRecordsCubit = context.watch<AccountRecordsBlocMapCubit>();
|
||||||
final activeConversationsBlocMapCubit =
|
final accountRecordCubit = accountRecordsCubit
|
||||||
context.read<ActiveConversationsBlocMapCubit>();
|
.operate(widget.superIdentityRecordKey, closure: (c) => c);
|
||||||
final contactListCubit = context.read<ContactListCubit>();
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
// resizeToAvoidBottomInset: false,
|
// resizeToAvoidBottomInset: false,
|
||||||
appBar: DefaultAppBar(
|
appBar: DefaultAppBar(
|
||||||
title: Text(translate('edit_account_page.titlebar')),
|
title: Text(translate('edit_account_page.titlebar')),
|
||||||
|
leading: Navigator.canPop(context)
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
actions: [
|
actions: [
|
||||||
const SignalStrengthMeterWidget(),
|
const SignalStrengthMeterWidget(),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -92,57 +102,35 @@ class _EditAccountPageState extends State<EditAccountPage> {
|
|||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final name = _formKey.currentState!
|
final name = formKey.currentState!
|
||||||
.fields[EditProfileForm.formFieldName]!.value as String;
|
.fields[EditProfileForm.formFieldName]!.value as String;
|
||||||
final pronouns = _formKey
|
final pronouns = formKey
|
||||||
.currentState!
|
.currentState!
|
||||||
.fields[EditProfileForm.formFieldPronouns]!
|
.fields[EditProfileForm.formFieldPronouns]!
|
||||||
.value as String? ??
|
.value as String? ??
|
||||||
'';
|
'';
|
||||||
final newProfile = widget.existingProfile.deepCopy()
|
final newProfile = widget.existingProfile.deepCopy()
|
||||||
..name = name
|
..name = name
|
||||||
..pronouns = pronouns;
|
..pronouns = pronouns
|
||||||
|
..timestamp = Veilid.instance.now().toInt64();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isInAsyncCall = true;
|
_isInAsyncCall = true;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
// Update account profile DHT record
|
// Update account profile DHT record
|
||||||
final newValue = await accountRecordCubit.record
|
// This triggers ConversationCubits to update
|
||||||
.tryWriteProtobuf(proto.Account.fromBuffer, newProfile);
|
await accountRecordCubit.updateProfile(newProfile);
|
||||||
if (newValue != null) {
|
|
||||||
if (context.mounted) {
|
|
||||||
await showErrorModal(
|
|
||||||
context,
|
|
||||||
translate('edit_account_page.error'),
|
|
||||||
'Failed to update profile online');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update local account profile
|
// Update local account profile
|
||||||
await AccountRepository.instance.editAccountProfile(
|
await AccountRepository.instance.editAccountProfile(
|
||||||
widget.superIdentityRecordKey, newProfile);
|
widget.superIdentityRecordKey, newProfile);
|
||||||
|
|
||||||
// Update all conversations with new profile
|
if (context.mounted) {
|
||||||
final updates = <Future<void>>[];
|
Navigator.canPop(context)
|
||||||
for (final key in activeConversationsBlocMapCubit.state.keys) {
|
? GoRouterHelper(context).pop()
|
||||||
await activeConversationsBlocMapCubit.operateAsync(key,
|
: GoRouterHelper(context).go('/');
|
||||||
closure: (cubit) async {
|
|
||||||
final newLocalConversation =
|
|
||||||
cubit.state.asData?.value.localConversation.deepCopy();
|
|
||||||
if (newLocalConversation != null) {
|
|
||||||
newLocalConversation.profile = newProfile;
|
|
||||||
updates.add(cubit.input.writeLocalConversation(
|
|
||||||
conversation: newLocalConversation));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for updates
|
|
||||||
await updates.wait;
|
|
||||||
|
|
||||||
// XXX: how to do this for non-chat contacts?
|
|
||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
@ -21,7 +21,6 @@ class NewAccountPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _NewAccountPageState extends State<NewAccountPage> {
|
class _NewAccountPageState extends State<NewAccountPage> {
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
|
||||||
bool _isInAsyncCall = false;
|
bool _isInAsyncCall = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -61,6 +60,14 @@ class _NewAccountPageState extends State<NewAccountPage> {
|
|||||||
// resizeToAvoidBottomInset: false,
|
// resizeToAvoidBottomInset: false,
|
||||||
appBar: DefaultAppBar(
|
appBar: DefaultAppBar(
|
||||||
title: Text(translate('new_account_page.titlebar')),
|
title: Text(translate('new_account_page.titlebar')),
|
||||||
|
leading: Navigator.canPop(context)
|
||||||
|
? IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
actions: [
|
actions: [
|
||||||
const SignalStrengthMeterWidget(),
|
const SignalStrengthMeterWidget(),
|
||||||
IconButton(
|
IconButton(
|
||||||
@ -77,9 +84,9 @@ class _NewAccountPageState extends State<NewAccountPage> {
|
|||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final name = _formKey.currentState!
|
final name = formKey.currentState!
|
||||||
.fields[EditProfileForm.formFieldName]!.value as String;
|
.fields[EditProfileForm.formFieldName]!.value as String;
|
||||||
final pronouns = _formKey
|
final pronouns = formKey
|
||||||
.currentState!
|
.currentState!
|
||||||
.fields[EditProfileForm.formFieldPronouns]!
|
.fields[EditProfileForm.formFieldPronouns]!
|
||||||
.value as String? ??
|
.value as String? ??
|
||||||
|
@ -13,6 +13,7 @@ class EditProfileForm extends StatefulWidget {
|
|||||||
required this.submitDisabledText,
|
required this.submitDisabledText,
|
||||||
super.key,
|
super.key,
|
||||||
this.onSubmit,
|
this.onSubmit,
|
||||||
|
this.initialValueCallback,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -23,6 +24,7 @@ class EditProfileForm extends StatefulWidget {
|
|||||||
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
|
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
|
||||||
final String submitText;
|
final String submitText;
|
||||||
final String submitDisabledText;
|
final String submitDisabledText;
|
||||||
|
final Object? Function(String key)? initialValueCallback;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||||
@ -34,7 +36,9 @@ class EditProfileForm extends StatefulWidget {
|
|||||||
Future<void> Function(
|
Future<void> Function(
|
||||||
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
|
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
|
||||||
..add(StringProperty('submitText', submitText))
|
..add(StringProperty('submitText', submitText))
|
||||||
..add(StringProperty('submitDisabledText', submitDisabledText));
|
..add(StringProperty('submitDisabledText', submitDisabledText))
|
||||||
|
..add(ObjectFlagProperty<Object? Function(String key)?>.has(
|
||||||
|
'initialValueCallback', initialValueCallback));
|
||||||
}
|
}
|
||||||
|
|
||||||
static const String formFieldName = 'name';
|
static const String formFieldName = 'name';
|
||||||
@ -62,6 +66,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
name: EditProfileForm.formFieldName,
|
name: EditProfileForm.formFieldName,
|
||||||
|
initialValue: widget.initialValueCallback
|
||||||
|
?.call(EditProfileForm.formFieldName) as String?,
|
||||||
decoration:
|
decoration:
|
||||||
InputDecoration(labelText: translate('account.form_name')),
|
InputDecoration(labelText: translate('account.form_name')),
|
||||||
maxLength: 64,
|
maxLength: 64,
|
||||||
@ -73,6 +79,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
|||||||
),
|
),
|
||||||
FormBuilderTextField(
|
FormBuilderTextField(
|
||||||
name: EditProfileForm.formFieldPronouns,
|
name: EditProfileForm.formFieldPronouns,
|
||||||
|
initialValue: widget.initialValueCallback
|
||||||
|
?.call(EditProfileForm.formFieldPronouns) as String?,
|
||||||
maxLength: 64,
|
maxLength: 64,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: translate('account.form_pronouns')),
|
labelText: translate('account.form_pronouns')),
|
||||||
|
@ -1,18 +1,12 @@
|
|||||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|
||||||
import 'package:flutter_translate/flutter_translate.dart';
|
import 'package:flutter_translate/flutter_translate.dart';
|
||||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:veilid_support/veilid_support.dart';
|
import 'package:veilid_support/veilid_support.dart';
|
||||||
|
|
||||||
import '../../layout/default_app_bar.dart';
|
import '../../layout/default_app_bar.dart';
|
||||||
import '../../theme/theme.dart';
|
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import '../../veilid_processor/veilid_processor.dart';
|
import '../../veilid_processor/veilid_processor.dart';
|
||||||
import '../account_manager.dart';
|
|
||||||
|
|
||||||
class ShowRecoveryKeyPage extends StatefulWidget {
|
class ShowRecoveryKeyPage extends StatefulWidget {
|
||||||
const ShowRecoveryKeyPage({required SecretKey secretKey, super.key})
|
const ShowRecoveryKeyPage({required SecretKey secretKey, super.key})
|
||||||
@ -57,7 +51,11 @@ class ShowRecoveryKeyPageState extends State<ShowRecoveryKeyPage> {
|
|||||||
Text('ASS: $secretKey'),
|
Text('ASS: $secretKey'),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
GoRouterHelper(context).go('/');
|
if (context.mounted) {
|
||||||
|
Navigator.canPop(context)
|
||||||
|
? GoRouterHelper(context).pop()
|
||||||
|
: GoRouterHelper(context).go('/');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Text(translate('button.finish')))
|
child: Text(translate('button.finish')))
|
||||||
]).paddingSymmetric(horizontal: 24, vertical: 8));
|
]).paddingSymmetric(horizontal: 24, vertical: 8));
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export 'edit_account_page.dart';
|
||||||
export 'new_account_page.dart';
|
export 'new_account_page.dart';
|
||||||
export 'profile_widget.dart';
|
export 'profile_widget.dart';
|
||||||
export 'show_recovery_key_page.dart';
|
export 'show_recovery_key_page.dart';
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:async_tools/async_tools.dart';
|
||||||
|
import 'package:protobuf/protobuf.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';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import 'conversation_cubit.dart';
|
import '../../conversation/cubits/conversation_cubit.dart';
|
||||||
|
|
||||||
//////////////////////////////////////////////////
|
//////////////////////////////////////////////////
|
||||||
// Mutable state for per-account contacts
|
// Mutable state for per-account contacts
|
||||||
@ -34,6 +36,54 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
|||||||
return dhtRecord;
|
return dhtRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _contactProfileUpdateMap.close();
|
||||||
|
await super.close();
|
||||||
|
}
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Public Interface
|
||||||
|
|
||||||
|
void followContactProfileChanges(TypedKey localConversationRecordKey,
|
||||||
|
Stream<proto.Profile?> profileStream, proto.Profile? profileState) {
|
||||||
|
_contactProfileUpdateMap
|
||||||
|
.follow(localConversationRecordKey, profileStream, profileState,
|
||||||
|
(remoteProfile) async {
|
||||||
|
if (remoteProfile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return updateContactRemoteProfile(
|
||||||
|
localConversationRecordKey: localConversationRecordKey,
|
||||||
|
remoteProfile: remoteProfile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateContactRemoteProfile({
|
||||||
|
required TypedKey localConversationRecordKey,
|
||||||
|
required proto.Profile remoteProfile,
|
||||||
|
}) async {
|
||||||
|
// Update contact's remoteProfile
|
||||||
|
await operateWriteEventual((writer) async {
|
||||||
|
for (var pos = 0; pos < writer.length; pos++) {
|
||||||
|
final c = await writer.getProtobuf(proto.Contact.fromBuffer, pos);
|
||||||
|
if (c != null &&
|
||||||
|
c.localConversationRecordKey.toVeilid() ==
|
||||||
|
localConversationRecordKey) {
|
||||||
|
if (c.remoteProfile == remoteProfile) {
|
||||||
|
// Unchanged
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
final newContact = c.deepCopy()..remoteProfile = remoteProfile;
|
||||||
|
final updated = await writer.tryWriteItemProtobuf(
|
||||||
|
proto.Contact.fromBuffer, pos, newContact);
|
||||||
|
if (!updated) {
|
||||||
|
throw DHTExceptionTryAgain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> createContact({
|
Future<void> createContact({
|
||||||
required proto.Profile remoteProfile,
|
required proto.Profile remoteProfile,
|
||||||
required SuperIdentity remoteSuperIdentity,
|
required SuperIdentity remoteSuperIdentity,
|
||||||
@ -100,4 +150,6 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final UnlockedAccountInfo _activeAccountInfo;
|
final UnlockedAccountInfo _activeAccountInfo;
|
||||||
|
final _contactProfileUpdateMap =
|
||||||
|
SingleStateProcessorMap<TypedKey, proto.Profile?>();
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,10 @@ import 'package:meta/meta.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';
|
||||||
|
import '../../chat_list/cubits/cubits.dart';
|
||||||
import '../../contacts/contacts.dart';
|
import '../../contacts/contacts.dart';
|
||||||
import '../../conversation/conversation.dart';
|
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import 'cubits.dart';
|
import '../conversation.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class ActiveConversationState extends Equatable {
|
class ActiveConversationState extends Equatable {
|
||||||
@ -43,11 +43,13 @@ typedef ActiveConversationsBlocMapState
|
|||||||
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||||
AsyncValue<ActiveConversationState>, ActiveConversationCubit>
|
AsyncValue<ActiveConversationState>, ActiveConversationCubit>
|
||||||
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
|
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
|
||||||
ActiveConversationsBlocMapCubit(
|
ActiveConversationsBlocMapCubit({
|
||||||
{required UnlockedAccountInfo unlockedAccountInfo,
|
required UnlockedAccountInfo unlockedAccountInfo,
|
||||||
required ContactListCubit contactListCubit})
|
required ContactListCubit contactListCubit,
|
||||||
: _activeAccountInfo = unlockedAccountInfo,
|
required AccountRecordCubit accountRecordCubit,
|
||||||
_contactListCubit = contactListCubit;
|
}) : _activeAccountInfo = unlockedAccountInfo,
|
||||||
|
_contactListCubit = contactListCubit,
|
||||||
|
_accountRecordCubit = accountRecordCubit;
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////
|
||||||
// Public Interface
|
// Public Interface
|
||||||
@ -57,30 +59,51 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
|||||||
|
|
||||||
// Add an active conversation to be tracked for changes
|
// Add an active conversation to be tracked for changes
|
||||||
Future<void> _addConversation({required proto.Contact contact}) async =>
|
Future<void> _addConversation({required proto.Contact contact}) async =>
|
||||||
add(() => MapEntry(
|
add(() {
|
||||||
contact.localConversationRecordKey.toVeilid(),
|
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
|
||||||
TransformerCubit(
|
final localConversationRecordKey =
|
||||||
ConversationCubit(
|
contact.localConversationRecordKey.toVeilid();
|
||||||
activeAccountInfo: _activeAccountInfo,
|
final remoteConversationRecordKey =
|
||||||
remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(),
|
contact.remoteConversationRecordKey.toVeilid();
|
||||||
localConversationRecordKey:
|
|
||||||
contact.localConversationRecordKey.toVeilid(),
|
// Conversation cubit the tracks the state between the local
|
||||||
remoteConversationRecordKey:
|
// and remote halves of a contact's relationship with this account
|
||||||
contact.remoteConversationRecordKey.toVeilid(),
|
final conversationCubit = ConversationCubit(
|
||||||
),
|
activeAccountInfo: _activeAccountInfo,
|
||||||
// Transformer that only passes through completed conversations
|
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||||
// along with the contact that corresponds to the completed
|
localConversationRecordKey: localConversationRecordKey,
|
||||||
// conversation
|
remoteConversationRecordKey: remoteConversationRecordKey,
|
||||||
transform: (avstate) => avstate.when(
|
)..watchAccountChanges(
|
||||||
data: (data) => (data.localConversation == null ||
|
_accountRecordCubit.stream, _accountRecordCubit.state);
|
||||||
data.remoteConversation == null)
|
_contactListCubit.followContactProfileChanges(
|
||||||
? const AsyncValue.loading()
|
localConversationRecordKey,
|
||||||
: AsyncValue.data(ActiveConversationState(
|
conversationCubit.stream.map((x) => x.map(
|
||||||
contact: contact,
|
data: (d) => d.value.remoteConversation?.profile,
|
||||||
localConversation: data.localConversation!,
|
loading: (_) => null,
|
||||||
remoteConversation: data.remoteConversation!)),
|
error: (_) => null)),
|
||||||
loading: AsyncValue.loading,
|
conversationCubit.state.asData?.value.remoteConversation?.profile);
|
||||||
error: AsyncValue.error))));
|
|
||||||
|
// Transformer that only passes through completed/active conversations
|
||||||
|
// along with the contact that corresponds to the completed
|
||||||
|
// conversation
|
||||||
|
final transformedCubit = TransformerCubit<
|
||||||
|
AsyncValue<ActiveConversationState>,
|
||||||
|
AsyncValue<ConversationState>,
|
||||||
|
ConversationCubit>(conversationCubit,
|
||||||
|
transform: (avstate) => avstate.when(
|
||||||
|
data: (data) => (data.localConversation == null ||
|
||||||
|
data.remoteConversation == null)
|
||||||
|
? const AsyncValue.loading()
|
||||||
|
: AsyncValue.data(ActiveConversationState(
|
||||||
|
contact: contact,
|
||||||
|
localConversation: data.localConversation!,
|
||||||
|
remoteConversation: data.remoteConversation!)),
|
||||||
|
loading: AsyncValue.loading,
|
||||||
|
error: AsyncValue.error));
|
||||||
|
|
||||||
|
return MapEntry(
|
||||||
|
contact.localConversationRecordKey.toVeilid(), transformedCubit);
|
||||||
|
});
|
||||||
|
|
||||||
/// StateFollower /////////////////////////
|
/// StateFollower /////////////////////////
|
||||||
|
|
||||||
@ -108,4 +131,5 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
|||||||
|
|
||||||
final UnlockedAccountInfo _activeAccountInfo;
|
final UnlockedAccountInfo _activeAccountInfo;
|
||||||
final ContactListCubit _contactListCubit;
|
final ContactListCubit _contactListCubit;
|
||||||
|
final AccountRecordCubit _accountRecordCubit;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import '../../chat/chat.dart';
|
|||||||
import '../../contacts/contacts.dart';
|
import '../../contacts/contacts.dart';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import 'active_conversations_bloc_map_cubit.dart';
|
import 'active_conversations_bloc_map_cubit.dart';
|
||||||
import 'chat_list_cubit.dart';
|
import '../../chat_list/cubits/chat_list_cubit.dart';
|
||||||
|
|
||||||
// Map of localConversationRecordKey to MessagesCubit
|
// Map of localConversationRecordKey to MessagesCubit
|
||||||
// Wraps a MessagesCubit to stream the latest messages to the state
|
// Wraps a MessagesCubit to stream the latest messages to the state
|
||||||
|
@ -9,12 +9,15 @@ import 'package:async_tools/async_tools.dart';
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:protobuf/protobuf.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';
|
||||||
import '../../proto/proto.dart' as proto;
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
|
|
||||||
|
const _sfUpdateAccountChange = 'updateAccountChange';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
class ConversationState extends Equatable {
|
class ConversationState extends Equatable {
|
||||||
const ConversationState(
|
const ConversationState(
|
||||||
@ -27,6 +30,9 @@ class ConversationState extends Equatable {
|
|||||||
List<Object?> get props => [localConversation, remoteConversation];
|
List<Object?> get props => [localConversation, remoteConversation];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents the control channel between two contacts
|
||||||
|
/// Used to pass profile, identity and status changes, and the messages key for
|
||||||
|
/// 1-1 chats
|
||||||
class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||||
ConversationCubit(
|
ConversationCubit(
|
||||||
{required UnlockedAccountInfo activeAccountInfo,
|
{required UnlockedAccountInfo activeAccountInfo,
|
||||||
@ -53,6 +59,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
debugName: 'ConversationCubit::LocalConversation',
|
debugName: 'ConversationCubit::LocalConversation',
|
||||||
parent: accountRecordKey,
|
parent: accountRecordKey,
|
||||||
crypto: crypto);
|
crypto: crypto);
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -80,6 +87,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
@override
|
@override
|
||||||
Future<void> close() async {
|
Future<void> close() async {
|
||||||
await _initWait();
|
await _initWait();
|
||||||
|
await _accountSubscription?.cancel();
|
||||||
await _localSubscription?.cancel();
|
await _localSubscription?.cancel();
|
||||||
await _remoteSubscription?.cancel();
|
await _remoteSubscription?.cancel();
|
||||||
await _localConversationCubit?.close();
|
await _localConversationCubit?.close();
|
||||||
@ -88,127 +96,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
await super.close();
|
await super.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
|
////////////////////////////////////////////////////////////////////////////
|
||||||
final newState = avconv.when(
|
// Public Interface
|
||||||
data: (conv) {
|
|
||||||
_incrementalState = ConversationState(
|
|
||||||
localConversation: conv,
|
|
||||||
remoteConversation: _incrementalState.remoteConversation);
|
|
||||||
// return loading still if state isn't complete
|
|
||||||
if ((_localConversationRecordKey != null &&
|
|
||||||
_incrementalState.localConversation == null) ||
|
|
||||||
(_remoteConversationRecordKey != null &&
|
|
||||||
_incrementalState.remoteConversation == null)) {
|
|
||||||
return const AsyncValue<ConversationState>.loading();
|
|
||||||
}
|
|
||||||
// state is complete, all required keys are open
|
|
||||||
return AsyncValue.data(_incrementalState);
|
|
||||||
},
|
|
||||||
loading: AsyncValue<ConversationState>.loading,
|
|
||||||
error: AsyncValue<ConversationState>.error,
|
|
||||||
);
|
|
||||||
emit(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
|
/// Initialize a local conversation
|
||||||
final newState = avconv.when(
|
/// If we were the initiator of the conversation there may be an
|
||||||
data: (conv) {
|
/// incomplete 'existingConversationRecord' that we need to fill
|
||||||
_incrementalState = ConversationState(
|
/// in now that we have the remote identity key
|
||||||
localConversation: _incrementalState.localConversation,
|
/// The ConversationCubit must not already have a local conversation
|
||||||
remoteConversation: conv);
|
/// The callback allows for more initialization to occur and for
|
||||||
// return loading still if state isn't complete
|
/// cleanup to delete records upon failure of the callback
|
||||||
if ((_localConversationRecordKey != null &&
|
|
||||||
_incrementalState.localConversation == null) ||
|
|
||||||
(_remoteConversationRecordKey != null &&
|
|
||||||
_incrementalState.remoteConversation == null)) {
|
|
||||||
return const AsyncValue<ConversationState>.loading();
|
|
||||||
}
|
|
||||||
// state is complete, all required keys are open
|
|
||||||
return AsyncValue.data(_incrementalState);
|
|
||||||
},
|
|
||||||
loading: AsyncValue<ConversationState>.loading,
|
|
||||||
error: AsyncValue<ConversationState>.error,
|
|
||||||
);
|
|
||||||
emit(newState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open local converation key
|
|
||||||
Future<void> _setLocalConversation(Future<DHTRecord> Function() open) async {
|
|
||||||
assert(_localConversationCubit == null,
|
|
||||||
'shoud not set local conversation twice');
|
|
||||||
_localConversationCubit = DefaultDHTRecordCubit(
|
|
||||||
open: open, decodeState: proto.Conversation.fromBuffer);
|
|
||||||
_localSubscription =
|
|
||||||
_localConversationCubit!.stream.listen(_updateLocalConversationState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open remote converation key
|
|
||||||
Future<void> _setRemoteConversation(Future<DHTRecord> Function() open) async {
|
|
||||||
assert(_remoteConversationCubit == null,
|
|
||||||
'shoud not set remote conversation twice');
|
|
||||||
_remoteConversationCubit = DefaultDHTRecordCubit(
|
|
||||||
open: open, decodeState: proto.Conversation.fromBuffer);
|
|
||||||
_remoteSubscription =
|
|
||||||
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> delete() async {
|
|
||||||
final pool = DHTRecordPool.instance;
|
|
||||||
|
|
||||||
await _initWait();
|
|
||||||
final localConversationCubit = _localConversationCubit;
|
|
||||||
final remoteConversationCubit = _remoteConversationCubit;
|
|
||||||
|
|
||||||
final deleteSet = DelayedWaitSet<void>();
|
|
||||||
|
|
||||||
if (localConversationCubit != null) {
|
|
||||||
final data = localConversationCubit.state.asData;
|
|
||||||
if (data == null) {
|
|
||||||
log.warning('could not delete local conversation');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSet.add(() async {
|
|
||||||
_localConversationCubit = null;
|
|
||||||
await localConversationCubit.close();
|
|
||||||
final conversation = data.value;
|
|
||||||
final messagesKey = conversation.messages.toVeilid();
|
|
||||||
await pool.deleteRecord(messagesKey);
|
|
||||||
await pool.deleteRecord(_localConversationRecordKey!);
|
|
||||||
_localConversationRecordKey = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteConversationCubit != null) {
|
|
||||||
final data = remoteConversationCubit.state.asData;
|
|
||||||
if (data == null) {
|
|
||||||
log.warning('could not delete remote conversation');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteSet.add(() async {
|
|
||||||
_remoteConversationCubit = null;
|
|
||||||
await remoteConversationCubit.close();
|
|
||||||
final conversation = data.value;
|
|
||||||
final messagesKey = conversation.messages.toVeilid();
|
|
||||||
await pool.deleteRecord(messagesKey);
|
|
||||||
await pool.deleteRecord(_remoteConversationRecordKey!);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Commit the delete futures
|
|
||||||
await deleteSet();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize a local conversation
|
|
||||||
// If we were the initiator of the conversation there may be an
|
|
||||||
// incomplete 'existingConversationRecord' that we need to fill
|
|
||||||
// in now that we have the remote identity key
|
|
||||||
// The ConversationCubit must not already have a local conversation
|
|
||||||
// The callback allows for more initialization to occur and for
|
|
||||||
// cleanup to delete records upon failure of the callback
|
|
||||||
Future<T> initLocalConversation<T>(
|
Future<T> initLocalConversation<T>(
|
||||||
{required proto.Profile profile,
|
{required proto.Profile profile,
|
||||||
required FutureOr<T> Function(DHTRecord) callback,
|
required FutureOr<T> Function(DHTRecord) callback,
|
||||||
@ -280,6 +177,167 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete the conversation keys associated with this conversation
|
||||||
|
Future<bool> delete() async {
|
||||||
|
final pool = DHTRecordPool.instance;
|
||||||
|
|
||||||
|
await _initWait();
|
||||||
|
final localConversationCubit = _localConversationCubit;
|
||||||
|
final remoteConversationCubit = _remoteConversationCubit;
|
||||||
|
|
||||||
|
final deleteSet = DelayedWaitSet<void>();
|
||||||
|
|
||||||
|
if (localConversationCubit != null) {
|
||||||
|
final data = localConversationCubit.state.asData;
|
||||||
|
if (data == null) {
|
||||||
|
log.warning('could not delete local conversation');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSet.add(() async {
|
||||||
|
_localConversationCubit = null;
|
||||||
|
await localConversationCubit.close();
|
||||||
|
final conversation = data.value;
|
||||||
|
final messagesKey = conversation.messages.toVeilid();
|
||||||
|
await pool.deleteRecord(messagesKey);
|
||||||
|
await pool.deleteRecord(_localConversationRecordKey!);
|
||||||
|
_localConversationRecordKey = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteConversationCubit != null) {
|
||||||
|
final data = remoteConversationCubit.state.asData;
|
||||||
|
if (data == null) {
|
||||||
|
log.warning('could not delete remote conversation');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteSet.add(() async {
|
||||||
|
_remoteConversationCubit = null;
|
||||||
|
await remoteConversationCubit.close();
|
||||||
|
final conversation = data.value;
|
||||||
|
final messagesKey = conversation.messages.toVeilid();
|
||||||
|
await pool.deleteRecord(messagesKey);
|
||||||
|
await pool.deleteRecord(_remoteConversationRecordKey!);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commit the delete futures
|
||||||
|
await deleteSet();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force refresh of conversation keys
|
||||||
|
Future<void> refresh() async {
|
||||||
|
await _initWait();
|
||||||
|
|
||||||
|
final lcc = _localConversationCubit;
|
||||||
|
final rcc = _remoteConversationCubit;
|
||||||
|
|
||||||
|
if (lcc != null) {
|
||||||
|
await lcc.refreshDefault();
|
||||||
|
}
|
||||||
|
if (rcc != null) {
|
||||||
|
await rcc.refreshDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watch for account record changes and update the conversation
|
||||||
|
void watchAccountChanges(Stream<AsyncValue<proto.Account>> accountStream,
|
||||||
|
AsyncValue<proto.Account> currentState) {
|
||||||
|
assert(_accountSubscription == null, 'only watch account once');
|
||||||
|
_accountSubscription = accountStream.listen(_updateAccountChange);
|
||||||
|
_updateAccountChange(currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Private Implementation
|
||||||
|
|
||||||
|
void _updateAccountChange(AsyncValue<proto.Account> avaccount) {
|
||||||
|
final account = avaccount.asData?.value;
|
||||||
|
if (account == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final cubit = _localConversationCubit;
|
||||||
|
if (cubit == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
serialFuture((this, _sfUpdateAccountChange), () async {
|
||||||
|
await cubit.record.eventualUpdateProtobuf(proto.Conversation.fromBuffer,
|
||||||
|
(old) async {
|
||||||
|
if (old == null || old.profile == account.profile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return old.deepCopy()..profile = account.profile;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateLocalConversationState(AsyncValue<proto.Conversation> avconv) {
|
||||||
|
final newState = avconv.when(
|
||||||
|
data: (conv) {
|
||||||
|
_incrementalState = ConversationState(
|
||||||
|
localConversation: conv,
|
||||||
|
remoteConversation: _incrementalState.remoteConversation);
|
||||||
|
// return loading still if state isn't complete
|
||||||
|
if ((_localConversationRecordKey != null &&
|
||||||
|
_incrementalState.localConversation == null) ||
|
||||||
|
(_remoteConversationRecordKey != null &&
|
||||||
|
_incrementalState.remoteConversation == null)) {
|
||||||
|
return const AsyncValue<ConversationState>.loading();
|
||||||
|
}
|
||||||
|
// state is complete, all required keys are open
|
||||||
|
return AsyncValue.data(_incrementalState);
|
||||||
|
},
|
||||||
|
loading: AsyncValue<ConversationState>.loading,
|
||||||
|
error: AsyncValue<ConversationState>.error,
|
||||||
|
);
|
||||||
|
emit(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateRemoteConversationState(AsyncValue<proto.Conversation> avconv) {
|
||||||
|
final newState = avconv.when(
|
||||||
|
data: (conv) {
|
||||||
|
_incrementalState = ConversationState(
|
||||||
|
localConversation: _incrementalState.localConversation,
|
||||||
|
remoteConversation: conv);
|
||||||
|
// return loading still if state isn't complete
|
||||||
|
if ((_localConversationRecordKey != null &&
|
||||||
|
_incrementalState.localConversation == null) ||
|
||||||
|
(_remoteConversationRecordKey != null &&
|
||||||
|
_incrementalState.remoteConversation == null)) {
|
||||||
|
return const AsyncValue<ConversationState>.loading();
|
||||||
|
}
|
||||||
|
// state is complete, all required keys are open
|
||||||
|
return AsyncValue.data(_incrementalState);
|
||||||
|
},
|
||||||
|
loading: AsyncValue<ConversationState>.loading,
|
||||||
|
error: AsyncValue<ConversationState>.error,
|
||||||
|
);
|
||||||
|
emit(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open local converation key
|
||||||
|
Future<void> _setLocalConversation(Future<DHTRecord> Function() open) async {
|
||||||
|
assert(_localConversationCubit == null,
|
||||||
|
'shoud not set local conversation twice');
|
||||||
|
_localConversationCubit = DefaultDHTRecordCubit(
|
||||||
|
open: open, decodeState: proto.Conversation.fromBuffer);
|
||||||
|
_localSubscription =
|
||||||
|
_localConversationCubit!.stream.listen(_updateLocalConversationState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open remote converation key
|
||||||
|
Future<void> _setRemoteConversation(Future<DHTRecord> Function() open) async {
|
||||||
|
assert(_remoteConversationCubit == null,
|
||||||
|
'shoud not set remote conversation twice');
|
||||||
|
_remoteConversationCubit = DefaultDHTRecordCubit(
|
||||||
|
open: open, decodeState: proto.Conversation.fromBuffer);
|
||||||
|
_remoteSubscription =
|
||||||
|
_remoteConversationCubit!.stream.listen(_updateRemoteConversationState);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize local messages
|
// Initialize local messages
|
||||||
Future<T> _initLocalMessages<T>({
|
Future<T> _initLocalMessages<T>({
|
||||||
required UnlockedAccountInfo activeAccountInfo,
|
required UnlockedAccountInfo activeAccountInfo,
|
||||||
@ -299,34 +357,6 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
.deleteScope((messages) async => await callback(messages));
|
.deleteScope((messages) async => await callback(messages));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Force refresh of conversation keys
|
|
||||||
Future<void> refresh() async {
|
|
||||||
await _initWait();
|
|
||||||
|
|
||||||
final lcc = _localConversationCubit;
|
|
||||||
final rcc = _remoteConversationCubit;
|
|
||||||
|
|
||||||
if (lcc != null) {
|
|
||||||
await lcc.refreshDefault();
|
|
||||||
}
|
|
||||||
if (rcc != null) {
|
|
||||||
await rcc.refreshDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<proto.Conversation?> writeLocalConversation({
|
|
||||||
required proto.Conversation conversation,
|
|
||||||
}) async {
|
|
||||||
final update = await _localConversationCubit!.record
|
|
||||||
.tryWriteProtobuf(proto.Conversation.fromBuffer, conversation);
|
|
||||||
|
|
||||||
if (update != null) {
|
|
||||||
_updateLocalConversationState(AsyncValue.data(conversation));
|
|
||||||
}
|
|
||||||
|
|
||||||
return update;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<VeilidCrypto> _cachedConversationCrypto() async {
|
Future<VeilidCrypto> _cachedConversationCrypto() async {
|
||||||
var conversationCrypto = _conversationCrypto;
|
var conversationCrypto = _conversationCrypto;
|
||||||
if (conversationCrypto != null) {
|
if (conversationCrypto != null) {
|
||||||
@ -339,6 +369,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
return conversationCrypto;
|
return conversationCrypto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Fields
|
||||||
|
|
||||||
final UnlockedAccountInfo _unlockedAccountInfo;
|
final UnlockedAccountInfo _unlockedAccountInfo;
|
||||||
final TypedKey _remoteIdentityPublicKey;
|
final TypedKey _remoteIdentityPublicKey;
|
||||||
TypedKey? _localConversationRecordKey;
|
TypedKey? _localConversationRecordKey;
|
||||||
@ -347,9 +380,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
|||||||
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
|
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
|
||||||
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
|
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
|
||||||
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
|
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
|
||||||
|
StreamSubscription<AsyncValue<proto.Account>>? _accountSubscription;
|
||||||
ConversationState _incrementalState = const ConversationState(
|
ConversationState _incrementalState = const ConversationState(
|
||||||
localConversation: null, remoteConversation: null);
|
localConversation: null, remoteConversation: null);
|
||||||
//
|
|
||||||
VeilidCrypto? _conversationCrypto;
|
VeilidCrypto? _conversationCrypto;
|
||||||
final WaitSet<void> _initWait = WaitSet();
|
final WaitSet<void> _initWait = WaitSet();
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import 'package:go_router/go_router.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';
|
||||||
|
import '../../../proto/proto.dart' as proto;
|
||||||
import '../../../theme/theme.dart';
|
import '../../../theme/theme.dart';
|
||||||
import '../../../tools/tools.dart';
|
import '../../../tools/tools.dart';
|
||||||
import '../../../veilid_processor/veilid_processor.dart';
|
import '../../../veilid_processor/veilid_processor.dart';
|
||||||
@ -37,8 +38,12 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _doEditClick(TypedKey superIdentityRecordKey) {
|
void _doEditClick(
|
||||||
//
|
TypedKey superIdentityRecordKey, proto.Profile existingProfile) {
|
||||||
|
singleFuture(this, () async {
|
||||||
|
await GoRouterHelper(context).push('/edit_account',
|
||||||
|
extra: [superIdentityRecordKey, existingProfile]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _wrapInBox({required Widget child, required Color color}) =>
|
Widget _wrapInBox({required Widget child, required Color color}) =>
|
||||||
@ -127,7 +132,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
|||||||
_doSwitchClick(superIdentityRecordKey);
|
_doSwitchClick(superIdentityRecordKey);
|
||||||
},
|
},
|
||||||
footerCallback: () {
|
footerCallback: () {
|
||||||
_doEditClick(superIdentityRecordKey);
|
_doEditClick(superIdentityRecordKey, value.profile);
|
||||||
}),
|
}),
|
||||||
loading: () => _wrapInBox(
|
loading: () => _wrapInBox(
|
||||||
child: buildProgressIndicator(),
|
child: buildProgressIndicator(),
|
||||||
|
@ -102,6 +102,9 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// XXX: Should probably eliminate this in favor
|
||||||
|
// of streaming changes into other cubits. Too much rebuilding!
|
||||||
|
// should not need to 'watch' all these cubits
|
||||||
final account = context.watch<AccountRecordCubit>().state.asData?.value;
|
final account = context.watch<AccountRecordCubit>().state.asData?.value;
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
return waitingPage();
|
return waitingPage();
|
||||||
@ -121,28 +124,29 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
|
|||||||
create: (context) => WaitingInvitationsBlocMapCubit(
|
create: (context) => WaitingInvitationsBlocMapCubit(
|
||||||
unlockedAccountInfo: widget.unlockedAccountInfo,
|
unlockedAccountInfo: widget.unlockedAccountInfo,
|
||||||
account: account)
|
account: account)
|
||||||
..follow(context.watch<ContactInvitationListCubit>())),
|
..follow(context.read<ContactInvitationListCubit>())),
|
||||||
// Chat Cubits
|
// Chat Cubits
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => ActiveChatCubit(null,
|
create: (context) => ActiveChatCubit(null,
|
||||||
routerCubit: context.watch<RouterCubit>())),
|
routerCubit: context.read<RouterCubit>())),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => ChatListCubit(
|
create: (context) => ChatListCubit(
|
||||||
unlockedAccountInfo: widget.unlockedAccountInfo,
|
unlockedAccountInfo: widget.unlockedAccountInfo,
|
||||||
activeChatCubit: context.watch<ActiveChatCubit>(),
|
activeChatCubit: context.read<ActiveChatCubit>(),
|
||||||
account: account)),
|
account: account)),
|
||||||
// Conversation Cubits
|
// Conversation Cubits
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => ActiveConversationsBlocMapCubit(
|
create: (context) => ActiveConversationsBlocMapCubit(
|
||||||
unlockedAccountInfo: widget.unlockedAccountInfo,
|
unlockedAccountInfo: widget.unlockedAccountInfo,
|
||||||
contactListCubit: context.watch<ContactListCubit>())
|
contactListCubit: context.read<ContactListCubit>(),
|
||||||
..follow(context.watch<ChatListCubit>())),
|
accountRecordCubit: context.read<AccountRecordCubit>())
|
||||||
|
..follow(context.read<ChatListCubit>())),
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => ActiveSingleContactChatBlocMapCubit(
|
create: (context) => ActiveSingleContactChatBlocMapCubit(
|
||||||
unlockedAccountInfo: widget.unlockedAccountInfo,
|
unlockedAccountInfo: widget.unlockedAccountInfo,
|
||||||
contactListCubit: context.watch<ContactListCubit>(),
|
contactListCubit: context.read<ContactListCubit>(),
|
||||||
chatListCubit: context.watch<ChatListCubit>())
|
chatListCubit: context.read<ChatListCubit>())
|
||||||
..follow(context.watch<ActiveConversationsBlocMapCubit>())),
|
..follow(context.read<ActiveConversationsBlocMapCubit>())),
|
||||||
],
|
],
|
||||||
child: MultiBlocListener(listeners: [
|
child: MultiBlocListener(listeners: [
|
||||||
BlocListener<WaitingInvitationsBlocMapCubit,
|
BlocListener<WaitingInvitationsBlocMapCubit,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
|
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
@ -13,12 +14,12 @@ import 'home_account_missing.dart';
|
|||||||
import 'home_no_active.dart';
|
import 'home_no_active.dart';
|
||||||
|
|
||||||
class HomeShell extends StatefulWidget {
|
class HomeShell extends StatefulWidget {
|
||||||
const HomeShell({required this.accountReadyBuilder, super.key});
|
const HomeShell({required this.child, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
HomeShellState createState() => HomeShellState();
|
HomeShellState createState() => HomeShellState();
|
||||||
|
|
||||||
final Builder accountReadyBuilder;
|
final Widget child;
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeShellState extends State<HomeShell> {
|
class HomeShellState extends State<HomeShell> {
|
||||||
@ -58,13 +59,17 @@ class HomeShellState extends State<HomeShell> {
|
|||||||
case AccountInfoStatus.accountLocked:
|
case AccountInfoStatus.accountLocked:
|
||||||
return const HomeAccountLocked();
|
return const HomeAccountLocked();
|
||||||
case AccountInfoStatus.accountReady:
|
case AccountInfoStatus.accountReady:
|
||||||
return MultiProvider(providers: [
|
return MultiBlocProvider(
|
||||||
Provider<UnlockedAccountInfo>.value(
|
providers: [
|
||||||
value: accountInfo.unlockedAccountInfo!,
|
BlocProvider<AccountRecordCubit>.value(value: activeCubit),
|
||||||
),
|
],
|
||||||
Provider<AccountRecordCubit>.value(value: activeCubit),
|
child: MultiProvider(providers: [
|
||||||
Provider<ZoomDrawerController>.value(value: _zoomDrawerController),
|
Provider<UnlockedAccountInfo>.value(
|
||||||
], child: widget.accountReadyBuilder);
|
value: accountInfo.unlockedAccountInfo!,
|
||||||
|
),
|
||||||
|
Provider<ZoomDrawerController>.value(
|
||||||
|
value: _zoomDrawerController),
|
||||||
|
], child: widget.child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1396,6 +1396,7 @@ class Profile extends $pb.GeneratedMessage {
|
|||||||
..aOS(4, _omitFieldNames ? '' : 'status')
|
..aOS(4, _omitFieldNames ? '' : 'status')
|
||||||
..e<Availability>(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values)
|
..e<Availability>(5, _omitFieldNames ? '' : 'availability', $pb.PbFieldType.OE, defaultOrMaker: Availability.AVAILABILITY_UNSPECIFIED, valueOf: Availability.valueOf, enumValues: Availability.values)
|
||||||
..aOM<DataReference>(6, _omitFieldNames ? '' : 'avatar', subBuilder: DataReference.create)
|
..aOM<DataReference>(6, _omitFieldNames ? '' : 'avatar', subBuilder: DataReference.create)
|
||||||
|
..a<$fixnum.Int64>(7, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
|
||||||
..hasRequiredFields = false
|
..hasRequiredFields = false
|
||||||
;
|
;
|
||||||
|
|
||||||
@ -1475,6 +1476,15 @@ class Profile extends $pb.GeneratedMessage {
|
|||||||
void clearAvatar() => clearField(6);
|
void clearAvatar() => clearField(6);
|
||||||
@$pb.TagNumber(6)
|
@$pb.TagNumber(6)
|
||||||
DataReference ensureAvatar() => $_ensure(5);
|
DataReference ensureAvatar() => $_ensure(5);
|
||||||
|
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$fixnum.Int64 get timestamp => $_getI64(6);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
set timestamp($fixnum.Int64 v) { $_setInt64(6, v); }
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
$core.bool hasTimestamp() => $_has(6);
|
||||||
|
@$pb.TagNumber(7)
|
||||||
|
void clearTimestamp() => clearField(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
class Account extends $pb.GeneratedMessage {
|
class Account extends $pb.GeneratedMessage {
|
||||||
|
@ -411,6 +411,7 @@ const Profile$json = {
|
|||||||
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
|
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
|
||||||
{'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'},
|
{'1': 'availability', '3': 5, '4': 1, '5': 14, '6': '.veilidchat.Availability', '10': 'availability'},
|
||||||
{'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'avatar', '17': true},
|
{'1': 'avatar', '3': 6, '4': 1, '5': 11, '6': '.veilidchat.DataReference', '9': 0, '10': 'avatar', '17': true},
|
||||||
|
{'1': 'timestamp', '3': 7, '4': 1, '5': 4, '10': 'timestamp'},
|
||||||
],
|
],
|
||||||
'8': [
|
'8': [
|
||||||
{'1': '_avatar'},
|
{'1': '_avatar'},
|
||||||
@ -422,8 +423,8 @@ final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
|
|||||||
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
|
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
|
||||||
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
|
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
|
||||||
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej'
|
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej'
|
||||||
'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQFC'
|
'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQES'
|
||||||
'CQoHX2F2YXRhcg==');
|
'HAoJdGltZXN0YW1wGAcgASgEUgl0aW1lc3RhbXBCCQoHX2F2YXRhcg==');
|
||||||
|
|
||||||
@$core.Deprecated('Use accountDescriptor instead')
|
@$core.Deprecated('Use accountDescriptor instead')
|
||||||
const Account$json = {
|
const Account$json = {
|
||||||
|
@ -311,6 +311,8 @@ message Profile {
|
|||||||
Availability availability = 5;
|
Availability availability = 5;
|
||||||
// Avatar
|
// Avatar
|
||||||
optional DataReference avatar = 6;
|
optional DataReference avatar = 6;
|
||||||
|
// Timestamp of last change
|
||||||
|
uint64 timestamp = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
// A record of an individual account
|
// A record of an individual account
|
||||||
|
@ -11,6 +11,7 @@ import 'package:veilid_support/veilid_support.dart';
|
|||||||
|
|
||||||
import '../../../account_manager/account_manager.dart';
|
import '../../../account_manager/account_manager.dart';
|
||||||
import '../../layout/layout.dart';
|
import '../../layout/layout.dart';
|
||||||
|
import '../../proto/proto.dart' as proto;
|
||||||
import '../../settings/settings.dart';
|
import '../../settings/settings.dart';
|
||||||
import '../../tools/tools.dart';
|
import '../../tools/tools.dart';
|
||||||
import '../../veilid_processor/views/developer.dart';
|
import '../../veilid_processor/views/developer.dart';
|
||||||
@ -20,6 +21,7 @@ part 'router_cubit.g.dart';
|
|||||||
|
|
||||||
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
|
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
|
||||||
final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
|
final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
|
||||||
|
final _activeNavKey = GlobalKey<NavigatorState>(debugLabel: 'activeNavKey');
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
class RouterState with _$RouterState {
|
class RouterState with _$RouterState {
|
||||||
@ -65,21 +67,34 @@ class RouterCubit extends Cubit<RouterState> {
|
|||||||
List<RouteBase> get routes => [
|
List<RouteBase> get routes => [
|
||||||
ShellRoute(
|
ShellRoute(
|
||||||
navigatorKey: _homeNavKey,
|
navigatorKey: _homeNavKey,
|
||||||
builder: (context, state, child) => HomeShell(
|
builder: (context, state, child) => HomeShell(child: child),
|
||||||
accountReadyBuilder: Builder(
|
|
||||||
builder: (context) =>
|
|
||||||
HomeAccountReadyShell(context: context, child: child))),
|
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
ShellRoute(
|
||||||
path: '/',
|
navigatorKey: _activeNavKey,
|
||||||
builder: (context, state) => const HomeAccountReadyMain(),
|
builder: (context, state, child) =>
|
||||||
),
|
HomeAccountReadyShell(context: context, child: child),
|
||||||
GoRoute(
|
routes: [
|
||||||
path: '/chat',
|
GoRoute(
|
||||||
builder: (context, state) => const HomeAccountReadyChat(),
|
path: '/',
|
||||||
),
|
builder: (context, state) => const HomeAccountReadyMain(),
|
||||||
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/chat',
|
||||||
|
builder: (context, state) => const HomeAccountReadyChat(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/edit_account',
|
||||||
|
builder: (context, state) {
|
||||||
|
final extra = state.extra! as List<Object?>;
|
||||||
|
return EditAccountPage(
|
||||||
|
superIdentityRecordKey: extra[0]! as TypedKey,
|
||||||
|
existingProfile: extra[1]! as proto.Profile,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/new_account',
|
path: '/new_account',
|
||||||
builder: (context, state) => const NewAccountPage(),
|
builder: (context, state) => const NewAccountPage(),
|
||||||
|
@ -14,7 +14,7 @@ dependencies:
|
|||||||
path: ../
|
path: ../
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
async_tools: ^0.1.2
|
async_tools: ^0.1.3
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
lint_hard: ^4.0.0
|
lint_hard: ^4.0.0
|
||||||
|
@ -308,7 +308,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
|||||||
/// Each attempt to write the value calls an update function with the
|
/// Each attempt to write the value calls an update function with the
|
||||||
/// old value to determine what new value should be attempted for that write.
|
/// old value to determine what new value should be attempted for that write.
|
||||||
Future<void> eventualUpdateBytes(
|
Future<void> eventualUpdateBytes(
|
||||||
Future<Uint8List> Function(Uint8List? oldValue) update,
|
Future<Uint8List?> Function(Uint8List? oldValue) update,
|
||||||
{int subkey = -1,
|
{int subkey = -1,
|
||||||
VeilidCrypto? crypto,
|
VeilidCrypto? crypto,
|
||||||
KeyPair? writer,
|
KeyPair? writer,
|
||||||
@ -323,7 +323,10 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
|||||||
do {
|
do {
|
||||||
// Update the data
|
// Update the data
|
||||||
final updatedValue = await update(oldValue);
|
final updatedValue = await update(oldValue);
|
||||||
|
if (updatedValue == null) {
|
||||||
|
// If null is returned from the update, stop trying to do the update
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Try to write it back to the network
|
// Try to write it back to the network
|
||||||
oldValue = await tryWriteBytes(updatedValue,
|
oldValue = await tryWriteBytes(updatedValue,
|
||||||
subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum);
|
subkey: subkey, crypto: crypto, writer: writer, outSeqNum: outSeqNum);
|
||||||
@ -389,7 +392,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
|||||||
|
|
||||||
/// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value
|
/// Like 'eventualUpdateBytes' but with JSON marshal/unmarshal of the value
|
||||||
Future<void> eventualUpdateJson<T>(
|
Future<void> eventualUpdateJson<T>(
|
||||||
T Function(dynamic) fromJson, Future<T> Function(T?) update,
|
T Function(dynamic) fromJson, Future<T?> Function(T?) update,
|
||||||
{int subkey = -1,
|
{int subkey = -1,
|
||||||
VeilidCrypto? crypto,
|
VeilidCrypto? crypto,
|
||||||
KeyPair? writer,
|
KeyPair? writer,
|
||||||
@ -399,7 +402,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
|||||||
|
|
||||||
/// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value
|
/// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value
|
||||||
Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>(
|
Future<void> eventualUpdateProtobuf<T extends GeneratedMessage>(
|
||||||
T Function(List<int>) fromBuffer, Future<T> Function(T?) update,
|
T Function(List<int>) fromBuffer, Future<T?> Function(T?) update,
|
||||||
{int subkey = -1,
|
{int subkey = -1,
|
||||||
VeilidCrypto? crypto,
|
VeilidCrypto? crypto,
|
||||||
KeyPair? writer,
|
KeyPair? writer,
|
||||||
|
@ -12,16 +12,19 @@ Uint8List jsonEncodeBytes(Object? object,
|
|||||||
Uint8List.fromList(
|
Uint8List.fromList(
|
||||||
utf8.encode(jsonEncode(object, toEncodable: toEncodable)));
|
utf8.encode(jsonEncode(object, toEncodable: toEncodable)));
|
||||||
|
|
||||||
Future<Uint8List> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
|
Future<Uint8List?> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
|
||||||
Uint8List? oldBytes, Future<T> Function(T?) update) async {
|
Uint8List? oldBytes, Future<T?> Function(T?) update) async {
|
||||||
final oldObj =
|
final oldObj =
|
||||||
oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes)));
|
oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes)));
|
||||||
final newObj = await update(oldObj);
|
final newObj = await update(oldObj);
|
||||||
|
if (newObj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return jsonEncodeBytes(newObj);
|
return jsonEncodeBytes(newObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> Function(Uint8List?) jsonUpdate<T>(
|
Future<Uint8List?> Function(Uint8List?) jsonUpdate<T>(
|
||||||
T Function(dynamic) fromJson, Future<T> Function(T?) update) =>
|
T Function(dynamic) fromJson, Future<T?> Function(T?) update) =>
|
||||||
(oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update);
|
(oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update);
|
||||||
|
|
||||||
T Function(Object?) genericFromJson<T>(
|
T Function(Object?) genericFromJson<T>(
|
||||||
|
@ -2,16 +2,19 @@ import 'dart:typed_data';
|
|||||||
|
|
||||||
import 'package:protobuf/protobuf.dart';
|
import 'package:protobuf/protobuf.dart';
|
||||||
|
|
||||||
Future<Uint8List> protobufUpdateBytes<T extends GeneratedMessage>(
|
Future<Uint8List?> protobufUpdateBytes<T extends GeneratedMessage>(
|
||||||
T Function(List<int>) fromBuffer,
|
T Function(List<int>) fromBuffer,
|
||||||
Uint8List? oldBytes,
|
Uint8List? oldBytes,
|
||||||
Future<T> Function(T?) update) async {
|
Future<T?> Function(T?) update) async {
|
||||||
final oldObj = oldBytes == null ? null : fromBuffer(oldBytes);
|
final oldObj = oldBytes == null ? null : fromBuffer(oldBytes);
|
||||||
final newObj = await update(oldObj);
|
final newObj = await update(oldObj);
|
||||||
|
if (newObj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return Uint8List.fromList(newObj.writeToBuffer());
|
return Uint8List.fromList(newObj.writeToBuffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uint8List> Function(Uint8List?)
|
Future<Uint8List?> Function(Uint8List?)
|
||||||
protobufUpdate<T extends GeneratedMessage>(
|
protobufUpdate<T extends GeneratedMessage>(
|
||||||
T Function(List<int>) fromBuffer, Future<T> Function(T?) update) =>
|
T Function(List<int>) fromBuffer, Future<T?> Function(T?) update) =>
|
||||||
(oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update);
|
(oldBytes) => protobufUpdateBytes(fromBuffer, oldBytes, update);
|
||||||
|
@ -36,11 +36,10 @@ packages:
|
|||||||
async_tools:
|
async_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: async_tools
|
path: "../../../dart_async_tools"
|
||||||
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684"
|
relative: true
|
||||||
url: "https://pub.dev"
|
source: path
|
||||||
source: hosted
|
version: "0.1.3"
|
||||||
version: "0.1.2"
|
|
||||||
bloc:
|
bloc:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -7,7 +7,7 @@ environment:
|
|||||||
sdk: '>=3.2.0 <4.0.0'
|
sdk: '>=3.2.0 <4.0.0'
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async_tools: ^0.1.2
|
async_tools: ^0.1.3
|
||||||
bloc: ^8.1.4
|
bloc: ^8.1.4
|
||||||
bloc_advanced_tools: ^0.1.3
|
bloc_advanced_tools: ^0.1.3
|
||||||
charcode: ^1.3.1
|
charcode: ^1.3.1
|
||||||
@ -25,10 +25,10 @@ dependencies:
|
|||||||
path: ../../../veilid/veilid-flutter
|
path: ../../../veilid/veilid-flutter
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
# async_tools:
|
async_tools:
|
||||||
# path: ../../../dart_async_tools
|
path: ../../../dart_async_tools
|
||||||
bloc_advanced_tools:
|
bloc_advanced_tools:
|
||||||
path: ../../../bloc_advanced_tools
|
path: ../../../bloc_advanced_tools
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.10
|
build_runner: ^2.4.10
|
||||||
|
@ -60,11 +60,10 @@ packages:
|
|||||||
async_tools:
|
async_tools:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: async_tools
|
path: "../dart_async_tools"
|
||||||
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684"
|
relative: true
|
||||||
url: "https://pub.dev"
|
source: path
|
||||||
source: hosted
|
version: "0.1.3"
|
||||||
version: "0.1.2"
|
|
||||||
awesome_extensions:
|
awesome_extensions:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -11,7 +11,7 @@ dependencies:
|
|||||||
animated_theme_switcher: ^2.0.10
|
animated_theme_switcher: ^2.0.10
|
||||||
ansicolor: ^2.0.2
|
ansicolor: ^2.0.2
|
||||||
archive: ^3.6.1
|
archive: ^3.6.1
|
||||||
async_tools: ^0.1.2
|
async_tools: ^0.1.3
|
||||||
awesome_extensions: ^2.0.16
|
awesome_extensions: ^2.0.16
|
||||||
badges: ^3.1.2
|
badges: ^3.1.2
|
||||||
basic_utils: ^5.7.0
|
basic_utils: ^5.7.0
|
||||||
@ -94,8 +94,8 @@ dependencies:
|
|||||||
zxing2: ^0.2.3
|
zxing2: ^0.2.3
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
# async_tools:
|
async_tools:
|
||||||
# path: ../dart_async_tools
|
path: ../dart_async_tools
|
||||||
bloc_advanced_tools:
|
bloc_advanced_tools:
|
||||||
path: ../bloc_advanced_tools
|
path: ../bloc_advanced_tools
|
||||||
# flutter_chat_ui:
|
# flutter_chat_ui:
|
||||||
|
Loading…
Reference in New Issue
Block a user