checkpoint

This commit is contained in:
John Smith 2023-01-09 22:50:34 -05:00
parent c22d6fcff8
commit 8c22bf8cc0
24 changed files with 673 additions and 56 deletions

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:animated_theme_switcher/animated_theme_switcher.dart';s
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'router/router.dart';
class VeilidChatApp extends StatelessWidget {
class VeilidChatApp extends ConsumerWidget {
const VeilidChatApp({
Key? key,
required this.theme,
@ -10,14 +12,16 @@ class VeilidChatApp extends StatelessWidget {
final ThemeData theme;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
return ThemeProvider(
initTheme: theme,
builder: (_, theme) {
return MaterialApp(
return MaterialApp.router(
routerConfig: router,
title: 'VeilidChat',
theme: theme,
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
},
);

96
lib/entities/contact.dart Normal file
View File

@ -0,0 +1,96 @@
class Contact {
String name;
String publicKey;
bool available;
Contact(this.name, this.publicKey) : available = false;
Contact.fromJson(Map<String, dynamic> json)
: name = json['name'],
publicKey = json['public_key'],
available = json['available'];
Map<String, dynamic> toJson() {
return {
'name': name,
'public_key': publicKey,
'available': available,
};
}
}
// // Synchronize the app state contact list with the internal one
// void sortAndStoreAppStateContactList(List<dynamic> ffasContactList) {
// ffasContactList.sortBy((element) => element["name"] as String);
// FFAppState().update(() {
// FFAppState().ContactList = ffasContactList;
// });
// }
// // Called when a new contact is added or an existing one is edited
// Future<String> vcsUpdateContact(
// String id,
// String name,
// String publicKey,
// ) async {
// var api = Veilid.instance;
// try {
// // if we are adding a new contact, make its id
// var newContact = false;
// if (id.isEmpty) {
// id = const Uuid().v4();
// newContact = true;
// }
// // Trim name
// name = name.trim();
// // Validate name and public key
// if (name.length > 127) {
// return "Name is too long.";
// }
// if (name.isEmpty) {
// return "Name can not be empty";
// }
// if (!isValidDHTKey(publicKey)) {
// return "Public key is not valid";
// }
// // update entry in internal contacts table
// var contactsDb = await api.openTableDB("contacts", 1);
// var contact = Contact(name, publicKey);
// await contactsDb.storeStringJson(0, id, contact);
// // update app state
// var contactJson = contact.toJson();
// contactJson['id'] = id;
// var ffasContactList = FFAppState().ContactList;
// if (newContact) {
// // Add new contact
// ffasContactList.add(contactJson);
// } else {
// // Update existing contact
// ffasContactList.forEachIndexedWhile((i, e) {
// if (e['id'] == id) {
// ffasContactList[i] = contactJson;
// return false;
// }
// return true;
// });
// }
// // Sort the contact list
// sortAndStoreAppStateContactList(ffasContactList);
// } catch (e) {
// return e.toString();
// }
// return "";
// }
// // Called when a contact is to be removed
// Future<void> vcsDeleteContact(String id) async {
// //
// }

View File

@ -17,7 +17,7 @@ void main() async {
var initTheme = themeService.initial;
runApp(
ProviderScope(
observers: [const StateLogger()],
observers: const [StateLogger()],
child: VeilidChatApp(theme: initTheme)),
);
}

29
lib/pages/chat.dart Normal file
View File

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ChatPage extends ConsumerWidget {
const ChatPage({super.key});
static const path = '/chat';
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(title: const Text("Chat")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("Home Page"),
ElevatedButton(
onPressed: () {
ref.watch(authNotifierProvider.notifier).logout();
},
child: const Text("Logout"),
),
],
),
),
);
}
}

32
lib/pages/contacts.dart Normal file
View File

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

View File

View File

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

View File

@ -1,48 +1,29 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
class HomePage extends ConsumerWidget {
const HomePage({super.key});
static const path = '/home';
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
appBar: AppBar(title: const Text("VeilidChat")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("Home Page"),
ElevatedButton(
onPressed: () {
ref.watch(authNotifierProvider.notifier).logout();
},
child: const Text("Logout"),
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}

13
lib/pages/index.dart Normal file
View File

@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
class IndexPage extends StatelessWidget {
const IndexPage({super.key});
static const path = '/';
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(child: Text("Index Page")),
);
}
}

32
lib/pages/login.dart Normal file
View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class LoginPage extends ConsumerWidget {
const LoginPage({super.key});
static const path = '/login';
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: null,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text("Login Page"),
ElevatedButton(
onPressed: () async {
ref.watch(authNotifierProvider.notifier).login(
"myEmail",
"myPassword",
);
},
child: const Text("Login"),
),
],
),
),
);
}
}

View File

2
lib/pages/pages.dart Normal file
View File

@ -0,0 +1,2 @@
export 'home.dart';
export 'splash.dart';

0
lib/pages/settings.dart Normal file
View File

21
lib/router/router.dart Normal file
View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'router_notifier.dart';
final _key = GlobalKey<NavigatorState>(debugLabel: 'routerKey');
/// This simple provider caches our GoRouter.
final routerProvider = Provider.autoDispose<GoRouter>((ref) {
final notifier = ref.watch(routerNotifierProvider.notifier);
return GoRouter(
navigatorKey: _key,
refreshListenable: notifier,
debugLogDiagnostics: true,
initialLocation: SplashPage.path,
routes: notifier.routes,
redirect: notifier.redirect,
);
});

View File

@ -0,0 +1,146 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../entities/user_role.dart';
import '../pages/pages.dart';
import '../state/auth.dart';
import '../state/permissions.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 {
VoidCallback? routerListener;
bool isAuth = false; // Useful for our global redirect functio
@override
Future<void> build() async {
// One could watch more providers and write logic accordingly
isAuth = await ref.watch(
authNotifierProvider.selectAsync((data) => data != null),
);
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
String? redirect(BuildContext context, GoRouterState state) {
if (this.state.isLoading || this.state.hasError) return null;
final isSplash = state.location == IndexPage.path;
if (isSplash) {
return isAuth ? HomePage.path : LoginPage.path;
}
final isLoggingIn = state.location == LoginPage.path;
if (isLoggingIn) return isAuth ? HomePage.path : null;
return isAuth ? null : IndexPage.path;
}
/// Our application routes. Obtained through code generation
List<GoRoute> get routes => [
GoRoute(
path: IndexPage.path,
builder: (context, state) => const IndexPage(),
),
GoRoute(
path: HomePage.path,
builder: (context, state) => const HomePage(),
redirect: (context, state) async {
if (state.location == HomePage.path) return null;
final roleListener = ProviderScope.containerOf(context).listen(
permissionsProvider.select((value) => value.valueOrNull),
(previous, next) {},
);
final userRole = roleListener.read();
final redirectTo = userRole?.redirectBasedOn(state.location);
roleListener.close();
return redirectTo;
},
routes: [
GoRoute(
path: AdminPage.path,
builder: (context, state) => const AdminPage(),
),
GoRoute(
path: UserPage.path,
builder: (context, state) => const UserPage(),
),
GoRoute(
path: GuestPage.path,
builder: (context, state) => const GuestPage(),
)
]),
GoRoute(
path: LoginPage.path,
builder: (context, state) => const LoginPage(),
),
];
/// Adds [GoRouter]'s listener as specified by its [Listenable].
/// [GoRouteInformationProvider] uses this method on creation to handle its
/// internal [ChangeNotifier].
/// Check out the internal implementation of [GoRouter] and
/// [GoRouteInformationProvider] to see this in action.
@override
void addListener(VoidCallback listener) {
routerListener = listener;
}
/// Removes [GoRouter]'s listener as specified by its [Listenable].
/// [GoRouteInformationProvider] uses this method when disposing,
/// so that it removes its callback when destroyed.
/// Check out the internal implementation of [GoRouter] and
/// [GoRouteInformationProvider] to see this in action.
@override
void removeListener(VoidCallback listener) {
routerListener = null;
}
}
final routerNotifierProvider =
AutoDisposeAsyncNotifierProvider<RouterNotifier, void>(() {
return RouterNotifier();
});
/// A simple extension to determine wherever should we redirect our users
extension RedirecttionBasedOnRole on UserRole {
/// Redirects the users based on [this] and its current [location]
String? redirectBasedOn(String location) {
switch (this) {
case UserRole.admin:
return null;
case UserRole.verifiedUser:
case UserRole.unverifiedUser:
if (location == AdminPage.path) return HomePage.path;
return null;
case UserRole.guest:
case UserRole.none:
if (location != HomePage.path) return HomePage.path;
return null;
}
}
}

View File

View File

@ -0,0 +1,6 @@
import 'package:veilid/veilid.dart';
Future<VeilidConfig> getVeilidChatConfig() async {
VeilidConfig config = await getDefaultVeilidConfig("VeilidChat");
return config;
}

View File

@ -0,0 +1,63 @@
import 'package:veilid/veilid.dart';
import 'package:flutter/foundation.dart';
import 'processor.dart';
Future<String> getVeilidVersion() async {
String veilidVersion;
try {
veilidVersion = Veilid.instance.veilidVersionString();
} on Exception {
veilidVersion = 'Failed to get veilid version.';
}
return veilidVersion;
}
// Initialize Veilid
// Call only once.
void _init() {
if (kIsWeb) {
var platformConfig = VeilidWASMConfig(
logging: VeilidWASMConfigLogging(
performance: VeilidWASMConfigLoggingPerformance(
enabled: true,
level: VeilidConfigLogLevel.debug,
logsInTimings: true,
logsInConsole: false),
api: VeilidWASMConfigLoggingApi(
enabled: true, level: VeilidConfigLogLevel.info)));
Veilid.instance.initializeVeilidCore(platformConfig.json);
} else {
var platformConfig = VeilidFFIConfig(
logging: VeilidFFIConfigLogging(
terminal: VeilidFFIConfigLoggingTerminal(
enabled: false,
level: VeilidConfigLogLevel.debug,
),
otlp: VeilidFFIConfigLoggingOtlp(
enabled: false,
level: VeilidConfigLogLevel.trace,
grpcEndpoint: "localhost:4317",
serviceName: "VeilidChat"),
api: VeilidFFIConfigLoggingApi(
enabled: true, level: VeilidConfigLogLevel.info)));
Veilid.instance.initializeVeilidCore(platformConfig.json);
}
}
// Called from FlutterFlow stub initialize() function upon Main page load
bool initialized = false;
Processor processor = Processor();
Future<void> initializeVeilid() async {
if (initialized) {
return;
}
// Init Veilid
_init();
// Startup Veilid
await processor.startup();
initialized = true;
}

View File

@ -0,0 +1,136 @@
import 'dart:async';
import 'package:veilid/veilid.dart';
import 'config.dart';
import 'veilid_log.dart';
import '../log/loggy.dart';
class Processor {
String _veilidVersion = "";
bool _startedUp = false;
Stream<VeilidUpdate>? _updateStream;
Future<void>? _updateProcessor;
Processor();
Future<void> startup() async {
if (_startedUp) {
return;
}
try {
_veilidVersion = Veilid.instance.veilidVersionString();
} on Exception {
_veilidVersion = 'Failed to get veilid version.';
}
// In case of hot restart shut down first
try {
await Veilid.instance.shutdownVeilidCore();
} on Exception {
//
}
var updateStream =
await Veilid.instance.startupVeilidCore(await getVeilidChatConfig());
_updateStream = updateStream;
_updateProcessor = processUpdates();
_startedUp = true;
await Veilid.instance.attach();
}
Future<void> shutdown() async {
if (!_startedUp) {
return;
}
await Veilid.instance.shutdownVeilidCore();
if (_updateProcessor != null) {
await _updateProcessor;
}
_updateProcessor = null;
_updateStream = null;
_startedUp = false;
}
Future<void> processUpdateAttachment(
VeilidUpdateAttachment updateAttachment) async {
//loggy.info("Attachment: ${updateAttachment.json}");
// Set connection meter and ui state for connection state
var connectionState = "";
var checkPublicInternet = false;
switch (updateAttachment.state.state) {
case AttachmentState.detached:
connectionState = "detached";
break;
case AttachmentState.detaching:
connectionState = "detaching";
break;
case AttachmentState.attaching:
connectionState = "attaching";
break;
case AttachmentState.attachedWeak:
checkPublicInternet = true;
connectionState = "weak";
break;
case AttachmentState.attachedGood:
checkPublicInternet = true;
connectionState = "good";
break;
case AttachmentState.attachedStrong:
checkPublicInternet = true;
connectionState = "strong";
break;
case AttachmentState.fullyAttached:
checkPublicInternet = true;
connectionState = "full";
break;
case AttachmentState.overAttached:
checkPublicInternet = true;
connectionState = "over";
break;
}
if (checkPublicInternet) {
if (!updateAttachment.state.publicInternetReady) {
connectionState = "attaching";
}
}
FFAppState().update(() {
FFAppState().ConnectionState = connectionState;
});
}
Future<void> processUpdateConfig(VeilidUpdateConfig updateConfig) async {
//loggy.info("Config: ${updateConfig.json}");
// xxx: store in flutterflow local state? do we need this for anything?
}
Future<void> processUpdateNetwork(VeilidUpdateNetwork updateNetwork) async {
//loggy.info("Network: ${updateNetwork.json}");
// xxx: store in flutterflow local state? do we need this for anything?
}
Future<void> processUpdates() async {
var stream = _updateStream;
if (stream != null) {
await for (final update in stream) {
if (update is VeilidLog) {
await processLog(update);
} else if (update is VeilidUpdateAttachment) {
await processUpdateAttachment(update);
} else if (update is VeilidUpdateConfig) {
await processUpdateConfig(update);
} else if (update is VeilidUpdateNetwork) {
await processUpdateNetwork(update);
} else if (update is VeilidAppMessage) {
log.info("AppMessage: ${update.json}");
} else if (update is VeilidAppCall) {
log.info("AppCall: ${update.json}");
} else {
log.trace("Update: ${update.json}");
}
}
}
}
}

View File

@ -0,0 +1,16 @@
import 'package:veilid/base64url_no_pad.dart';
bool isValidDHTKey(String key) {
if (key.length != 43) {
return false;
}
try {
var dec = base64UrlNoPadDecode(key);
if (dec.length != 32) {
return false;
}
} catch (e) {
return false;
}
return true;
}

View File

View File

@ -254,6 +254,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
go_router:
dependency: "direct main"
description:
name: go_router
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.1"
graphs:
dependency: transitive
description:
@ -688,4 +695,4 @@ packages:
version: "3.1.1"
sdks:
dart: ">=2.18.6 <3.0.0"
flutter: ">=3.0.0"
flutter: ">=3.3.0"

View File

@ -47,6 +47,7 @@ dependencies:
path: ../veilid/veilid-flutter
animated_theme_switcher: ^2.0.7
shared_preferences: ^2.0.15
go_router: ^6.0.1
dev_dependencies:
flutter_test:

View File

@ -5,26 +5,26 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_test/flutter_test.dart';
import 'package:veilidchat/main.dart';
// import 'package:veilidchat/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// // Build our app and trigger a frame.
// await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// // Verify that our counter starts at 0.
// expect(find.text('0'), findsOneWidget);
// expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// // Tap the '+' icon and trigger a frame.
// await tester.tap(find.byIcon(Icons.add));
// await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
// // Verify that our counter has incremented.
// expect(find.text('0'), findsNothing);
// expect(find.text('1'), findsOneWidget);
// });
}