mirror of
https://gitlab.com/veilid/veilidchat.git
synced 2025-05-31 03:54:30 -04:00
xfer
This commit is contained in:
parent
08dab73757
commit
c08878b35a
39 changed files with 771 additions and 218 deletions
|
@ -25,6 +25,7 @@ class VeilidChatApp extends ConsumerWidget {
|
|||
return LocalizationProvider(
|
||||
state: LocalizationProvider.of(context).state,
|
||||
child: MaterialApp.router(
|
||||
debugShowCheckedModeBanner: false,
|
||||
routerConfig: router,
|
||||
title: 'VeilidChat',
|
||||
theme: theme,
|
||||
|
|
63
lib/components/account_bubble.dart
Normal file
63
lib/components/account_bubble.dart
Normal file
|
@ -0,0 +1,63 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:user_profile_avatar/user_profile_avatar.dart';
|
||||
|
||||
import '../providers/local_accounts.dart';
|
||||
import '../providers/logins.dart';
|
||||
|
||||
class AccountBubble extends ConsumerWidget {
|
||||
const AccountBubble({super.key});
|
||||
|
||||
void _onReorder(WidgetRef ref, int oldIndex, int newIndex) {
|
||||
final accounts = ref.read(localAccountsProvider.notifier);
|
||||
accounts.reorderAccount(oldIndex, newIndex);
|
||||
// xxx fix this so we can await this properly, use FutureBuilder or whatever
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
final accounts = ref.watch(localAccountsProvider);
|
||||
final logins = ref.watch(loginsProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: const Text('Accounts'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: 'Accessibility',
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content:
|
||||
Text('Accessibility and language options coming soon')));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(),
|
||||
accounts.when(
|
||||
error: (obj, err) => Text("error loading accounts: $err"),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
data: (accountList) => ReorderableGridView.extent(
|
||||
maxCrossAxisExtent: 128,
|
||||
onReorder: (oldIndex, newIndex) =>
|
||||
_onReorder(ref, oldIndex, newIndex),
|
||||
children: accountList.map((account) {
|
||||
return AccountBubble(account);
|
||||
}),
|
||||
)),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -12,9 +12,9 @@ part 'user_login.g.dart';
|
|||
class UserLogin with _$UserLogin {
|
||||
const factory UserLogin({
|
||||
// Master record key for the user used to index the local accounts table
|
||||
required TypedKey accountMasterKey,
|
||||
required TypedKey accountMasterRecordKey,
|
||||
// The identity secret as unlocked from the local accounts table
|
||||
required TypedSecret secretKey,
|
||||
required TypedSecret identitySecret,
|
||||
// The time this login was most recently used
|
||||
required Timestamp lastActive,
|
||||
}) = _UserLogin;
|
||||
|
|
|
@ -21,9 +21,9 @@ UserLogin _$UserLoginFromJson(Map<String, dynamic> json) {
|
|||
/// @nodoc
|
||||
mixin _$UserLogin {
|
||||
// Master record key for the user used to index the local accounts table
|
||||
Typed<FixedEncodedString43> get accountMasterKey =>
|
||||
Typed<FixedEncodedString43> get accountMasterRecordKey =>
|
||||
throw _privateConstructorUsedError; // The identity secret as unlocked from the local accounts table
|
||||
Typed<FixedEncodedString43> get secretKey =>
|
||||
Typed<FixedEncodedString43> get identitySecret =>
|
||||
throw _privateConstructorUsedError; // The time this login was most recently used
|
||||
Timestamp get lastActive => throw _privateConstructorUsedError;
|
||||
|
||||
|
@ -39,8 +39,8 @@ abstract class $UserLoginCopyWith<$Res> {
|
|||
_$UserLoginCopyWithImpl<$Res, UserLogin>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{Typed<FixedEncodedString43> accountMasterKey,
|
||||
Typed<FixedEncodedString43> secretKey,
|
||||
{Typed<FixedEncodedString43> accountMasterRecordKey,
|
||||
Typed<FixedEncodedString43> identitySecret,
|
||||
Timestamp lastActive});
|
||||
}
|
||||
|
||||
|
@ -57,18 +57,18 @@ class _$UserLoginCopyWithImpl<$Res, $Val extends UserLogin>
|
|||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? accountMasterKey = null,
|
||||
Object? secretKey = null,
|
||||
Object? accountMasterRecordKey = null,
|
||||
Object? identitySecret = null,
|
||||
Object? lastActive = null,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
accountMasterKey: null == accountMasterKey
|
||||
? _value.accountMasterKey
|
||||
: accountMasterKey // ignore: cast_nullable_to_non_nullable
|
||||
accountMasterRecordKey: null == accountMasterRecordKey
|
||||
? _value.accountMasterRecordKey
|
||||
: accountMasterRecordKey // ignore: cast_nullable_to_non_nullable
|
||||
as Typed<FixedEncodedString43>,
|
||||
secretKey: null == secretKey
|
||||
? _value.secretKey
|
||||
: secretKey // ignore: cast_nullable_to_non_nullable
|
||||
identitySecret: null == identitySecret
|
||||
? _value.identitySecret
|
||||
: identitySecret // ignore: cast_nullable_to_non_nullable
|
||||
as Typed<FixedEncodedString43>,
|
||||
lastActive: null == lastActive
|
||||
? _value.lastActive
|
||||
|
@ -86,8 +86,8 @@ abstract class _$$_UserLoginCopyWith<$Res> implements $UserLoginCopyWith<$Res> {
|
|||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{Typed<FixedEncodedString43> accountMasterKey,
|
||||
Typed<FixedEncodedString43> secretKey,
|
||||
{Typed<FixedEncodedString43> accountMasterRecordKey,
|
||||
Typed<FixedEncodedString43> identitySecret,
|
||||
Timestamp lastActive});
|
||||
}
|
||||
|
||||
|
@ -102,18 +102,18 @@ class __$$_UserLoginCopyWithImpl<$Res>
|
|||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? accountMasterKey = null,
|
||||
Object? secretKey = null,
|
||||
Object? accountMasterRecordKey = null,
|
||||
Object? identitySecret = null,
|
||||
Object? lastActive = null,
|
||||
}) {
|
||||
return _then(_$_UserLogin(
|
||||
accountMasterKey: null == accountMasterKey
|
||||
? _value.accountMasterKey
|
||||
: accountMasterKey // ignore: cast_nullable_to_non_nullable
|
||||
accountMasterRecordKey: null == accountMasterRecordKey
|
||||
? _value.accountMasterRecordKey
|
||||
: accountMasterRecordKey // ignore: cast_nullable_to_non_nullable
|
||||
as Typed<FixedEncodedString43>,
|
||||
secretKey: null == secretKey
|
||||
? _value.secretKey
|
||||
: secretKey // ignore: cast_nullable_to_non_nullable
|
||||
identitySecret: null == identitySecret
|
||||
? _value.identitySecret
|
||||
: identitySecret // ignore: cast_nullable_to_non_nullable
|
||||
as Typed<FixedEncodedString43>,
|
||||
lastActive: null == lastActive
|
||||
? _value.lastActive
|
||||
|
@ -127,8 +127,8 @@ class __$$_UserLoginCopyWithImpl<$Res>
|
|||
@JsonSerializable()
|
||||
class _$_UserLogin implements _UserLogin {
|
||||
const _$_UserLogin(
|
||||
{required this.accountMasterKey,
|
||||
required this.secretKey,
|
||||
{required this.accountMasterRecordKey,
|
||||
required this.identitySecret,
|
||||
required this.lastActive});
|
||||
|
||||
factory _$_UserLogin.fromJson(Map<String, dynamic> json) =>
|
||||
|
@ -136,17 +136,17 @@ class _$_UserLogin implements _UserLogin {
|
|||
|
||||
// Master record key for the user used to index the local accounts table
|
||||
@override
|
||||
final Typed<FixedEncodedString43> accountMasterKey;
|
||||
final Typed<FixedEncodedString43> accountMasterRecordKey;
|
||||
// The identity secret as unlocked from the local accounts table
|
||||
@override
|
||||
final Typed<FixedEncodedString43> secretKey;
|
||||
final Typed<FixedEncodedString43> identitySecret;
|
||||
// The time this login was most recently used
|
||||
@override
|
||||
final Timestamp lastActive;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserLogin(accountMasterKey: $accountMasterKey, secretKey: $secretKey, lastActive: $lastActive)';
|
||||
return 'UserLogin(accountMasterRecordKey: $accountMasterRecordKey, identitySecret: $identitySecret, lastActive: $lastActive)';
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -154,18 +154,18 @@ class _$_UserLogin implements _UserLogin {
|
|||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$_UserLogin &&
|
||||
(identical(other.accountMasterKey, accountMasterKey) ||
|
||||
other.accountMasterKey == accountMasterKey) &&
|
||||
(identical(other.secretKey, secretKey) ||
|
||||
other.secretKey == secretKey) &&
|
||||
(identical(other.accountMasterRecordKey, accountMasterRecordKey) ||
|
||||
other.accountMasterRecordKey == accountMasterRecordKey) &&
|
||||
(identical(other.identitySecret, identitySecret) ||
|
||||
other.identitySecret == identitySecret) &&
|
||||
(identical(other.lastActive, lastActive) ||
|
||||
other.lastActive == lastActive));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, accountMasterKey, secretKey, lastActive);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType, accountMasterRecordKey, identitySecret, lastActive);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
|
@ -183,17 +183,17 @@ class _$_UserLogin implements _UserLogin {
|
|||
|
||||
abstract class _UserLogin implements UserLogin {
|
||||
const factory _UserLogin(
|
||||
{required final Typed<FixedEncodedString43> accountMasterKey,
|
||||
required final Typed<FixedEncodedString43> secretKey,
|
||||
{required final Typed<FixedEncodedString43> accountMasterRecordKey,
|
||||
required final Typed<FixedEncodedString43> identitySecret,
|
||||
required final Timestamp lastActive}) = _$_UserLogin;
|
||||
|
||||
factory _UserLogin.fromJson(Map<String, dynamic> json) =
|
||||
_$_UserLogin.fromJson;
|
||||
|
||||
@override // Master record key for the user used to index the local accounts table
|
||||
Typed<FixedEncodedString43> get accountMasterKey;
|
||||
Typed<FixedEncodedString43> get accountMasterRecordKey;
|
||||
@override // The identity secret as unlocked from the local accounts table
|
||||
Typed<FixedEncodedString43> get secretKey;
|
||||
Typed<FixedEncodedString43> get identitySecret;
|
||||
@override // The time this login was most recently used
|
||||
Timestamp get lastActive;
|
||||
@override
|
||||
|
|
|
@ -7,16 +7,17 @@ part of 'user_login.dart';
|
|||
// **************************************************************************
|
||||
|
||||
_$_UserLogin _$$_UserLoginFromJson(Map<String, dynamic> json) => _$_UserLogin(
|
||||
accountMasterKey:
|
||||
Typed<FixedEncodedString43>.fromJson(json['account_master_key']),
|
||||
secretKey: Typed<FixedEncodedString43>.fromJson(json['secret_key']),
|
||||
accountMasterRecordKey: Typed<FixedEncodedString43>.fromJson(
|
||||
json['account_master_record_key']),
|
||||
identitySecret:
|
||||
Typed<FixedEncodedString43>.fromJson(json['identity_secret']),
|
||||
lastActive: Timestamp.fromJson(json['last_active']),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$$_UserLoginToJson(_$_UserLogin instance) =>
|
||||
<String, dynamic>{
|
||||
'account_master_key': instance.accountMasterKey.toJson(),
|
||||
'secret_key': instance.secretKey.toJson(),
|
||||
'account_master_record_key': instance.accountMasterRecordKey.toJson(),
|
||||
'identity_secret': instance.identitySecret.toJson(),
|
||||
'last_active': instance.lastActive.toJson(),
|
||||
};
|
||||
|
||||
|
|
|
@ -3,12 +3,14 @@ import 'dart:async';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
import 'log/log.dart';
|
||||
import 'veilid_support/veilid_support.dart';
|
||||
import 'theming/theming.dart';
|
||||
import 'app.dart';
|
||||
import 'dart:io';
|
||||
import 'package:flutter_translate/flutter_translate.dart';
|
||||
|
||||
void main() async {
|
||||
// Disable all debugprints in release mode
|
||||
|
@ -29,13 +31,30 @@ void main() async {
|
|||
final themeService = await ThemeService.instance;
|
||||
var initTheme = themeService.initial;
|
||||
|
||||
// Start up Veilid and Veilid processor in the background
|
||||
unawaited(initializeVeilid());
|
||||
// Manage window on desktop platforms
|
||||
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
|
||||
await windowManager.ensureInitialized();
|
||||
|
||||
const windowOptions = WindowOptions(
|
||||
size: Size(768, 1024),
|
||||
center: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
skipTaskbar: false,
|
||||
titleBarStyle: TitleBarStyle.hidden,
|
||||
);
|
||||
windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
await windowManager.show();
|
||||
await windowManager.focus();
|
||||
});
|
||||
}
|
||||
|
||||
// Make localization delegate
|
||||
var delegate = await LocalizationDelegate.create(
|
||||
fallbackLocale: 'en_US', supportedLocales: ['en_US']);
|
||||
|
||||
// Start up Veilid and Veilid processor in the background
|
||||
unawaited(initializeVeilid());
|
||||
|
||||
// Run the app
|
||||
// Hot reloads will only restart this part, not Veilid
|
||||
runApp(ProviderScope(
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:radix_colors/radix_colors.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class IndexPage extends StatelessWidget {
|
||||
const IndexPage({super.key});
|
||||
|
@ -6,8 +9,34 @@ class IndexPage extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(child: Text("Index Page")),
|
||||
);
|
||||
windowManager.setTitleBarStyle(TitleBarStyle.hidden);
|
||||
return Scaffold(
|
||||
body: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
RadixColors.dark.plum.step4,
|
||||
RadixColors.dark.plum.step2,
|
||||
])),
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: SvgPicture.asset(
|
||||
"assets/images/icon.svg",
|
||||
)),
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: SvgPicture.asset(
|
||||
"assets/images/title.svg",
|
||||
))
|
||||
]))),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,61 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../state/active_logins_state.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'package:reorderable_grid/reorderable_grid.dart';
|
||||
|
||||
import '../providers/local_accounts.dart';
|
||||
import '../providers/logins.dart';
|
||||
|
||||
class LoginPage extends ConsumerWidget {
|
||||
const LoginPage({super.key});
|
||||
static const path = '/login';
|
||||
|
||||
void _onReorder(WidgetRef ref, int oldIndex, int newIndex) {
|
||||
final accounts = ref.read(localAccountsProvider.notifier);
|
||||
accounts.reorderAccount(oldIndex, newIndex);
|
||||
// xxx fix this so we can await this properly, use FutureBuilder or whatever
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
windowManager.setTitleBarStyle(TitleBarStyle.normal);
|
||||
final accounts = ref.watch(localAccountsProvider);
|
||||
final logins = ref.watch(loginsProvider);
|
||||
|
||||
return Scaffold(
|
||||
appBar: null,
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
title: const Text('Accounts'),
|
||||
actions: <Widget>[
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
tooltip: 'Accessibility',
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content:
|
||||
Text('Accessibility and language options coming soon')));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text("Login Page"),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
await ref.watch(authNotifierProvider.notifier).login(
|
||||
"myEmail",
|
||||
"myPassword",
|
||||
);
|
||||
},
|
||||
child: const Text("Login"),
|
||||
),
|
||||
const Spacer(),
|
||||
accounts.when(
|
||||
error: (obj, err) => Text("error loading accounts: $err"),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
data: (accountList) => ReorderableGridView.extent(
|
||||
maxCrossAxisExtent: 128,
|
||||
onReorder: (oldIndex, newIndex) =>
|
||||
_onReorder(ref, oldIndex, newIndex),
|
||||
children: accountList.map((account) {
|
||||
return AccountBubble(account);
|
||||
}),
|
||||
)),
|
||||
const Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -10,6 +10,7 @@ import '../tools/tools.dart';
|
|||
import '../veilid_support/veilid_support.dart';
|
||||
import '../entities/entities.dart';
|
||||
import '../entities/proto.dart' as proto;
|
||||
import 'logins.dart';
|
||||
|
||||
part 'local_accounts.g.dart';
|
||||
|
||||
|
@ -38,58 +39,15 @@ class LocalAccounts extends _$LocalAccounts
|
|||
//////////////////////////////////////////////////////////////
|
||||
/// Mutators and Selectors
|
||||
|
||||
/// Creates a new master identity and returns it with its secrets
|
||||
Future<IdentityMasterWithSecrets> newIdentityMaster() async {
|
||||
final crypto = await Veilid.instance.bestCryptoSystem();
|
||||
final dhtctx = (await Veilid.instance.routingContext())
|
||||
.withPrivacy()
|
||||
.withSequencing(Sequencing.ensureOrdered);
|
||||
|
||||
// IdentityMaster DHT record is public/unencrypted
|
||||
return (await DHTRecord.create(dhtctx,
|
||||
crypto: const DHTRecordCryptoPublic()))
|
||||
.deleteScope((masterRec) async {
|
||||
// Identity record is private
|
||||
return (await DHTRecord.create(dhtctx)).deleteScope((identityRec) async {
|
||||
// Make IdentityMaster
|
||||
final masterRecordKey = masterRec.key();
|
||||
final masterOwner = masterRec.ownerKeyPair()!;
|
||||
final masterSigBuf = masterRecordKey.decode()
|
||||
..addAll(masterOwner.key.decode());
|
||||
|
||||
final identityRecordKey = identityRec.key();
|
||||
final identityOwner = identityRec.ownerKeyPair()!;
|
||||
final identitySigBuf = identityRecordKey.decode()
|
||||
..addAll(identityOwner.key.decode());
|
||||
|
||||
final identitySignature =
|
||||
await crypto.signWithKeyPair(masterOwner, identitySigBuf);
|
||||
final masterSignature =
|
||||
await crypto.signWithKeyPair(identityOwner, masterSigBuf);
|
||||
|
||||
final identityMaster = IdentityMaster(
|
||||
identityRecordKey: identityRecordKey,
|
||||
identityPublicKey: identityOwner.key,
|
||||
masterRecordKey: masterRecordKey,
|
||||
masterPublicKey: masterOwner.key,
|
||||
identitySignature: identitySignature,
|
||||
masterSignature: masterSignature);
|
||||
|
||||
// Write identity master to master dht key
|
||||
await masterRec.eventualWriteJson(identityMaster);
|
||||
|
||||
// Make empty identity
|
||||
const identity = Identity(accountRecords: IMapConst({}));
|
||||
|
||||
// Write empty identity to identity dht key
|
||||
await identityRec.eventualWriteJson(identity);
|
||||
|
||||
return IdentityMasterWithSecrets(
|
||||
identityMaster: identityMaster,
|
||||
masterSecret: masterOwner.secret,
|
||||
identitySecret: identityOwner.secret);
|
||||
});
|
||||
});
|
||||
/// Reorder accounts
|
||||
Future<void> reorderAccount(int oldIndex, int newIndex) async {
|
||||
final localAccounts = state.requireValue;
|
||||
var removedItem = Output<LocalAccount>();
|
||||
final updated = localAccounts
|
||||
.removeAt(oldIndex, removedItem)
|
||||
.insert(newIndex, removedItem.value!);
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
}
|
||||
|
||||
/// Creates a new account associated with master identity
|
||||
|
@ -99,6 +57,7 @@ class LocalAccounts extends _$LocalAccounts
|
|||
EncryptionKeyType encryptionKeyType,
|
||||
String encryptionKey,
|
||||
proto.Account account) async {
|
||||
final veilid = await eventualVeilid.future;
|
||||
final localAccounts = state.requireValue;
|
||||
|
||||
// Encrypt identitySecret with key
|
||||
|
@ -111,8 +70,8 @@ class LocalAccounts extends _$LocalAccounts
|
|||
identitySecretSaltBytes = Uint8List(0);
|
||||
case EncryptionKeyType.pin:
|
||||
case EncryptionKeyType.password:
|
||||
final cs = await Veilid.instance
|
||||
.getCryptoSystem(identityMaster.identityRecordKey.kind);
|
||||
final cs =
|
||||
await veilid.getCryptoSystem(identityMaster.identityRecordKey.kind);
|
||||
final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey));
|
||||
final nonce = await cs.randomNonce();
|
||||
identitySecretSaltBytes = nonce.decode();
|
||||
|
@ -135,7 +94,7 @@ class LocalAccounts extends _$LocalAccounts
|
|||
/////// Add account with profile to DHT
|
||||
|
||||
// Create private routing context
|
||||
final dhtctx = (await Veilid.instance.routingContext())
|
||||
final dhtctx = (await veilid.routingContext())
|
||||
.withPrivacy()
|
||||
.withSequencing(Sequencing.ensureOrdered);
|
||||
|
||||
|
@ -171,6 +130,22 @@ class LocalAccounts extends _$LocalAccounts
|
|||
return localAccount;
|
||||
}
|
||||
|
||||
/// Remove an account and wipe the messages for this account from this device
|
||||
Future<bool> deleteAccount(TypedKey accountMasterRecordKey) async {
|
||||
final logins = ref.read(loginsProvider.notifier);
|
||||
await logins.logout(accountMasterRecordKey);
|
||||
|
||||
final localAccounts = state.requireValue;
|
||||
final updated = localAccounts.removeWhere(
|
||||
(la) => la.identityMaster.masterRecordKey == accountMasterRecordKey);
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
|
||||
// xxx todo: wipe messages
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Import an account from another VeilidChat instance
|
||||
|
||||
/// Recover an account with the master identity secret
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'local_accounts.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$localAccountsHash() => r'41f70b78e71c8b3faa2cd1f2a96084fbb373324c';
|
||||
String _$localAccountsHash() => r'694236fa91156c00b3f7d6985fbc55b8871646ab';
|
||||
|
||||
/// See also [LocalAccounts].
|
||||
@ProviderFor(LocalAccounts)
|
||||
|
|
|
@ -4,10 +4,10 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:veilid/veilid.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:veilidchat/providers/repositories.dart';
|
||||
|
||||
import '../veilid_support/veilid_support.dart';
|
||||
import '../entities/entities.dart';
|
||||
import 'local_accounts.dart';
|
||||
|
||||
part 'logins.g.dart';
|
||||
|
||||
|
@ -34,19 +34,20 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||
//////////////////////////////////////////////////////////////
|
||||
/// Mutators and Selectors
|
||||
|
||||
Future<void> setActiveUserLogin(TypedKey accountMasterKey) async {
|
||||
Future<void> switchToAccount(TypedKey? accountMasterRecordKey) async {
|
||||
final current = state.requireValue;
|
||||
for (final userLogin in current.userLogins) {
|
||||
if (userLogin.accountMasterRecordKey == accountMasterKey) {
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(activeUserLogin: accountMasterKey));
|
||||
return;
|
||||
}
|
||||
if (accountMasterRecordKey != null) {
|
||||
// Assert the specified record key can be found, will throw if not
|
||||
final _ = current.userLogins.firstWhere(
|
||||
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey);
|
||||
}
|
||||
throw Exception("User not found");
|
||||
final updated = current.copyWith(activeUserLogin: accountMasterRecordKey);
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
}
|
||||
|
||||
Future<bool> loginWithNone(TypedKey accountMasterRecordKey) async {
|
||||
final veilid = await eventualVeilid.future;
|
||||
final localAccounts = ref.read(localAccountsProvider).requireValue;
|
||||
|
||||
// Get account, throws if not found
|
||||
|
@ -64,7 +65,7 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||
SecretKey.fromBytes(localAccount.identitySecretKeyBytes);
|
||||
|
||||
// Validate this secret with the identity public key
|
||||
final cs = await Veilid.instance
|
||||
final cs = await veilid
|
||||
.getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind);
|
||||
final keyOk = await cs.validateKeyPair(
|
||||
localAccount.identityMaster.identityPublicKey, identitySecret);
|
||||
|
@ -74,15 +75,15 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||
|
||||
// Add to user logins and select it
|
||||
final current = state.requireValue;
|
||||
final now = Veilid.instance.now();
|
||||
final now = veilid.now();
|
||||
final updated = current.copyWith(
|
||||
userLogins: current.userLogins.replaceFirstWhere(
|
||||
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey,
|
||||
(ul) => ul != null
|
||||
? ul.copyWith(lastActive: now)
|
||||
: UserLogin(
|
||||
accountMasterKey: accountMasterRecordKey,
|
||||
secretKey:
|
||||
accountMasterRecordKey: accountMasterRecordKey,
|
||||
identitySecret:
|
||||
TypedSecret(kind: cs.kind(), value: identitySecret),
|
||||
lastActive: now),
|
||||
addIfNotFound: true),
|
||||
|
@ -95,6 +96,7 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||
|
||||
Future<bool> loginWithPasswordOrPin(
|
||||
TypedKey accountMasterRecordKey, String encryptionKey) async {
|
||||
final veilid = await eventualVeilid.future;
|
||||
final localAccounts = ref.read(localAccountsProvider).requireValue;
|
||||
|
||||
// Get account, throws if not found
|
||||
|
@ -108,7 +110,7 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||
localAccount.encryptionKeyType != EncryptionKeyType.pin) {
|
||||
throw Exception("Wrong authentication type");
|
||||
}
|
||||
final cs = await Veilid.instance
|
||||
final cs = await veilid
|
||||
.getCryptoSystem(localAccount.identityMaster.identityRecordKey.kind);
|
||||
final ekbytes = Uint8List.fromList(utf8.encode(encryptionKey));
|
||||
final eksalt = localAccount.identitySecretSaltBytes;
|
||||
|
@ -126,15 +128,15 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||
|
||||
// Add to user logins and select it
|
||||
final current = state.requireValue;
|
||||
final now = Veilid.instance.now();
|
||||
final now = veilid.now();
|
||||
final updated = current.copyWith(
|
||||
userLogins: current.userLogins.replaceFirstWhere(
|
||||
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey,
|
||||
(ul) => ul != null
|
||||
? ul.copyWith(lastActive: now)
|
||||
: UserLogin(
|
||||
accountMasterKey: accountMasterRecordKey,
|
||||
secretKey:
|
||||
accountMasterRecordKey: accountMasterRecordKey,
|
||||
identitySecret:
|
||||
TypedSecret(kind: cs.kind(), value: identitySecret),
|
||||
lastActive: now),
|
||||
addIfNotFound: true),
|
||||
|
@ -145,25 +147,18 @@ class Logins extends _$Logins with AsyncTableDBBacked<ActiveLogins> {
|
|||
return true;
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
Future<void> logout(TypedKey? accountMasterRecordKey) async {
|
||||
final current = state.requireValue;
|
||||
if (current.activeUserLogin == null) {
|
||||
final logoutUser = accountMasterRecordKey ?? current.activeUserLogin;
|
||||
if (logoutUser == null) {
|
||||
return;
|
||||
}
|
||||
final updated = current.copyWith(
|
||||
activeUserLogin: null,
|
||||
userLogins: current.userLogins.removeWhere(
|
||||
(ul) => ul.accountMasterRecordKey == current.activeUserLogin));
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
}
|
||||
|
||||
Future<void> switchToAccount(TypedKey accountMasterRecordKey) async {
|
||||
final current = state.requireValue;
|
||||
final userLogin = current.userLogins.firstWhere(
|
||||
(ul) => ul.accountMasterRecordKey == accountMasterRecordKey);
|
||||
final updated =
|
||||
current.copyWith(activeUserLogin: userLogin.accountMasterRecordKey);
|
||||
activeUserLogin: current.activeUserLogin == logoutUser
|
||||
? null
|
||||
: current.activeUserLogin,
|
||||
userLogins: current.userLogins
|
||||
.removeWhere((ul) => ul.accountMasterRecordKey == logoutUser));
|
||||
await store(updated);
|
||||
state = AsyncValue.data(updated);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ part of 'logins.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$loginsHash() => r'379514b8b6623d59cb89b4f128be5953d5ea62a6';
|
||||
String _$loginsHash() => r'cf7f1a2343340a4dc4ebfa4009e846d0a39c0167';
|
||||
|
||||
/// See also [Logins].
|
||||
@ProviderFor(Logins)
|
||||
|
|
|
@ -3,56 +3,42 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../pages/pages.dart';
|
||||
import '../state/active_logins_state.dart';
|
||||
import '../providers/logins.dart';
|
||||
|
||||
/// This notifier is meant to implement the [Listenable] our [GoRouter] needs.
|
||||
///
|
||||
/// We aim to trigger redirects whenever's needed.
|
||||
/// This is done by calling our (only) listener everytime we want to notify stuff.
|
||||
/// This allows to centralize global redirecting logic in this class.
|
||||
/// In this simple case, we just listen to auth changes.
|
||||
///
|
||||
/// SIDE NOTE.
|
||||
/// This might look overcomplicated at a first glance;
|
||||
/// Instead, this method aims to follow some good some good practices:
|
||||
/// 1. It doesn't require us to pipe down any `ref` parameter
|
||||
/// 2. It works as a complete replacement for [ChangeNotifier] (it's a [Listenable] implementation)
|
||||
/// 3. It allows for listening to multiple providers if needed (we do have a [Ref] now!)
|
||||
class RouterNotifier extends AutoDisposeAsyncNotifier<void>
|
||||
implements Listenable {
|
||||
/// GoRouter listener
|
||||
VoidCallback? routerListener;
|
||||
bool isAuth = false; // Useful for our global redirect function
|
||||
|
||||
/// Router state for redirect
|
||||
bool hasActiveUserLogin = false;
|
||||
|
||||
/// AsyncNotifier build
|
||||
@override
|
||||
Future<void> build() async {
|
||||
// One could watch more providers and write logic accordingly
|
||||
|
||||
isAuth = await ref.watch(
|
||||
authNotifierProvider.selectAsync((data) => data != null),
|
||||
hasActiveUserLogin = await ref.watch(
|
||||
loginsProvider.selectAsync((data) => data.activeUserLogin != null),
|
||||
);
|
||||
|
||||
// When this notifier's state changes, inform GoRouter
|
||||
ref.listenSelf((_, __) {
|
||||
// One could write more conditional logic for when to call redirection
|
||||
if (state.isLoading) return;
|
||||
routerListener?.call();
|
||||
});
|
||||
}
|
||||
|
||||
/// Redirects the user when our authentication changes
|
||||
/// Redirects when our state changes
|
||||
String? redirect(BuildContext context, GoRouterState state) {
|
||||
if (this.state.isLoading || this.state.hasError) return null;
|
||||
|
||||
final isIndex = state.location == IndexPage.path;
|
||||
if (isIndex) {
|
||||
return isAuth ? HomePage.path : LoginPage.path;
|
||||
switch (state.location) {
|
||||
case IndexPage.path:
|
||||
return hasActiveUserLogin ? HomePage.path : LoginPage.path;
|
||||
case LoginPage.path:
|
||||
return hasActiveUserLogin ? HomePage.path : null;
|
||||
default:
|
||||
return hasActiveUserLogin ? null : LoginPage.path;
|
||||
}
|
||||
|
||||
final isLoggingIn = state.location == LoginPage.path;
|
||||
if (isLoggingIn) {
|
||||
return isAuth ? HomePage.path : null;
|
||||
}
|
||||
|
||||
return isAuth ? null : IndexPage.path;
|
||||
}
|
||||
|
||||
/// Our application routes
|
||||
|
@ -103,6 +89,9 @@ class RouterNotifier extends AutoDisposeAsyncNotifier<void>
|
|||
),
|
||||
];
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
/// Listenable
|
||||
|
||||
/// Adds [GoRouter]'s listener as specified by its [Listenable].
|
||||
/// [GoRouteInformationProvider] uses this method on creation to handle its
|
||||
/// internal [ChangeNotifier].
|
||||
|
|
|
@ -24,7 +24,8 @@ class ThemeService {
|
|||
String? themeName = prefs.getString('previousThemeName');
|
||||
if (themeName == null) {
|
||||
final isPlatformDark =
|
||||
WidgetsBinding.instance.window.platformBrightness == Brightness.dark;
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
|
||||
Brightness.dark;
|
||||
themeName = isPlatformDark ? 'light' : 'dark';
|
||||
}
|
||||
return themeName;
|
||||
|
@ -34,7 +35,8 @@ class ThemeService {
|
|||
String? themeName = prefs.getString('theme');
|
||||
if (themeName == null) {
|
||||
final isPlatformDark =
|
||||
WidgetsBinding.instance.window.platformBrightness == Brightness.dark;
|
||||
WidgetsBinding.instance.platformDispatcher.platformBrightness ==
|
||||
Brightness.dark;
|
||||
themeName = isPlatformDark ? 'dark' : 'light';
|
||||
}
|
||||
return allThemes[themeName];
|
||||
|
|
|
@ -3,6 +3,8 @@ import 'dart:async';
|
|||
import 'package:veilid/veilid.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'veilid_init.dart';
|
||||
|
||||
abstract class DHTRecordCrypto {
|
||||
FutureOr<Uint8List> encrypt(Uint8List data, int subkey);
|
||||
FutureOr<Uint8List> decrypt(Uint8List data, int subkey);
|
||||
|
@ -21,15 +23,16 @@ class DHTRecordCryptoPrivate implements DHTRecordCrypto {
|
|||
|
||||
static Future<DHTRecordCryptoPrivate> fromTypedKeyPair(
|
||||
TypedKeyPair typedKeyPair) async {
|
||||
final cryptoSystem =
|
||||
await Veilid.instance.getCryptoSystem(typedKeyPair.kind);
|
||||
final veilid = await eventualVeilid.future;
|
||||
final cryptoSystem = await veilid.getCryptoSystem(typedKeyPair.kind);
|
||||
final secretKey = typedKeyPair.secret;
|
||||
return DHTRecordCryptoPrivate._(cryptoSystem, secretKey);
|
||||
}
|
||||
|
||||
static Future<DHTRecordCryptoPrivate> fromSecret(
|
||||
CryptoKind kind, SharedSecret secretKey) async {
|
||||
final cryptoSystem = await Veilid.instance.getCryptoSystem(kind);
|
||||
final veilid = await eventualVeilid.future;
|
||||
final cryptoSystem = await veilid.getCryptoSystem(kind);
|
||||
return DHTRecordCryptoPrivate._(cryptoSystem, secretKey);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,9 @@ import 'veilid_support.dart';
|
|||
|
||||
/// Creates a new master identity and returns it with its secrets
|
||||
Future<IdentityMasterWithSecrets> newIdentityMaster() async {
|
||||
final crypto = await Veilid.instance.bestCryptoSystem();
|
||||
final dhtctx = (await Veilid.instance.routingContext())
|
||||
final veilid = await eventualVeilid.future;
|
||||
final crypto = await veilid.bestCryptoSystem();
|
||||
final dhtctx = (await veilid.routingContext())
|
||||
.withPrivacy()
|
||||
.withSequencing(Sequencing.ensureOrdered);
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ class Processor {
|
|||
_veilidVersion = 'Failed to get veilid version.';
|
||||
}
|
||||
|
||||
log.info("Veilid version: $_veilidVersion");
|
||||
|
||||
// In case of hot restart shut down first
|
||||
try {
|
||||
await Veilid.instance.shutdownVeilidCore();
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import 'package:veilid/veilid.dart';
|
||||
import 'veilid_init.dart';
|
||||
|
||||
Future<T> tableScope<T>(
|
||||
String name, Future<T> Function(VeilidTableDB tdb) callback,
|
||||
{int columnCount = 1}) async {
|
||||
VeilidTableDB tableDB = await Veilid.instance.openTableDB(name, columnCount);
|
||||
final veilid = await eventualVeilid.future;
|
||||
VeilidTableDB tableDB = await veilid.openTableDB(name, columnCount);
|
||||
try {
|
||||
return await callback(tableDB);
|
||||
} finally {
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:veilid/veilid.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'processor.dart';
|
||||
import 'veilid_log.dart';
|
||||
|
||||
part 'veilid_init.g.dart';
|
||||
|
||||
Future<String> getVeilidVersion() async {
|
||||
String veilidVersion;
|
||||
try {
|
||||
|
@ -45,11 +51,12 @@ void _initVeilid() {
|
|||
}
|
||||
}
|
||||
|
||||
bool initialized = false;
|
||||
Completer<Veilid> eventualVeilid = Completer<Veilid>();
|
||||
Processor processor = Processor();
|
||||
|
||||
Future<void> initializeVeilid() async {
|
||||
if (initialized) {
|
||||
// Ensure this runs only once
|
||||
if (eventualVeilid.isCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -62,5 +69,12 @@ Future<void> initializeVeilid() async {
|
|||
// Startup Veilid
|
||||
await processor.startup();
|
||||
|
||||
initialized = true;
|
||||
// Share the initialized veilid instance to the rest of the app
|
||||
eventualVeilid.complete(Veilid.instance);
|
||||
}
|
||||
|
||||
// Expose the Veilid instance as a FutureProvider
|
||||
@riverpod
|
||||
FutureOr<Veilid> veilidInstance(VeilidInstanceRef ref) async {
|
||||
return await eventualVeilid.future;
|
||||
}
|
24
lib/veilid_support/veilid_init.g.dart
Normal file
24
lib/veilid_support/veilid_init.g.dart
Normal file
|
@ -0,0 +1,24 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'veilid_init.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$veilidInstanceHash() => r'6086fc1e7a83e7af81ee05ee84954507d38cb748';
|
||||
|
||||
/// See also [veilidInstance].
|
||||
@ProviderFor(veilidInstance)
|
||||
final veilidInstanceProvider = AutoDisposeFutureProvider<Veilid>.internal(
|
||||
veilidInstance,
|
||||
name: r'veilidInstanceProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$veilidInstanceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef VeilidInstanceRef = AutoDisposeFutureProviderRef<Veilid>;
|
||||
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions
|
|
@ -1,7 +1,7 @@
|
|||
export 'config.dart';
|
||||
export 'processor.dart';
|
||||
export 'veilid_log.dart';
|
||||
export 'init.dart';
|
||||
export 'veilid_init.dart';
|
||||
export 'dht_record.dart';
|
||||
export 'dht_record_crypto.dart';
|
||||
export 'table_db.dart';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue