This commit is contained in:
Christien Rioux 2024-01-26 21:02:11 -05:00
parent b35b618a4d
commit 7cf44ef192
21 changed files with 338 additions and 82 deletions

View File

@ -1,2 +1,2 @@
export 'views/views.dart';
export 'repository/contact_invitation_repository.dart'; export 'repository/contact_invitation_repository.dart';
export 'views/views.dart';

View File

@ -1,2 +1,2 @@
export 'accepted_contact.dart'; export 'accepted_contact.dart';
export 'valid_contact_invitation.dart'; export '../repository/valid_contact_invitation.dart';

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:veilid_support/veilid_support.dart'; import 'package:veilid_support/veilid_support.dart';
@ -27,6 +29,7 @@ class InvitationStatus {
////////////////////////////////////////////////// //////////////////////////////////////////////////
////////////////////////////////////////////////// //////////////////////////////////////////////////
// Mutable state for per-account contact invitations // Mutable state for per-account contact invitations
class ContactInvitationRepository { class ContactInvitationRepository {
@ -37,9 +40,14 @@ class ContactInvitationRepository {
}) : _activeAccountInfo = activeAccountInfo, }) : _activeAccountInfo = activeAccountInfo,
_account = account, _account = account,
_dhtRecord = dhtRecord; _dhtRecord = dhtRecord;
void dispose() {
unawaited(close());
}
static Future<ContactInvitationRepository> open( static Future<ContactInvitationRepository> open(
ActiveAccountInfo activeAccountInfo, proto.Account account) async { ActiveAccountInfo activeAccountInfo, proto.Account account) async {
final accountRecordKey = final accountRecordKey =
activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey; activeAccountInfo.userLogin.accountRecordInfo.accountRecord.recordKey;

View File

@ -136,7 +136,7 @@ class SendInviteDialogState extends State<SendInviteDialog> {
navigator.pop(); navigator.pop();
return; return;
} }
final generator = createContactInvitation( final generator = ContactInvitationRespositoryxxx.createContactInvitation(
activeAccountInfo: activeAccountInfo, activeAccountInfo: activeAccountInfo,
encryptionKeyType: _encryptionKeyType, encryptionKeyType: _encryptionKeyType,
encryptionKey: _encryptionKey, encryptionKey: _encryptionKey,

View File

@ -1 +0,0 @@

View File

@ -1,30 +0,0 @@
import 'package:flutter/material.dart';
class ContactsPage extends StatelessWidget {
const ContactsPage({super.key});
static const path = '/contacts';
@override
Widget build(
BuildContext context,
) =>
const Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Contacts Page'),
// ElevatedButton(
// onPressed: () async {
// ref.watch(authNotifierProvider.notifier).login(
// "myEmail",
// "myPassword",
// );
// },
// child: const Text("Login"),
// ),
],
),
),
);
}

87
lib/layout/home/home.dart Normal file
View File

@ -0,0 +1,87 @@
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:veilid_support/veilid_support.dart';
import '../../account_manager/account_manager.dart';
import '../../theme/theme.dart';
import '../../tools/tools.dart';
import 'home_account_invalid.dart';
import 'home_account_locked.dart';
import 'home_account_missing.dart';
import 'home_account_ready.dart';
import 'home_account_ready/home_account_ready.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
HomePageState createState() => HomePageState();
}
class HomePageState extends State<HomePage> with TickerProviderStateMixin {
final _unfocusNode = FocusNode();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
await changeWindowSetup(
TitleBarStyle.normal, OrientationCapability.normal);
});
}
@override
void dispose() {
_unfocusNode.dispose();
super.dispose();
}
Widget buildWithLogin(BuildContext context, IList<LocalAccount> localAccounts,
Typed<FixedEncodedString43>? activeUserLogin) {
if (activeUserLogin == null) {
// If no logged in user is active, show the loading panel
return waitingPage(context);
}
final accountInfo = AccountRepository.instance
.getAccountInfo(accountMasterRecordKey: activeUserLogin)!;
switch (accountInfo.status) {
case AccountInfoStatus.noAccount:
return const HomeAccountMissing();
case AccountInfoStatus.accountInvalid:
return const HomeAccountInvalid();
case AccountInfoStatus.accountLocked:
return const HomeAccountLocked();
case AccountInfoStatus.accountReady:
return BlocProvider(
create: (context) => AccountRecordCubit(
record: accountInfo.activeAccountInfo!.accountRecord),
child: context.watch<AccountRecordCubit>().state.builder(
(context, account) => HomeAccountReady(
localAccounts: localAccounts,
activeUserLogin: activeUserLogin,
activeAccountInfo: accountInfo.activeAccountInfo!,
account: account)));
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!;
final activeUserLogin = context.watch<ActiveUserLoginCubit>().state;
final localAccounts = context.watch<LocalAccountsCubit>().state;
return SafeArea(
child: GestureDetector(
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode),
child: DecoratedBox(
decoration: BoxDecoration(
color: scale.primaryScale.activeElementBackground),
child:
buildWithLogin(context, localAccounts, activeUserLogin))));
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
class HomeAccountInvalid extends StatefulWidget {
const HomeAccountInvalid({super.key});
@override
HomeAccountInvalidState createState() => HomeAccountInvalidState();
}
class HomeAccountInvalidState extends State<HomeAccountInvalid> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) => const Text('Account invalid');
}
// xxx: delete invalid account
// Future.delayed(0.ms, () async {
// await showErrorModal(context, translate('home.invalid_account_title'),
// translate('home.invalid_account_text'));
// // Delete account
// await AccountRepository.instance.deleteLocalAccount(activeUserLogin);
// // Switch to no active user login
// await AccountRepository.instance.switchToAccount(null);
// });

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
class HomeAccountLocked extends StatefulWidget {
const HomeAccountLocked({super.key});
@override
HomeAccountLockedState createState() => HomeAccountLockedState();
}
class HomeAccountLockedState extends State<HomeAccountLocked> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) => const Text('Account locked');
}

View File

@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
class HomeAccountMissing extends StatefulWidget {
const HomeAccountMissing({super.key});
@override
HomeAccountMissingState createState() => HomeAccountMissingState();
}
class HomeAccountMissingState extends State<HomeAccountMissing> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) => const Text('Account missing');
}
// xxx click to delete missing account or add to postframecallback
// Future.delayed(0.ms, () async {
// await showErrorModal(context, translate('home.missing_account_title'),
// translate('home.missing_account_text'));
// // Delete account
// await AccountRepository.instance.deleteLocalAccount(activeUserLogin);
// // Switch to no active user login
// await AccountRepository.instance.switchToAccount(null);
// });

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../chat/chat.dart'; import '../../../chat/chat.dart';
import '../tools/tools.dart'; import '../../../tools/tools.dart';
class ChatOnlyPage extends StatefulWidget { class ChatOnlyPage extends StatefulWidget {
const ChatOnlyPage({super.key}); const ChatOnlyPage({super.key});

View File

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:awesome_extensions/awesome_extensions.dart'; import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -7,36 +9,61 @@ import 'package:flutter_translate/flutter_translate.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 '../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../chat/chat.dart'; import '../../../contact_invitation/contact_invitation.dart';
import '../theme/theme.dart'; import '../../../proto/proto.dart' as proto;
import '../tools/tools.dart'; import '../../../theme/theme.dart';
import 'main_pager/main_pager.dart'; import '../../../tools/tools.dart';
class HomePage extends StatefulWidget { class HomeAccountReady extends StatefulWidget {
const HomePage({super.key}); const HomeAccountReady(
{required IList<LocalAccount> localAccounts,
required TypedKey activeUserLogin,
required ActiveAccountInfo activeAccountInfo,
required proto.Account account,
super.key})
: _localAccounts = localAccounts,
_activeUserLogin = activeUserLogin,
_activeAccountInfo = activeAccountInfo,
_account = account;
final IList<LocalAccount> _localAccounts;
final TypedKey _activeUserLogin;
final ActiveAccountInfo _activeAccountInfo;
final proto.Account _account;
@override @override
HomePageState createState() => HomePageState(); HomeAccountReadyState createState() => HomeAccountReadyState();
} }
class HomePageState extends State<HomePage> with TickerProviderStateMixin { class HomeAccountReadyState extends State<HomeAccountReady>
final _unfocusNode = FocusNode(); with TickerProviderStateMixin {
//
ContactInvitationRepository? _contactInvitationRepository;
//
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async { // Async initialize repositories for the active user
await changeWindowSetup( // xxx: this should not be necessary
TitleBarStyle.normal, OrientationCapability.normal); // xxx: but RepositoryProvider doesn't call dispose()
Future.delayed(Duration.zero, () async {
//
final cir = await ContactInvitationRepository.open(
widget._activeAccountInfo, widget._account);
setState(() {
_contactInvitationRepository = cir;
});
}); });
} }
@override @override
void dispose() { void dispose() {
_unfocusNode.dispose();
super.dispose(); super.dispose();
_contactInvitationRepository?.dispose();
} }
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
@ -65,6 +92,8 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
final theme = Theme.of(context); final theme = Theme.of(context);
final scale = theme.extension<ScaleScheme>()!; final scale = theme.extension<ScaleScheme>()!;
xxx get rid of the cubit here and
return BlocProvider( return BlocProvider(
create: (context) => AccountRecordCubit(record: accountRecord), create: (context) => AccountRecordCubit(record: accountRecord),
child: Column(children: <Widget>[ child: Column(children: <Widget>[
@ -104,6 +133,8 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
])); ]));
} }
xxx get rid of this whole function
Widget buildUserPanel() => Builder(builder: (context) { Widget buildUserPanel() => Builder(builder: (context) {
final activeUserLogin = context.watch<ActiveUserLoginCubit>().state; final activeUserLogin = context.watch<ActiveUserLoginCubit>().state;
final localAccounts = context.watch<LocalAccountsCubit>().state; final localAccounts = context.watch<LocalAccountsCubit>().state;
@ -190,21 +221,15 @@ class HomePageState extends State<HomePage> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); if (_contactInvitationRepository == null) {
final scale = theme.extension<ScaleScheme>()!; return waitingPage(context);
}
return SafeArea( return responsiveVisibility(
child: GestureDetector( context: context,
onTap: () => FocusScope.of(context).requestFocus(_unfocusNode), phone: false,
child: DecoratedBox( )
decoration: BoxDecoration( ? buildTablet()
color: scale.primaryScale.activeElementBackground), : buildPhone();
child: responsiveVisibility(
context: context,
phone: false,
)
? buildTablet()
: buildPhone(),
)));
} }
} }

View File

@ -5,11 +5,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_translate/flutter_translate.dart'; import 'package:flutter_translate/flutter_translate.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;
import '../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../contact_invitation/contact_invitation.dart'; import '../../../contact_invitation/contact_invitation.dart';
import '../../contacts/contacts.dart'; import '../../../contacts/contacts.dart';
import '../../theme/theme.dart'; import '../../../theme/theme.dart';
class AccountPage extends StatefulWidget { class AccountPage extends StatefulWidget {
const AccountPage({ const AccountPage({

View File

@ -2,9 +2,9 @@ import 'package:awesome_extensions/awesome_extensions.dart';
import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../../proto/proto.dart' as proto; import '../../../../proto/proto.dart' as proto;
import '../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../tools/tools.dart'; import '../../../tools/tools.dart';
class ChatsPage extends StatefulWidget { class ChatsPage extends StatefulWidget {
const ChatsPage({super.key}); const ChatsPage({super.key});

View File

@ -11,14 +11,14 @@ import 'package:stylish_bottom_bar/model/bar_items.dart';
import 'package:stylish_bottom_bar/stylish_bottom_bar.dart'; import 'package:stylish_bottom_bar/stylish_bottom_bar.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;
import '../../../tools/tools.dart'; import '../../../../tools/tools.dart';
import '../../account_manager/account_manager.dart'; import '../../../account_manager/account_manager.dart';
import '../../contact_invitation/contact_invitation.dart'; import '../../../contact_invitation/contact_invitation.dart';
import '../../theme/theme.dart'; import '../../../theme/theme.dart';
import 'account.dart'; import 'account_page.dart';
import 'bottom_sheet_action_button.dart'; import 'bottom_sheet_action_button.dart';
import 'chats.dart'; import 'chats_page.dart';
class MainPager extends StatefulWidget { class MainPager extends StatefulWidget {
const MainPager( const MainPager(

View File

@ -0,0 +1,25 @@
import 'package:flutter/material.dart';
import '../../tools/tools.dart';
class HomeNoActive extends StatefulWidget {
const HomeNoActive({super.key});
@override
HomeNoActiveState createState() => HomeNoActiveState();
}
class HomeNoActiveState extends State<HomeNoActive> {
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) => waitingPage(context);
}

View File

@ -1,4 +1,4 @@
export 'chat_only.dart'; export 'home/home_account_ready/chat_only.dart';
export 'default_app_bar.dart'; export 'default_app_bar.dart';
export 'edit_account.dart'; export 'edit_account.dart';
export 'edit_contact.dart'; export 'edit_contact.dart';

View File

@ -60,11 +60,14 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
parentByChild: IMap(), parentByChild: IMap(),
rootRecords: ISet()), rootRecords: ISet()),
_opened = <TypedKey, DHTRecord>{}, _opened = <TypedKey, DHTRecord>{},
_locks = AsyncTagLock(),
_routingContext = routingContext, _routingContext = routingContext,
_veilid = veilid; _veilid = veilid;
// Persistent DHT record list // Persistent DHT record list
DHTRecordPoolAllocations _state; DHTRecordPoolAllocations _state;
// Lock table to ensure we don't open the same record more than once
final AsyncTagLock<TypedKey> _locks;
// Which DHT records are currently open // Which DHT records are currently open
final Map<TypedKey, DHTRecord> _opened; final Map<TypedKey, DHTRecord> _opened;
// Default routing context to use for new keys // Default routing context to use for new keys
@ -115,6 +118,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
if (rec == null) { if (rec == null) {
throw StateError('record already closed'); throw StateError('record already closed');
} }
_locks.unlockTag(key);
} }
Future<void> deleteDeep(TypedKey parent) async { Future<void> deleteDeep(TypedKey parent) async {
@ -247,6 +251,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
TypedKey? parent, TypedKey? parent,
int defaultSubkey = 0, int defaultSubkey = 0,
DHTRecordCrypto? crypto}) async { DHTRecordCrypto? crypto}) async {
await _locks.lockTag(recordKey);
final dhtctx = routingContext ?? _routingContext; final dhtctx = routingContext ?? _routingContext;
late final DHTRecord rec; late final DHTRecord rec;
@ -278,6 +284,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
int defaultSubkey = 0, int defaultSubkey = 0,
DHTRecordCrypto? crypto, DHTRecordCrypto? crypto,
}) async { }) async {
await _locks.lockTag(recordKey);
final dhtctx = routingContext ?? _routingContext; final dhtctx = routingContext ?? _routingContext;
late final DHTRecord rec; late final DHTRecord rec;
@ -325,7 +333,7 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
crypto: crypto, crypto: crypto,
); );
/// Look up an opened DHRRecord /// Look up an opened DHTRecord
DHTRecord? getOpenedRecord(TypedKey recordKey) => _opened[recordKey]; DHTRecord? getOpenedRecord(TypedKey recordKey) => _opened[recordKey];
/// Get the parent of a DHTRecord key if it exists /// Get the parent of a DHTRecord key if it exists

View File

@ -0,0 +1,45 @@
import 'package:mutex/mutex.dart';
class _AsyncTagLockEntry {
_AsyncTagLockEntry()
: mutex = Mutex(),
waitingCount = 1;
//
Mutex mutex;
int waitingCount;
}
class AsyncTagLock<T> {
AsyncTagLock()
: _tableLock = Mutex(),
_locks = {};
Future<void> lockTag(T tag) async {
await _tableLock.protect(() async {
var lockEntry = _locks[tag];
if (lockEntry != null) {
lockEntry.waitingCount++;
} else {
lockEntry = _locks[tag] = _AsyncTagLockEntry();
}
await lockEntry.mutex.acquire();
lockEntry.waitingCount--;
});
}
void unlockTag(T tag) {
final lockEntry = _locks[tag]!;
if (lockEntry.waitingCount == 0) {
// If nobody is waiting for the mutex we can just drop it
_locks.remove(tag);
} else {
// Someone's waiting for the tag lock so release the mutex for it
lockEntry.mutex.release();
}
}
//
final Mutex _tableLock;
final Map<T, _AsyncTagLockEntry> _locks;
}

View File

@ -6,6 +6,7 @@ library veilid_support;
export 'package:veilid/veilid.dart'; export 'package:veilid/veilid.dart';
export 'dht_support/dht_support.dart'; export 'dht_support/dht_support.dart';
export 'src/async_tag_lock.dart';
export 'src/async_value.dart'; export 'src/async_value.dart';
export 'src/config.dart'; export 'src/config.dart';
export 'src/identity.dart'; export 'src/identity.dart';