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 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../proto/proto.dart' as proto;
|
||||
@ -7,6 +8,11 @@ import '../account_manager.dart';
|
||||
|
||||
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> {
|
||||
AccountRecordCubit(
|
||||
{required AccountRepository accountRepository,
|
||||
@ -35,4 +41,16 @@ class AccountRecordCubit extends DefaultDHTRecordCubit<AccountRecordState> {
|
||||
Future<void> close() async {
|
||||
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
|
||||
= 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,
|
||||
AsyncValue<AccountRecordState>, AccountRecordCubit>
|
||||
with StateMapFollower<UserLoginsState, TypedKey, UserLogin> {
|
||||
|
@ -8,8 +8,6 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:protobuf/protobuf.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 '../../proto/proto.dart' as proto;
|
||||
import '../../theme/theme.dart';
|
||||
@ -41,7 +39,6 @@ class EditAccountPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _EditAccountPageState extends State<EditAccountPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
bool _isInAsyncCall = false;
|
||||
|
||||
@override
|
||||
@ -58,24 +55,37 @@ class _EditAccountPageState extends State<EditAccountPage> {
|
||||
{required Future<void> Function(GlobalKey<FormBuilderState>)
|
||||
onSubmit}) =>
|
||||
EditProfileForm(
|
||||
header: translate('edit_account_page.header'),
|
||||
instructions: translate('edit_account_page.instructions'),
|
||||
submitText: translate('edit_account_page.update'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
onSubmit: onSubmit);
|
||||
header: translate('edit_account_page.header'),
|
||||
instructions: translate('edit_account_page.instructions'),
|
||||
submitText: translate('edit_account_page.update'),
|
||||
submitDisabledText: translate('button.waiting_for_network'),
|
||||
onSubmit: onSubmit,
|
||||
initialValueCallback: (key) => switch (key) {
|
||||
EditProfileForm.formFieldName => widget.existingProfile.name,
|
||||
EditProfileForm.formFieldPronouns => widget.existingProfile.pronouns,
|
||||
String() => throw UnimplementedError(),
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final displayModalHUD = _isInAsyncCall;
|
||||
final accountRecordCubit = context.read<AccountRecordCubit>();
|
||||
final activeConversationsBlocMapCubit =
|
||||
context.read<ActiveConversationsBlocMapCubit>();
|
||||
final contactListCubit = context.read<ContactListCubit>();
|
||||
final accountRecordsCubit = context.watch<AccountRecordsBlocMapCubit>();
|
||||
final accountRecordCubit = accountRecordsCubit
|
||||
.operate(widget.superIdentityRecordKey, closure: (c) => c);
|
||||
|
||||
return Scaffold(
|
||||
// resizeToAvoidBottomInset: false,
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(translate('edit_account_page.titlebar')),
|
||||
leading: Navigator.canPop(context)
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
const SignalStrengthMeterWidget(),
|
||||
IconButton(
|
||||
@ -92,57 +102,35 @@ class _EditAccountPageState extends State<EditAccountPage> {
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
final name = _formKey.currentState!
|
||||
final name = formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldName]!.value as String;
|
||||
final pronouns = _formKey
|
||||
final pronouns = formKey
|
||||
.currentState!
|
||||
.fields[EditProfileForm.formFieldPronouns]!
|
||||
.value as String? ??
|
||||
'';
|
||||
final newProfile = widget.existingProfile.deepCopy()
|
||||
..name = name
|
||||
..pronouns = pronouns;
|
||||
..pronouns = pronouns
|
||||
..timestamp = Veilid.instance.now().toInt64();
|
||||
|
||||
setState(() {
|
||||
_isInAsyncCall = true;
|
||||
});
|
||||
try {
|
||||
// Update account profile DHT record
|
||||
final newValue = await accountRecordCubit.record
|
||||
.tryWriteProtobuf(proto.Account.fromBuffer, newProfile);
|
||||
if (newValue != null) {
|
||||
if (context.mounted) {
|
||||
await showErrorModal(
|
||||
context,
|
||||
translate('edit_account_page.error'),
|
||||
'Failed to update profile online');
|
||||
return;
|
||||
}
|
||||
}
|
||||
// This triggers ConversationCubits to update
|
||||
await accountRecordCubit.updateProfile(newProfile);
|
||||
|
||||
// Update local account profile
|
||||
await AccountRepository.instance.editAccountProfile(
|
||||
widget.superIdentityRecordKey, newProfile);
|
||||
|
||||
// Update all conversations with new profile
|
||||
final updates = <Future<void>>[];
|
||||
for (final key in activeConversationsBlocMapCubit.state.keys) {
|
||||
await activeConversationsBlocMapCubit.operateAsync(key,
|
||||
closure: (cubit) async {
|
||||
final newLocalConversation =
|
||||
cubit.state.asData?.value.localConversation.deepCopy();
|
||||
if (newLocalConversation != null) {
|
||||
newLocalConversation.profile = newProfile;
|
||||
updates.add(cubit.input.writeLocalConversation(
|
||||
conversation: newLocalConversation));
|
||||
}
|
||||
});
|
||||
if (context.mounted) {
|
||||
Navigator.canPop(context)
|
||||
? GoRouterHelper(context).pop()
|
||||
: GoRouterHelper(context).go('/');
|
||||
}
|
||||
|
||||
// Wait for updates
|
||||
await updates.wait;
|
||||
|
||||
// XXX: how to do this for non-chat contacts?
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
|
@ -21,7 +21,6 @@ class NewAccountPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NewAccountPageState extends State<NewAccountPage> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
bool _isInAsyncCall = false;
|
||||
|
||||
@override
|
||||
@ -61,6 +60,14 @@ class _NewAccountPageState extends State<NewAccountPage> {
|
||||
// resizeToAvoidBottomInset: false,
|
||||
appBar: DefaultAppBar(
|
||||
title: Text(translate('new_account_page.titlebar')),
|
||||
leading: Navigator.canPop(context)
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
actions: [
|
||||
const SignalStrengthMeterWidget(),
|
||||
IconButton(
|
||||
@ -77,9 +84,9 @@ class _NewAccountPageState extends State<NewAccountPage> {
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
try {
|
||||
final name = _formKey.currentState!
|
||||
final name = formKey.currentState!
|
||||
.fields[EditProfileForm.formFieldName]!.value as String;
|
||||
final pronouns = _formKey
|
||||
final pronouns = formKey
|
||||
.currentState!
|
||||
.fields[EditProfileForm.formFieldPronouns]!
|
||||
.value as String? ??
|
||||
|
@ -13,6 +13,7 @@ class EditProfileForm extends StatefulWidget {
|
||||
required this.submitDisabledText,
|
||||
super.key,
|
||||
this.onSubmit,
|
||||
this.initialValueCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -23,6 +24,7 @@ class EditProfileForm extends StatefulWidget {
|
||||
final Future<void> Function(GlobalKey<FormBuilderState>)? onSubmit;
|
||||
final String submitText;
|
||||
final String submitDisabledText;
|
||||
final Object? Function(String key)? initialValueCallback;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
@ -34,7 +36,9 @@ class EditProfileForm extends StatefulWidget {
|
||||
Future<void> Function(
|
||||
GlobalKey<FormBuilderState> p1)?>.has('onSubmit', onSubmit))
|
||||
..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';
|
||||
@ -62,6 +66,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
FormBuilderTextField(
|
||||
autofocus: true,
|
||||
name: EditProfileForm.formFieldName,
|
||||
initialValue: widget.initialValueCallback
|
||||
?.call(EditProfileForm.formFieldName) as String?,
|
||||
decoration:
|
||||
InputDecoration(labelText: translate('account.form_name')),
|
||||
maxLength: 64,
|
||||
@ -73,6 +79,8 @@ class _EditProfileFormState extends State<EditProfileForm> {
|
||||
),
|
||||
FormBuilderTextField(
|
||||
name: EditProfileForm.formFieldPronouns,
|
||||
initialValue: widget.initialValueCallback
|
||||
?.call(EditProfileForm.formFieldPronouns) as String?,
|
||||
maxLength: 64,
|
||||
decoration: InputDecoration(
|
||||
labelText: translate('account.form_pronouns')),
|
||||
|
@ -1,18 +1,12 @@
|
||||
import 'package:awesome_extensions/awesome_extensions.dart';
|
||||
import 'package:flutter/foundation.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:form_builder_validators/form_builder_validators.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../layout/default_app_bar.dart';
|
||||
import '../../theme/theme.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/veilid_processor.dart';
|
||||
import '../account_manager.dart';
|
||||
|
||||
class ShowRecoveryKeyPage extends StatefulWidget {
|
||||
const ShowRecoveryKeyPage({required SecretKey secretKey, super.key})
|
||||
@ -57,7 +51,11 @@ class ShowRecoveryKeyPageState extends State<ShowRecoveryKeyPage> {
|
||||
Text('ASS: $secretKey'),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
GoRouterHelper(context).go('/');
|
||||
if (context.mounted) {
|
||||
Navigator.canPop(context)
|
||||
? GoRouterHelper(context).pop()
|
||||
: GoRouterHelper(context).go('/');
|
||||
}
|
||||
},
|
||||
child: Text(translate('button.finish')))
|
||||
]).paddingSymmetric(horizontal: 24, vertical: 8));
|
||||
|
@ -1,3 +1,4 @@
|
||||
export 'edit_account_page.dart';
|
||||
export 'new_account_page.dart';
|
||||
export 'profile_widget.dart';
|
||||
export 'show_recovery_key_page.dart';
|
||||
|
@ -1,12 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:async_tools/async_tools.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
import 'conversation_cubit.dart';
|
||||
import '../../conversation/cubits/conversation_cubit.dart';
|
||||
|
||||
//////////////////////////////////////////////////
|
||||
// Mutable state for per-account contacts
|
||||
@ -34,6 +36,54 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||
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({
|
||||
required proto.Profile remoteProfile,
|
||||
required SuperIdentity remoteSuperIdentity,
|
||||
@ -100,4 +150,6 @@ class ContactListCubit extends DHTShortArrayCubit<proto.Contact> {
|
||||
}
|
||||
|
||||
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 '../../account_manager/account_manager.dart';
|
||||
import '../../chat_list/cubits/cubits.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../conversation/conversation.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import 'cubits.dart';
|
||||
import '../conversation.dart';
|
||||
|
||||
@immutable
|
||||
class ActiveConversationState extends Equatable {
|
||||
@ -43,11 +43,13 @@ typedef ActiveConversationsBlocMapState
|
||||
class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
AsyncValue<ActiveConversationState>, ActiveConversationCubit>
|
||||
with StateMapFollower<ChatListCubitState, TypedKey, proto.Chat> {
|
||||
ActiveConversationsBlocMapCubit(
|
||||
{required UnlockedAccountInfo unlockedAccountInfo,
|
||||
required ContactListCubit contactListCubit})
|
||||
: _activeAccountInfo = unlockedAccountInfo,
|
||||
_contactListCubit = contactListCubit;
|
||||
ActiveConversationsBlocMapCubit({
|
||||
required UnlockedAccountInfo unlockedAccountInfo,
|
||||
required ContactListCubit contactListCubit,
|
||||
required AccountRecordCubit accountRecordCubit,
|
||||
}) : _activeAccountInfo = unlockedAccountInfo,
|
||||
_contactListCubit = contactListCubit,
|
||||
_accountRecordCubit = accountRecordCubit;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Public Interface
|
||||
@ -57,30 +59,51 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
|
||||
// Add an active conversation to be tracked for changes
|
||||
Future<void> _addConversation({required proto.Contact contact}) async =>
|
||||
add(() => MapEntry(
|
||||
contact.localConversationRecordKey.toVeilid(),
|
||||
TransformerCubit(
|
||||
ConversationCubit(
|
||||
activeAccountInfo: _activeAccountInfo,
|
||||
remoteIdentityPublicKey: contact.identityPublicKey.toVeilid(),
|
||||
localConversationRecordKey:
|
||||
contact.localConversationRecordKey.toVeilid(),
|
||||
remoteConversationRecordKey:
|
||||
contact.remoteConversationRecordKey.toVeilid(),
|
||||
),
|
||||
// Transformer that only passes through completed conversations
|
||||
// along with the contact that corresponds to the completed
|
||||
// conversation
|
||||
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))));
|
||||
add(() {
|
||||
final remoteIdentityPublicKey = contact.identityPublicKey.toVeilid();
|
||||
final localConversationRecordKey =
|
||||
contact.localConversationRecordKey.toVeilid();
|
||||
final remoteConversationRecordKey =
|
||||
contact.remoteConversationRecordKey.toVeilid();
|
||||
|
||||
// Conversation cubit the tracks the state between the local
|
||||
// and remote halves of a contact's relationship with this account
|
||||
final conversationCubit = ConversationCubit(
|
||||
activeAccountInfo: _activeAccountInfo,
|
||||
remoteIdentityPublicKey: remoteIdentityPublicKey,
|
||||
localConversationRecordKey: localConversationRecordKey,
|
||||
remoteConversationRecordKey: remoteConversationRecordKey,
|
||||
)..watchAccountChanges(
|
||||
_accountRecordCubit.stream, _accountRecordCubit.state);
|
||||
_contactListCubit.followContactProfileChanges(
|
||||
localConversationRecordKey,
|
||||
conversationCubit.stream.map((x) => x.map(
|
||||
data: (d) => d.value.remoteConversation?.profile,
|
||||
loading: (_) => null,
|
||||
error: (_) => null)),
|
||||
conversationCubit.state.asData?.value.remoteConversation?.profile);
|
||||
|
||||
// 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 /////////////////////////
|
||||
|
||||
@ -108,4 +131,5 @@ class ActiveConversationsBlocMapCubit extends BlocMapCubit<TypedKey,
|
||||
|
||||
final UnlockedAccountInfo _activeAccountInfo;
|
||||
final ContactListCubit _contactListCubit;
|
||||
final AccountRecordCubit _accountRecordCubit;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import '../../chat/chat.dart';
|
||||
import '../../contacts/contacts.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
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
|
||||
// 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:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../account_manager/account_manager.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../tools/tools.dart';
|
||||
|
||||
const _sfUpdateAccountChange = 'updateAccountChange';
|
||||
|
||||
@immutable
|
||||
class ConversationState extends Equatable {
|
||||
const ConversationState(
|
||||
@ -27,6 +30,9 @@ class ConversationState extends Equatable {
|
||||
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>> {
|
||||
ConversationCubit(
|
||||
{required UnlockedAccountInfo activeAccountInfo,
|
||||
@ -53,6 +59,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
debugName: 'ConversationCubit::LocalConversation',
|
||||
parent: accountRecordKey,
|
||||
crypto: crypto);
|
||||
|
||||
return record;
|
||||
});
|
||||
});
|
||||
@ -80,6 +87,7 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _initWait();
|
||||
await _accountSubscription?.cancel();
|
||||
await _localSubscription?.cancel();
|
||||
await _remoteSubscription?.cancel();
|
||||
await _localConversationCubit?.close();
|
||||
@ -88,127 +96,16 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
await super.close();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Public Interface
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
/// 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>(
|
||||
{required proto.Profile profile,
|
||||
required FutureOr<T> Function(DHTRecord) callback,
|
||||
@ -280,6 +177,167 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
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
|
||||
Future<T> _initLocalMessages<T>({
|
||||
required UnlockedAccountInfo activeAccountInfo,
|
||||
@ -299,34 +357,6 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
.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 {
|
||||
var conversationCrypto = _conversationCrypto;
|
||||
if (conversationCrypto != null) {
|
||||
@ -339,6 +369,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
return conversationCrypto;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Fields
|
||||
|
||||
final UnlockedAccountInfo _unlockedAccountInfo;
|
||||
final TypedKey _remoteIdentityPublicKey;
|
||||
TypedKey? _localConversationRecordKey;
|
||||
@ -347,9 +380,9 @@ class ConversationCubit extends Cubit<AsyncValue<ConversationState>> {
|
||||
DefaultDHTRecordCubit<proto.Conversation>? _remoteConversationCubit;
|
||||
StreamSubscription<AsyncValue<proto.Conversation>>? _localSubscription;
|
||||
StreamSubscription<AsyncValue<proto.Conversation>>? _remoteSubscription;
|
||||
StreamSubscription<AsyncValue<proto.Account>>? _accountSubscription;
|
||||
ConversationState _incrementalState = const ConversationState(
|
||||
localConversation: null, remoteConversation: null);
|
||||
//
|
||||
VeilidCrypto? _conversationCrypto;
|
||||
final WaitSet<void> _initWait = WaitSet();
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../../account_manager/account_manager.dart';
|
||||
import '../../../proto/proto.dart' as proto;
|
||||
import '../../../theme/theme.dart';
|
||||
import '../../../tools/tools.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}) =>
|
||||
@ -127,7 +132,7 @@ class _DrawerMenuState extends State<DrawerMenu> {
|
||||
_doSwitchClick(superIdentityRecordKey);
|
||||
},
|
||||
footerCallback: () {
|
||||
_doEditClick(superIdentityRecordKey);
|
||||
_doEditClick(superIdentityRecordKey, value.profile);
|
||||
}),
|
||||
loading: () => _wrapInBox(
|
||||
child: buildProgressIndicator(),
|
||||
|
@ -102,6 +102,9 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
|
||||
|
||||
@override
|
||||
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;
|
||||
if (account == null) {
|
||||
return waitingPage();
|
||||
@ -121,28 +124,29 @@ class HomeAccountReadyShellState extends State<HomeAccountReadyShell> {
|
||||
create: (context) => WaitingInvitationsBlocMapCubit(
|
||||
unlockedAccountInfo: widget.unlockedAccountInfo,
|
||||
account: account)
|
||||
..follow(context.watch<ContactInvitationListCubit>())),
|
||||
..follow(context.read<ContactInvitationListCubit>())),
|
||||
// Chat Cubits
|
||||
BlocProvider(
|
||||
create: (context) => ActiveChatCubit(null,
|
||||
routerCubit: context.watch<RouterCubit>())),
|
||||
routerCubit: context.read<RouterCubit>())),
|
||||
BlocProvider(
|
||||
create: (context) => ChatListCubit(
|
||||
unlockedAccountInfo: widget.unlockedAccountInfo,
|
||||
activeChatCubit: context.watch<ActiveChatCubit>(),
|
||||
activeChatCubit: context.read<ActiveChatCubit>(),
|
||||
account: account)),
|
||||
// Conversation Cubits
|
||||
BlocProvider(
|
||||
create: (context) => ActiveConversationsBlocMapCubit(
|
||||
unlockedAccountInfo: widget.unlockedAccountInfo,
|
||||
contactListCubit: context.watch<ContactListCubit>())
|
||||
..follow(context.watch<ChatListCubit>())),
|
||||
contactListCubit: context.read<ContactListCubit>(),
|
||||
accountRecordCubit: context.read<AccountRecordCubit>())
|
||||
..follow(context.read<ChatListCubit>())),
|
||||
BlocProvider(
|
||||
create: (context) => ActiveSingleContactChatBlocMapCubit(
|
||||
unlockedAccountInfo: widget.unlockedAccountInfo,
|
||||
contactListCubit: context.watch<ContactListCubit>(),
|
||||
chatListCubit: context.watch<ChatListCubit>())
|
||||
..follow(context.watch<ActiveConversationsBlocMapCubit>())),
|
||||
contactListCubit: context.read<ContactListCubit>(),
|
||||
chatListCubit: context.read<ChatListCubit>())
|
||||
..follow(context.read<ActiveConversationsBlocMapCubit>())),
|
||||
],
|
||||
child: MultiBlocListener(listeners: [
|
||||
BlocListener<WaitingInvitationsBlocMapCubit,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_zoom_drawer/flutter_zoom_drawer.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@ -13,12 +14,12 @@ import 'home_account_missing.dart';
|
||||
import 'home_no_active.dart';
|
||||
|
||||
class HomeShell extends StatefulWidget {
|
||||
const HomeShell({required this.accountReadyBuilder, super.key});
|
||||
const HomeShell({required this.child, super.key});
|
||||
|
||||
@override
|
||||
HomeShellState createState() => HomeShellState();
|
||||
|
||||
final Builder accountReadyBuilder;
|
||||
final Widget child;
|
||||
}
|
||||
|
||||
class HomeShellState extends State<HomeShell> {
|
||||
@ -58,13 +59,17 @@ class HomeShellState extends State<HomeShell> {
|
||||
case AccountInfoStatus.accountLocked:
|
||||
return const HomeAccountLocked();
|
||||
case AccountInfoStatus.accountReady:
|
||||
return MultiProvider(providers: [
|
||||
Provider<UnlockedAccountInfo>.value(
|
||||
value: accountInfo.unlockedAccountInfo!,
|
||||
),
|
||||
Provider<AccountRecordCubit>.value(value: activeCubit),
|
||||
Provider<ZoomDrawerController>.value(value: _zoomDrawerController),
|
||||
], child: widget.accountReadyBuilder);
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<AccountRecordCubit>.value(value: activeCubit),
|
||||
],
|
||||
child: MultiProvider(providers: [
|
||||
Provider<UnlockedAccountInfo>.value(
|
||||
value: accountInfo.unlockedAccountInfo!,
|
||||
),
|
||||
Provider<ZoomDrawerController>.value(
|
||||
value: _zoomDrawerController),
|
||||
], child: widget.child));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1396,6 +1396,7 @@ class Profile extends $pb.GeneratedMessage {
|
||||
..aOS(4, _omitFieldNames ? '' : 'status')
|
||||
..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)
|
||||
..a<$fixnum.Int64>(7, _omitFieldNames ? '' : 'timestamp', $pb.PbFieldType.OU6, defaultOrMaker: $fixnum.Int64.ZERO)
|
||||
..hasRequiredFields = false
|
||||
;
|
||||
|
||||
@ -1475,6 +1476,15 @@ class Profile extends $pb.GeneratedMessage {
|
||||
void clearAvatar() => clearField(6);
|
||||
@$pb.TagNumber(6)
|
||||
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 {
|
||||
|
@ -411,6 +411,7 @@ const Profile$json = {
|
||||
{'1': 'status', '3': 4, '4': 1, '5': 9, '10': 'status'},
|
||||
{'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': 'timestamp', '3': 7, '4': 1, '5': 4, '10': 'timestamp'},
|
||||
],
|
||||
'8': [
|
||||
{'1': '_avatar'},
|
||||
@ -422,8 +423,8 @@ final $typed_data.Uint8List profileDescriptor = $convert.base64Decode(
|
||||
'CgdQcm9maWxlEhIKBG5hbWUYASABKAlSBG5hbWUSGgoIcHJvbm91bnMYAiABKAlSCHByb25vdW'
|
||||
'5zEhQKBWFib3V0GAMgASgJUgVhYm91dBIWCgZzdGF0dXMYBCABKAlSBnN0YXR1cxI8CgxhdmFp'
|
||||
'bGFiaWxpdHkYBSABKA4yGC52ZWlsaWRjaGF0LkF2YWlsYWJpbGl0eVIMYXZhaWxhYmlsaXR5Ej'
|
||||
'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQFC'
|
||||
'CQoHX2F2YXRhcg==');
|
||||
'YKBmF2YXRhchgGIAEoCzIZLnZlaWxpZGNoYXQuRGF0YVJlZmVyZW5jZUgAUgZhdmF0YXKIAQES'
|
||||
'HAoJdGltZXN0YW1wGAcgASgEUgl0aW1lc3RhbXBCCQoHX2F2YXRhcg==');
|
||||
|
||||
@$core.Deprecated('Use accountDescriptor instead')
|
||||
const Account$json = {
|
||||
|
@ -311,6 +311,8 @@ message Profile {
|
||||
Availability availability = 5;
|
||||
// Avatar
|
||||
optional DataReference avatar = 6;
|
||||
// Timestamp of last change
|
||||
uint64 timestamp = 7;
|
||||
}
|
||||
|
||||
// A record of an individual account
|
||||
|
@ -11,6 +11,7 @@ import 'package:veilid_support/veilid_support.dart';
|
||||
|
||||
import '../../../account_manager/account_manager.dart';
|
||||
import '../../layout/layout.dart';
|
||||
import '../../proto/proto.dart' as proto;
|
||||
import '../../settings/settings.dart';
|
||||
import '../../tools/tools.dart';
|
||||
import '../../veilid_processor/views/developer.dart';
|
||||
@ -20,6 +21,7 @@ part 'router_cubit.g.dart';
|
||||
|
||||
final _rootNavKey = GlobalKey<NavigatorState>(debugLabel: 'rootNavKey');
|
||||
final _homeNavKey = GlobalKey<NavigatorState>(debugLabel: 'homeNavKey');
|
||||
final _activeNavKey = GlobalKey<NavigatorState>(debugLabel: 'activeNavKey');
|
||||
|
||||
@freezed
|
||||
class RouterState with _$RouterState {
|
||||
@ -65,21 +67,34 @@ class RouterCubit extends Cubit<RouterState> {
|
||||
List<RouteBase> get routes => [
|
||||
ShellRoute(
|
||||
navigatorKey: _homeNavKey,
|
||||
builder: (context, state, child) => HomeShell(
|
||||
accountReadyBuilder: Builder(
|
||||
builder: (context) =>
|
||||
HomeAccountReadyShell(context: context, child: child))),
|
||||
builder: (context, state, child) => HomeShell(child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (context, state) => const HomeAccountReadyMain(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/chat',
|
||||
builder: (context, state) => const HomeAccountReadyChat(),
|
||||
),
|
||||
ShellRoute(
|
||||
navigatorKey: _activeNavKey,
|
||||
builder: (context, state, child) =>
|
||||
HomeAccountReadyShell(context: context, child: child),
|
||||
routes: [
|
||||
GoRoute(
|
||||
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(
|
||||
path: '/new_account',
|
||||
builder: (context, state) => const NewAccountPage(),
|
||||
|
@ -14,7 +14,7 @@ dependencies:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
async_tools: ^0.1.2
|
||||
async_tools: ^0.1.3
|
||||
integration_test:
|
||||
sdk: flutter
|
||||
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
|
||||
/// old value to determine what new value should be attempted for that write.
|
||||
Future<void> eventualUpdateBytes(
|
||||
Future<Uint8List> Function(Uint8List? oldValue) update,
|
||||
Future<Uint8List?> Function(Uint8List? oldValue) update,
|
||||
{int subkey = -1,
|
||||
VeilidCrypto? crypto,
|
||||
KeyPair? writer,
|
||||
@ -323,7 +323,10 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||
do {
|
||||
// Update the data
|
||||
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
|
||||
oldValue = await tryWriteBytes(updatedValue,
|
||||
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
|
||||
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,
|
||||
VeilidCrypto? crypto,
|
||||
KeyPair? writer,
|
||||
@ -399,7 +402,7 @@ class DHTRecord implements DHTDeleteable<DHTRecord> {
|
||||
|
||||
/// Like 'eventualUpdateBytes' but with protobuf marshal/unmarshal of the value
|
||||
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,
|
||||
VeilidCrypto? crypto,
|
||||
KeyPair? writer,
|
||||
|
@ -12,16 +12,19 @@ Uint8List jsonEncodeBytes(Object? object,
|
||||
Uint8List.fromList(
|
||||
utf8.encode(jsonEncode(object, toEncodable: toEncodable)));
|
||||
|
||||
Future<Uint8List> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
|
||||
Uint8List? oldBytes, Future<T> Function(T?) update) async {
|
||||
Future<Uint8List?> jsonUpdateBytes<T>(T Function(dynamic) fromJson,
|
||||
Uint8List? oldBytes, Future<T?> Function(T?) update) async {
|
||||
final oldObj =
|
||||
oldBytes == null ? null : fromJson(jsonDecode(utf8.decode(oldBytes)));
|
||||
final newObj = await update(oldObj);
|
||||
if (newObj == null) {
|
||||
return null;
|
||||
}
|
||||
return jsonEncodeBytes(newObj);
|
||||
}
|
||||
|
||||
Future<Uint8List> Function(Uint8List?) jsonUpdate<T>(
|
||||
T Function(dynamic) fromJson, Future<T> Function(T?) update) =>
|
||||
Future<Uint8List?> Function(Uint8List?) jsonUpdate<T>(
|
||||
T Function(dynamic) fromJson, Future<T?> Function(T?) update) =>
|
||||
(oldBytes) => jsonUpdateBytes(fromJson, oldBytes, update);
|
||||
|
||||
T Function(Object?) genericFromJson<T>(
|
||||
|
@ -2,16 +2,19 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
|
||||
Future<Uint8List> protobufUpdateBytes<T extends GeneratedMessage>(
|
||||
Future<Uint8List?> protobufUpdateBytes<T extends GeneratedMessage>(
|
||||
T Function(List<int>) fromBuffer,
|
||||
Uint8List? oldBytes,
|
||||
Future<T> Function(T?) update) async {
|
||||
Future<T?> Function(T?) update) async {
|
||||
final oldObj = oldBytes == null ? null : fromBuffer(oldBytes);
|
||||
final newObj = await update(oldObj);
|
||||
if (newObj == null) {
|
||||
return null;
|
||||
}
|
||||
return Uint8List.fromList(newObj.writeToBuffer());
|
||||
}
|
||||
|
||||
Future<Uint8List> Function(Uint8List?)
|
||||
Future<Uint8List?> Function(Uint8List?)
|
||||
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);
|
||||
|
@ -36,11 +36,10 @@ packages:
|
||||
async_tools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async_tools
|
||||
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
path: "../../../dart_async_tools"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.3"
|
||||
bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -7,7 +7,7 @@ environment:
|
||||
sdk: '>=3.2.0 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
async_tools: ^0.1.2
|
||||
async_tools: ^0.1.3
|
||||
bloc: ^8.1.4
|
||||
bloc_advanced_tools: ^0.1.3
|
||||
charcode: ^1.3.1
|
||||
@ -25,10 +25,10 @@ dependencies:
|
||||
path: ../../../veilid/veilid-flutter
|
||||
|
||||
dependency_overrides:
|
||||
# async_tools:
|
||||
# path: ../../../dart_async_tools
|
||||
bloc_advanced_tools:
|
||||
path: ../../../bloc_advanced_tools
|
||||
async_tools:
|
||||
path: ../../../dart_async_tools
|
||||
bloc_advanced_tools:
|
||||
path: ../../../bloc_advanced_tools
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.10
|
||||
|
@ -60,11 +60,10 @@ packages:
|
||||
async_tools:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: async_tools
|
||||
sha256: "72590010ed6c6f5cbd5d40e33834abc08a43da6a73ac3c3645517d53899b8684"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
path: "../dart_async_tools"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.3"
|
||||
awesome_extensions:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -11,7 +11,7 @@ dependencies:
|
||||
animated_theme_switcher: ^2.0.10
|
||||
ansicolor: ^2.0.2
|
||||
archive: ^3.6.1
|
||||
async_tools: ^0.1.2
|
||||
async_tools: ^0.1.3
|
||||
awesome_extensions: ^2.0.16
|
||||
badges: ^3.1.2
|
||||
basic_utils: ^5.7.0
|
||||
@ -94,8 +94,8 @@ dependencies:
|
||||
zxing2: ^0.2.3
|
||||
|
||||
dependency_overrides:
|
||||
# async_tools:
|
||||
# path: ../dart_async_tools
|
||||
async_tools:
|
||||
path: ../dart_async_tools
|
||||
bloc_advanced_tools:
|
||||
path: ../bloc_advanced_tools
|
||||
# flutter_chat_ui:
|
||||
|
Loading…
Reference in New Issue
Block a user