From acfeca7f9644732bd4ade9e0c92fa5b683dd2733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 9 Mar 2023 09:49:01 +0100 Subject: [PATCH 01/33] Time to get serious.. --- kilochat/README.md | 7 + kilochat/analysis_options.yaml | 9 + kilochat/lib/avatar.dart | 21 + kilochat/lib/debug_widget.dart | 41 + kilochat/lib/firebase_options.dart | 76 ++ kilochat/lib/main.dart | 354 ++++++ kilochat/lib/model.dart | 124 ++ kilochat/lib/model.g.dart | 376 ++++++ kilochat/lib/profile_form.dart | 141 +++ kilochat/lib/providers.dart | 119 ++ kilochat/lib/providers.g.dart | 197 +++ kilochat/lib/realm_ui.dart | 167 +++ kilochat/lib/repository.dart | 81 ++ kilochat/lib/tiles.dart | 195 +++ kilochat/lib/widget_builders.dart | 6 + .../Flutter/GeneratedPluginRegistrant.swift | 20 + .../ephemeral/Flutter-Generated.xcconfig | 11 + .../ephemeral/flutter_export_environment.sh | 12 + .../macos/Runner/DebugProfile.entitlements | 14 + kilochat/macos/Runner/Release.entitlements | 10 + kilochat/pubspec.lock | 1127 +++++++++++++++++ kilochat/pubspec.yaml | 54 + 22 files changed, 3162 insertions(+) create mode 100644 kilochat/README.md create mode 100644 kilochat/analysis_options.yaml create mode 100644 kilochat/lib/avatar.dart create mode 100644 kilochat/lib/debug_widget.dart create mode 100644 kilochat/lib/firebase_options.dart create mode 100644 kilochat/lib/main.dart create mode 100644 kilochat/lib/model.dart create mode 100644 kilochat/lib/model.g.dart create mode 100644 kilochat/lib/profile_form.dart create mode 100644 kilochat/lib/providers.dart create mode 100644 kilochat/lib/providers.g.dart create mode 100644 kilochat/lib/realm_ui.dart create mode 100644 kilochat/lib/repository.dart create mode 100644 kilochat/lib/tiles.dart create mode 100644 kilochat/lib/widget_builders.dart create mode 100644 kilochat/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 kilochat/macos/Flutter/ephemeral/Flutter-Generated.xcconfig create mode 100755 kilochat/macos/Flutter/ephemeral/flutter_export_environment.sh create mode 100644 kilochat/macos/Runner/DebugProfile.entitlements create mode 100644 kilochat/macos/Runner/Release.entitlements create mode 100644 kilochat/pubspec.lock create mode 100644 kilochat/pubspec.yaml diff --git a/kilochat/README.md b/kilochat/README.md new file mode 100644 index 0000000..2c5fe5e --- /dev/null +++ b/kilochat/README.md @@ -0,0 +1,7 @@ +# kilochat + +A sample chat application build with Flutter, Realm and Atlas Device Sync. + +# DISCLAIMER + +This is low priority work in progress. Eventually we hope to make this a well documented example of best practices, but that is *not* the current state. \ No newline at end of file diff --git a/kilochat/analysis_options.yaml b/kilochat/analysis_options.yaml new file mode 100644 index 0000000..daa1826 --- /dev/null +++ b/kilochat/analysis_options.yaml @@ -0,0 +1,9 @@ + +include: package:flutter_lints/flutter.yaml + +linter: + rules: + +analyzer: + plugins: + - custom_lint \ No newline at end of file diff --git a/kilochat/lib/avatar.dart b/kilochat/lib/avatar.dart new file mode 100644 index 0000000..ad2bdc4 --- /dev/null +++ b/kilochat/lib/avatar.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:kilochat/model.dart'; +import 'package:random_avatar/random_avatar.dart'; + +class MyAvatar extends StatelessWidget { + const MyAvatar({ + super.key, + this.user, + }); + + final UserProfile? user; + + String get _userId => user?.id ?? 'N/A'; + @override + Widget build(BuildContext context) { + return CircleAvatar( + backgroundColor: Color(_userId.hashCode | 0x22000000), + child: RandomAvatar(_userId, width: 40, trBackground: true), + ); + } +} diff --git a/kilochat/lib/debug_widget.dart b/kilochat/lib/debug_widget.dart new file mode 100644 index 0000000..18462d9 --- /dev/null +++ b/kilochat/lib/debug_widget.dart @@ -0,0 +1,41 @@ +import 'package:flutter/widgets.dart'; + +class DebugWidget extends StatefulWidget { + final T child; + + const DebugWidget({super.key, required this.child}); + + @override + State createState() => _DebugWidgetState(); +} + +class _DebugWidgetState extends State> { + @override + void initState() { + print('$T@$hashCode initState'); + super.initState(); + } + + @override + void didUpdateWidget(covariant DebugWidget oldWidget) { + print('$T@$hashCode didUpdateWidget'); // TODO: remove + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + print('$T@$hashCode dispose'); // TODO: remove + super.dispose(); + } + + @override + void didChangeDependencies() { + print('$T@$hashCode didChangeDependencies'); // TODO: remove + super.didChangeDependencies(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/kilochat/lib/firebase_options.dart b/kilochat/lib/firebase_options.dart new file mode 100644 index 0000000..c2d8d9f --- /dev/null +++ b/kilochat/lib/firebase_options.dart @@ -0,0 +1,76 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for web - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyDm3ZNJi-WewDKmi6m6-pZKCIVNKsPC_eQ', + appId: '1:371214127756:android:1ab8f0af3f604e0b60a1a9', + messagingSenderId: '371214127756', + projectId: 'kilochat-realm', + storageBucket: 'kilochat-realm.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDJ0UFt4Nrz2OnBQvI3qio72Abw76bcRG4', + appId: '1:371214127756:ios:ba86b9e1d8b4750860a1a9', + messagingSenderId: '371214127756', + projectId: 'kilochat-realm', + storageBucket: 'kilochat-realm.appspot.com', + iosClientId: '371214127756-7hg0fj2a2u8jbo8ur7veael6vfoboan7.apps.googleusercontent.com', + iosBundleId: 'com.example.kilochat', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDJ0UFt4Nrz2OnBQvI3qio72Abw76bcRG4', + appId: '1:371214127756:ios:672fefd8423f42c560a1a9', + messagingSenderId: '371214127756', + projectId: 'kilochat-realm', + storageBucket: 'kilochat-realm.appspot.com', + iosClientId: '371214127756-qpak0482c07firum3oukirg1kajoe5d7.apps.googleusercontent.com', + iosBundleId: 'com.example.kilochat.RunnerTests', + ); +} diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart new file mode 100644 index 0000000..e4cd3a1 --- /dev/null +++ b/kilochat/lib/main.dart @@ -0,0 +1,354 @@ +import 'dart:async'; + +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:firebase_ui_oauth_apple/firebase_ui_oauth_apple.dart'; +import 'package:firebase_ui_oauth_facebook/firebase_ui_oauth_facebook.dart'; +import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kilochat/debug_widget.dart'; +import 'package:kilochat/widget_builders.dart'; +import 'package:statsfl/statsfl.dart'; + +import 'avatar.dart'; +import 'firebase_options.dart'; +import 'model.dart'; +import 'profile_form.dart'; +import 'providers.dart'; +import 'realm_ui.dart'; +import 'tiles.dart'; + +const freedomBlue = Color(0xff0057b7); +const energizingYellow = Color(0xffffd700); + +enum Routes { + logIn, + chat; + + String get id => toString(); +} + +Future main() async { + Animate.restartOnHotReload = true; + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + FirebaseUIAuth.configureProviders([ + EmailAuthProvider(), + AppleProvider(), + FacebookProvider(clientId: ''), + GoogleProvider(clientId: ''), + ]); + runApp( + StatsFl( + maxFps: 60, + showText: false, + child: ProviderScope( + child: Builder( + builder: (context) { + final ref = ProviderScope.containerOf(context); + final firebaseUserController = + ref.read(firebaseUserProvider.notifier); + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: freedomBlue, + inversePrimary: energizingYellow, + ), + ), + initialRoute: firebaseUserController.state == null + ? Routes.logIn.id + : Routes.chat.id, + routes: { + Routes.logIn.id: (context) { + return SignInScreen(actions: [ + AuthStateChangeAction((context, state) async { + firebaseUserController.state = state.user; + Navigator.pushReplacementNamed(context, Routes.chat.id); + }), + ]); + }, + Routes.chat.id: (context) { + return const Material(child: MyApp()); + }, + }, + ); + }, + ), + ), + ), + ); +} + +class MyApp extends ConsumerWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final focusedChannel = ref.watch(focusedChannelProvider); + final repository = ref.watch(repositoryProvider); + return repository.when( + error: buildErrorWidget, + loading: buildLoadingWidget, + data: (repository) { + final user = repository.user; + return Scaffold( + appBar: AppBar( + title: focusedChannel == null + ? const Text('Kilochat') + : Text('# ${focusedChannel.name}'), + actions: [ + IconButton( + onPressed: () => showSearch( + context: context, + delegate: RealmSearchDelegate( + repository.searchMessage, + (context, item, animation) => + MessageTile(message: item, animation: animation), + )), + icon: const Icon(Icons.search), + ), + IconButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ProfileForm(initialProfile: user))); + }, + icon: MyAvatar(user: user), + ), + ], + ), + body: const Center(child: Text('Choose a channel')) + .animate(target: focusedChannel == null ? 0 : 1) + .crossfade(builder: (context) => const ChatWidget()), + drawer: Builder( + builder: (context) { + return Drawer( + child: SafeArea( + child: ChannelsView( + onTap: (channel) { + ref.read(focusedChannelProvider.notifier).focus(channel); + Scaffold.of(context).closeDrawer(); + }, + ), + ), + ); + }, + ), + ); + }, + ); + } +} + +class ChatWidget extends ConsumerStatefulWidget { + const ChatWidget({super.key}); + + @override + ConsumerState createState() => _ChatWidgetState(); +} + +class _ChatWidgetState extends ConsumerState { + final controller = TextEditingController(); + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded(child: MessagesView(scrollController: scrollController)), + Ink( + color: Theme.of(context).colorScheme.inversePrimary, + padding: const EdgeInsets.only(left: 16, bottom: 16, right: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + child: TextField( + minLines: 1, + maxLines: 5, + controller: controller, + decoration: const InputDecoration( + label: Text('Enter next message'), + border: InputBorder.none, + hintText: 'here..', + ), + textInputAction: TextInputAction.done, + onSubmitted: _postNewMessage, + ), + ), + ), + IconButton( + enableFeedback: true, + onPressed: () => _postNewMessage(controller.text), + icon: const Icon(Icons.send), + ) + ], + ), + ), + ], + ); + } + + void _postNewMessage(String text) async { + if (text.isNotEmpty) { + final channel = ref.read(focusedChannelProvider); + final repository = ref.read(repositoryProvider).requireValue; + repository.postNewMessage(channel!, text); + controller.clear(); + await scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } +} + +class MessagesView extends ConsumerWidget { + const MessagesView({ + super.key, + this.scrollController, + }); + + final ScrollController? scrollController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final channel = ref.watch(focusedChannelProvider); + final messages = ref.watch(messagesProvider(channel)); + return messages.when( + error: buildErrorWidget, + loading: buildLoadingWidget, + data: (messages) => RealmAnimatedList( + results: messages, + itemBuilder: (context, item, animation) { + return DebugWidget( + child: MessageTile(message: item, animation: animation)); + }, + reverse: true, + controller: scrollController, + ), + ); + } +} + +class ChannelsView extends ConsumerWidget { + const ChannelsView({super.key, this.onTap}); + + final void Function(Channel)? onTap; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final focusedChannel = ref.watch(focusedChannelProvider); + final repository = ref.watch(repositoryProvider); + return repository.when( + error: buildErrorWidget, + loading: buildLoadingWidget, + data: (repository) { + final user = repository.user; + final channels = user.channels.asResults(); + return ListTileTheme( + data: ListTileThemeData( + //dense: true, + selectedTileColor: Theme.of(context).colorScheme.inversePrimary, + ), + child: Column( + children: [ + Expanded( + child: RealmAnimatedList( + results: channels, + itemBuilder: (_, channel, animation) => ChannelTile( + channel: channel, + animation: animation, + onTap: () => onTap?.call(channel), + onDismissed: (_) => + repository.unsubscribeFromChannel(channel), + selected: channel == focusedChannel, + ), + ), + ), + Row( + children: [ + IconButton( + onPressed: () { + showSearch( + context: context, + delegate: ChannelSearchDelegate( + (query) => repository.searchChannel(query), + (_, channel, animation) => StatefulBuilder( + builder: (_, setState) { + return ChannelTile( + channel: channel, + animation: animation, + onTap: () => setState(() { + repository.subscribeToChannel(channel); + }), + selected: !channel.isFrozen && + user.channels.contains(channel), + ); + }, + ), + ), + ); + }, + icon: const Icon(Icons.add), + ) + ], + ), + const AboutListTile( + applicationName: 'kilochat', + applicationVersion: '1.0.0', + applicationIcon: Icon(Icons.send), + applicationLegalese: 'Copyright 2023 MongoDB', + dense: true, + aboutBoxChildren: [ + SizedBox(height: 10), + Text( + 'A chat application using Flutter, Realm, and Atlas ' + 'Device Sync. Written in less than 1K lines of code ' + '(excluding generated code), hence the name.', + textScaleFactor: 0.8, + ) + ], + ), + ], + ), + ); + }, + ); + } +} + +class ChannelSearchDelegate extends RealmSearchDelegate { + bool _showSubscribed = true; + + ChannelSearchDelegate(super.resultsBuilder, super.itemBuilder); + + @override + List buildActions(BuildContext context) { + final repository = + ProviderScope.containerOf(context).read(repositoryProvider).value; + return [ + if (query.isNotEmpty && repository != null) + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + final channel = repository.createChannel(query.trim()); + repository.subscribeToChannel(channel); + close(context, null); + }, + ), + StatefulBuilder(builder: (_, setState) { + return Switch( + value: _showSubscribed, + onChanged: (value) => setState(() => _showSubscribed = value), + ); + }), + ...super.buildActions(context), + ]; + } +} diff --git a/kilochat/lib/model.dart b/kilochat/lib/model.dart new file mode 100644 index 0000000..4e3cff5 --- /dev/null +++ b/kilochat/lib/model.dart @@ -0,0 +1,124 @@ +import 'package:realm/realm.dart'; + +part 'model.g.dart'; + +@RealmModel() +class _Channel { + @PrimaryKey() + @MapTo('_id') + late ObjectId id; + + @MapTo('owner_id') + late String ownerId; // matches user.id + + late _Channel? parent; + late String name; + late int count; +} + +@RealmModel() +class _Message { + @PrimaryKey() + @MapTo('_id') + late ObjectId id; + + @MapTo('owner_id') + late String ownerId; // matches owner.id + _UserProfile? owner; + + @Indexed() + @MapTo('channel_id') + late ObjectId channelId; // matches channel.id + + _Channel? channel; + + @Indexed(RealmIndexType.fullText) + late String text; + + @Backlink(#message) + late Iterable<_Reaction> reactions; +} + +@RealmModel() +class _Reaction { + @PrimaryKey() + @MapTo('_id') + late ObjectId id; + + @MapTo('owner_id') + late String ownerId; // matches owner.id + late _UserProfile? owner; + + late _Message? message; + + late int emojiUnicode = 0; + String get emoji => String.fromCharCode(emojiUnicode); + set emoji(String value) => emojiUnicode = value.runes.first; +} + +extension ReactionEx on Reaction { + static Reaction create(UserProfile user, Message message, String emoji) { + // Composite key + final oid = ObjectId.fromValues( + Object.hash(user.id, message.id, emoji), // rotate for more entropy + Object.hash(message.id, emoji, user.id), + Object.hash(emoji, user.id, message.id), + ); + return Reaction( + oid, + user.id, + owner: user, + message: message, + emojiUnicode: emoji.runes.first, + ); + } +} + +enum Gender { + unknown, + male, + female, + other; + + factory Gender.parse(String input) => tryParse(input)!; + + static final _nameMap = values.asNameMap(); + static Gender? tryParse(String input) => _nameMap[input]; +} + +@RealmModel() +class _UserProfile { + @PrimaryKey() + @MapTo('_id') + late String id; // matches user.id + + @MapTo('owner_id') + late String ownerId; // matches user.id + + var deactivated = false; + + String? name; + + String? email; + + int? age; + + @MapTo('gender') + int genderAsInt = 0; // Gender.unknown.index; <-- not a const expression + Gender get gender => Gender.values[genderAsInt]; + set gender(Gender value) => genderAsInt = value.index; + + @MapTo('status_emoji') + int? statusEmojiUnicode; + String? get statusEmoji { + final s = statusEmojiUnicode; + if (s == null) return null; + return String.fromCharCode(s); + } + + set statusEmoji(String? value) => statusEmojiUnicode = value?.runes.first; + + var typing = false; + + late Set<_Channel> channels; +} diff --git a/kilochat/lib/model.g.dart b/kilochat/lib/model.g.dart new file mode 100644 index 0000000..0c923be --- /dev/null +++ b/kilochat/lib/model.g.dart @@ -0,0 +1,376 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'model.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +class Channel extends _Channel with RealmEntity, RealmObjectBase, RealmObject { + Channel( + ObjectId id, + String ownerId, + String name, + int count, { + Channel? parent, + }) { + RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set(this, 'owner_id', ownerId); + RealmObjectBase.set(this, 'parent', parent); + RealmObjectBase.set(this, 'name', name); + RealmObjectBase.set(this, 'count', count); + } + + Channel._(); + + @override + ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; + @override + set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + + @override + String get ownerId => RealmObjectBase.get(this, 'owner_id') as String; + @override + set ownerId(String value) => RealmObjectBase.set(this, 'owner_id', value); + + @override + Channel? get parent => + RealmObjectBase.get(this, 'parent') as Channel?; + @override + set parent(covariant Channel? value) => + RealmObjectBase.set(this, 'parent', value); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + int get count => RealmObjectBase.get(this, 'count') as int; + @override + set count(int value) => RealmObjectBase.set(this, 'count', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Channel freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Channel._); + return const SchemaObject(ObjectType.realmObject, Channel, 'Channel', [ + SchemaProperty('id', RealmPropertyType.objectid, + mapTo: '_id', primaryKey: true), + SchemaProperty('ownerId', RealmPropertyType.string, mapTo: 'owner_id'), + SchemaProperty('parent', RealmPropertyType.object, + optional: true, linkTarget: 'Channel'), + SchemaProperty('name', RealmPropertyType.string), + SchemaProperty('count', RealmPropertyType.int), + ]); + } +} + +class Message extends _Message with RealmEntity, RealmObjectBase, RealmObject { + Message( + ObjectId id, + String ownerId, + ObjectId channelId, + String text, { + UserProfile? owner, + Channel? channel, + }) { + RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set(this, 'owner_id', ownerId); + RealmObjectBase.set(this, 'owner', owner); + RealmObjectBase.set(this, 'channel_id', channelId); + RealmObjectBase.set(this, 'channel', channel); + RealmObjectBase.set(this, 'text', text); + } + + Message._(); + + @override + ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; + @override + set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + + @override + String get ownerId => RealmObjectBase.get(this, 'owner_id') as String; + @override + set ownerId(String value) => RealmObjectBase.set(this, 'owner_id', value); + + @override + UserProfile? get owner => + RealmObjectBase.get(this, 'owner') as UserProfile?; + @override + set owner(covariant UserProfile? value) => + RealmObjectBase.set(this, 'owner', value); + + @override + ObjectId get channelId => + RealmObjectBase.get(this, 'channel_id') as ObjectId; + @override + set channelId(ObjectId value) => + RealmObjectBase.set(this, 'channel_id', value); + + @override + Channel? get channel => + RealmObjectBase.get(this, 'channel') as Channel?; + @override + set channel(covariant Channel? value) => + RealmObjectBase.set(this, 'channel', value); + + @override + String get text => RealmObjectBase.get(this, 'text') as String; + @override + set text(String value) => RealmObjectBase.set(this, 'text', value); + + @override + RealmResults get reactions { + if (!isManaged) { + throw RealmError('Using backlinks is only possible for managed objects.'); + } + return RealmObjectBase.get(this, 'reactions') + as RealmResults; + } + + @override + set reactions(covariant RealmResults value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Message freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Message._); + return const SchemaObject(ObjectType.realmObject, Message, 'Message', [ + SchemaProperty('id', RealmPropertyType.objectid, + mapTo: '_id', primaryKey: true), + SchemaProperty('ownerId', RealmPropertyType.string, mapTo: 'owner_id'), + SchemaProperty('owner', RealmPropertyType.object, + optional: true, linkTarget: 'UserProfile'), + SchemaProperty('channelId', RealmPropertyType.objectid, + mapTo: 'channel_id', indexType: RealmIndexType.regular), + SchemaProperty('channel', RealmPropertyType.object, + optional: true, linkTarget: 'Channel'), + SchemaProperty('text', RealmPropertyType.string, + indexType: RealmIndexType.fullText), + SchemaProperty('reactions', RealmPropertyType.linkingObjects, + linkOriginProperty: 'message', + collectionType: RealmCollectionType.list, + linkTarget: 'Reaction'), + ]); + } +} + +class Reaction extends _Reaction + with RealmEntity, RealmObjectBase, RealmObject { + static var _defaultsSet = false; + + Reaction( + ObjectId id, + String ownerId, { + UserProfile? owner, + Message? message, + int emojiUnicode = 0, + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + 'emojiUnicode': 0, + }); + } + RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set(this, 'owner_id', ownerId); + RealmObjectBase.set(this, 'owner', owner); + RealmObjectBase.set(this, 'message', message); + RealmObjectBase.set(this, 'emojiUnicode', emojiUnicode); + } + + Reaction._(); + + @override + ObjectId get id => RealmObjectBase.get(this, '_id') as ObjectId; + @override + set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + + @override + String get ownerId => RealmObjectBase.get(this, 'owner_id') as String; + @override + set ownerId(String value) => RealmObjectBase.set(this, 'owner_id', value); + + @override + UserProfile? get owner => + RealmObjectBase.get(this, 'owner') as UserProfile?; + @override + set owner(covariant UserProfile? value) => + RealmObjectBase.set(this, 'owner', value); + + @override + Message? get message => + RealmObjectBase.get(this, 'message') as Message?; + @override + set message(covariant Message? value) => + RealmObjectBase.set(this, 'message', value); + + @override + int get emojiUnicode => RealmObjectBase.get(this, 'emojiUnicode') as int; + @override + set emojiUnicode(int value) => + RealmObjectBase.set(this, 'emojiUnicode', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Reaction freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Reaction._); + return const SchemaObject(ObjectType.realmObject, Reaction, 'Reaction', [ + SchemaProperty('id', RealmPropertyType.objectid, + mapTo: '_id', primaryKey: true), + SchemaProperty('ownerId', RealmPropertyType.string, mapTo: 'owner_id'), + SchemaProperty('owner', RealmPropertyType.object, + optional: true, linkTarget: 'UserProfile'), + SchemaProperty('message', RealmPropertyType.object, + optional: true, linkTarget: 'Message'), + SchemaProperty('emojiUnicode', RealmPropertyType.int), + ]); + } +} + +class UserProfile extends _UserProfile + with RealmEntity, RealmObjectBase, RealmObject { + static var _defaultsSet = false; + + UserProfile( + String id, + String ownerId, { + bool deactivated = false, + String? name, + String? email, + int? age, + int genderAsInt = 0, + int? statusEmojiUnicode, + bool typing = false, + Set channels = const {}, + }) { + if (!_defaultsSet) { + _defaultsSet = RealmObjectBase.setDefaults({ + 'deactivated': false, + 'gender': 0, + 'typing': false, + }); + } + RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set(this, 'owner_id', ownerId); + RealmObjectBase.set(this, 'deactivated', deactivated); + RealmObjectBase.set(this, 'name', name); + RealmObjectBase.set(this, 'email', email); + RealmObjectBase.set(this, 'age', age); + RealmObjectBase.set(this, 'gender', genderAsInt); + RealmObjectBase.set(this, 'status_emoji', statusEmojiUnicode); + RealmObjectBase.set(this, 'typing', typing); + RealmObjectBase.set>( + this, 'channels', RealmSet(channels)); + } + + UserProfile._(); + + @override + String get id => RealmObjectBase.get(this, '_id') as String; + @override + set id(String value) => RealmObjectBase.set(this, '_id', value); + + @override + String get ownerId => RealmObjectBase.get(this, 'owner_id') as String; + @override + set ownerId(String value) => RealmObjectBase.set(this, 'owner_id', value); + + @override + bool get deactivated => + RealmObjectBase.get(this, 'deactivated') as bool; + @override + set deactivated(bool value) => + RealmObjectBase.set(this, 'deactivated', value); + + @override + String? get name => RealmObjectBase.get(this, 'name') as String?; + @override + set name(String? value) => RealmObjectBase.set(this, 'name', value); + + @override + String? get email => RealmObjectBase.get(this, 'email') as String?; + @override + set email(String? value) => RealmObjectBase.set(this, 'email', value); + + @override + int? get age => RealmObjectBase.get(this, 'age') as int?; + @override + set age(int? value) => RealmObjectBase.set(this, 'age', value); + + @override + int get genderAsInt => RealmObjectBase.get(this, 'gender') as int; + @override + set genderAsInt(int value) => RealmObjectBase.set(this, 'gender', value); + + @override + int? get statusEmojiUnicode => + RealmObjectBase.get(this, 'status_emoji') as int?; + @override + set statusEmojiUnicode(int? value) => + RealmObjectBase.set(this, 'status_emoji', value); + + @override + bool get typing => RealmObjectBase.get(this, 'typing') as bool; + @override + set typing(bool value) => RealmObjectBase.set(this, 'typing', value); + + @override + RealmSet get channels => + RealmObjectBase.get(this, 'channels') as RealmSet; + @override + set channels(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + UserProfile freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(UserProfile._); + return const SchemaObject( + ObjectType.realmObject, UserProfile, 'UserProfile', [ + SchemaProperty('id', RealmPropertyType.string, + mapTo: '_id', primaryKey: true), + SchemaProperty('ownerId', RealmPropertyType.string, mapTo: 'owner_id'), + SchemaProperty('deactivated', RealmPropertyType.bool), + SchemaProperty('name', RealmPropertyType.string, optional: true), + SchemaProperty('email', RealmPropertyType.string, optional: true), + SchemaProperty('age', RealmPropertyType.int, optional: true), + SchemaProperty('genderAsInt', RealmPropertyType.int, mapTo: 'gender'), + SchemaProperty('statusEmojiUnicode', RealmPropertyType.int, + mapTo: 'status_emoji', optional: true), + SchemaProperty('typing', RealmPropertyType.bool), + SchemaProperty('channels', RealmPropertyType.object, + linkTarget: 'Channel', collectionType: RealmCollectionType.set), + ]); + } +} diff --git a/kilochat/lib/profile_form.dart b/kilochat/lib/profile_form.dart new file mode 100644 index 0000000..511ec3c --- /dev/null +++ b/kilochat/lib/profile_form.dart @@ -0,0 +1,141 @@ +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'widget_builders.dart'; +import 'model.dart'; +import 'providers.dart'; + +class ProfileForm extends ConsumerStatefulWidget { + final UserProfile initialProfile; + + const ProfileForm({super.key, required this.initialProfile}); + + @override + ConsumerState createState() => _ProfileFormState(); +} + +class _ProfileFormState extends ConsumerState { + final _formKey = GlobalKey(); + + late UserProfile _profile; + + @override + void initState() { + super.initState(); + _profile = UserProfile( + widget.initialProfile.id, + widget.initialProfile.ownerId, + name: widget.initialProfile.name, + email: widget.initialProfile.email, + age: widget.initialProfile.age, + genderAsInt: widget.initialProfile.genderAsInt, + ); + } + + @override + Widget build(BuildContext context) { + var repository = ref.watch(repositoryProvider); + return repository.when( + error: buildErrorWidget, + loading: buildLoadingWidget, + data: (repository) => Scaffold( + appBar: AppBar( + title: const Text('Edit Profile'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + decoration: const InputDecoration(labelText: 'Name'), + initialValue: _profile.name, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your name'; + } + return null; + }, + onSaved: (value) { + _profile.name = value; + }, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Email'), + initialValue: _profile.email, + validator: (value) { + if (value == null) return null; // okay to leave out + if (value.isEmpty) { + return 'Please enter your email'; + } + if (!value.contains('@')) { + return 'Please enter a valid email'; + } + return null; + }, + onSaved: (value) { + _profile.email = value!; + }, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'Age'), + initialValue: _profile.age?.toString(), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null) return null; // okay to leave out + if (value.isEmpty) { + return 'Please enter your age'; + } + if (int.tryParse(value) == null) { + return 'Please enter a valid age'; + } + return null; + }, + onSaved: (value) { + _profile.age = int.parse(value!); + }, + ), + DropdownButtonFormField( + decoration: const InputDecoration(labelText: 'Gender'), + value: _profile.gender, + items: Gender.values + .map((gender) => DropdownMenuItem( + value: gender, + child: Text(gender.name), + )) + .toList(), + onChanged: (value) { + _profile.gender = value!; + }, + ), + const SizedBox(height: 16.0), + Row(children: [ + TextButton.icon( + onPressed: () { + if (_formKey.currentState!.validate()) { + _formKey.currentState!.save(); + repository.updateUserProfile(_profile); + Navigator.pop(context); + } + }, + icon: const Icon(Icons.save), + label: const Text('Save Changes'), + ), + const Spacer(), + const SignOutButton() + ]), + ] + .animate(interval: 100.ms) + .fade() + .slideX(begin: 1, duration: 300.ms), + ), + ), + ), + ), + ); + } +} diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart new file mode 100644 index 0000000..ee9f714 --- /dev/null +++ b/kilochat/lib/providers.dart @@ -0,0 +1,119 @@ +import 'dart:io'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:firebase_auth/firebase_auth.dart' as firebase; +import 'package:realm/realm.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'model.dart'; +import 'repository.dart'; + +part 'providers.g.dart'; + +final firebaseUserProvider = StateProvider( + (ref) => firebase.FirebaseAuth.instance.currentUser, +); + +class FocusedChannel extends Notifier { + @override + Channel? build() => null; + + void focus(Channel channel) => state = channel; +} + +final focusedChannelProvider = + NotifierProvider(FocusedChannel.new); + +@riverpod +Stream app(AppRef ref) async* { + final app = App(AppConfiguration('kilochat-app-ighux')); + yield app; + await for (final _ in Connectivity().onConnectivityChanged) { + app.reconnect(); + } +} + +@riverpod +Future localRealm(LocalRealmRef ref) async => + await Realm.open(Configuration.local([])); + +@riverpod +Stream> messages( + MessagesRef ref, + Channel? channel, +) async* { + final repository = await ref.watch(repositoryProvider.future); + if (channel == null) return; + yield repository.messages(channel); +} + +@riverpod +Future repository(RepositoryRef ref) async { + final realm = await ref.watch(syncedRealmProvider.future); + final user = await ref.watch(userProfileProvider.future); + return Repository(realm, user); +} + +@riverpod +Future syncedRealm(SyncedRealmRef ref) async { + final user = await ref.watch(userProvider.future); + final realm = await Realm.open(Configuration.flexibleSync( + user, + [ + Channel.schema, + Message.schema, + Reaction.schema, + UserProfile.schema, + ], + )); + realm.subscriptions.update((mutableSubscriptions) { + mutableSubscriptions + ..add(realm.all()) + ..add(realm.all()) + ..add(realm.all()) + ..add(realm.all()); + }); + await realm.subscriptions.waitForSynchronization(); + await realm.syncSession.waitForDownload(); + return realm; +} + +@riverpod +Stream user(UserRef ref) async* { + final app = await ref.watch(appProvider.future); + final firebaseUser = ref.watch(firebaseUserProvider); + + var user = app.currentUser; + if (user == null) { + if (firebaseUser != null) { + final jwt = await firebaseUser.getIdToken(); + user = await app.logIn(Credentials.jwt(jwt)); + } + } + if (user != null) yield user; +} + +@riverpod +Stream userProfile(UserProfileRef ref) async* { + final user = await ref.watch(userProvider.future); + final realm = await ref.watch(syncedRealmProvider.future); + final userProfile = realm.findOrAdd( + user.id, + (id) { + return UserProfile(id, id); + }, + ); + yield userProfile; + await for (final change in userProfile.changes) { + final deactivated = !change.isDeleted && change.object.deactivated; + if (deactivated) { + try { + await user.app.removeUser(user); + await firebase.FirebaseAuth.instance.signOut(); + } finally { + exit(64); + } + } + } +} diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart new file mode 100644 index 0000000..78fe8c5 --- /dev/null +++ b/kilochat/lib/providers.g.dart @@ -0,0 +1,197 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$appHash() => r'b3bc4e02df3b254bcad8a3e6db9f2030a06a0ce4'; + +/// See also [app]. +@ProviderFor(app) +final appProvider = AutoDisposeStreamProvider.internal( + app, + name: r'appProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$appHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef AppRef = AutoDisposeStreamProviderRef; +String _$localRealmHash() => r'31ba36028bf0e66b3453c3e4a89113e0e6ed5e92'; + +/// See also [localRealm]. +@ProviderFor(localRealm) +final localRealmProvider = AutoDisposeFutureProvider.internal( + localRealm, + name: r'localRealmProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$localRealmHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef LocalRealmRef = AutoDisposeFutureProviderRef; +String _$messagesHash() => r'c191f05ca61d99c07b2cd9d267be737b75892d5c'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +typedef MessagesRef = AutoDisposeStreamProviderRef>; + +/// See also [messages]. +@ProviderFor(messages) +const messagesProvider = MessagesFamily(); + +/// See also [messages]. +class MessagesFamily extends Family>> { + /// See also [messages]. + const MessagesFamily(); + + /// See also [messages]. + MessagesProvider call( + dynamic channel, + ) { + return MessagesProvider( + channel, + ); + } + + @override + MessagesProvider getProviderOverride( + covariant MessagesProvider provider, + ) { + return call( + provider.channel, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'messagesProvider'; +} + +/// See also [messages]. +class MessagesProvider + extends AutoDisposeStreamProvider> { + /// See also [messages]. + MessagesProvider( + this.channel, + ) : super.internal( + (ref) => messages( + ref, + channel, + ), + from: messagesProvider, + name: r'messagesProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$messagesHash, + dependencies: MessagesFamily._dependencies, + allTransitiveDependencies: MessagesFamily._allTransitiveDependencies, + ); + + final dynamic channel; + + @override + bool operator ==(Object other) { + return other is MessagesProvider && other.channel == channel; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, channel.hashCode); + + return _SystemHash.finish(hash); + } +} + +String _$repositoryHash() => r'9735e5fd79954002433a9b2b517fa72087982b00'; + +/// See also [repository]. +@ProviderFor(repository) +final repositoryProvider = AutoDisposeFutureProvider.internal( + repository, + name: r'repositoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$repositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef RepositoryRef = AutoDisposeFutureProviderRef; +String _$syncedRealmHash() => r'9554cdc8a5829d0efa00aaade4fa4915c85f7f69'; + +/// See also [syncedRealm]. +@ProviderFor(syncedRealm) +final syncedRealmProvider = AutoDisposeFutureProvider.internal( + syncedRealm, + name: r'syncedRealmProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$syncedRealmHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef SyncedRealmRef = AutoDisposeFutureProviderRef; +String _$userHash() => r'3162f8129e247644d301b4259e2b47c828d85eb4'; + +/// See also [user]. +@ProviderFor(user) +final userProvider = AutoDisposeStreamProvider.internal( + user, + name: r'userProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$userHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef UserRef = AutoDisposeStreamProviderRef; +String _$userProfileHash() => r'33b2c3a07b2745665b33664efb14839edf046050'; + +/// See also [userProfile]. +@ProviderFor(userProfile) +final userProfileProvider = AutoDisposeStreamProvider.internal( + userProfile, + name: r'userProfileProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$userProfileHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef UserProfileRef = AutoDisposeStreamProviderRef; +// 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 diff --git a/kilochat/lib/realm_ui.dart b/kilochat/lib/realm_ui.dart new file mode 100644 index 0000000..d209bed --- /dev/null +++ b/kilochat/lib/realm_ui.dart @@ -0,0 +1,167 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:realm/realm.dart'; + +typedef ItemWidgetBuilder = Widget Function( + BuildContext context, + E item, + Animation animation, +); + +typedef ErrorWidgetBuilder = Widget Function( + BuildContext context, + Object? error, + StackTrace stackTrace, +); + +extension on Stream> { + Stream> _updateAnimatedList( + GlobalKey listKey, + ItemWidgetBuilder removedItemBuilder, + ) async* { + RealmResults? previous; + await for (final change in this) { + final state = listKey.currentState; + if (previous != null) { + for (final index in change.deleted.reversed) { + final toDie = previous[index]; + state?.removeItem( + index, + (context, animation) => + removedItemBuilder(context, toDie, animation), + ); + } + } + for (final index in change.inserted) { + state?.insertItem(index); + } + final r = change.results; + previous = r.isValid ? r.freeze() : null; + yield change; + } + } +} + +class RealmAnimatedList extends StatefulWidget { + const RealmAnimatedList({ + super.key, + required this.results, + required this.itemBuilder, + ItemWidgetBuilder? removedItemBuilder, + this.loading, + this.error, + this.reverse = false, + this.controller, + this.scrollDirection = Axis.vertical, + }) : removedItemBuilder = removedItemBuilder ?? itemBuilder; + final RealmResults results; + final ItemWidgetBuilder itemBuilder; + final ItemWidgetBuilder removedItemBuilder; + + final WidgetBuilder? loading; + final ErrorWidgetBuilder? error; + + final bool reverse; + final ScrollController? controller; + final Axis scrollDirection; + + @override + State createState() => _RealmAnimatedListState(); +} + +extension on RealmResults { + bool get isLive => isValid && !isFrozen; + Stream> get safeChanges async* { + if (isLive) yield* changes; + } +} + +class _RealmAnimatedListState extends State> { + final _listKey = GlobalKey(); + StreamSubscription? _subscription; + + void _updateSubscription() { + _subscription?.cancel(); + _subscription = widget.results.safeChanges + .skip(1) // skip initial results + ._updateAnimatedList(_listKey, widget.removedItemBuilder) + .listen((_) {}); + } + + @override + void initState() { + super.initState(); + _updateSubscription(); + } + + @override + void didUpdateWidget(covariant RealmAnimatedList oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.results != oldWidget.results) { + _updateSubscription(); + } + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final items = widget.results; + return AnimatedList( + key: _listKey, + controller: widget.controller, + scrollDirection: widget.scrollDirection, + initialItemCount: items.length, + itemBuilder: (context, index, animation) => + index < items.length // why is this needed :-/ + ? widget.itemBuilder(context, items[index], animation) + : Container(), + reverse: widget.reverse, + ); + } +} + +class RealmSearchDelegate extends SearchDelegate { + final RealmResults Function(String query) resultsBuilder; + final ItemWidgetBuilder itemBuilder; + + RealmSearchDelegate(this.resultsBuilder, this.itemBuilder); + + @override + List buildActions(BuildContext context) { + // Create a list of actions to display in the app bar + return [ + IconButton( + icon: const Icon(Icons.clear), + onPressed: () => query = '', + ), + ]; + } + + @override + Widget buildLeading(BuildContext context) { + // Create a widget to display as the leading icon in the app bar + return IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => close(context, null), + ); + } + + @override + Widget buildResults(BuildContext context) { + return RealmAnimatedList( + results: resultsBuilder(query), + itemBuilder: itemBuilder, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + return buildResults(context); + } +} diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart new file mode 100644 index 0000000..f3d5132 --- /dev/null +++ b/kilochat/lib/repository.dart @@ -0,0 +1,81 @@ +import 'package:realm/realm.dart'; + +import 'model.dart'; + +class Repository { + final Realm _realm; + final UserProfile user; + + Repository(this._realm, this.user); + + late RealmResults allChannels = + _realm.query('TRUEPREDICATE SORT(name ASCENDING)'); + + late RealmResults allMessages = + _realm.query('TRUEPREDICATE SORT(id DESCENDING)'); + + RealmResults messages(Channel channel) => + _realm.query(r'channel == $0 SORT(id DESCENDING)', [channel]); + + void updateUserProfile(UserProfile newProfile) => + _realm.write(() => _realm.add(newProfile, update: true)); + + void postNewMessage(Channel channel, String text) { + _realm.write(() { + _realm.add(Message( + ObjectId(), + user.id, + channel.id, // not a link + text, + channel: channel, + owner: user, + )); + }); + } + + void editMessage(Message message, String text) => + _realm.write(() => message.text = text); + + void deleteMessage(Message message) => + _realm.write(() => _realm.delete(message)); + + RealmResults searchMessage(String text) { + return _realm.query( + r'text TEXT $0 SORT(id DESCENDING)', + [text], + ); + } + + void addReaction(Message message, String emoji) => + _realm.addOrUpdate(ReactionEx.create(user, message, emoji)); + + void deleteReaction(Reaction reaction) => + _realm.write(() => _realm.delete(reaction)); + + Channel createChannel(String name) => + _realm.write(() => _realm.add(Channel(ObjectId(), user.id, name, 0))); + + void subscribeToChannel(Channel channel) => + _realm.write(() => user.channels.add(channel)); + + void unsubscribeFromChannel(Channel channel) => + _realm.write(() => user.channels.remove(channel)); + + void deleteChannel(Channel channel) => + _realm.write(() => _realm.delete(channel)); + + RealmResults searchChannel(String name) { + return _realm.query( + r'name CONTAINS $0 SORT(name ASCENDING)', + [name], + ); + } +} + +extension RealmEx on Realm { + T findOrAdd(I id, T Function(I id) factory) => + write(() => find(id) ?? add(factory(id))); + + T addOrUpdate(T object) => + write(() => add(object, update: true)); +} diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart new file mode 100644 index 0000000..0895ed7 --- /dev/null +++ b/kilochat/lib/tiles.dart @@ -0,0 +1,195 @@ +import 'package:animated_emoji/emoji.dart'; +import 'package:animated_emoji/emojis.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kilochat/debug_widget.dart'; +import 'package:kilochat/realm_ui.dart'; + +import 'avatar.dart'; +import 'model.dart'; +import 'providers.dart'; + +class ChannelTile extends StatelessWidget { + final Channel channel; + final Animation animation; + final void Function()? onTap; + final void Function(DismissDirection)? onDismissed; + final bool selected; + + const ChannelTile({ + super.key, + required this.channel, + required this.animation, + this.onTap, + this.onDismissed, + this.selected = false, + }); + + @override + Widget build(BuildContext context) { + return AnimatedDismissibleTile( + key: ValueKey(channel.id), + animation: animation, + title: Text('# ${channel.name}'), + onTap: onTap, + onDismissed: onDismissed, + selected: selected, + ); + } +} + +class MessageTile extends ConsumerWidget { + const MessageTile({ + super.key, + required this.message, + required this.animation, + }); + + final Message message; + final Animation animation; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userProvider).value; + final repo = ref.watch(repositoryProvider).value; + + return AnimatedDismissibleTile( + key: ValueKey(message.id), + animation: animation, + leading: MyAvatar(user: message.owner), + title: DebugWidget( + child: Text(message.owner?.name ?? 'N/A ${message.ownerId}')), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyTransition(animation: animation, child: Text(message.text)), + SizedBox( + height: 50, + child: DebugWidget( + child: RealmAnimatedList( + results: message.reactions, + scrollDirection: Axis.horizontal, + itemBuilder: (context, reaction, animation) { + final owner = reaction.owner?.id == user?.id; + return MyTransition( + key: ValueKey(reaction.id), + axis: Axis.horizontal, + animation: animation, + child: Chip( + visualDensity: VisualDensity.compact, + avatar: MyAvatar(user: reaction.owner), + label: AnimatedEmoji( + AnimatedEmojiData( + 'u${reaction.emojiUnicode.toRadixString(16)}', + ), + repeat: false, + size: 20, + errorWidget: Text(reaction.emoji), + ), + onDeleted: + owner ? () => repo?.deleteReaction(reaction) : null, + ), + ); + }, + ), + ), + ) + ], + ), + trailing: IconButton( + icon: const Icon(Icons.add), + onPressed: () { + (ref.read(repositoryProvider.future)).then((repository) { + repository.addReaction(message, '\u{1f605}'); //'👍'); + }); + }, + ), + onDismissed: message.ownerId != user?.id + ? null + : (direction) async { + (await ref.read(repositoryProvider.future)) + .deleteMessage(message); + }, + ); + } +} + +class AnimatedDismissibleTile extends StatelessWidget { + final Animation animation; + final Widget? leading; + final Widget? trailing; + final Widget? title; + final Widget? subtitle; + final bool selected; + final void Function()? onTap; + final void Function()? onLongPress; + final void Function(DismissDirection direction)? onDismissed; + + const AnimatedDismissibleTile({ + required Key key, + required this.animation, + this.leading, + this.trailing, + this.title, + this.subtitle, + this.onTap, + this.onLongPress, + this.onDismissed, + this.selected = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final tile = MyTransition( + key: key, + animation: animation, + child: ListTile( + leading: leading, + trailing: trailing, + title: title, + subtitle: subtitle, + selected: selected, + onTap: onTap, + onLongPress: onLongPress, + )); + + if (onDismissed == null) return tile; + + final colorScheme = Theme.of(context).colorScheme; + return Dismissible( + key: key!, + background: Container(color: colorScheme.onErrorContainer), + onDismissed: onDismissed, + resizeDuration: null, + child: tile, + ); + } +} + +class MyTransition extends StatelessWidget { + const MyTransition({ + super.key, + required this.animation, + required this.child, + this.axis = Axis.vertical, + }); + + final Animation animation; + final Widget child; + final Axis axis; + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: animation, + child: SizeTransition( + axis: axis, + sizeFactor: animation, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: child, + ), + ), + ); + } +} diff --git a/kilochat/lib/widget_builders.dart b/kilochat/lib/widget_builders.dart new file mode 100644 index 0000000..49834d5 --- /dev/null +++ b/kilochat/lib/widget_builders.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +Widget buildErrorWidget(Object error, StackTrace? stackTrace) => + ErrorWidget(error); + +Widget buildLoadingWidget() => const Center(child: CircularProgressIndicator()); diff --git a/kilochat/macos/Flutter/GeneratedPluginRegistrant.swift b/kilochat/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..2f9d55e --- /dev/null +++ b/kilochat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,20 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import connectivity_plus +import desktop_webview_auth +import firebase_auth +import firebase_core +import realm + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DesktopWebviewAuthPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewAuthPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + RealmPlugin.register(with: registry.registrar(forPlugin: "RealmPlugin")) +} diff --git a/kilochat/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/kilochat/macos/Flutter/ephemeral/Flutter-Generated.xcconfig new file mode 100644 index 0000000..b68427d --- /dev/null +++ b/kilochat/macos/Flutter/ephemeral/Flutter-Generated.xcconfig @@ -0,0 +1,11 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/Users/kasper/Projects/flutter/master +FLUTTER_APPLICATION_PATH=/Users/kasper/Projects/mongodb/experiments/kilochat/kilochat +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=1.0.0 +FLUTTER_BUILD_NUMBER=1 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/kilochat/macos/Flutter/ephemeral/flutter_export_environment.sh b/kilochat/macos/Flutter/ephemeral/flutter_export_environment.sh new file mode 100755 index 0000000..d4f7a6f --- /dev/null +++ b/kilochat/macos/Flutter/ephemeral/flutter_export_environment.sh @@ -0,0 +1,12 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/Users/kasper/Projects/flutter/master" +export "FLUTTER_APPLICATION_PATH=/Users/kasper/Projects/mongodb/experiments/kilochat/kilochat" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=1.0.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/kilochat/macos/Runner/DebugProfile.entitlements b/kilochat/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..08c3ab1 --- /dev/null +++ b/kilochat/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/kilochat/macos/Runner/Release.entitlements b/kilochat/macos/Runner/Release.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/kilochat/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock new file mode 100644 index 0000000..33f428f --- /dev/null +++ b/kilochat/pubspec.lock @@ -0,0 +1,1127 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: "405666cd3cf0ee0a48d21ec67e65406aad2c726d9fa58840d3375e7bdcd32a07" + url: "https://pub.dev" + source: hosted + version: "60.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: a742f71d7f3484253a623b30e19256aa4668ecbb3de6ad1beb0bcf8d4777ecd8 + url: "https://pub.dev" + source: hosted + version: "1.3.3" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "1952250bd005bacb895a01bf1b4dc00e3ba1c526cf47dca54dfe24979c65f5b3" + url: "https://pub.dev" + source: hosted + version: "5.12.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" + animated_emoji: + dependency: "direct main" + description: + name: animated_emoji + sha256: "1be7a676dffa68c3d3c4230bf89b064afb461a7b64e7d21fe112519d643d9544" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + archive: + dependency: transitive + description: + name: archive + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + url: "https://pub.dev" + source: hosted + version: "3.3.7" + args: + dependency: transitive + description: + name: args + sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + build_cli_annotations: + dependency: transitive + description: + name: build_cli_annotations + sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 + url: "https://pub.dev" + source: hosted + version: "2.1.0" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + build_runner: + dependency: transitive + description: + name: build_runner + sha256: "5e1929ad37d48bd382b124266cb8e521de5548d406a45a5ae6656c13dab73e37" + url: "https://pub.dev" + source: hosted + version: "2.4.5" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + url: "https://pub.dev" + source: hosted + version: "7.2.10" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + url: "https://pub.dev" + source: hosted + version: "8.6.1" + cancellation_token: + dependency: transitive + description: + name: cancellation_token + sha256: "44891ef71d605bc59ef7974c403630d8e8506fcd897a29c3e38466ef69e5c4eb" + url: "https://pub.dev" + source: hosted + version: "1.6.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" + source: hosted + version: "4.5.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + url: "https://pub.dev" + source: hosted + version: "1.17.2" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "8599ae9edca5ff96163fca3e36f8e481ea917d1e71cdad912c084b5579913f34" + url: "https://pub.dev" + source: hosted + version: "4.0.1" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" + source: hosted + version: "1.2.4" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "3ce36c04d30c60cde295588c6185b3f9800e6c18f6670a7ffdb3d5eab39bb942" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: "73d09c9848e9f6d5c3e0a1809eac841a8d7ea123d0849feefa040e1ad60b6d06" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "9170d9db2daf774aa2251a3bc98e4ba903c7702ab07aa438bc83bd3c9a0de57f" + url: "https://pub.dev" + source: hosted + version: "0.4.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + url: "https://pub.dev" + source: hosted + version: "2.3.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + desktop_webview_auth: + dependency: transitive + description: + name: desktop_webview_auth + sha256: f90d9fa88344fb1446d4f876ee2a12d07f4345f96fdb787d3ce1c76bd9d41feb + url: "https://pub.dev" + source: hosted + version: "0.0.13" + email_validator: + dependency: transitive + description: + name: email_validator + sha256: e9a90f27ab2b915a27d7f9c2a7ddda5dd752d6942616ee83529b686fc086221b + url: "https://pub.dev" + source: hosted + version: "2.1.17" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: f693c0aa998b1101453878951b171b69f0db5199003df1c943b33493a1de7917 + url: "https://pub.dev" + source: hosted + version: "4.6.3" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "689ae048b78ad088ba31acdec45f5badb56201e749ed8b534947a7303ddb32aa" + url: "https://pub.dev" + source: hosted + version: "6.15.3" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: f35d637a1707afd51f30090bb5234b381d5071ccbfef09b8c393bc7c65e440cd + url: "https://pub.dev" + source: hosted + version: "5.5.3" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: a4a99204da264a0aa9d54a332ea0315ce7b0768075139c77abefe98093dd98be + url: "https://pub.dev" + source: hosted + version: "2.14.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 + url: "https://pub.dev" + source: hosted + version: "4.8.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "0fd5c4b228de29b55fac38aed0d9e42514b3d3bd47675de52bf7f8fccaf922fa" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + firebase_dynamic_links: + dependency: transitive + description: + name: firebase_dynamic_links + sha256: "9b984d0abd227a702451a997abcca763f4dbf67e260dad60e5506d55e3eff244" + url: "https://pub.dev" + source: hosted + version: "5.3.3" + firebase_dynamic_links_platform_interface: + dependency: transitive + description: + name: firebase_dynamic_links_platform_interface + sha256: "6ef00a0be18f3231e9727f7c4b31db89dbfa16792098beb850603c30854560ff" + url: "https://pub.dev" + source: hosted + version: "0.2.6+3" + firebase_ui_auth: + dependency: "direct main" + description: + name: firebase_ui_auth + sha256: efb4a77c2784158c1b332594499bd7e1b4c59d5d3bc5d694b5aae6ba5b6e249b + url: "https://pub.dev" + source: hosted + version: "1.5.0" + firebase_ui_localizations: + dependency: transitive + description: + name: firebase_ui_localizations + sha256: b13be7432af3eed2ff6f2ed1c55a9afc32ffa7376fcada9e16be055fad6415ed + url: "https://pub.dev" + source: hosted + version: "1.5.0" + firebase_ui_oauth: + dependency: transitive + description: + name: firebase_ui_oauth + sha256: "1dfbb637093419809a1451b2550ac57180c91580ba15cad71332b16282785f17" + url: "https://pub.dev" + source: hosted + version: "1.4.4" + firebase_ui_oauth_apple: + dependency: "direct main" + description: + name: firebase_ui_oauth_apple + sha256: "47b135f64f5f29e671f213fe6a82b662bb7c6ae89d240ee6446db3d4d49b5dcf" + url: "https://pub.dev" + source: hosted + version: "1.2.4" + firebase_ui_oauth_facebook: + dependency: "direct main" + description: + name: firebase_ui_oauth_facebook + sha256: "146f24e9e4c273488513248907e7f4a15ba3f13409b0e26db2d455056b1f283b" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + firebase_ui_oauth_google: + dependency: "direct main" + description: + name: firebase_ui_oauth_google + sha256: a622b9dbb658809d82a18575a2aa46166ca35f0c640ced509efe3819ca29cbe1 + url: "https://pub.dev" + source: hosted + version: "1.2.4" + firebase_ui_shared: + dependency: transitive + description: + name: firebase_ui_shared + sha256: "6f36f067d955d41591aacf68aafbaec7053571f2f6ed495da8bfa803f7c633b7" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: be54662837a6e66cc53ee88549e808c625275e0faf5a43e11cf3182cb0bd1b02 + url: "https://pub.dev" + source: hosted + version: "4.2.0" + flutter_facebook_auth: + dependency: transitive + description: + name: flutter_facebook_auth + sha256: "50dc3eef562acbe1e4cfad478053c9c16f9eaac49ad14ec48f00ed9dae1ba0cd" + url: "https://pub.dev" + source: hosted + version: "4.4.1+1" + flutter_facebook_auth_platform_interface: + dependency: transitive + description: + name: flutter_facebook_auth_platform_interface + sha256: "7950f5f8a6f2270c5d29af2a514733987db1191f70838fa777b282e47365f8c8" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flutter_facebook_auth_web: + dependency: transitive + description: + name: flutter_facebook_auth_web + sha256: "0f732e968c929a3c11a215ded802557576230ff0a0794c88941a8e92ff07b2eb" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 + url: "https://pub.dev" + source: hosted + version: "2.3.6" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + url: "https://pub.dev" + source: hosted + version: "2.0.7" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "7940fdc3b1035db4d65d387c1bdd6f9574deaa6777411569c05ecc25672efacd" + url: "https://pub.dev" + source: hosted + version: "0.2.1" + google_sign_in: + dependency: transitive + description: + name: google_sign_in + sha256: aab6fdc41374014494f9e9026b9859e7309639d50a0bf4a2a412467a5ae4abc6 + url: "https://pub.dev" + source: hosted + version: "6.1.4" + google_sign_in_android: + dependency: transitive + description: + name: google_sign_in_android + sha256: f58a17ac07d783d000786a6c313fa4a0d2ee599a346d69b24fc48fb378d5d150 + url: "https://pub.dev" + source: hosted + version: "6.1.16" + google_sign_in_ios: + dependency: transitive + description: + name: google_sign_in_ios + sha256: "6ec0e13a4c5c646471b9f6a25ceb3ae76d339889d4c0f79b729bf0714215a63e" + url: "https://pub.dev" + source: hosted + version: "5.6.2" + google_sign_in_platform_interface: + dependency: transitive + description: + name: google_sign_in_platform_interface + sha256: e69553c0fc6a76216e9d06a8c3767e291ad9be42171f879aab7ab708569d4393 + url: "https://pub.dev" + source: hosted + version: "2.4.1" + google_sign_in_web: + dependency: transitive + description: + name: google_sign_in_web + sha256: "69b9ce0e760945ff52337921a8b5871592b74c92f85e7632293310701eea68cc" + url: "https://pub.dev" + source: hosted + version: "0.12.0+2" + graphs: + dependency: transitive + description: + name: graphs + sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "728c0613556c1d153f7e7f4a367cffacc3f5a677d7f6497a1c2b35add4e6dacf" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + http: + dependency: transitive + description: + name: http + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + url: "https://pub.dev" + source: hosted + version: "4.8.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + lottie: + dependency: transitive + description: + name: lottie + sha256: f461105d3a35887b27089abf9c292334478dd292f7b47ecdccb6ae5c37a22c80 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + url: "https://pub.dev" + source: hosted + version: "0.12.16" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + meta: + dependency: transitive + description: + name: meta + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + objectid: + dependency: transitive + description: + name: objectid + sha256: "22fa972000d3256f10d06323a9dcbf4b564fb03fdb9024399e3a6c1d9902f914" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" + source: hosted + version: "1.0.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + url: "https://pub.dev" + source: hosted + version: "5.4.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + url: "https://pub.dev" + source: hosted + version: "3.7.3" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + quiver: + dependency: transitive + description: + name: quiver + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" + source: hosted + version: "3.2.1" + random_avatar: + dependency: "direct main" + description: + name: random_avatar + sha256: "1468b060ac4324fa4f6aeeced732079638d9b6a64838b705ecbe9f208bd1609b" + url: "https://pub.dev" + source: hosted + version: "0.0.8" + realm: + dependency: "direct main" + description: + name: realm + sha256: "818bd06d65cf736dab3e41111a9f641d129083a6056788544ea6a94697fbac2e" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + realm_common: + dependency: transitive + description: + name: realm_common + sha256: b7fefe203362d9afc21094fdc1d97c9ada95c25f80fc5c352a0dd3f3cbf34625 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + realm_generator: + dependency: transitive + description: + name: realm_generator + sha256: "22a3b16b3f1830af221d7b07c847e85a44873a82d8b90f67bdd81cc4a2b64f2a" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + riverpod: + dependency: "direct main" + description: + name: riverpod + sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" + url: "https://pub.dev" + source: hosted + version: "2.3.6" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "1b2632a6fc0b659c923a4dcc7cd5da42476f5b3294c70c86c971e63bdd443384" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: cedd6a54b6f5764ffd5c05df57b6676bfc8c01978e14ee60a2c16891038820fe + url: "https://pub.dev" + source: hosted + version: "2.1.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: cd0595de57ccf5d944ff4b0f68289e11ac6a2eff1e3dfd1d884a43f6f3bcee5e + url: "https://pub.dev" + source: hosted + version: "2.2.3" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "043ff016011be4c5887b3239bfbca05d284bdb68db0a5363cee0242b7567e250" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" + source: hosted + version: "0.27.7" + sane_uuid: + dependency: transitive + description: + name: sane_uuid + sha256: "615f6d25e6d7e0c59ea0fb5ce0cdf043ce110af15b19b3c35ca1441e5bb06972" + url: "https://pub.dev" + source: hosted + version: "1.0.0-alpha.4" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" + source: hosted + version: "0.7.2+1" + statsfl: + dependency: "direct main" + description: + name: statsfl + sha256: "12901533eee23cc04f01858f1e6851beec27d927a9afa694bb3d68403a5e0d13" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + tar: + dependency: transitive + description: + name: tar + sha256: "85ffd53e277f2bac8afa2885e6b195e26937e9c402424c3d88d92fd920b56de9" + url: "https://pub.dev" + source: hosted + version: "0.5.6" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + url: "https://pub.dev" + source: hosted + version: "1.1.7" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b8c67f5fa3897b122cf60fe9ff314f7b0ef71eab25c5f8b771480bc338f48823 + url: "https://pub.dev" + source: hosted + version: "11.7.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + url: "https://pub.dev" + source: hosted + version: "2.4.0" + xml: + dependency: transitive + description: + name: xml + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" +sdks: + dart: ">=3.1.0-185.0.dev <4.0.0" + flutter: ">=3.10.2" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml new file mode 100644 index 0000000..4050e6a --- /dev/null +++ b/kilochat/pubspec.yaml @@ -0,0 +1,54 @@ +name: kilochat +description: A chat app built with Flutter, Realm and Atlas. + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: ^3.0.0 + +dependencies: + flutter: + sdk: flutter + + animated_emoji: ^2.0.0 + collection: ^1.17.0 + connectivity_plus: ^4.0.1 + firebase_auth: ^4.2.10 + firebase_core: ^2.8.0 + firebase_ui_auth: ^1.1.16 + firebase_ui_oauth_apple: ^1.0.23 + firebase_ui_oauth_facebook: ^1.0.23 + firebase_ui_oauth_google: ^1.0.23 + flutter_animate: ^4.0.0 + flutter_riverpod: ^2.3.0 + random_avatar: ^0.0.8 + realm: ^1.0.3 + riverpod_annotation: ^2.0.0 + riverpod: ^2.3.0 + statsfl: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^2.0.0 + custom_lint: ^0.4.0 + riverpod_lint: any + riverpod_generator: ^2.0.0 + +# dependency_overrides: +# flutter_svg: ^2.0.0 # to force resolve conflict between firebase_ui_auth and random_avatar :-/ +# intl: 0.18.0 + +flutter: + uses-material-design: true + fonts: + - family: SocialIcons + fonts: + - asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf + +# dependency_overrides: +# realm: +# path: ../../realm-dart/flutter/realm_flutter From 9cf780b3e177248f92013d7e1a05ce953d6cd078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 29 Jun 2023 12:46:52 +0200 Subject: [PATCH 02/33] Bump minSdkVersion to 19 on Android --- kilochat/android/app/build.gradle | 72 +++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 kilochat/android/app/build.gradle diff --git a/kilochat/android/app/build.gradle b/kilochat/android/app/build.gradle new file mode 100644 index 0000000..7939b25 --- /dev/null +++ b/kilochat/android/app/build.gradle @@ -0,0 +1,72 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + namespace "com.example.kilochat" + compileSdkVersion flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.example.kilochat" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion 19 // bump to support firebase + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} From 9aab00f249a1366cb3f9fe59b93c9bee7e746701 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 29 Jun 2023 12:49:27 +0200 Subject: [PATCH 03/33] Minimize macos platform project --- .../Flutter/GeneratedPluginRegistrant.swift | 20 ------------------- .../ephemeral/Flutter-Generated.xcconfig | 11 ---------- .../ephemeral/flutter_export_environment.sh | 12 ----------- 3 files changed, 43 deletions(-) delete mode 100644 kilochat/macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 kilochat/macos/Flutter/ephemeral/Flutter-Generated.xcconfig delete mode 100755 kilochat/macos/Flutter/ephemeral/flutter_export_environment.sh diff --git a/kilochat/macos/Flutter/GeneratedPluginRegistrant.swift b/kilochat/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 2f9d55e..0000000 --- a/kilochat/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import connectivity_plus -import desktop_webview_auth -import firebase_auth -import firebase_core -import realm - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) - DesktopWebviewAuthPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewAuthPlugin")) - FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) - FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - RealmPlugin.register(with: registry.registrar(forPlugin: "RealmPlugin")) -} diff --git a/kilochat/macos/Flutter/ephemeral/Flutter-Generated.xcconfig b/kilochat/macos/Flutter/ephemeral/Flutter-Generated.xcconfig deleted file mode 100644 index b68427d..0000000 --- a/kilochat/macos/Flutter/ephemeral/Flutter-Generated.xcconfig +++ /dev/null @@ -1,11 +0,0 @@ -// This is a generated file; do not edit or check into version control. -FLUTTER_ROOT=/Users/kasper/Projects/flutter/master -FLUTTER_APPLICATION_PATH=/Users/kasper/Projects/mongodb/experiments/kilochat/kilochat -COCOAPODS_PARALLEL_CODE_SIGN=true -FLUTTER_BUILD_DIR=build -FLUTTER_BUILD_NAME=1.0.0 -FLUTTER_BUILD_NUMBER=1 -DART_OBFUSCATION=false -TRACK_WIDGET_CREATION=true -TREE_SHAKE_ICONS=false -PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/kilochat/macos/Flutter/ephemeral/flutter_export_environment.sh b/kilochat/macos/Flutter/ephemeral/flutter_export_environment.sh deleted file mode 100755 index d4f7a6f..0000000 --- a/kilochat/macos/Flutter/ephemeral/flutter_export_environment.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -# This is a generated file; do not edit or check into version control. -export "FLUTTER_ROOT=/Users/kasper/Projects/flutter/master" -export "FLUTTER_APPLICATION_PATH=/Users/kasper/Projects/mongodb/experiments/kilochat/kilochat" -export "COCOAPODS_PARALLEL_CODE_SIGN=true" -export "FLUTTER_BUILD_DIR=build" -export "FLUTTER_BUILD_NAME=1.0.0" -export "FLUTTER_BUILD_NUMBER=1" -export "DART_OBFUSCATION=false" -export "TRACK_WIDGET_CREATION=true" -export "TREE_SHAKE_ICONS=false" -export "PACKAGE_CONFIG=.dart_tool/package_config.json" From 6f2324c8a9a0cb8ded34ab004332cca53f150d80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 29 Jun 2023 11:56:43 +0200 Subject: [PATCH 04/33] Update README (wip) --- kilochat/README.md | 132 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 1 deletion(-) diff --git a/kilochat/README.md b/kilochat/README.md index 2c5fe5e..0519f2e 100644 --- a/kilochat/README.md +++ b/kilochat/README.md @@ -1,6 +1,136 @@ # kilochat -A sample chat application build with Flutter, Realm and Atlas Device Sync. +A sample chat application loosely based on slack, but build with Flutter, Realm, and Atlas Device Sync. + +It aims to give a more thorough and realistic example of how to use Realm and Atlas Device Sync with Flutter, than most of the sample apps. + +The app demonstrates how to: +1. Re-use credentials on app restart to allow for _offline login_ +1. Use federated jwt-based authentication with Firebase Auth. +1. React to connectivity changes, displaying current state. +1. Handle soft synchronization errors. +1. Handle client resets (hard sync errors). +1. Display a snackbar when interesting data is synced to device. +1. Animate list views as changes happen in a realm that impacts a live query. +1. Update flexible sync subscriptions to only sync a subset of data dynamically. +1. Handle presence information in a scalable way, using a body-list. +1. Maintain a leader-board collection using an Atlas function that trigger on changes. +1. Use rule-based permissions to ensure users can only manipulate their own data. + +It also serves as an example of an app architecture that works well with Realm. +Depending on how you choose to count, it does so in just 1-2K lines of code, hence the name. + +## Getting started + +### Prerequisites + +You need Flutter 3.10.2 or later. + +Create the platform projects you are interested in. Only the bare minimum modifications are added in the repo, so you need to create the platform projects on your end. You can choose any platforms except web. +```shell +flutter create . --platforms=android,ios,macos,windows,linux # realm does not support web (yet) +flutter pub get +``` + +You will need to login with either a google account or email and password. For the latter we have setup 9 different test users (`test1@test.com`, ..., `test9@test.com`), with passwords matching pattern `test!Atlas`. There is no ongoing moderation, so please behave or we will have to disable these shared accounts. + +## Setting up your own backend + +To make it easy to get started we have a shared backend setup already, but be aware that it is managed by us at MongoDB and shared with the entire world. + +> :warning: This also means that this repo contains a set of Firebase API keys, which while **not** really [secret](https://firebase.google.com/docs/projects/api-keys) as such, is not something we would recommend in general. +> +> The repo also contains an atlas application identifier (`app_id`) which is **not** a secret, but should not be shared if the app service has developer mode enabled. The reason for this being that developer mode allows clients to do automatic schema changes + +To setup your own backend you need to setup both Firebase Auth with Google and Atlas Device Sync with MongoDB. + +You will need the following CLI tools installed to follow these instructions: +- `flutterfire`, +- `atlas`, and +- `atlas-app-services-cli`. + +You can get these with: +```shell +dart pub global activate flutterfire +npm install -g atlas-app-services-cli # TODO: is this officially released yet? (or fo we need to use realm-cli?) +brew install mongodb_atlas_cli # TODO: on macos +``` + +### Firebase Auth + +You can follow Google's [instructions](https://firebase.google.com/docs/auth/flutter/start) for setting up Firebase Authentication with Flutter. + +### Atlas Device Sync + +First you need to setup a cluster. +Then you need to setup Atlas Device Sync. +Then you need to push the schema. Normally during development you would just enable developer mode, and have the client inform the server of the schema implicitly, but this is not how you would go about it in production, so for this sample we will not enable developer mode. +## Offline login + +There really is no such thing as _offline login_. A server handshake is always required to perform a login. However realm will cache the returned credentials, and you can re-use these on app restart. + +This is done simply by checking `app.currentUser` +```dart +final app = App(AppConfiguration(...)); +final user = app.currentUser; +``` +if `user` is not null there is already a valid logged-in user, otherwise we need to initiate authentication. +This is similar to how your email client works on your phone. + +Realm will try to refresh the associated tokens whenever needed. Obviously that will require the device to be online, but as tokens only need to be valid when actually performing requests, this will require the device to be online anyway. For most practical purposes a user only needs to login on a given device once. + +In this sample we federate authentication to firebase. Firebase also persists the currently logged in user, so even if `app.currentUser == null` then we may still have a valid jwt token from firebase, so that the user avoid actually performing authentication. + + +## Subscriptions + +For scalability we don't want to mindlessly push all messages to all users. Instead we need to define what messages a given user is interested in. This is done by maintaining a set of channels that a given user is +currently _subscribed_ to on the user's profile. The user can search for, add and remove channels in the app. + +When this happens the subscriptions on the realm needs to be updated. Note that this needs to happen on all devices where the user is currently logged in, as the set of subscribed channels is user-, not device- specific. Hence we need to sync this information and react to changes on all devices where the user is +currently logged in. + +## Presence + +Presence information is the small green, empty, or sleeping orb you see on other users avatar in apps like slack. + +The first problem with presence information is that you want to update it even if a user is _not_ present. Typically this is done by having each user send heart-beats, so that a missing heart-beat indicates the user is not currently present. + +A naive approach ends up sending every heart-beat to every user. Creating a storm of communication in an n-complete graph (n being the number of users). + +Instead it is better for the server to detect the missing heart-beat and only update the presence of a user +when it happens. This information can then be pushed out to other online users. If a device is itself offline +it should indicate that the presence information of other users are unreliable, since it may have missed an update. The actual heart-beats are prime candidates for asymmetric sync, but we don't support that yet in the +Flutter Realm SDK, so instead we will just update a `heartbeat` timestamp on the `UserProfile`. + +Still, this can amount to a lot of updates. To further reduce the amount of pushed presence data it is common to use body-lists. For each user we maintain a lists of other users that they are interested to know presence about. Explicit friends, or just people they recently communicated with. + +In a chat-app like this one you also typically want _super-fresh_ information about who is typing messages in the current channel. + +A last thing to notice is that it is okay to skip to the latest presence information in most cases. It is +not important for a user to know how many time a friend went online/offline while she herself was offline. +Realm is build to deliver every update in a reliable manner, but by "resetting" the presence subscription +on startup we can indicate it is okay to skip over un-synced data. + +The presence system used here is still rather simple. We refer t [A Scalable Server Architecture for Mobile Presence Service in Social Network Applications](https://www.researchgate.net/publication/232657317_A_Scalable_Server_Architecture_for_Mobile_Presence_Service_in_Social_Network_Applications) for the interested reader. + +## Leader-board + +## Rule-based permissions + +Kilochat is a mostly public system, yet we still wan't to prevent bad actors from impersonating others, and make changes to other peoples data, be it their messages, reactions, or user profile. + +While the app as implemented won't allow such edits, we cannot rely on the app to enforce this. A malicious actor could reverse engineer the app and implement a version without such restrictions. + +Instead it falls on the server to enforce the rules of the system. In our case it is very simple. A user cannot edit objects created by other users, and cannot impersonate others. + +This has some implications on our model classes. First, all model classes has an extra `ownerId` property that must be filled with `user.id` upon creation. The backend will check that `ownerId` on any edit match the user id of the session, or revoke the edit (/creation). If this happens, the perpetrator will receive a compensating write, and undo the invalid change, assuming it was due to a bug and not a malicious intent. + +A more subtle implication is that a message cannot contain a list of reactions, as only the message owner would be able to update this list. Instead a reaction refer to the message it pertains to and the reactions are accessible from the message via a named backlink property. Each reaction is owned by the user who added it. + +The ownership role goes for channels as well. Anyone can create a channel, but only the owner can update or +delete it. The ability to create channels would usually be reserved for admin users, but alas this is after +all just a toy system. # DISCLAIMER From edca42ca6d1934ed28fad46930f0b14c00fe0521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 29 Jun 2023 20:34:08 +0200 Subject: [PATCH 05/33] WIP --- kilochat/README.md | 38 ++++++++++++++++++++++---------------- kilochat/lib/model.dart | 4 ++-- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/kilochat/README.md b/kilochat/README.md index 0519f2e..d74923a 100644 --- a/kilochat/README.md +++ b/kilochat/README.md @@ -1,10 +1,11 @@ # kilochat -A sample chat application loosely based on slack, but build with Flutter, Realm, and Atlas Device Sync. +A sample chat application loosely based on slack, but build with Flutter, Realm, and Atlas Device Sync. It aims to give a more thorough and realistic example of how to use Realm and Atlas Device Sync with Flutter, than most of the sample apps. The app demonstrates how to: + 1. Re-use credentials on app restart to allow for _offline login_ 1. Use federated jwt-based authentication with Firebase Auth. 1. React to connectivity changes, displaying current state. @@ -27,6 +28,7 @@ Depending on how you choose to count, it does so in just 1-2K lines of code, hen You need Flutter 3.10.2 or later. Create the platform projects you are interested in. Only the bare minimum modifications are added in the repo, so you need to create the platform projects on your end. You can choose any platforms except web. + ```shell flutter create . --platforms=android,ios,macos,windows,linux # realm does not support web (yet) flutter pub get @@ -40,16 +42,18 @@ To make it easy to get started we have a shared backend setup already, but be aw > :warning: This also means that this repo contains a set of Firebase API keys, which while **not** really [secret](https://firebase.google.com/docs/projects/api-keys) as such, is not something we would recommend in general. > -> The repo also contains an atlas application identifier (`app_id`) which is **not** a secret, but should not be shared if the app service has developer mode enabled. The reason for this being that developer mode allows clients to do automatic schema changes +> The repo also contains an atlas application identifier (`app_id`) which is **not** a secret, but should not be shared if the app service has developer mode enabled. The reason for this being that developer mode allows clients to do automatic schema changes To setup your own backend you need to setup both Firebase Auth with Google and Atlas Device Sync with MongoDB. You will need the following CLI tools installed to follow these instructions: -- `flutterfire`, -- `atlas`, and + +- `flutterfire`, +- `atlas`, and - `atlas-app-services-cli`. You can get these with: + ```shell dart pub global activate flutterfire npm install -g atlas-app-services-cli # TODO: is this officially released yet? (or fo we need to use realm-cli?) @@ -58,36 +62,38 @@ brew install mongodb_atlas_cli # TODO: on macos ### Firebase Auth -You can follow Google's [instructions](https://firebase.google.com/docs/auth/flutter/start) for setting up Firebase Authentication with Flutter. +You can follow Google's [instructions](https://firebase.google.com/docs/auth/flutter/start) for setting up Firebase Authentication with Flutter. ### Atlas Device Sync First you need to setup a cluster. Then you need to setup Atlas Device Sync. Then you need to push the schema. Normally during development you would just enable developer mode, and have the client inform the server of the schema implicitly, but this is not how you would go about it in production, so for this sample we will not enable developer mode. + ## Offline login There really is no such thing as _offline login_. A server handshake is always required to perform a login. However realm will cache the returned credentials, and you can re-use these on app restart. This is done simply by checking `app.currentUser` + ```dart final app = App(AppConfiguration(...)); final user = app.currentUser; ``` + if `user` is not null there is already a valid logged-in user, otherwise we need to initiate authentication. -This is similar to how your email client works on your phone. +This is similar to how your email client works on your phone. Realm will try to refresh the associated tokens whenever needed. Obviously that will require the device to be online, but as tokens only need to be valid when actually performing requests, this will require the device to be online anyway. For most practical purposes a user only needs to login on a given device once. -In this sample we federate authentication to firebase. Firebase also persists the currently logged in user, so even if `app.currentUser == null` then we may still have a valid jwt token from firebase, so that the user avoid actually performing authentication. - +In this sample we federate authentication to firebase. Firebase also persists the currently logged in user, so even if `app.currentUser == null` then we may still have a valid jwt token from firebase, so that the user avoid actually performing authentication. ## Subscriptions -For scalability we don't want to mindlessly push all messages to all users. Instead we need to define what messages a given user is interested in. This is done by maintaining a set of channels that a given user is +For scalability we don't want to mindlessly push all messages to all users. Instead we need to define what messages a given user is interested in. This is done by maintaining a set of channels that a given user is currently _subscribed_ to on the user's profile. The user can search for, add and remove channels in the app. -When this happens the subscriptions on the realm needs to be updated. Note that this needs to happen on all devices where the user is currently logged in, as the set of subscribed channels is user-, not device- specific. Hence we need to sync this information and react to changes on all devices where the user is +When this happens the subscriptions on the realm needs to be updated. Note that this needs to happen on all devices where the user is currently logged in, as the set of subscribed channels is user-, not device- specific. Hence we need to sync this information and react to changes on all devices where the user is currently logged in. ## Presence @@ -99,7 +105,7 @@ The first problem with presence information is that you want to update it even i A naive approach ends up sending every heart-beat to every user. Creating a storm of communication in an n-complete graph (n being the number of users). Instead it is better for the server to detect the missing heart-beat and only update the presence of a user -when it happens. This information can then be pushed out to other online users. If a device is itself offline +when it happens. This information can then be pushed out to other online users. If a device is itself offline it should indicate that the presence information of other users are unreliable, since it may have missed an update. The actual heart-beats are prime candidates for asymmetric sync, but we don't support that yet in the Flutter Realm SDK, so instead we will just update a `heartbeat` timestamp on the `UserProfile`. @@ -107,7 +113,7 @@ Still, this can amount to a lot of updates. To further reduce the amount of push In a chat-app like this one you also typically want _super-fresh_ information about who is typing messages in the current channel. -A last thing to notice is that it is okay to skip to the latest presence information in most cases. It is +A last thing to notice is that it is okay to skip to the latest presence information in most cases. It is not important for a user to know how many time a friend went online/offline while she herself was offline. Realm is build to deliver every update in a reliable manner, but by "resetting" the presence subscription on startup we can indicate it is okay to skip over un-synced data. @@ -120,7 +126,7 @@ The presence system used here is still rather simple. We refer t [A Scalable Ser Kilochat is a mostly public system, yet we still wan't to prevent bad actors from impersonating others, and make changes to other peoples data, be it their messages, reactions, or user profile. -While the app as implemented won't allow such edits, we cannot rely on the app to enforce this. A malicious actor could reverse engineer the app and implement a version without such restrictions. +While the app as implemented won't allow such edits, we cannot rely on the app to enforce this. A malicious actor could reverse engineer the app and implement a version without such restrictions. Instead it falls on the server to enforce the rules of the system. In our case it is very simple. A user cannot edit objects created by other users, and cannot impersonate others. @@ -128,10 +134,10 @@ This has some implications on our model classes. First, all model classes has an A more subtle implication is that a message cannot contain a list of reactions, as only the message owner would be able to update this list. Instead a reaction refer to the message it pertains to and the reactions are accessible from the message via a named backlink property. Each reaction is owned by the user who added it. -The ownership role goes for channels as well. Anyone can create a channel, but only the owner can update or -delete it. The ability to create channels would usually be reserved for admin users, but alas this is after +The ownership role goes for channels as well. Anyone can create a channel, but only the owner can update or +delete it. The ability to create channels would usually be reserved for admin users, but alas this is after all just a toy system. # DISCLAIMER -This is low priority work in progress. Eventually we hope to make this a well documented example of best practices, but that is *not* the current state. \ No newline at end of file +This is low priority work in progress. Eventually we hope to make this a well documented example of best practices, but that is _not_ the current state. diff --git a/kilochat/lib/model.dart b/kilochat/lib/model.dart index 4e3cff5..0b59030 100644 --- a/kilochat/lib/model.dart +++ b/kilochat/lib/model.dart @@ -13,7 +13,7 @@ class _Channel { late _Channel? parent; late String name; - late int count; + late int count; // not used yet (await RealmInteger support) } @RealmModel() @@ -104,7 +104,7 @@ class _UserProfile { int? age; @MapTo('gender') - int genderAsInt = 0; // Gender.unknown.index; <-- not a const expression + int genderAsInt = 0; // = Gender.unknown.index; // <-- not a const expression Gender get gender => Gender.values[genderAsInt]; set gender(Gender value) => genderAsInt = value.index; From d5fdf0cf985f2b61dc00e8408e870d56c37b8c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 30 Jun 2023 11:39:12 +0200 Subject: [PATCH 06/33] Do the async open dance --- kilochat/README.md | 3 ++- kilochat/lib/providers.dart | 27 +++++++++++++++++++++++---- kilochat/lib/realm_ui.dart | 3 +-- kilochat/pubspec.yaml | 1 + 4 files changed, 27 insertions(+), 7 deletions(-) diff --git a/kilochat/README.md b/kilochat/README.md index d74923a..a3d9a80 100644 --- a/kilochat/README.md +++ b/kilochat/README.md @@ -8,7 +8,8 @@ The app demonstrates how to: 1. Re-use credentials on app restart to allow for _offline login_ 1. Use federated jwt-based authentication with Firebase Auth. -1. React to connectivity changes, displaying current state. +1. React to connectivity changes to quickly recover. +1. Display sync connection state. 1. Handle soft synchronization errors. 1. Handle client resets (hard sync errors). 1. Display a snackbar when interesting data is synced to device. diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index ee9f714..44e4e99 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; @@ -5,6 +6,7 @@ import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:realm/realm.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:cancellation_token/cancellation_token.dart'; import 'model.dart'; import 'repository.dart'; @@ -58,7 +60,7 @@ Future repository(RepositoryRef ref) async { @riverpod Future syncedRealm(SyncedRealmRef ref) async { final user = await ref.watch(userProvider.future); - final realm = await Realm.open(Configuration.flexibleSync( + var config = Configuration.flexibleSync( user, [ Channel.schema, @@ -66,16 +68,33 @@ Future syncedRealm(SyncedRealmRef ref) async { Reaction.schema, UserProfile.schema, ], - )); + ); + late Realm realm; + final ct = TimeoutCancellationToken(const Duration(seconds: 1)); + try { + if (File(config.path).existsSync()) { + realm = Realm(config); + } else { + realm = await Realm.open( + config, + cancellationToken: ct, + ); + } + // await realm.subscriptions.waitForSynchronization(); // does not support cancellation yet + } catch (_) { + realm = Realm(config); + } realm.subscriptions.update((mutableSubscriptions) { + // TODO: way too simple mutableSubscriptions ..add(realm.all()) ..add(realm.all()) ..add(realm.all()) ..add(realm.all()); }); - await realm.subscriptions.waitForSynchronization(); - await realm.syncSession.waitForDownload(); + try { + await realm.syncSession.waitForDownload(ct); + } catch (_) {} // ignore return realm; } diff --git a/kilochat/lib/realm_ui.dart b/kilochat/lib/realm_ui.dart index d209bed..4a404f3 100644 --- a/kilochat/lib/realm_ui.dart +++ b/kilochat/lib/realm_ui.dart @@ -84,9 +84,8 @@ class _RealmAnimatedListState extends State> { void _updateSubscription() { _subscription?.cancel(); _subscription = widget.results.safeChanges - .skip(1) // skip initial results ._updateAnimatedList(_listKey, widget.removedItemBuilder) - .listen((_) {}); + .listen((_) => setState(() {})); } @override diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index 4050e6a..5288008 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: sdk: flutter animated_emoji: ^2.0.0 + cancellation_token: ^1.5.0 collection: ^1.17.0 connectivity_plus: ^4.0.1 firebase_auth: ^4.2.10 From 24e8c93e84ba408f5c6b47d1d4d7240b8f54a0d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 13 Jul 2023 11:56:05 +0200 Subject: [PATCH 07/33] Muse about model in README,md --- kilochat/README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/kilochat/README.md b/kilochat/README.md index a3d9a80..1cfa972 100644 --- a/kilochat/README.md +++ b/kilochat/README.md @@ -139,6 +139,46 @@ The ownership role goes for channels as well. Anyone can create a channel, but o delete it. The ability to create channels would usually be reserved for admin users, but alas this is after all just a toy system. +# A bit about modelling + +One of the most fundamental thing for a chat system is to keep messages in order. This can cause some challenges in an offline capable system. + +Had this been an online system, we could simply use an auto-incrementing integer on the chat messages +to maintain a global order, but we do not have that luxury. And yes, I know Firebase has auto-incrementing integers, but those won't work when offline. Don't get me started on why Firebase is +not an offline-first capable storage solution at all. + +Instead a simple approach is to stamp each message with the local clock of the originating system, and +order by time. If you have done distributed programming before you are likely aware of the problem with this idea, that is clock-drift. Despite NNTP etc. clocks on different devices are not perfectly aligned. +Also the clock on a single device can be discontinuous, ie. someone might set the clock back in time. + +A more subtle problem is + +This can all cause a reordering of the messages which obscure the meaning of the conversation in subtle ways. + +For a mostly online chat system you can argue that it might be acceptable to ignore this problem, but for +sake of argument lets us try to solve it. + +First lets do a small detour and briefly elaborate on the problem of interleaving, which is very pertinent in collaborative text editors, so let us that as an example. + +Say you have a sequence of characters "AB" and one peer inserts "xy" to get "AxyB" while another peer inserts "12" to get "A12B", we want the sequence to converge to either "Axy12B" or "A12xyB", but avoid interleaving the edits like fx "Ax1y2B". + +You might say, can't I just use a realm list for this? Yes, and no. Realm lists are almost perfect for this. They will automatically resolve conflicts, and maintain an eventually consistent order, even do the right thing with regards to interleaving runs in all but the most unlikely of situations. They are basically build for this, so why not just add a realm list of messages on the channel object and use that? + +The problem is ownership. Who should be allowed to edit the list? If we allow everybody to write to the list, anybody could potentially remove other people's messages. We could prevent that in the client code, but as discussed earlier we cannot really trust code running on the clients. So what to do? + +Instead of maintaining a list of messages on the channel, we let each messages point to its channel. This +way we can ensure that only the owner of the message can remove or add it. This ends our little detour and brings us right back to the original problem. + +So what to do? + +Well, while the fact that we are not allowed to edit other people's messages prevents us from using a realm list, the problem is really not to bad as for a full collaborative editor. We can track properties such as an index and owner for each message, which would be prohibitively expensive to do for each character in an editor. + +Also, we always add the new messages to the end. This means, that if we _believe_ we did the last message `m` in the channel, and we want to add a new one, we can instead just update `m` by appending. Only if we _know_ we didn't add the latest, we need to create a new one. This will keep our messages and that of our peers lumped together together in runs, if there for some reason is a lag in distributing the messages between peers. + +We still need to maintain the order somehow. To do this each new message will be given a new index that is one larger than max index of any seen message in the channel sofar. There may be duplicates if some peers have have not seen all existing messages when creating a new one. To handle this we define the order as the sort of the indexes, and secondarily the owner id to ensure a stable sort. + +Note that we may introduce holes in the sequence this way (or by later deletions), but that is alright. Realm will easily find the _n'th_ message in the channel given the sort, irregardless of the actual values of index, and owner id. + # DISCLAIMER This is low priority work in progress. Eventually we hope to make this a well documented example of best practices, but that is _not_ the current state. From 54eb7ef809a8550c33821159a57802d91fbf8d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 13 Jul 2023 13:52:25 +0200 Subject: [PATCH 08/33] Wrok-around set notification issue by converting to results - go_router etc. --- kilochat/lib/debug_widget.dart | 8 +-- kilochat/lib/main.dart | 55 +++++++++++-------- kilochat/lib/model.dart | 2 + kilochat/lib/model.g.dart | 12 +++++ kilochat/lib/providers.dart | 21 ++++---- kilochat/lib/providers.g.dart | 2 +- kilochat/lib/repository.dart | 40 +++++++++++++- kilochat/lib/tiles.dart | 51 +++++++++--------- kilochat/pubspec.lock | 99 +++++++++++++++++----------------- kilochat/pubspec.yaml | 14 +++-- 10 files changed, 180 insertions(+), 124 deletions(-) diff --git a/kilochat/lib/debug_widget.dart b/kilochat/lib/debug_widget.dart index 18462d9..16a5967 100644 --- a/kilochat/lib/debug_widget.dart +++ b/kilochat/lib/debug_widget.dart @@ -12,25 +12,25 @@ class DebugWidget extends StatefulWidget { class _DebugWidgetState extends State> { @override void initState() { - print('$T@$hashCode initState'); + debugPrint('$T@$hashCode initState'); super.initState(); } @override void didUpdateWidget(covariant DebugWidget oldWidget) { - print('$T@$hashCode didUpdateWidget'); // TODO: remove + debugPrint('$T@$hashCode didUpdateWidget'); super.didUpdateWidget(oldWidget); } @override void dispose() { - print('$T@$hashCode dispose'); // TODO: remove + debugPrint('$T@$hashCode dispose'); super.dispose(); } @override void didChangeDependencies() { - print('$T@$hashCode didChangeDependencies'); // TODO: remove + debugPrint('$T@$hashCode didChangeDependencies'); super.didChangeDependencies(); } diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart index e4cd3a1..faba2ea 100644 --- a/kilochat/lib/main.dart +++ b/kilochat/lib/main.dart @@ -8,6 +8,7 @@ import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:kilochat/debug_widget.dart'; import 'package:kilochat/widget_builders.dart'; import 'package:statsfl/statsfl.dart'; @@ -27,9 +28,37 @@ enum Routes { logIn, chat; - String get id => toString(); + String get path => '/$this'; } +extension RoutesX on Routes { + go(BuildContext context) => context.go(path); +} + +final _router = GoRouter( + initialLocation: Routes.logIn.path, + routes: [ + GoRoute( + path: Routes.chat.path, + builder: (context, state) => const MyApp(), + ), + GoRoute( + path: Routes.logIn.path, + builder: (context, state) { + return SignInScreen(actions: [ + AuthStateChangeAction((context, state) async { + final ref = ProviderScope.containerOf(context); + final firebaseUserController = + ref.read(firebaseUserProvider.notifier); + firebaseUserController.state = state.user; + Routes.chat.go(context); + }), + ]); + }, + ), + ], +); + Future main() async { Animate.restartOnHotReload = true; WidgetsFlutterBinding.ensureInitialized(); @@ -48,9 +77,8 @@ Future main() async { child: Builder( builder: (context) { final ref = ProviderScope.containerOf(context); - final firebaseUserController = - ref.read(firebaseUserProvider.notifier); - return MaterialApp( + return MaterialApp.router( + routerConfig: _router, debugShowCheckedModeBanner: false, theme: ThemeData( colorScheme: ColorScheme.fromSeed( @@ -58,22 +86,6 @@ Future main() async { inversePrimary: energizingYellow, ), ), - initialRoute: firebaseUserController.state == null - ? Routes.logIn.id - : Routes.chat.id, - routes: { - Routes.logIn.id: (context) { - return SignInScreen(actions: [ - AuthStateChangeAction((context, state) async { - firebaseUserController.state = state.user; - Navigator.pushReplacementNamed(context, Routes.chat.id); - }), - ]); - }, - Routes.chat.id: (context) { - return const Material(child: MyApp()); - }, - }, ); }, ), @@ -226,8 +238,7 @@ class MessagesView extends ConsumerWidget { data: (messages) => RealmAnimatedList( results: messages, itemBuilder: (context, item, animation) { - return DebugWidget( - child: MessageTile(message: item, animation: animation)); + return MessageTile(message: item, animation: animation); }, reverse: true, controller: scrollController, diff --git a/kilochat/lib/model.dart b/kilochat/lib/model.dart index 0b59030..10aba9b 100644 --- a/kilochat/lib/model.dart +++ b/kilochat/lib/model.dart @@ -121,4 +121,6 @@ class _UserProfile { var typing = false; late Set<_Channel> channels; + + late Set<_UserProfile> bodies; } diff --git a/kilochat/lib/model.g.dart b/kilochat/lib/model.g.dart index 0c923be..d870c3e 100644 --- a/kilochat/lib/model.g.dart +++ b/kilochat/lib/model.g.dart @@ -266,6 +266,7 @@ class UserProfile extends _UserProfile int? statusEmojiUnicode, bool typing = false, Set channels = const {}, + Set bodies = const {}, }) { if (!_defaultsSet) { _defaultsSet = RealmObjectBase.setDefaults({ @@ -285,6 +286,8 @@ class UserProfile extends _UserProfile RealmObjectBase.set(this, 'typing', typing); RealmObjectBase.set>( this, 'channels', RealmSet(channels)); + RealmObjectBase.set>( + this, 'bodies', RealmSet(bodies)); } UserProfile._(); @@ -345,6 +348,13 @@ class UserProfile extends _UserProfile set channels(covariant RealmSet value) => throw RealmUnsupportedSetError(); + @override + RealmSet get bodies => + RealmObjectBase.get(this, 'bodies') as RealmSet; + @override + set bodies(covariant RealmSet value) => + throw RealmUnsupportedSetError(); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -371,6 +381,8 @@ class UserProfile extends _UserProfile SchemaProperty('typing', RealmPropertyType.bool), SchemaProperty('channels', RealmPropertyType.object, linkTarget: 'Channel', collectionType: RealmCollectionType.set), + SchemaProperty('bodies', RealmPropertyType.object, + linkTarget: 'UserProfile', collectionType: RealmCollectionType.set), ]); } } diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 44e4e99..5111a47 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -69,8 +69,9 @@ Future syncedRealm(SyncedRealmRef ref) async { UserProfile.schema, ], ); + Realm.logger.level = RealmLogLevel.debug; late Realm realm; - final ct = TimeoutCancellationToken(const Duration(seconds: 1)); + final ct = TimeoutCancellationToken(const Duration(seconds: 3)); try { if (File(config.path).existsSync()) { realm = Realm(config); @@ -80,19 +81,19 @@ Future syncedRealm(SyncedRealmRef ref) async { cancellationToken: ct, ); } - // await realm.subscriptions.waitForSynchronization(); // does not support cancellation yet } catch (_) { realm = Realm(config); } - realm.subscriptions.update((mutableSubscriptions) { - // TODO: way too simple - mutableSubscriptions - ..add(realm.all()) - ..add(realm.all()) - ..add(realm.all()) - ..add(realm.all()); - }); try { + realm.subscriptions.update((mutableSubscriptions) { + // TODO: way too simple + mutableSubscriptions + ..add(realm.all()) + ..add(realm.query('channelId == null')) + ..add(realm.all()) + ..add(realm.all()); + // await realm.subscriptions.waitForSynchronization(); // does not support cancellation yet + }); await realm.syncSession.waitForDownload(ct); } catch (_) {} // ignore return realm; diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index 78fe8c5..5363a0d 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -152,7 +152,7 @@ final repositoryProvider = AutoDisposeFutureProvider.internal( ); typedef RepositoryRef = AutoDisposeFutureProviderRef; -String _$syncedRealmHash() => r'9554cdc8a5829d0efa00aaade4fa4915c85f7f69'; +String _$syncedRealmHash() => r'117ac19cc8c9a184486823a858bfca8a396224f0'; /// See also [syncedRealm]. @ProviderFor(syncedRealm) diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index f3d5132..d5a01a8 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -1,12 +1,49 @@ +import 'dart:async'; + import 'package:realm/realm.dart'; import 'model.dart'; +extension on MutableSubscriptionSet { + void subscribe(Channel channel) => add( + name: '${channel.id}', + channel.realm.query(r'channelId == $0', [channel.id]), + ); + void unsubscribe(Channel channel) => removeByName('${channel.id}'); +} + +extension on Stream> { + Stream> updateSubscriptions() async* { + RealmResults? previous; + await for (final change in this) { + final results = change.results; + results.realm.subscriptions.update((mutableSubscriptions) { + if (previous != null) { + for (final i in change.deleted.reversed) { + mutableSubscriptions.unsubscribe(previous!.elementAt(i)); + } + } + for (final i in change.inserted) { + mutableSubscriptions.subscribe(results.elementAt(i)); + } + previous = results.freeze(); + }); + yield change; + } + } +} + class Repository { final Realm _realm; final UserProfile user; + final StreamSubscription _subscription; - Repository(this._realm, this.user); + Repository(this._realm, this.user) + : _subscription = user.channels + .asResults() + .changes + .updateSubscriptions() + .listen((_) {}); late RealmResults allChannels = _realm.query('TRUEPREDICATE SORT(name ASCENDING)'); @@ -40,6 +77,7 @@ class Repository { _realm.write(() => _realm.delete(message)); RealmResults searchMessage(String text) { + if (text.isEmpty) return allMessages; return _realm.query( r'text TEXT $0 SORT(id DESCENDING)', [text], diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index 0895ed7..5ce794c 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -57,41 +57,38 @@ class MessageTile extends ConsumerWidget { key: ValueKey(message.id), animation: animation, leading: MyAvatar(user: message.owner), - title: DebugWidget( - child: Text(message.owner?.name ?? 'N/A ${message.ownerId}')), + title: Text(message.owner?.name ?? 'N/A ${message.ownerId}'), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MyTransition(animation: animation, child: Text(message.text)), SizedBox( height: 50, - child: DebugWidget( - child: RealmAnimatedList( - results: message.reactions, - scrollDirection: Axis.horizontal, - itemBuilder: (context, reaction, animation) { - final owner = reaction.owner?.id == user?.id; - return MyTransition( - key: ValueKey(reaction.id), - axis: Axis.horizontal, - animation: animation, - child: Chip( - visualDensity: VisualDensity.compact, - avatar: MyAvatar(user: reaction.owner), - label: AnimatedEmoji( - AnimatedEmojiData( - 'u${reaction.emojiUnicode.toRadixString(16)}', - ), - repeat: false, - size: 20, - errorWidget: Text(reaction.emoji), + child: RealmAnimatedList( + results: message.reactions, + scrollDirection: Axis.horizontal, + itemBuilder: (context, reaction, animation) { + final owner = reaction.owner?.id == user?.id; + return MyTransition( + key: ValueKey(reaction.id), + axis: Axis.horizontal, + animation: animation, + child: Chip( + visualDensity: VisualDensity.compact, + avatar: MyAvatar(user: reaction.owner), + label: AnimatedEmoji( + AnimatedEmojiData( + 'u${reaction.emojiUnicode.toRadixString(16)}', ), - onDeleted: - owner ? () => repo?.deleteReaction(reaction) : null, + repeat: false, + size: 20, + errorWidget: Text(reaction.emoji), ), - ); - }, - ), + onDeleted: + owner ? () => repo?.deleteReaction(reaction) : null, + ), + ); + }, ), ) ], diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 33f428f..9653969 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -77,10 +77,10 @@ packages: dependency: transitive description: name: build - sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" build_cli_annotations: dependency: transitive description: @@ -109,18 +109,18 @@ packages: dependency: transitive description: name: build_resolvers - sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" build_runner: dependency: transitive description: name: build_runner - sha256: "5e1929ad37d48bd382b124266cb8e521de5548d406a45a5ae6656c13dab73e37" + sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "2.4.6" build_runner_core: dependency: transitive description: @@ -146,13 +146,13 @@ packages: source: hosted version: "8.6.1" cancellation_token: - dependency: transitive + dependency: "direct main" description: name: cancellation_token - sha256: "44891ef71d605bc59ef7974c403630d8e8506fcd897a29c3e38466ef69e5c4eb" + sha256: "7bacc556338b9f84e4db991805fdfa37fa1eda3689b94185bdc7459099455d71" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "2.0.0" characters: dependency: transitive description: @@ -205,10 +205,10 @@ packages: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.17.1" connectivity_plus: dependency: "direct main" description: @@ -269,10 +269,10 @@ packages: dependency: transitive description: name: dart_style - sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad + sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" dbus: dependency: transitive description: @@ -490,10 +490,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" flutter_localizations: dependency: transitive description: flutter @@ -549,6 +549,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "1531542666c2d052c44bbf6e2b48011bf3771da0404b94c60eabec1228a62906" + url: "https://pub.dev" + source: hosted + version: "9.0.0" google_identity_services_web: dependency: transitive description: @@ -569,10 +577,10 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: f58a17ac07d783d000786a6c313fa4a0d2ee599a346d69b24fc48fb378d5d150 + sha256: "258864d68b68f2b2846c10283b7afb95f26ff3307c410a2841a010d034b65439" url: "https://pub.dev" source: hosted - version: "6.1.16" + version: "6.1.17" google_sign_in_ios: dependency: transitive description: @@ -641,10 +649,10 @@ packages: dependency: transitive description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.18.0" io: dependency: transitive description: @@ -697,18 +705,18 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.2.0" meta: dependency: transitive description: @@ -832,26 +840,23 @@ packages: realm: dependency: "direct main" description: - name: realm - sha256: "818bd06d65cf736dab3e41111a9f641d129083a6056788544ea6a94697fbac2e" - url: "https://pub.dev" - source: hosted + path: "../../../realm-dart/flutter/realm_flutter" + relative: true + source: path version: "1.3.0" realm_common: dependency: transitive description: - name: realm_common - sha256: b7fefe203362d9afc21094fdc1d97c9ada95c25f80fc5c352a0dd3f3cbf34625 - url: "https://pub.dev" - source: hosted + path: "../../../realm-dart/common" + relative: true + source: path version: "1.3.0" realm_generator: dependency: transitive description: - name: realm_generator - sha256: "22a3b16b3f1830af221d7b07c847e85a44873a82d8b90f67bdd81cc4a2b64f2a" - url: "https://pub.dev" - source: hosted + path: "../../../realm-dart/generator" + relative: true + source: path version: "1.3.0" riverpod: dependency: "direct main" @@ -934,18 +939,18 @@ packages: dependency: transitive description: name: source_gen - sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.9.1" stack_trace: dependency: transitive description: @@ -1014,10 +1019,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.5.1" timing: dependency: transitive description: @@ -1090,14 +1095,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - web: - dependency: transitive - description: - name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 - url: "https://pub.dev" - source: hosted - version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1123,5 +1120,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.2" + dart: ">=3.0.2 <4.0.0" + flutter: ">=3.10.0" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index 5288008..c619364 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -24,8 +24,9 @@ dependencies: firebase_ui_oauth_google: ^1.0.23 flutter_animate: ^4.0.0 flutter_riverpod: ^2.3.0 + go_router: ^9.0.0 random_avatar: ^0.0.8 - realm: ^1.0.3 + realm: ^1.3.0 riverpod_annotation: ^2.0.0 riverpod: ^2.3.0 statsfl: ^2.0.0 @@ -39,10 +40,6 @@ dev_dependencies: riverpod_lint: any riverpod_generator: ^2.0.0 -# dependency_overrides: -# flutter_svg: ^2.0.0 # to force resolve conflict between firebase_ui_auth and random_avatar :-/ -# intl: 0.18.0 - flutter: uses-material-design: true fonts: @@ -50,6 +47,7 @@ flutter: fonts: - asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf -# dependency_overrides: -# realm: -# path: ../../realm-dart/flutter/realm_flutter +dependency_overrides: + cancellation_token: ^2.0.0 + realm: + path: ../../../realm-dart/flutter/realm_flutter From 83e1586105ea245a9d7576e5ed1c0e524356c7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 13 Jul 2023 17:03:25 +0200 Subject: [PATCH 09/33] Use index, id for ordering --- kilochat/lib/model.dart | 4 ++++ kilochat/lib/model.g.dart | 9 +++++++++ kilochat/lib/providers.g.dart | 2 +- kilochat/lib/repository.dart | 37 +++++++++++++++++++++++------------ 4 files changed, 39 insertions(+), 13 deletions(-) diff --git a/kilochat/lib/model.dart b/kilochat/lib/model.dart index 10aba9b..497588c 100644 --- a/kilochat/lib/model.dart +++ b/kilochat/lib/model.dart @@ -22,6 +22,10 @@ class _Message { @MapTo('_id') late ObjectId id; + // one larger than the previous seen message's index in the channel + @Indexed() + late int index; + @MapTo('owner_id') late String ownerId; // matches owner.id _UserProfile? owner; diff --git a/kilochat/lib/model.g.dart b/kilochat/lib/model.g.dart index d870c3e..339e27c 100644 --- a/kilochat/lib/model.g.dart +++ b/kilochat/lib/model.g.dart @@ -76,6 +76,7 @@ class Channel extends _Channel with RealmEntity, RealmObjectBase, RealmObject { class Message extends _Message with RealmEntity, RealmObjectBase, RealmObject { Message( ObjectId id, + int index, String ownerId, ObjectId channelId, String text, { @@ -83,6 +84,7 @@ class Message extends _Message with RealmEntity, RealmObjectBase, RealmObject { Channel? channel, }) { RealmObjectBase.set(this, '_id', id); + RealmObjectBase.set(this, 'index', index); RealmObjectBase.set(this, 'owner_id', ownerId); RealmObjectBase.set(this, 'owner', owner); RealmObjectBase.set(this, 'channel_id', channelId); @@ -97,6 +99,11 @@ class Message extends _Message with RealmEntity, RealmObjectBase, RealmObject { @override set id(ObjectId value) => RealmObjectBase.set(this, '_id', value); + @override + int get index => RealmObjectBase.get(this, 'index') as int; + @override + set index(int value) => RealmObjectBase.set(this, 'index', value); + @override String get ownerId => RealmObjectBase.get(this, 'owner_id') as String; @override @@ -155,6 +162,8 @@ class Message extends _Message with RealmEntity, RealmObjectBase, RealmObject { return const SchemaObject(ObjectType.realmObject, Message, 'Message', [ SchemaProperty('id', RealmPropertyType.objectid, mapTo: '_id', primaryKey: true), + SchemaProperty('index', RealmPropertyType.int, + indexType: RealmIndexType.regular), SchemaProperty('ownerId', RealmPropertyType.string, mapTo: 'owner_id'), SchemaProperty('owner', RealmPropertyType.object, optional: true, linkTarget: 'UserProfile'), diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index 5363a0d..fffd9a0 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -152,7 +152,7 @@ final repositoryProvider = AutoDisposeFutureProvider.internal( ); typedef RepositoryRef = AutoDisposeFutureProviderRef; -String _$syncedRealmHash() => r'117ac19cc8c9a184486823a858bfca8a396224f0'; +String _$syncedRealmHash() => r'894d981cdc1510e4d50afbf8f059998237daea7a'; /// See also [syncedRealm]. @ProviderFor(syncedRealm) diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index d5a01a8..8db8ed2 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -51,23 +51,36 @@ class Repository { late RealmResults allMessages = _realm.query('TRUEPREDICATE SORT(id DESCENDING)'); - RealmResults messages(Channel channel) => - _realm.query(r'channel == $0 SORT(id DESCENDING)', [channel]); + RealmResults messages(Channel channel) => _realm + .query(r'channel == $0 SORT(index DESC, id ASC)', [channel]); void updateUserProfile(UserProfile newProfile) => _realm.write(() => _realm.add(newProfile, update: true)); void postNewMessage(Channel channel, String text) { - _realm.write(() { - _realm.add(Message( - ObjectId(), - user.id, - channel.id, // not a link - text, - channel: channel, - owner: user, - )); - }); + // messages are sorted latest first + final lastMessage = messages(channel).firstOrNull; + if (lastMessage != null && + lastMessage.owner == user && + lastMessage.reactions.isEmpty) { + // if we own last message, and there are no reactions, then update in place + final newText = '${lastMessage.text}\n\n$text'; + editMessage(lastMessage, newText); + } else { + _realm.write(() { + // create new message with highest local index + // NOTE: Index may be duplicated if multiple clients are posting at once + _realm.add(Message( + ObjectId(), + (lastMessage?.index ?? 0) + 1, // increment index + user.id, + channel.id, // not a link + text, + channel: channel, + owner: user, + )); + }); + } } void editMessage(Message message, String text) => From 06b396afd222777f0d96e6b9e9679c062032e753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Sat, 29 Jul 2023 15:03:03 +0200 Subject: [PATCH 10/33] Add .gitignore --- kilochat/.gitignore | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 kilochat/.gitignore diff --git a/kilochat/.gitignore b/kilochat/.gitignore new file mode 100644 index 0000000..c6410bc --- /dev/null +++ b/kilochat/.gitignore @@ -0,0 +1,52 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release + +# Platform related. Generate platform-specific files with flutter create . --platforms +# Note that some files are force added for convenience for macos +android/ +ios/ +linux/ +macos/ +windows/ From 75dc91d44af4b806c803e2020d4e207d2d00ad71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 3 Aug 2023 08:51:26 +0200 Subject: [PATCH 11/33] Cleanup etc. --- kilochat/lib/main.dart | 33 ++++------ kilochat/lib/providers.dart | 2 +- kilochat/lib/providers.g.dart | 5 +- kilochat/lib/tiles.dart | 1 - kilochat/pubspec.lock | 111 +++++++++++++++++----------------- kilochat/pubspec.yaml | 9 ++- 6 files changed, 78 insertions(+), 83 deletions(-) diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart index faba2ea..8ea1a6d 100644 --- a/kilochat/lib/main.dart +++ b/kilochat/lib/main.dart @@ -9,9 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:kilochat/debug_widget.dart'; import 'package:kilochat/widget_builders.dart'; -import 'package:statsfl/statsfl.dart'; import 'avatar.dart'; import 'firebase_options.dart'; @@ -70,25 +68,20 @@ Future main() async { GoogleProvider(clientId: ''), ]); runApp( - StatsFl( - maxFps: 60, - showText: false, - child: ProviderScope( - child: Builder( - builder: (context) { - final ref = ProviderScope.containerOf(context); - return MaterialApp.router( - routerConfig: _router, - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: freedomBlue, - inversePrimary: energizingYellow, - ), + ProviderScope( + child: Builder( + builder: (context) { + return MaterialApp.router( + routerConfig: _router, + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: freedomBlue, + inversePrimary: energizingYellow, ), - ); - }, - ), + ), + ); + }, ), ), ); diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 5111a47..869e841 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -108,7 +108,7 @@ Stream user(UserRef ref) async* { if (user == null) { if (firebaseUser != null) { final jwt = await firebaseUser.getIdToken(); - user = await app.logIn(Credentials.jwt(jwt)); + user = await app.logIn(Credentials.jwt(jwt!)); } } if (user != null) yield user; diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index fffd9a0..43d8869 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -166,7 +166,7 @@ final syncedRealmProvider = AutoDisposeFutureProvider.internal( ); typedef SyncedRealmRef = AutoDisposeFutureProviderRef; -String _$userHash() => r'3162f8129e247644d301b4259e2b47c828d85eb4'; +String _$userHash() => r'b1ca9042230ed76890f0226e50c4d47255094ce4'; /// See also [user]. @ProviderFor(user) @@ -194,4 +194,5 @@ final userProfileProvider = AutoDisposeStreamProvider.internal( ); typedef UserProfileRef = AutoDisposeStreamProviderRef; -// 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 +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index 5ce794c..57c6f7e 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -2,7 +2,6 @@ import 'package:animated_emoji/emoji.dart'; import 'package:animated_emoji/emojis.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kilochat/debug_widget.dart'; import 'package:kilochat/realm_ui.dart'; import 'avatar.dart'; diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 9653969..9b8e689 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: a742f71d7f3484253a623b30e19256aa4668ecbb3de6ad1beb0bcf8d4777ecd8 + sha256: "5dce45a06d386358334eb1689108db6455d90ceb0d75848d5f4819283d4ee2b8" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "1.3.4" analyzer: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: "direct main" description: name: cancellation_token - sha256: "7bacc556338b9f84e4db991805fdfa37fa1eda3689b94185bdc7459099455d71" + sha256: "44891ef71d605bc59ef7974c403630d8e8506fcd897a29c3e38466ef69e5c4eb" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.6.1" characters: dependency: transitive description: @@ -213,10 +213,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "8599ae9edca5ff96163fca3e36f8e481ea917d1e71cdad912c084b5579913f34" + sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -325,34 +325,34 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: f693c0aa998b1101453878951b171b69f0db5199003df1c943b33493a1de7917 + sha256: "49fd35ce06f2530dd460e5dc123235731cb61dd7c76b0af4b6e190404880d04d" url: "https://pub.dev" source: hosted - version: "4.6.3" + version: "4.7.2" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "689ae048b78ad088ba31acdec45f5badb56201e749ed8b534947a7303ddb32aa" + sha256: "817f3ceb84ef5e9adaaf50cf7a19255f6ffcdd12c6f9e9aa4cf00fc7f2eb3cfb" url: "https://pub.dev" source: hosted - version: "6.15.3" + version: "6.16.1" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: f35d637a1707afd51f30090bb5234b381d5071ccbfef09b8c393bc7c65e440cd + sha256: e9044778287f1ff8f9f4cee7e247b03ec87bb8977e0e65ad27dc337e196132e8 url: "https://pub.dev" source: hosted - version: "5.5.3" + version: "5.6.2" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: a4a99204da264a0aa9d54a332ea0315ce7b0768075139c77abefe98093dd98be + sha256: "2e9324f719e90200dc7d3c4f5d2abc26052f9f2b995d3b6626c47a0dfe1c8192" url: "https://pub.dev" source: hosted - version: "2.14.0" + version: "2.15.0" firebase_core_platform_interface: dependency: transitive description: @@ -373,26 +373,26 @@ packages: dependency: transitive description: name: firebase_dynamic_links - sha256: "9b984d0abd227a702451a997abcca763f4dbf67e260dad60e5506d55e3eff244" + sha256: "4872f4d7e94736041398bc3490c2ddd87ee159d6b051ba01ca2708e5260a7ebe" url: "https://pub.dev" source: hosted - version: "5.3.3" + version: "5.3.4" firebase_dynamic_links_platform_interface: dependency: transitive description: name: firebase_dynamic_links_platform_interface - sha256: "6ef00a0be18f3231e9727f7c4b31db89dbfa16792098beb850603c30854560ff" + sha256: "946fccfefb67e26bf63e392f1b3917d79ea031d3071488f0c5e8ab72de8219ab" url: "https://pub.dev" source: hosted - version: "0.2.6+3" + version: "0.2.6+4" firebase_ui_auth: dependency: "direct main" description: name: firebase_ui_auth - sha256: efb4a77c2784158c1b332594499bd7e1b4c59d5d3bc5d694b5aae6ba5b6e249b + sha256: e439571fcad7ed48450eed8d64c70b93765526b876327055469806a51101eff0 url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.2" firebase_ui_localizations: dependency: transitive description: @@ -405,18 +405,18 @@ packages: dependency: transitive description: name: firebase_ui_oauth - sha256: "1dfbb637093419809a1451b2550ac57180c91580ba15cad71332b16282785f17" + sha256: "2cbe5a8996134f1a57205a5ebfa5863c5d599c03a07aae70867de2ecdfbbde0e" url: "https://pub.dev" source: hosted - version: "1.4.4" + version: "1.4.7" firebase_ui_oauth_apple: dependency: "direct main" description: name: firebase_ui_oauth_apple - sha256: "47b135f64f5f29e671f213fe6a82b662bb7c6ae89d240ee6446db3d4d49b5dcf" + sha256: "9cd85a9c85bb3d2e43d5a37cb4b4b7efcfc6a718dadbabb36afb91b1413b4de3" url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "1.2.7" firebase_ui_oauth_facebook: dependency: "direct main" description: @@ -429,10 +429,10 @@ packages: dependency: "direct main" description: name: firebase_ui_oauth_google - sha256: a622b9dbb658809d82a18575a2aa46166ca35f0c640ced509efe3819ca29cbe1 + sha256: "0e255ced67023871040b3d349c966a281a54ce1a093128733ccbd0a6b00a95d1" url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "1.2.7" firebase_ui_shared: dependency: transitive description: @@ -458,10 +458,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: be54662837a6e66cc53ee88549e808c625275e0faf5a43e11cf3182cb0bd1b02 + sha256: "62f346340a96192070e31e3f2a1bd30a28530f1fe8be978821e06cd56b74d6d2" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.2.0+1" flutter_facebook_auth: dependency: transitive description: @@ -529,10 +529,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.1" frontend_server_client: dependency: transitive description: @@ -553,10 +553,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "1531542666c2d052c44bbf6e2b48011bf3771da0404b94c60eabec1228a62906" + sha256: b3cadd2cd59a4103fd5f6bc572ca75111264698784e927aa471921c3477d5475 url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "10.0.0" google_identity_services_web: dependency: transitive description: @@ -577,10 +577,10 @@ packages: dependency: transitive description: name: google_sign_in_android - sha256: "258864d68b68f2b2846c10283b7afb95f26ff3307c410a2841a010d034b65439" + sha256: "8d60a787b29cb7d2bcf29230865f4a91f17323c6ac5b6b9027a6418e48d9ffc3" url: "https://pub.dev" source: hosted - version: "6.1.17" + version: "6.1.18" google_sign_in_ios: dependency: transitive description: @@ -697,10 +697,10 @@ packages: dependency: transitive description: name: lottie - sha256: f461105d3a35887b27089abf9c292334478dd292f7b47ecdccb6ae5c37a22c80 + sha256: "0793a5866062e5cc8a8b24892fa94c3095953ea914a7fdf790f550dd7537fe60" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" matcher: dependency: transitive description: @@ -785,10 +785,10 @@ packages: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pointycastle: dependency: transitive description: @@ -840,23 +840,26 @@ packages: realm: dependency: "direct main" description: - path: "../../../realm-dart/flutter/realm_flutter" - relative: true - source: path + name: realm + sha256: "818bd06d65cf736dab3e41111a9f641d129083a6056788544ea6a94697fbac2e" + url: "https://pub.dev" + source: hosted version: "1.3.0" realm_common: dependency: transitive description: - path: "../../../realm-dart/common" - relative: true - source: path + name: realm_common + sha256: b7fefe203362d9afc21094fdc1d97c9ada95c25f80fc5c352a0dd3f3cbf34625 + url: "https://pub.dev" + source: hosted version: "1.3.0" realm_generator: dependency: transitive description: - path: "../../../realm-dart/generator" - relative: true - source: path + name: realm_generator + sha256: "22a3b16b3f1830af221d7b07c847e85a44873a82d8b90f67bdd81cc4a2b64f2a" + url: "https://pub.dev" + source: hosted version: "1.3.0" riverpod: dependency: "direct main" @@ -886,18 +889,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: cd0595de57ccf5d944ff4b0f68289e11ac6a2eff1e3dfd1d884a43f6f3bcee5e + sha256: "691180275664a5420c87d72c1ed26ef8404d32b823807540172bfd1660425376" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "043ff016011be4c5887b3239bfbca05d284bdb68db0a5363cee0242b7567e250" + sha256: "17ad319914ac6863c64524e598913c0f17e30688aca8f5b7509e96d6e372d493" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" rxdart: dependency: transitive description: @@ -1083,10 +1086,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b8c67f5fa3897b122cf60fe9ff314f7b0ef71eab25c5f8b771480bc338f48823 + sha256: ada49637c27973c183dad90beb6bd781eea4c9f5f955d35da172de0af7bd3440 url: "https://pub.dev" source: hosted - version: "11.7.2" + version: "11.8.0" watcher: dependency: transitive description: @@ -1121,4 +1124,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.0.2 <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.10.2" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index c619364..e8da769 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: firebase_ui_oauth_google: ^1.0.23 flutter_animate: ^4.0.0 flutter_riverpod: ^2.3.0 - go_router: ^9.0.0 + go_router: ^10.0.0 random_avatar: ^0.0.8 realm: ^1.3.0 riverpod_annotation: ^2.0.0 @@ -47,7 +47,6 @@ flutter: fonts: - asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf -dependency_overrides: - cancellation_token: ^2.0.0 - realm: - path: ../../../realm-dart/flutter/realm_flutter +# dependency_overrides: +# realm: +# path: ../../../realm-dart/flutter/realm_flutter From 36b55432a12221a368c963bc057a906b5098b9d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 4 Aug 2023 08:53:49 +0200 Subject: [PATCH 12/33] Multi-realm refactor (wip) --- kilochat/lib/channel_search_delegate.dart | 36 +++ kilochat/lib/channels_view.dart | 98 +++++++ kilochat/lib/chat_screen.dart | 87 ++++++ kilochat/lib/chat_widget.dart | 71 +++++ kilochat/lib/display_toast.dart | 39 +++ kilochat/lib/main.dart | 336 +--------------------- kilochat/lib/messages_view.dart | 34 +++ kilochat/lib/repository.dart | 16 +- kilochat/lib/router.dart | 63 ++++ kilochat/lib/settings.dart | 47 +++ kilochat/lib/settings.g.dart | 82 ++++++ kilochat/lib/workspace_view.dart | 144 ++++++++++ 12 files changed, 724 insertions(+), 329 deletions(-) create mode 100644 kilochat/lib/channel_search_delegate.dart create mode 100644 kilochat/lib/channels_view.dart create mode 100644 kilochat/lib/chat_screen.dart create mode 100644 kilochat/lib/chat_widget.dart create mode 100644 kilochat/lib/display_toast.dart create mode 100644 kilochat/lib/messages_view.dart create mode 100644 kilochat/lib/router.dart create mode 100644 kilochat/lib/settings.dart create mode 100644 kilochat/lib/settings.g.dart create mode 100644 kilochat/lib/workspace_view.dart diff --git a/kilochat/lib/channel_search_delegate.dart b/kilochat/lib/channel_search_delegate.dart new file mode 100644 index 0000000..528aef4 --- /dev/null +++ b/kilochat/lib/channel_search_delegate.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kilochat/providers.dart'; + +import 'model.dart'; +import 'realm_ui.dart'; + +class ChannelSearchDelegate extends RealmSearchDelegate { + bool _showSubscribed = true; + + ChannelSearchDelegate(super.resultsBuilder, super.itemBuilder); + + @override + List buildActions(BuildContext context) { + final repository = + ProviderScope.containerOf(context).read(repositoryProvider).value; + return [ + if (query.isNotEmpty && repository != null) + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + final channel = repository.createChannel(query.trim()); + repository.subscribeToChannel(channel); + close(context, null); + }, + ), + StatefulBuilder(builder: (_, setState) { + return Switch( + value: _showSubscribed, + onChanged: (value) => setState(() => _showSubscribed = value), + ); + }), + ...super.buildActions(context), + ]; + } +} diff --git a/kilochat/lib/channels_view.dart b/kilochat/lib/channels_view.dart new file mode 100644 index 0000000..3f51a52 --- /dev/null +++ b/kilochat/lib/channels_view.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kilochat/settings.dart'; + +import 'channel_search_delegate.dart'; +import 'model.dart'; +import 'providers.dart'; +import 'realm_ui.dart'; +import 'router.dart'; +import 'tiles.dart'; +import 'widget_builders.dart'; + +class ChannelsView extends ConsumerWidget { + const ChannelsView({super.key, this.onTap}); + + final void Function(Channel)? onTap; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final focusedChannel = ref.watch(focusedChannelProvider); + final repository = ref.watch(repositoryProvider); + return repository.when( + error: buildErrorWidget, + loading: buildLoadingWidget, + data: (repository) { + final user = repository.user; + final channels = user.channels.asResults(); + return ListTileTheme( + data: ListTileThemeData( + selectedTileColor: Theme.of(context).colorScheme.inversePrimary, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextButton( + onPressed: () => Routes.chooseWorkspace.go(context), + child: Text(currentWorkspace?.name ?? 'No workspace selected'), + ), + Expanded( + child: RealmAnimatedList( + results: channels, + itemBuilder: (_, channel, animation) => ChannelTile( + channel: channel, + animation: animation, + onTap: () => onTap?.call(channel), + onDismissed: (_) => + repository.unsubscribeFromChannel(channel), + selected: channel == focusedChannel, + ), + ), + ), + IconButton( + onPressed: () { + showSearch( + context: context, + delegate: ChannelSearchDelegate( + (query) => repository.searchChannel(query), + (_, channel, animation) => StatefulBuilder( + builder: (_, setState) { + return ChannelTile( + channel: channel, + animation: animation, + onTap: () => setState(() { + repository.subscribeToChannel(channel); + }), + selected: !channel.isFrozen && + user.channels.contains(channel), + ); + }, + ), + ), + ); + }, + icon: const Icon(Icons.add), + ), + const AboutListTile( + applicationName: 'kilochat', + applicationVersion: '1.0.0', + applicationIcon: Icon(Icons.send), + applicationLegalese: 'Copyright 2023 MongoDB', + dense: true, + aboutBoxChildren: [ + SizedBox(height: 10), + Text( + 'A chat application using Flutter, Realm, and Atlas ' + 'Device Sync. Written in less than 1K lines of code ' + '(excluding generated code), hence the name.', + textScaleFactor: 0.8, + ) + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart new file mode 100644 index 0000000..44144f9 --- /dev/null +++ b/kilochat/lib/chat_screen.dart @@ -0,0 +1,87 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kilochat/channels_view.dart'; +import 'package:kilochat/display_toast.dart'; + +import 'avatar.dart'; +import 'chat_widget.dart'; +import 'profile_form.dart'; +import 'providers.dart'; +import 'realm_ui.dart'; +import 'settings.dart'; +import 'tiles.dart'; +import 'widget_builders.dart'; + +class ChatScreen extends ConsumerWidget { + const ChatScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final focusedChannel = ref.watch(focusedChannelProvider); + final repository = ref.watch(repositoryProvider); + return repository.when( + error: buildErrorWidget, + loading: buildLoadingWidget, + data: (repository) { + final user = repository.user; + return Scaffold( + appBar: AppBar( + title: Text( + '${currentWorkspace?.name} / ${focusedChannel?.name ?? ''}'), + actions: [ + IconButton( + onPressed: () => showSearch( + context: context, + delegate: RealmSearchDelegate( + repository.searchMessage, + (context, item, animation) => + MessageTile(message: item, animation: animation), + )), + icon: const Icon(Icons.search), + ), + IconButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) => ProfileForm(initialProfile: user))); + }, + icon: MyAvatar(user: user), + ), + ], + ), + body: const Center(child: Text('Choose a channel')) + .animate(target: focusedChannel == null ? 0 : 1) + .crossfade(builder: (context) { + return DisplayToast( + stream: repository.x, + builder: (message) => SnackBar( + content: MessageTile( + message: message, animation: AnimationController()), + showCloseIcon: true, + closeIconColor: Colors.white, + behavior: SnackBarBehavior.floating, + ), + child: const ChatWidget(), + ); + }), + drawer: Builder( + builder: (context) { + return Drawer( + child: SafeArea( + child: ChannelsView( + onTap: (channel) { + ref.read(focusedChannelProvider.notifier).focus(channel); + Scaffold.of(context).closeDrawer(); + }, + ), + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/kilochat/lib/chat_widget.dart b/kilochat/lib/chat_widget.dart new file mode 100644 index 0000000..1e852df --- /dev/null +++ b/kilochat/lib/chat_widget.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'messages_view.dart'; +import 'providers.dart'; + +class ChatWidget extends ConsumerStatefulWidget { + const ChatWidget({super.key}); + + @override + ConsumerState createState() => _ChatWidgetState(); +} + +class _ChatWidgetState extends ConsumerState { + final controller = TextEditingController(); + final scrollController = ScrollController(); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded(child: MessagesView(scrollController: scrollController)), + Ink( + color: Theme.of(context).colorScheme.inversePrimary, + padding: const EdgeInsets.only(left: 16, bottom: 16, right: 16), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: AnimatedSize( + duration: const Duration(milliseconds: 300), + child: TextField( + minLines: 1, + maxLines: 5, + controller: controller, + decoration: const InputDecoration( + label: Text('Enter next message'), + border: InputBorder.none, + hintText: 'here..', + ), + textInputAction: TextInputAction.done, + onSubmitted: _postNewMessage, + ), + ), + ), + IconButton( + enableFeedback: true, + onPressed: () => _postNewMessage(controller.text), + icon: const Icon(Icons.send), + ) + ], + ), + ), + ], + ); + } + + void _postNewMessage(String text) async { + if (text.isNotEmpty) { + final channel = ref.read(focusedChannelProvider); + final repository = ref.read(repositoryProvider).requireValue; + repository.postNewMessage(channel!, text); + controller.clear(); + await scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + } +} diff --git a/kilochat/lib/display_toast.dart b/kilochat/lib/display_toast.dart new file mode 100644 index 0000000..544a89e --- /dev/null +++ b/kilochat/lib/display_toast.dart @@ -0,0 +1,39 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; + +class DisplayToast extends StatefulWidget { + const DisplayToast({ + Key? key, + required this.child, + required this.stream, + required this.builder, + }) : super(key: key); + + final Widget child; + final Stream stream; + final SnackBar Function(T) builder; + + @override + State> createState() => _DisplayToastState(); +} + +class _DisplayToastState extends State> { + late StreamSubscription _subscription; + + @override + void initState() { + super.initState(); + _subscription = widget.stream.listen((event) { + ScaffoldMessenger.of(context).showSnackBar(widget.builder(event)); + }); + } + + @override + Widget build(BuildContext context) => widget.child; + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +} diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart index 8ea1a6d..98720ae 100644 --- a/kilochat/lib/main.dart +++ b/kilochat/lib/main.dart @@ -8,55 +8,13 @@ import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:kilochat/widget_builders.dart'; -import 'avatar.dart'; import 'firebase_options.dart'; -import 'model.dart'; -import 'profile_form.dart'; -import 'providers.dart'; -import 'realm_ui.dart'; -import 'tiles.dart'; +import 'router.dart'; const freedomBlue = Color(0xff0057b7); const energizingYellow = Color(0xffffd700); -enum Routes { - logIn, - chat; - - String get path => '/$this'; -} - -extension RoutesX on Routes { - go(BuildContext context) => context.go(path); -} - -final _router = GoRouter( - initialLocation: Routes.logIn.path, - routes: [ - GoRoute( - path: Routes.chat.path, - builder: (context, state) => const MyApp(), - ), - GoRoute( - path: Routes.logIn.path, - builder: (context, state) { - return SignInScreen(actions: [ - AuthStateChangeAction((context, state) async { - final ref = ProviderScope.containerOf(context); - final firebaseUserController = - ref.read(firebaseUserProvider.notifier); - firebaseUserController.state = state.user; - Routes.chat.go(context); - }), - ]); - }, - ), - ], -); - Future main() async { Animate.restartOnHotReload = true; WidgetsFlutterBinding.ensureInitialized(); @@ -69,290 +27,16 @@ Future main() async { ]); runApp( ProviderScope( - child: Builder( - builder: (context) { - return MaterialApp.router( - routerConfig: _router, - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: freedomBlue, - inversePrimary: energizingYellow, - ), - ), - ); - }, - ), - ), - ); -} - -class MyApp extends ConsumerWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final focusedChannel = ref.watch(focusedChannelProvider); - final repository = ref.watch(repositoryProvider); - return repository.when( - error: buildErrorWidget, - loading: buildLoadingWidget, - data: (repository) { - final user = repository.user; - return Scaffold( - appBar: AppBar( - title: focusedChannel == null - ? const Text('Kilochat') - : Text('# ${focusedChannel.name}'), - actions: [ - IconButton( - onPressed: () => showSearch( - context: context, - delegate: RealmSearchDelegate( - repository.searchMessage, - (context, item, animation) => - MessageTile(message: item, animation: animation), - )), - icon: const Icon(Icons.search), - ), - IconButton( - onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ProfileForm(initialProfile: user))); - }, - icon: MyAvatar(user: user), - ), - ], - ), - body: const Center(child: Text('Choose a channel')) - .animate(target: focusedChannel == null ? 0 : 1) - .crossfade(builder: (context) => const ChatWidget()), - drawer: Builder( - builder: (context) { - return Drawer( - child: SafeArea( - child: ChannelsView( - onTap: (channel) { - ref.read(focusedChannelProvider.notifier).focus(channel); - Scaffold.of(context).closeDrawer(); - }, - ), - ), - ); - }, - ), - ); - }, - ); - } -} - -class ChatWidget extends ConsumerStatefulWidget { - const ChatWidget({super.key}); - - @override - ConsumerState createState() => _ChatWidgetState(); -} - -class _ChatWidgetState extends ConsumerState { - final controller = TextEditingController(); - final scrollController = ScrollController(); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Expanded(child: MessagesView(scrollController: scrollController)), - Ink( - color: Theme.of(context).colorScheme.inversePrimary, - padding: const EdgeInsets.only(left: 16, bottom: 16, right: 16), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: TextField( - minLines: 1, - maxLines: 5, - controller: controller, - decoration: const InputDecoration( - label: Text('Enter next message'), - border: InputBorder.none, - hintText: 'here..', - ), - textInputAction: TextInputAction.done, - onSubmitted: _postNewMessage, - ), - ), - ), - IconButton( - enableFeedback: true, - onPressed: () => _postNewMessage(controller.text), - icon: const Icon(Icons.send), - ) - ], + child: MaterialApp.router( + routerConfig: router, + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed( + seedColor: freedomBlue, + inversePrimary: energizingYellow, ), ), - ], - ); - } - - void _postNewMessage(String text) async { - if (text.isNotEmpty) { - final channel = ref.read(focusedChannelProvider); - final repository = ref.read(repositoryProvider).requireValue; - repository.postNewMessage(channel!, text); - controller.clear(); - await scrollController.animateTo( - 0, - duration: const Duration(milliseconds: 300), - curve: Curves.easeOut, - ); - } - } -} - -class MessagesView extends ConsumerWidget { - const MessagesView({ - super.key, - this.scrollController, - }); - - final ScrollController? scrollController; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final channel = ref.watch(focusedChannelProvider); - final messages = ref.watch(messagesProvider(channel)); - return messages.when( - error: buildErrorWidget, - loading: buildLoadingWidget, - data: (messages) => RealmAnimatedList( - results: messages, - itemBuilder: (context, item, animation) { - return MessageTile(message: item, animation: animation); - }, - reverse: true, - controller: scrollController, ), - ); - } -} - -class ChannelsView extends ConsumerWidget { - const ChannelsView({super.key, this.onTap}); - - final void Function(Channel)? onTap; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final focusedChannel = ref.watch(focusedChannelProvider); - final repository = ref.watch(repositoryProvider); - return repository.when( - error: buildErrorWidget, - loading: buildLoadingWidget, - data: (repository) { - final user = repository.user; - final channels = user.channels.asResults(); - return ListTileTheme( - data: ListTileThemeData( - //dense: true, - selectedTileColor: Theme.of(context).colorScheme.inversePrimary, - ), - child: Column( - children: [ - Expanded( - child: RealmAnimatedList( - results: channels, - itemBuilder: (_, channel, animation) => ChannelTile( - channel: channel, - animation: animation, - onTap: () => onTap?.call(channel), - onDismissed: (_) => - repository.unsubscribeFromChannel(channel), - selected: channel == focusedChannel, - ), - ), - ), - Row( - children: [ - IconButton( - onPressed: () { - showSearch( - context: context, - delegate: ChannelSearchDelegate( - (query) => repository.searchChannel(query), - (_, channel, animation) => StatefulBuilder( - builder: (_, setState) { - return ChannelTile( - channel: channel, - animation: animation, - onTap: () => setState(() { - repository.subscribeToChannel(channel); - }), - selected: !channel.isFrozen && - user.channels.contains(channel), - ); - }, - ), - ), - ); - }, - icon: const Icon(Icons.add), - ) - ], - ), - const AboutListTile( - applicationName: 'kilochat', - applicationVersion: '1.0.0', - applicationIcon: Icon(Icons.send), - applicationLegalese: 'Copyright 2023 MongoDB', - dense: true, - aboutBoxChildren: [ - SizedBox(height: 10), - Text( - 'A chat application using Flutter, Realm, and Atlas ' - 'Device Sync. Written in less than 1K lines of code ' - '(excluding generated code), hence the name.', - textScaleFactor: 0.8, - ) - ], - ), - ], - ), - ); - }, - ); - } -} - -class ChannelSearchDelegate extends RealmSearchDelegate { - bool _showSubscribed = true; - - ChannelSearchDelegate(super.resultsBuilder, super.itemBuilder); - - @override - List buildActions(BuildContext context) { - final repository = - ProviderScope.containerOf(context).read(repositoryProvider).value; - return [ - if (query.isNotEmpty && repository != null) - IconButton( - icon: const Icon(Icons.add), - onPressed: () { - final channel = repository.createChannel(query.trim()); - repository.subscribeToChannel(channel); - close(context, null); - }, - ), - StatefulBuilder(builder: (_, setState) { - return Switch( - value: _showSubscribed, - onChanged: (value) => setState(() => _showSubscribed = value), - ); - }), - ...super.buildActions(context), - ]; - } + ), + ); } diff --git a/kilochat/lib/messages_view.dart b/kilochat/lib/messages_view.dart new file mode 100644 index 0000000..380525a --- /dev/null +++ b/kilochat/lib/messages_view.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'providers.dart'; +import 'realm_ui.dart'; +import 'tiles.dart'; +import 'widget_builders.dart'; + +class MessagesView extends ConsumerWidget { + const MessagesView({ + super.key, + this.scrollController, + }); + + final ScrollController? scrollController; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final channel = ref.watch(focusedChannelProvider); + final messages = ref.watch(messagesProvider(channel)); + return messages.when( + error: buildErrorWidget, + loading: buildLoadingWidget, + data: (messages) => RealmAnimatedList( + results: messages, + itemBuilder: (context, item, animation) { + return MessageTile(message: item, animation: animation); + }, + reverse: true, + controller: scrollController, + ), + ); + } +} diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index 8db8ed2..fd0794a 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:kilochat/providers.dart'; import 'package:realm/realm.dart'; import 'model.dart'; @@ -22,9 +23,13 @@ extension on Stream> { for (final i in change.deleted.reversed) { mutableSubscriptions.unsubscribe(previous!.elementAt(i)); } - } - for (final i in change.inserted) { - mutableSubscriptions.subscribe(results.elementAt(i)); + for (final i in change.inserted) { + mutableSubscriptions.subscribe(results.elementAt(i)); + } + } else { + for (final channel in results) { + mutableSubscriptions.subscribe(channel); + } } previous = results.freeze(); }); @@ -54,6 +59,11 @@ class Repository { RealmResults messages(Channel channel) => _realm .query(r'channel == $0 SORT(index DESC, id ASC)', [channel]); + late Stream x = + allMessages.changes.where((c) => c.inserted.isNotEmpty).map((c) { + return c.results.first; + }); + void updateUserProfile(UserProfile newProfile) => _realm.write(() => _realm.add(newProfile, update: true)); diff --git a/kilochat/lib/router.dart b/kilochat/lib/router.dart new file mode 100644 index 0000000..526f87b --- /dev/null +++ b/kilochat/lib/router.dart @@ -0,0 +1,63 @@ +import 'package:firebase_ui_auth/firebase_ui_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import 'chat_screen.dart'; +import 'providers.dart'; +import 'settings.dart'; +import 'workspace_view.dart'; + +enum Routes { + chat, + chooseWorkspace, + logIn, + profile; + + String get name => '$this'; + String get path => '/$this'; +} + +extension RoutesX on Routes { + go(BuildContext context) => context.go(path); +} + +final router = GoRouter( + initialLocation: currentWorkspace == null + ? Routes.chooseWorkspace.path // no workspace, so choose one + : (currentWorkspace!.app.currentUser == null // no user, so log in + ? Routes.logIn.path + : Routes.chat.path), // otherwise, go directly to chat + routes: [ + GoRoute( + path: Routes.chat.path, + builder: (context, state) => const ChatScreen(), + ), + GoRoute( + path: Routes.chooseWorkspace.path, + builder: (context, state) { + return const WorkspaceScreen(); + }), + GoRoute( + path: Routes.logIn.path, + builder: (context, state) { + return SignInScreen(actions: [ + AuthStateChangeAction((context, state) async { + final ref = ProviderScope.containerOf(context); + final firebaseUserController = + ref.read(firebaseUserProvider.notifier); + firebaseUserController.state = state.user; + Routes.chat.go(context); + }), + ]); + }, + ), + GoRoute( + path: Routes.profile.path, + builder: (context, state) { + return const Placeholder(); + // return ProfileForm(); + }, + ), + ], +); diff --git a/kilochat/lib/settings.dart b/kilochat/lib/settings.dart new file mode 100644 index 0000000..a3f3e5c --- /dev/null +++ b/kilochat/lib/settings.dart @@ -0,0 +1,47 @@ +import 'package:realm/realm.dart'; + +part 'settings.g.dart'; + +@RealmModel() +class _Workspace { + @PrimaryKey() + late String appId; // atlas app service id + late String name; // for display + + @Ignored() + late final App app = App(AppConfiguration(appId)); +} + +@RealmModel() +class _Settings { + _Workspace? workspace; // current workspace +} + +final _realm = Realm( + Configuration.local( + [Settings.schema, Workspace.schema], + path: 'settings.realm', + ), +); + +// local persisted singleton +final _settings = _realm + .write(() => _realm.all().firstOrNull ?? _realm.add(Settings())); + +Workspace? get currentWorkspace => _settings.workspace; +set currentWorkspace(Workspace? value) => + _realm.write(() => _settings.workspace = value); + +void addOrUpdateWorkspace(Workspace workspace) { + _realm.write(() => _realm.add(workspace, update: true)); +} + +void deleteWorkspace(Workspace workspace) { + _realm.write(() => _realm.delete(workspace)); +} + +Stream get workspaceChanges => + _settings.changes.map((c) => c.object.workspace); + +RealmResults get workspaces => + _realm.query('TRUEPREDICATE SORT(name ASCENDING)'); diff --git a/kilochat/lib/settings.g.dart b/kilochat/lib/settings.g.dart new file mode 100644 index 0000000..eeee672 --- /dev/null +++ b/kilochat/lib/settings.g.dart @@ -0,0 +1,82 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'settings.dart'; + +// ************************************************************************** +// RealmObjectGenerator +// ************************************************************************** + +class Workspace extends _Workspace + with RealmEntity, RealmObjectBase, RealmObject { + Workspace( + String appId, + String name, + ) { + RealmObjectBase.set(this, 'appId', appId); + RealmObjectBase.set(this, 'name', name); + } + + Workspace._(); + + @override + String get appId => RealmObjectBase.get(this, 'appId') as String; + @override + set appId(String value) => RealmObjectBase.set(this, 'appId', value); + + @override + String get name => RealmObjectBase.get(this, 'name') as String; + @override + set name(String value) => RealmObjectBase.set(this, 'name', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Workspace freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Workspace._); + return const SchemaObject(ObjectType.realmObject, Workspace, 'Workspace', [ + SchemaProperty('appId', RealmPropertyType.string, primaryKey: true), + SchemaProperty('name', RealmPropertyType.string), + ]); + } +} + +class Settings extends _Settings + with RealmEntity, RealmObjectBase, RealmObject { + Settings({ + Workspace? workspace, + }) { + RealmObjectBase.set(this, 'workspace', workspace); + } + + Settings._(); + + @override + Workspace? get workspace => + RealmObjectBase.get(this, 'workspace') as Workspace?; + @override + set workspace(covariant Workspace? value) => + RealmObjectBase.set(this, 'workspace', value); + + @override + Stream> get changes => + RealmObjectBase.getChanges(this); + + @override + Settings freeze() => RealmObjectBase.freezeObject(this); + + static SchemaObject get schema => _schema ??= _initSchema(); + static SchemaObject? _schema; + static SchemaObject _initSchema() { + RealmObjectBase.registerFactory(Settings._); + return const SchemaObject(ObjectType.realmObject, Settings, 'Settings', [ + SchemaProperty('workspace', RealmPropertyType.object, + optional: true, linkTarget: 'Workspace'), + ]); + } +} diff --git a/kilochat/lib/workspace_view.dart b/kilochat/lib/workspace_view.dart new file mode 100644 index 0000000..dd372e8 --- /dev/null +++ b/kilochat/lib/workspace_view.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kilochat/realm_ui.dart'; +import 'package:kilochat/settings.dart'; + +import 'router.dart'; + +class WorkspaceView extends ConsumerWidget { + const WorkspaceView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return RealmAnimatedList( + results: workspaces, + itemBuilder: (context, item, animation) { + return ListTile( + onTap: () { + currentWorkspace = item; + Routes.chat.go(context); + }, // change workspace + onLongPress: () => showDialog( + context: context, + builder: (_) { + return WorkspaceForm(initialWorkspace: item); // edit workspace + }, + ), + title: Text('# ${item.name}'), + ); + }, + ); + } +} + +class WorkspaceScreen extends StatelessWidget { + const WorkspaceScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: const WorkspaceView(), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.add), + onPressed: () async { + await showDialog( + context: context, + builder: (context) { + return const Dialog( + child: Padding( + padding: EdgeInsets.all(8.0), + child: Wrap(children: [WorkspaceForm()]), + ), + ); + }, + ); + }, + ), + ); + } +} + +class WorkspaceForm extends StatefulWidget { + const WorkspaceForm({ + super.key, + this.initialWorkspace, + }); + + final Workspace? initialWorkspace; + + @override + State createState() => _WorkspaceFormState(); +} + +class _WorkspaceFormState extends State { + final formKey = GlobalKey(); + late final Workspace workspace; + + @override + void initState() { + workspace = Workspace( + widget.initialWorkspace?.appId ?? '', + widget.initialWorkspace?.name ?? '', + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Create a workspace'), + const SizedBox(height: 4), + TextFormField( + decoration: const InputDecoration(labelText: 'name'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter name to display'; + } + return null; + }, + onSaved: (newValue) { + workspace.name = newValue!; + }, + ), + TextFormField( + decoration: const InputDecoration(labelText: 'appId'), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a valid Atlas App ID'; + } + return null; + }, + onSaved: (newValue) { + workspace.appId = newValue!; + }, + ), + const SizedBox(height: 4), + Row( + children: [ + const Spacer(), + TextButton.icon( + onPressed: () { + final state = formKey.currentState!; + if (state.validate()) { + state.save(); + addOrUpdateWorkspace(workspace); + currentWorkspace = workspace; + // TODO: save workspace + Navigator.pop(context); + } + }, + icon: const Icon(Icons.save), + label: const Text('Save'), + ), + ], + ), + ], + ), + ); + } +} From b3365efb7d86d64ba7d1ae52745fa9cdda4faf24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 4 Aug 2023 12:29:18 +0200 Subject: [PATCH 13/33] Toasts working.. still needs a bit of love --- kilochat/lib/chat_screen.dart | 21 ++++++++----- kilochat/lib/display_toast.dart | 55 +++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 44144f9..3f66eda 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -56,12 +54,19 @@ class ChatScreen extends ConsumerWidget { .crossfade(builder: (context) { return DisplayToast( stream: repository.x, - builder: (message) => SnackBar( - content: MessageTile( - message: message, animation: AnimationController()), - showCloseIcon: true, - closeIconColor: Colors.white, - behavior: SnackBarBehavior.floating, + builder: (message, animation) => // + //Text(message.text), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + color: Colors.yellow, + padding: const EdgeInsets.all(8), + width: double.infinity, + child: Text('in channel "${message.channel!.name}"'), + ), + MessageTile(message: message, animation: animation), + ], ), child: const ChatWidget(), ); diff --git a/kilochat/lib/display_toast.dart b/kilochat/lib/display_toast.dart index 544a89e..617f09a 100644 --- a/kilochat/lib/display_toast.dart +++ b/kilochat/lib/display_toast.dart @@ -11,29 +11,72 @@ class DisplayToast extends StatefulWidget { final Widget child; final Stream stream; - final SnackBar Function(T) builder; + final Widget Function(T, Animation animation) builder; @override State> createState() => _DisplayToastState(); } -class _DisplayToastState extends State> { +class _DisplayToastState extends State> + with TickerProviderStateMixin { late StreamSubscription _subscription; + late final _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + final _overlayKey = GlobalKey(); + @override void initState() { super.initState(); - _subscription = widget.stream.listen((event) { - ScaffoldMessenger.of(context).showSnackBar(widget.builder(event)); - }); + _subscription = widget.stream.asyncMap((event) async { + final entry = OverlayEntry( + builder: (context) => Positioned( + left: 10, + right: 10, + top: 10, + child: FadeTransition( + opacity: _controller, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black), + boxShadow: kElevationToShadow[4], + color: Colors.white, + ), + child: widget.builder(event, _controller), + ), + ), + ), + ); + final state = _overlayKey.currentState; + if (state != null) { + state.insert(entry); + _controller.reset(); + await _controller.forward(); + await Future.delayed(const Duration(seconds: 3)); + await _controller.reverse(); + entry.remove(); + } + }).listen((_) {}); } @override - Widget build(BuildContext context) => widget.child; + Widget build(BuildContext context) { + return Stack( + children: [ + widget.child, + IgnorePointer(child: Overlay(key: _overlayKey)), + ], + ); + } @override void dispose() { _subscription.cancel(); + _controller.dispose(); super.dispose(); } } From db9c45439fe07c2c9349e40f3fc77efb1d8bca5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Mon, 7 Aug 2023 13:14:31 +0200 Subject: [PATCH 14/33] Adaptive/responsive layout (wip) --- kilochat/lib/chat_screen.dart | 69 +++++++++++++++++++++-------------- kilochat/lib/main.dart | 1 + kilochat/lib/providers.dart | 13 +++++-- kilochat/lib/repository.dart | 3 +- kilochat/lib/settings.dart | 1 + kilochat/lib/split_view.dart | 45 +++++++++++++++++++++++ kilochat/pubspec.lock | 8 ---- kilochat/pubspec.yaml | 1 - 8 files changed, 99 insertions(+), 42 deletions(-) create mode 100644 kilochat/lib/split_view.dart diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 3f66eda..cd7516e 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -1,15 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kilochat/channels_view.dart'; -import 'package:kilochat/display_toast.dart'; import 'avatar.dart'; +import 'channels_view.dart'; import 'chat_widget.dart'; +import 'display_toast.dart'; +import 'model.dart'; import 'profile_form.dart'; import 'providers.dart'; import 'realm_ui.dart'; +import 'repository.dart'; import 'settings.dart'; +import 'split_view.dart'; import 'tiles.dart'; import 'widget_builders.dart'; @@ -20,6 +23,8 @@ class ChatScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final focusedChannel = ref.watch(focusedChannelProvider); final repository = ref.watch(repositoryProvider); + final twoPane = MediaQuery.of(context).size.width > 700; + const menuWidth = 300.0; return repository.when( error: buildErrorWidget, loading: buildLoadingWidget, @@ -42,21 +47,20 @@ class ChatScreen extends ConsumerWidget { ), IconButton( onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ProfileForm(initialProfile: user))); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + ProfileForm(initialProfile: user)), + ); }, icon: MyAvatar(user: user), ), ], ), - body: const Center(child: Text('Choose a channel')) - .animate(target: focusedChannel == null ? 0 : 1) - .crossfade(builder: (context) { - return DisplayToast( - stream: repository.x, - builder: (message, animation) => // - //Text(message.text), - Column( + body: DisplayToast( + stream: repository.x, + builder: (message, animation) { + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( @@ -67,26 +71,37 @@ class ChatScreen extends ConsumerWidget { ), MessageTile(message: message, animation: animation), ], - ), - child: const ChatWidget(), - ); - }), - drawer: Builder( - builder: (context) { - return Drawer( - child: SafeArea( - child: ChannelsView( - onTap: (channel) { - ref.read(focusedChannelProvider.notifier).focus(channel); - Scaffold.of(context).closeDrawer(); - }, - ), - ), ); }, + child: SplitView( + showLeft: twoPane, + leftWidth: menuWidth, + rightPane: _buildChatPane(focusedChannel, repository), + leftPane: _buildChannelPane(ref, context), + ), ), + drawer: twoPane + ? null // no drawer on large screens + : Drawer(child: _buildChannelPane(ref, context)), ); }, ); } + + Widget _buildChannelPane(WidgetRef ref, BuildContext context) { + return SafeArea( + child: ChannelsView( + onTap: (channel) { + ref.read(focusedChannelProvider.notifier).focus(channel); + Scaffold.of(context).closeDrawer(); + }, + ), + ); + } + + Widget _buildChatPane(Channel? focusedChannel, Repository repository) { + return const Center(child: Text('Choose a channel')) + .animate(target: focusedChannel == null ? 0 : 1) + .crossfade(builder: (context) => const ChatWidget()); + } } diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart index 98720ae..6be29e1 100644 --- a/kilochat/lib/main.dart +++ b/kilochat/lib/main.dart @@ -31,6 +31,7 @@ Future main() async { routerConfig: router, debugShowCheckedModeBanner: false, theme: ThemeData( + //useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: freedomBlue, inversePrimary: energizingYellow, diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 869e841..e2def04 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -10,6 +10,7 @@ import 'package:cancellation_token/cancellation_token.dart'; import 'model.dart'; import 'repository.dart'; +import 'settings.dart'; part 'providers.g.dart'; @@ -29,10 +30,14 @@ final focusedChannelProvider = @riverpod Stream app(AppRef ref) async* { - final app = App(AppConfiguration('kilochat-app-ighux')); - yield app; - await for (final _ in Connectivity().onConnectivityChanged) { - app.reconnect(); + StreamSubscription? subscription; + await for (final ws in workspaceChanges) { + subscription?.cancel(); + if (ws == null) continue; + subscription = Connectivity().onConnectivityChanged.listen((_) { + ws.app.reconnect(); + }); + yield ws.app; } } diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index fd0794a..afe9df4 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:kilochat/providers.dart'; import 'package:realm/realm.dart'; import 'model.dart'; @@ -61,7 +60,7 @@ class Repository { late Stream x = allMessages.changes.where((c) => c.inserted.isNotEmpty).map((c) { - return c.results.first; + return c.results[c.inserted.last]; }); void updateUserProfile(UserProfile newProfile) => diff --git a/kilochat/lib/settings.dart b/kilochat/lib/settings.dart index a3f3e5c..57cfb2c 100644 --- a/kilochat/lib/settings.dart +++ b/kilochat/lib/settings.dart @@ -7,6 +7,7 @@ class _Workspace { @PrimaryKey() late String appId; // atlas app service id late String name; // for display + String? currentChannelId; // current channel @Ignored() late final App app = App(AppConfiguration(appId)); diff --git a/kilochat/lib/split_view.dart b/kilochat/lib/split_view.dart new file mode 100644 index 0000000..893d2d9 --- /dev/null +++ b/kilochat/lib/split_view.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class SplitView extends StatelessWidget { + final bool showLeft; + final double leftWidth; + final Widget leftPane; + final Widget rightPane; + + const SplitView({ + super.key, + required this.showLeft, + required this.leftWidth, + required this.leftPane, + required this.rightPane, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + AnimatedContainer( + decoration: const BoxDecoration( + border: Border( + right: BorderSide(color: Colors.black, width: 0.5), + ), + ), + clipBehavior: Clip.hardEdge, + width: showLeft ? leftWidth : 0, + duration: const Duration(milliseconds: 300), + child: OverflowBox( + alignment: Alignment.centerRight, + minWidth: leftWidth, + maxWidth: leftWidth, + child: leftPane, + ), + ), + Expanded( + child: ClipRect( + child: rightPane, + ), + ), + ], + ); + } +} diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 9b8e689..2af42cf 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -970,14 +970,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2+1" - statsfl: - dependency: "direct main" - description: - name: statsfl - sha256: "12901533eee23cc04f01858f1e6851beec27d927a9afa694bb3d68403a5e0d13" - url: "https://pub.dev" - source: hosted - version: "2.3.0" stream_channel: dependency: transitive description: diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index e8da769..6206450 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -29,7 +29,6 @@ dependencies: realm: ^1.3.0 riverpod_annotation: ^2.0.0 riverpod: ^2.3.0 - statsfl: ^2.0.0 dev_dependencies: flutter_test: From 665b05b57ed0b6c38297ae7d3759ea717a602f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 9 Aug 2023 10:15:37 +0200 Subject: [PATCH 15/33] Add mason brick to configure app service --- kilochat/bricks/kilochat_appx/CHANGELOG.md | 3 + kilochat/bricks/kilochat_appx/LICENSE | 1 + kilochat/bricks/kilochat_appx/README.md | 27 +++++++++ .../{{app_name}}/auth/custom_user_data.json | 3 + .../{{app_name}}/auth/providers.json | 29 ++++++++++ .../data_sources/{{service_name}}/config.json | 10 ++++ .../{{db_name}}/Channel/relationships.json | 8 +++ .../{{db_name}}/Channel/rules.json | 21 +++++++ .../{{db_name}}/Channel/schema.json | 27 +++++++++ .../{{db_name}}/Message/relationships.json | 14 +++++ .../{{db_name}}/Message/rules.json | 21 +++++++ .../{{db_name}}/Message/schema.json | 34 ++++++++++++ .../{{db_name}}/Reaction/relationships.json | 14 +++++ .../{{db_name}}/Reaction/rules.json | 21 +++++++ .../{{db_name}}/Reaction/schema.json | 26 +++++++++ .../UserProfile/relationships.json | 14 +++++ .../{{db_name}}/UserProfile/rules.json | 21 +++++++ .../{{db_name}}/UserProfile/schema.json | 54 ++++++++++++++++++ .../__brick__/{{app_name}}/realm_config.json | 7 +++ .../__brick__/{{app_name}}/sync/config.json | 13 +++++ kilochat/bricks/kilochat_appx/brick.yaml | 55 +++++++++++++++++++ kilochat/mason.yaml | 3 + 22 files changed, 426 insertions(+) create mode 100644 kilochat/bricks/kilochat_appx/CHANGELOG.md create mode 100644 kilochat/bricks/kilochat_appx/LICENSE create mode 100644 kilochat/bricks/kilochat_appx/README.md create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/custom_user_data.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/providers.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/config.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/relationships.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/rules.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/schema.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/relationships.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/rules.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/schema.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/relationships.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/rules.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/schema.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/relationships.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/rules.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/schema.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/realm_config.json create mode 100644 kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/sync/config.json create mode 100644 kilochat/bricks/kilochat_appx/brick.yaml create mode 100644 kilochat/mason.yaml diff --git a/kilochat/bricks/kilochat_appx/CHANGELOG.md b/kilochat/bricks/kilochat_appx/CHANGELOG.md new file mode 100644 index 0000000..f0640d6 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/CHANGELOG.md @@ -0,0 +1,3 @@ +# 0.1.0+1 + +- TODO: Describe initial release. diff --git a/kilochat/bricks/kilochat_appx/LICENSE b/kilochat/bricks/kilochat_appx/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/kilochat/bricks/kilochat_appx/README.md b/kilochat/bricks/kilochat_appx/README.md new file mode 100644 index 0000000..ab85769 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/README.md @@ -0,0 +1,27 @@ +# kilochat_appx + +[![Powered by Mason](https://img.shields.io/endpoint?url=https%3A%2F%2Ftinyurl.com%2Fmason-badge)](https://github.com/felangel/mason) + +A new brick created with the Mason CLI. + +_Generated by [mason][1] 🧱_ + +## Getting Started 🚀 + +This is a starting point for a new brick. +A few resources to get you started if this is your first brick template: + +- [Official Mason Documentation][2] +- [Code generation with Mason Blog][3] +- [Very Good Livestream: Felix Angelov Demos Mason][4] +- [Flutter Package of the Week: Mason][5] +- [Observable Flutter: Building a Mason brick][6] +- [Meet Mason: Flutter Vikings 2022][7] + +[1]: https://github.com/felangel/mason +[2]: https://docs.brickhub.dev +[3]: https://verygood.ventures/blog/code-generation-with-mason +[4]: https://youtu.be/G4PTjA6tpTU +[5]: https://youtu.be/qjA0JFiPMnQ +[6]: https://youtu.be/o8B1EfcUisw +[7]: https://youtu.be/LXhgiF5HiQg diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/custom_user_data.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/custom_user_data.json new file mode 100644 index 0000000..a82d0fb --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/custom_user_data.json @@ -0,0 +1,3 @@ +{ + "enabled": false +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/providers.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/providers.json new file mode 100644 index 0000000..6fd5fdc --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/providers.json @@ -0,0 +1,29 @@ +{ + "anon-user": { + "name": "anon-user", + "type": "anon-user", + "disabled": true + }, + "api-key": { + "name": "api-key", + "type": "api-key", + "disabled": true + }, + "custom-token": { + "name": "custom-token", + "type": "custom-token", + "config": { + "audience": [ + "{{audience}}" + ], + "jwkURI": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com", + "requireAnyAudience": false, + "signingAlgorithm": "", + "useJWKURI": true + }, + "secret_config": { + "signingKeys": [] + }, + "disabled": false + } +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/config.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/config.json new file mode 100644 index 0000000..911f94f --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/config.json @@ -0,0 +1,10 @@ +{ + "name": "{{service_name}}", + "type": "mongodb-atlas", + "config": { + "clusterName": "{{cluster_name}}", + "readPreference": "primary", + "wireProtocolEnabled": false + }, + "version": 1 +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/relationships.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/relationships.json new file mode 100644 index 0000000..525f601 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/relationships.json @@ -0,0 +1,8 @@ +{ + "parent": { + "ref": "#/relationship/{{service_name}}/{{db_name}}/Channel", + "source_key": "parent", + "foreign_key": "_id", + "is_list": false + } +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/rules.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/rules.json new file mode 100644 index 0000000..a7bbf51 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/rules.json @@ -0,0 +1,21 @@ +{ + "collection": "Channel", + "database": "{{db_name}}", + "roles": [ + { + "name": "readAllWriteOwn", + "apply_when": {}, + "document_filters": { + "write": { + "owner_id": "%%user.id" + }, + "read": true + }, + "read": true, + "write": true, + "insert": true, + "delete": true, + "search": true + } + ] +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/schema.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/schema.json new file mode 100644 index 0000000..5c3c59b --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Channel/schema.json @@ -0,0 +1,27 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "count": { + "bsonType": "long" + }, + "name": { + "bsonType": "string" + }, + "owner_id": { + "bsonType": "string" + }, + "parent": { + "bsonType": "objectId" + } + }, + "required": [ + "_id", + "count", + "name", + "owner_id" + ], + "title": "Channel", + "type": "object" +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/relationships.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/relationships.json new file mode 100644 index 0000000..818f6af --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/relationships.json @@ -0,0 +1,14 @@ +{ + "channel": { + "ref": "#/relationship/{{service_name}}/{{db_name}}/Channel", + "source_key": "channel", + "foreign_key": "_id", + "is_list": false + }, + "owner": { + "ref": "#/relationship/{{service_name}}/{{db_name}}/UserProfile", + "source_key": "owner", + "foreign_key": "_id", + "is_list": false + } +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/rules.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/rules.json new file mode 100644 index 0000000..b732fd5 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/rules.json @@ -0,0 +1,21 @@ +{ + "collection": "Message", + "database": "{{db_name}}", + "roles": [ + { + "name": "readAllWriteOwn", + "apply_when": {}, + "document_filters": { + "write": { + "owner_id": "%%user.id" + }, + "read": true + }, + "read": true, + "write": true, + "insert": true, + "delete": true, + "search": true + } + ] +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/schema.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/schema.json new file mode 100644 index 0000000..99c59f2 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Message/schema.json @@ -0,0 +1,34 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "channel": { + "bsonType": "objectId" + }, + "channel_id": { + "bsonType": "objectId" + }, + "index": { + "bsonType": "long" + }, + "owner": { + "bsonType": "string" + }, + "owner_id": { + "bsonType": "string" + }, + "text": { + "bsonType": "string" + } + }, + "required": [ + "_id", + "channel_id", + "index", + "owner_id", + "text" + ], + "title": "Message", + "type": "object" +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/relationships.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/relationships.json new file mode 100644 index 0000000..0d5c9cd --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/relationships.json @@ -0,0 +1,14 @@ +{ + "message": { + "ref": "#/relationship/{{service_name}}/{{db_name}}/Message", + "source_key": "message", + "foreign_key": "_id", + "is_list": false + }, + "owner": { + "ref": "#/relationship/{{service_name}}/{{db_name}}/UserProfile", + "source_key": "owner", + "foreign_key": "_id", + "is_list": false + } +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/rules.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/rules.json new file mode 100644 index 0000000..f7038e6 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/rules.json @@ -0,0 +1,21 @@ +{ + "collection": "Reaction", + "database": "{{db_name}}", + "roles": [ + { + "name": "readAllWriteOwn", + "apply_when": {}, + "document_filters": { + "write": { + "owner_id": "%%user.id" + }, + "read": true + }, + "read": true, + "write": true, + "insert": true, + "delete": true, + "search": true + } + ] +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/schema.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/schema.json new file mode 100644 index 0000000..54f9ccd --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/Reaction/schema.json @@ -0,0 +1,26 @@ +{ + "properties": { + "_id": { + "bsonType": "objectId" + }, + "emojiUnicode": { + "bsonType": "long" + }, + "message": { + "bsonType": "objectId" + }, + "owner": { + "bsonType": "string" + }, + "owner_id": { + "bsonType": "string" + } + }, + "required": [ + "_id", + "emojiUnicode", + "owner_id" + ], + "title": "Reaction", + "type": "object" +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/relationships.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/relationships.json new file mode 100644 index 0000000..8a9bff2 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/relationships.json @@ -0,0 +1,14 @@ +{ + "bodies": { + "ref": "#/relationship/{{service_name}}/{{db_name}}/UserProfile", + "source_key": "bodies", + "foreign_key": "_id", + "is_list": true + }, + "channels": { + "ref": "#/relationship/{{service_name}}/{{db_name}}/Channel", + "source_key": "channels", + "foreign_key": "_id", + "is_list": true + } +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/rules.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/rules.json new file mode 100644 index 0000000..1d9a29d --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/rules.json @@ -0,0 +1,21 @@ +{ + "collection": "UserProfile", + "database": "{{db_name}}", + "roles": [ + { + "name": "readAllWriteOwn", + "apply_when": {}, + "document_filters": { + "write": { + "owner_id": "%%user.id" + }, + "read": true + }, + "read": true, + "write": true, + "insert": true, + "delete": true, + "search": true + } + ] +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/schema.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/schema.json new file mode 100644 index 0000000..44f9cc5 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/data_sources/{{service_name}}/{{db_name}}/UserProfile/schema.json @@ -0,0 +1,54 @@ +{ + "properties": { + "_id": { + "bsonType": "string" + }, + "age": { + "bsonType": "long" + }, + "bodies": { + "bsonType": "array", + "items": { + "bsonType": "string" + }, + "uniqueItems": true + }, + "channels": { + "bsonType": "array", + "items": { + "bsonType": "objectId" + }, + "uniqueItems": true + }, + "deactivated": { + "bsonType": "bool" + }, + "email": { + "bsonType": "string" + }, + "gender": { + "bsonType": "long" + }, + "name": { + "bsonType": "string" + }, + "owner_id": { + "bsonType": "string" + }, + "status_emoji": { + "bsonType": "long" + }, + "typing": { + "bsonType": "bool" + } + }, + "required": [ + "_id", + "deactivated", + "gender", + "owner_id", + "typing" + ], + "title": "UserProfile", + "type": "object" +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/realm_config.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/realm_config.json new file mode 100644 index 0000000..b7824ca --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/realm_config.json @@ -0,0 +1,7 @@ +{ + "config_version": 20210101, + "name": "{{app_name}}", + "location": "DE-FF", + "provider_region": "aws-eu-central-1", + "deployment_model": "LOCAL" +} diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/sync/config.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/sync/config.json new file mode 100644 index 0000000..36a8b33 --- /dev/null +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/sync/config.json @@ -0,0 +1,13 @@ +{ + "type": "flexible", + "state": "enabled", + "development_mode_enabled": true, + "service_name": "{{service_name}}", + "database_name": "{{db_name}}", + "client_max_offline_days": 30, + "is_recovery_mode_disabled": false, + "queryable_fields_names": [ + "owner_id", + "channel_id" + ] +} diff --git a/kilochat/bricks/kilochat_appx/brick.yaml b/kilochat/bricks/kilochat_appx/brick.yaml new file mode 100644 index 0000000..3eb943d --- /dev/null +++ b/kilochat/bricks/kilochat_appx/brick.yaml @@ -0,0 +1,55 @@ +name: kilochat_appx +description: >- + A new brick to create app service configuration for an Atlas Device Sync + enabled appx app, that fits the kilochat app. + +# The following defines the brick repository url. (TODO) +# Uncomment and update the following line before publishing the brick. +# repository: https://github.com/my_org/my_repo + +# The following defines the version and build number for your brick. +# A version number is three numbers separated by dots, like 1.2.34 +# followed by an optional build number (separated by a +). +version: 0.1.0+1 + +# The following defines the environment for the current brick. +# It includes the version of mason that the brick requires. +environment: + mason: ">=0.1.0-dev.50 <0.1.0" + +# Variables specify dynamic values that your brick depends on. +# Zero or more variables can be specified for a given brick. +# Each variable has: +# * a type (string, number, boolean, enum, array, or list) +# * an optional short description +# * an optional default value +# * an optional list of default values (array only) +# * an optional prompt phrase used when asking for the variable +# * a list of values (enums only) +# * an optional separator (list only) +vars: + service_name: + type: string + description: Service name + default: kilochat-appx-service + prompt: What is your service name? + app_name: + type: string + description: Atlas app name + default: kilochat-app + prompt: What is your Atlas app name? + cluster_name: + type: string + description: Atlas cluster name + default: kilochat-cluster + prompt: What is your Atlas cluster name? + db_name: + type: string + description: Atlas database name + default: kilochat-db + prompt: What is your Atlas database name? + audience: + type: string + description: Firebase jwt provider audience + default: kilochat-realm + prompt: What is your Firebase jwt provider audience? diff --git a/kilochat/mason.yaml b/kilochat/mason.yaml new file mode 100644 index 0000000..bef92f5 --- /dev/null +++ b/kilochat/mason.yaml @@ -0,0 +1,3 @@ +bricks: + kilochat_appx: + path: bricks/kilochat_appx \ No newline at end of file From dca6b2376edce9582e63785cf622faa71382979b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 9 Aug 2023 10:17:29 +0200 Subject: [PATCH 16/33] Router redirect, realm path, etc. --- kilochat/lib/providers.dart | 7 ++++--- kilochat/lib/router.dart | 26 ++++++++++++++++---------- kilochat/lib/settings.dart | 3 ++- kilochat/pubspec.lock | 2 +- kilochat/pubspec.yaml | 1 + 5 files changed, 24 insertions(+), 15 deletions(-) diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index e2def04..6302efa 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -91,7 +91,6 @@ Future syncedRealm(SyncedRealmRef ref) async { } try { realm.subscriptions.update((mutableSubscriptions) { - // TODO: way too simple mutableSubscriptions ..add(realm.all()) ..add(realm.query('channelId == null')) @@ -100,7 +99,7 @@ Future syncedRealm(SyncedRealmRef ref) async { // await realm.subscriptions.waitForSynchronization(); // does not support cancellation yet }); await realm.syncSession.waitForDownload(ct); - } catch (_) {} // ignore + } on TimeoutException catch (_) {} // ignore and proceed return realm; } @@ -113,7 +112,9 @@ Stream user(UserRef ref) async* { if (user == null) { if (firebaseUser != null) { final jwt = await firebaseUser.getIdToken(); - user = await app.logIn(Credentials.jwt(jwt!)); + if (jwt != null) { + user = await app.logIn(Credentials.jwt(jwt)); + } } } if (user != null) yield user; diff --git a/kilochat/lib/router.dart b/kilochat/lib/router.dart index 526f87b..021f900 100644 --- a/kilochat/lib/router.dart +++ b/kilochat/lib/router.dart @@ -23,21 +23,16 @@ extension RoutesX on Routes { } final router = GoRouter( - initialLocation: currentWorkspace == null - ? Routes.chooseWorkspace.path // no workspace, so choose one - : (currentWorkspace!.app.currentUser == null // no user, so log in - ? Routes.logIn.path - : Routes.chat.path), // otherwise, go directly to chat + initialLocation: Routes.chat.path, // redirect will fix if needed routes: [ GoRoute( path: Routes.chat.path, - builder: (context, state) => const ChatScreen(), + builder: (_, __) => const ChatScreen(), ), GoRoute( - path: Routes.chooseWorkspace.path, - builder: (context, state) { - return const WorkspaceScreen(); - }), + path: Routes.chooseWorkspace.path, + builder: (_, __) => const WorkspaceScreen(), + ), GoRoute( path: Routes.logIn.path, builder: (context, state) { @@ -60,4 +55,15 @@ final router = GoRouter( }, ), ], + redirect: (context, state) async { + // always okay to switch workspace + if (state.path == Routes.chat.path) return null; + + // check if workspace or log in is needed + return currentWorkspace == null + ? Routes.chooseWorkspace.path // no workspace, so choose one + : (currentWorkspace!.app.currentUser == null // no user, so log in + ? Routes.logIn.path + : null); // otherwise, navigation okay + }, ); diff --git a/kilochat/lib/settings.dart b/kilochat/lib/settings.dart index 57cfb2c..d2fb60b 100644 --- a/kilochat/lib/settings.dart +++ b/kilochat/lib/settings.dart @@ -1,4 +1,5 @@ import 'package:realm/realm.dart'; +import 'package:path/path.dart' as path; part 'settings.g.dart'; @@ -21,7 +22,7 @@ class _Settings { final _realm = Realm( Configuration.local( [Settings.schema, Workspace.schema], - path: 'settings.realm', + path: path.join(Configuration.defaultStoragePath, 'settings.realm'), ), ); diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 2af42cf..e57d43f 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -758,7 +758,7 @@ packages: source: hosted version: "2.1.0" path: - dependency: transitive + dependency: "direct main" description: name: path sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index 6206450..a40b32d 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -25,6 +25,7 @@ dependencies: flutter_animate: ^4.0.0 flutter_riverpod: ^2.3.0 go_router: ^10.0.0 + path: ^1.0.0 random_avatar: ^0.0.8 realm: ^1.3.0 riverpod_annotation: ^2.0.0 From 3cdd268a64a637be7661fedb857f103ceca69baf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 9 Aug 2023 12:55:23 +0200 Subject: [PATCH 17/33] Show current ConnectionState --- kilochat/lib/chat_screen.dart | 8 +++++++ kilochat/lib/realm_connectivity.dart | 36 ++++++++++++++++++++++++++++ kilochat/lib/repository.dart | 3 +++ 3 files changed, 47 insertions(+) create mode 100644 kilochat/lib/realm_connectivity.dart diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index cd7516e..a979c0c 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kilochat/realm_connectivity.dart'; import 'avatar.dart'; import 'channels_view.dart'; @@ -23,6 +24,7 @@ class ChatScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final focusedChannel = ref.watch(focusedChannelProvider); final repository = ref.watch(repositoryProvider); + final app = ref.watch(appProvider).value; final twoPane = MediaQuery.of(context).size.width > 700; const menuWidth = 300.0; return repository.when( @@ -35,6 +37,12 @@ class ChatScreen extends ConsumerWidget { title: Text( '${currentWorkspace?.name} / ${focusedChannel?.name ?? ''}'), actions: [ + IconButton( + onPressed: () => app?.reconnect(), + icon: RealmConnectivityIndicator( + changes: repository.connectionStateChanges, + ), + ), IconButton( onPressed: () => showSearch( context: context, diff --git a/kilochat/lib/realm_connectivity.dart b/kilochat/lib/realm_connectivity.dart new file mode 100644 index 0000000..b23f88d --- /dev/null +++ b/kilochat/lib/realm_connectivity.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart' hide ConnectionState; +import 'package:realm/realm.dart'; + +class RealmConnectivityIndicator extends StatelessWidget { + const RealmConnectivityIndicator({ + super.key, + this.builder = _defaultBuilder, + required this.changes, + }); + + final Widget Function(ConnectionStateChange connectionState) builder; + final Stream changes; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: changes, + builder: (context, snapshot) { + final data = snapshot.data; + return data != null ? builder(data) : const SizedBox.shrink(); + }, + ); + } + + static Widget _defaultBuilder(ConnectionStateChange connectionState) { + final current = connectionState.current; + return AnimatedSwitcher( + key: ValueKey(current), + duration: kThemeAnimationDuration, + child: switch (current) { + ConnectionState.disconnected => const Icon(Icons.cloud_off), + ConnectionState.connecting => const Icon(Icons.cloud_sync), + ConnectionState.connected => const Icon(Icons.cloud_done), + }); + } +} diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index afe9df4..c2298eb 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -63,6 +63,9 @@ class Repository { return c.results[c.inserted.last]; }); + late Stream connectionStateChanges = + _realm.syncSession.connectionStateChanges; + void updateUserProfile(UserProfile newProfile) => _realm.write(() => _realm.add(newProfile, update: true)); From 4023bb9ab02a660867f6c683493b454f2affc167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 9 Aug 2023 13:18:04 +0200 Subject: [PATCH 18/33] Cleanup imports --- kilochat/README.md | 6 +- kilochat/lib/avatar.dart | 3 +- kilochat/lib/channel_search_delegate.dart | 4 +- kilochat/lib/channels_view.dart | 4 +- kilochat/lib/chat_screen.dart | 4 +- kilochat/lib/display_toast.dart | 1 + kilochat/lib/messages_view.dart | 2 +- kilochat/lib/realm_connectivity.dart | 36 ----- kilochat/lib/realm_ui.dart | 166 ---------------------- kilochat/lib/settings.dart | 2 +- kilochat/lib/tiles.dart | 2 +- kilochat/lib/workspace_view.dart | 4 +- 12 files changed, 18 insertions(+), 216 deletions(-) delete mode 100644 kilochat/lib/realm_connectivity.dart delete mode 100644 kilochat/lib/realm_ui.dart diff --git a/kilochat/README.md b/kilochat/README.md index 1cfa972..cf58b23 100644 --- a/kilochat/README.md +++ b/kilochat/README.md @@ -12,12 +12,14 @@ The app demonstrates how to: 1. Display sync connection state. 1. Handle soft synchronization errors. 1. Handle client resets (hard sync errors). -1. Display a snackbar when interesting data is synced to device. +1. Display a toast when interesting data is synced to device. 1. Animate list views as changes happen in a realm that impacts a live query. 1. Update flexible sync subscriptions to only sync a subset of data dynamically. +1. Use rule-based permissions to ensure users can only manipulate their own data. + +TODO: 1. Handle presence information in a scalable way, using a body-list. 1. Maintain a leader-board collection using an Atlas function that trigger on changes. -1. Use rule-based permissions to ensure users can only manipulate their own data. It also serves as an example of an app architecture that works well with Realm. Depending on how you choose to count, it does so in just 1-2K lines of code, hence the name. diff --git a/kilochat/lib/avatar.dart b/kilochat/lib/avatar.dart index ad2bdc4..df44969 100644 --- a/kilochat/lib/avatar.dart +++ b/kilochat/lib/avatar.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:kilochat/model.dart'; import 'package:random_avatar/random_avatar.dart'; +import 'model.dart'; + class MyAvatar extends StatelessWidget { const MyAvatar({ super.key, diff --git a/kilochat/lib/channel_search_delegate.dart b/kilochat/lib/channel_search_delegate.dart index 528aef4..fbf8ec7 100644 --- a/kilochat/lib/channel_search_delegate.dart +++ b/kilochat/lib/channel_search_delegate.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kilochat/providers.dart'; import 'model.dart'; -import 'realm_ui.dart'; +import 'providers.dart'; +import 'realm_ui/realm_search_delegate.dart'; class ChannelSearchDelegate extends RealmSearchDelegate { bool _showSubscribed = true; diff --git a/kilochat/lib/channels_view.dart b/kilochat/lib/channels_view.dart index 3f51a52..50ea97e 100644 --- a/kilochat/lib/channels_view.dart +++ b/kilochat/lib/channels_view.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kilochat/settings.dart'; import 'channel_search_delegate.dart'; import 'model.dart'; import 'providers.dart'; -import 'realm_ui.dart'; +import 'realm_ui/realm_animated_list.dart'; import 'router.dart'; +import 'settings.dart'; import 'tiles.dart'; import 'widget_builders.dart'; diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index a979c0c..1e2028b 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kilochat/realm_connectivity.dart'; import 'avatar.dart'; import 'channels_view.dart'; @@ -10,7 +9,8 @@ import 'display_toast.dart'; import 'model.dart'; import 'profile_form.dart'; import 'providers.dart'; -import 'realm_ui.dart'; +import 'realm_ui/realm_connectivity_indicator.dart'; +import 'realm_ui/realm_search_delegate.dart'; import 'repository.dart'; import 'settings.dart'; import 'split_view.dart'; diff --git a/kilochat/lib/display_toast.dart b/kilochat/lib/display_toast.dart index 617f09a..d8f0a48 100644 --- a/kilochat/lib/display_toast.dart +++ b/kilochat/lib/display_toast.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:flutter/material.dart'; class DisplayToast extends StatefulWidget { diff --git a/kilochat/lib/messages_view.dart b/kilochat/lib/messages_view.dart index 380525a..a871ba9 100644 --- a/kilochat/lib/messages_view.dart +++ b/kilochat/lib/messages_view.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'providers.dart'; -import 'realm_ui.dart'; +import 'realm_ui/realm_animated_list.dart'; import 'tiles.dart'; import 'widget_builders.dart'; diff --git a/kilochat/lib/realm_connectivity.dart b/kilochat/lib/realm_connectivity.dart deleted file mode 100644 index b23f88d..0000000 --- a/kilochat/lib/realm_connectivity.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart' hide ConnectionState; -import 'package:realm/realm.dart'; - -class RealmConnectivityIndicator extends StatelessWidget { - const RealmConnectivityIndicator({ - super.key, - this.builder = _defaultBuilder, - required this.changes, - }); - - final Widget Function(ConnectionStateChange connectionState) builder; - final Stream changes; - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: changes, - builder: (context, snapshot) { - final data = snapshot.data; - return data != null ? builder(data) : const SizedBox.shrink(); - }, - ); - } - - static Widget _defaultBuilder(ConnectionStateChange connectionState) { - final current = connectionState.current; - return AnimatedSwitcher( - key: ValueKey(current), - duration: kThemeAnimationDuration, - child: switch (current) { - ConnectionState.disconnected => const Icon(Icons.cloud_off), - ConnectionState.connecting => const Icon(Icons.cloud_sync), - ConnectionState.connected => const Icon(Icons.cloud_done), - }); - } -} diff --git a/kilochat/lib/realm_ui.dart b/kilochat/lib/realm_ui.dart deleted file mode 100644 index 4a404f3..0000000 --- a/kilochat/lib/realm_ui.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:realm/realm.dart'; - -typedef ItemWidgetBuilder = Widget Function( - BuildContext context, - E item, - Animation animation, -); - -typedef ErrorWidgetBuilder = Widget Function( - BuildContext context, - Object? error, - StackTrace stackTrace, -); - -extension on Stream> { - Stream> _updateAnimatedList( - GlobalKey listKey, - ItemWidgetBuilder removedItemBuilder, - ) async* { - RealmResults? previous; - await for (final change in this) { - final state = listKey.currentState; - if (previous != null) { - for (final index in change.deleted.reversed) { - final toDie = previous[index]; - state?.removeItem( - index, - (context, animation) => - removedItemBuilder(context, toDie, animation), - ); - } - } - for (final index in change.inserted) { - state?.insertItem(index); - } - final r = change.results; - previous = r.isValid ? r.freeze() : null; - yield change; - } - } -} - -class RealmAnimatedList extends StatefulWidget { - const RealmAnimatedList({ - super.key, - required this.results, - required this.itemBuilder, - ItemWidgetBuilder? removedItemBuilder, - this.loading, - this.error, - this.reverse = false, - this.controller, - this.scrollDirection = Axis.vertical, - }) : removedItemBuilder = removedItemBuilder ?? itemBuilder; - final RealmResults results; - final ItemWidgetBuilder itemBuilder; - final ItemWidgetBuilder removedItemBuilder; - - final WidgetBuilder? loading; - final ErrorWidgetBuilder? error; - - final bool reverse; - final ScrollController? controller; - final Axis scrollDirection; - - @override - State createState() => _RealmAnimatedListState(); -} - -extension on RealmResults { - bool get isLive => isValid && !isFrozen; - Stream> get safeChanges async* { - if (isLive) yield* changes; - } -} - -class _RealmAnimatedListState extends State> { - final _listKey = GlobalKey(); - StreamSubscription? _subscription; - - void _updateSubscription() { - _subscription?.cancel(); - _subscription = widget.results.safeChanges - ._updateAnimatedList(_listKey, widget.removedItemBuilder) - .listen((_) => setState(() {})); - } - - @override - void initState() { - super.initState(); - _updateSubscription(); - } - - @override - void didUpdateWidget(covariant RealmAnimatedList oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.results != oldWidget.results) { - _updateSubscription(); - } - } - - @override - void dispose() { - _subscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final items = widget.results; - return AnimatedList( - key: _listKey, - controller: widget.controller, - scrollDirection: widget.scrollDirection, - initialItemCount: items.length, - itemBuilder: (context, index, animation) => - index < items.length // why is this needed :-/ - ? widget.itemBuilder(context, items[index], animation) - : Container(), - reverse: widget.reverse, - ); - } -} - -class RealmSearchDelegate extends SearchDelegate { - final RealmResults Function(String query) resultsBuilder; - final ItemWidgetBuilder itemBuilder; - - RealmSearchDelegate(this.resultsBuilder, this.itemBuilder); - - @override - List buildActions(BuildContext context) { - // Create a list of actions to display in the app bar - return [ - IconButton( - icon: const Icon(Icons.clear), - onPressed: () => query = '', - ), - ]; - } - - @override - Widget buildLeading(BuildContext context) { - // Create a widget to display as the leading icon in the app bar - return IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => close(context, null), - ); - } - - @override - Widget buildResults(BuildContext context) { - return RealmAnimatedList( - results: resultsBuilder(query), - itemBuilder: itemBuilder, - ); - } - - @override - Widget buildSuggestions(BuildContext context) { - return buildResults(context); - } -} diff --git a/kilochat/lib/settings.dart b/kilochat/lib/settings.dart index d2fb60b..a0eb471 100644 --- a/kilochat/lib/settings.dart +++ b/kilochat/lib/settings.dart @@ -1,5 +1,5 @@ -import 'package:realm/realm.dart'; import 'package:path/path.dart' as path; +import 'package:realm/realm.dart'; part 'settings.g.dart'; diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index 57c6f7e..7ba180b 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -2,11 +2,11 @@ import 'package:animated_emoji/emoji.dart'; import 'package:animated_emoji/emojis.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kilochat/realm_ui.dart'; import 'avatar.dart'; import 'model.dart'; import 'providers.dart'; +import 'realm_ui/realm_animated_list.dart'; class ChannelTile extends StatelessWidget { final Channel channel; diff --git a/kilochat/lib/workspace_view.dart b/kilochat/lib/workspace_view.dart index dd372e8..e811ae9 100644 --- a/kilochat/lib/workspace_view.dart +++ b/kilochat/lib/workspace_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kilochat/realm_ui.dart'; -import 'package:kilochat/settings.dart'; +import 'realm_ui/realm_animated_list.dart'; import 'router.dart'; +import 'settings.dart'; class WorkspaceView extends ConsumerWidget { const WorkspaceView({super.key}); From ece6a4e5e046e1323271a9e6baeff6b43320aa19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 9 Aug 2023 21:32:18 +0200 Subject: [PATCH 19/33] Markdown support, re-worked login (wip) --- kilochat/lib/messages_view.dart | 8 +-- kilochat/lib/profile_form.dart | 2 +- kilochat/lib/providers.dart | 37 +++++------ kilochat/lib/providers.g.dart | 112 ++------------------------------ kilochat/lib/repository.dart | 10 ++- kilochat/lib/router.dart | 33 ++++++---- kilochat/lib/settings.g.dart | 15 ++++- kilochat/lib/tiles.dart | 18 ++++- kilochat/pubspec.lock | 55 ++++++++++------ kilochat/pubspec.yaml | 11 ++-- 10 files changed, 124 insertions(+), 177 deletions(-) diff --git a/kilochat/lib/messages_view.dart b/kilochat/lib/messages_view.dart index a871ba9..abf475f 100644 --- a/kilochat/lib/messages_view.dart +++ b/kilochat/lib/messages_view.dart @@ -17,12 +17,12 @@ class MessagesView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final channel = ref.watch(focusedChannelProvider); - final messages = ref.watch(messagesProvider(channel)); - return messages.when( + final repository = ref.watch(repositoryProvider); + return repository.when( error: buildErrorWidget, loading: buildLoadingWidget, - data: (messages) => RealmAnimatedList( - results: messages, + data: (repository) => RealmAnimatedList( + results: repository.messages(channel!), itemBuilder: (context, item, animation) { return MessageTile(message: item, animation: animation); }, diff --git a/kilochat/lib/profile_form.dart b/kilochat/lib/profile_form.dart index 511ec3c..63ca8a6 100644 --- a/kilochat/lib/profile_form.dart +++ b/kilochat/lib/profile_form.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'widget_builders.dart'; import 'model.dart'; import 'providers.dart'; +import 'widget_builders.dart'; class ProfileForm extends ConsumerStatefulWidget { final UserProfile initialProfile; diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 6302efa..d696982 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -1,23 +1,19 @@ import 'dart:async'; import 'dart:io'; +import 'package:cancellation_token/cancellation_token.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:realm/realm.dart'; -import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:cancellation_token/cancellation_token.dart'; +import 'firebase_user_provider.dart'; import 'model.dart'; import 'repository.dart'; import 'settings.dart'; part 'providers.g.dart'; -final firebaseUserProvider = StateProvider( - (ref) => firebase.FirebaseAuth.instance.currentUser, -); - class FocusedChannel extends Notifier { @override Channel? build() => null; @@ -45,21 +41,13 @@ Stream app(AppRef ref) async* { Future localRealm(LocalRealmRef ref) async => await Realm.open(Configuration.local([])); -@riverpod -Stream> messages( - MessagesRef ref, - Channel? channel, -) async* { - final repository = await ref.watch(repositoryProvider.future); - if (channel == null) return; - yield repository.messages(channel); -} - @riverpod Future repository(RepositoryRef ref) async { final realm = await ref.watch(syncedRealmProvider.future); final user = await ref.watch(userProfileProvider.future); - return Repository(realm, user); + final repo = Repository(realm, user); + ref.onDispose(repo.dispose); + return repo; } @riverpod @@ -76,14 +64,19 @@ Future syncedRealm(SyncedRealmRef ref) async { ); Realm.logger.level = RealmLogLevel.debug; late Realm realm; - final ct = TimeoutCancellationToken(const Duration(seconds: 3)); + final ct = TimeoutCancellationToken(const Duration(seconds: 30)); try { - if (File(config.path).existsSync()) { + if (await File(config.path).exists()) { realm = Realm(config); } else { realm = await Realm.open( config, cancellationToken: ct, + onProgressCallback: (progress) { + final transferred = progress.transferredBytes; + final transferable = progress.transferableBytes; + print('Sync progress: $transferred / $transferable'); + }, ); } } catch (_) { @@ -96,8 +89,8 @@ Future syncedRealm(SyncedRealmRef ref) async { ..add(realm.query('channelId == null')) ..add(realm.all()) ..add(realm.all()); - // await realm.subscriptions.waitForSynchronization(); // does not support cancellation yet }); + // await realm.subscriptions.waitForSynchronization(ct); // does not support cancellation yet await realm.syncSession.waitForDownload(ct); } on TimeoutException catch (_) {} // ignore and proceed return realm; @@ -106,12 +99,12 @@ Future syncedRealm(SyncedRealmRef ref) async { @riverpod Stream user(UserRef ref) async* { final app = await ref.watch(appProvider.future); - final firebaseUser = ref.watch(firebaseUserProvider); + final firebaseUser = await ref.watch(firebaseUserProvider.future); var user = app.currentUser; if (user == null) { if (firebaseUser != null) { - final jwt = await firebaseUser.getIdToken(); + final jwt = await firebaseUser.getIdToken(true); // force refresh if (jwt != null) { user = await app.logIn(Credentials.jwt(jwt)); } diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index 43d8869..fb723d6 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$appHash() => r'b3bc4e02df3b254bcad8a3e6db9f2030a06a0ce4'; +String _$appHash() => r'4b65979ca6a10371a96ba49c5ebf78105d4acc3c'; /// See also [app]. @ProviderFor(app) @@ -34,111 +34,7 @@ final localRealmProvider = AutoDisposeFutureProvider.internal( ); typedef LocalRealmRef = AutoDisposeFutureProviderRef; -String _$messagesHash() => r'c191f05ca61d99c07b2cd9d267be737b75892d5c'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -typedef MessagesRef = AutoDisposeStreamProviderRef>; - -/// See also [messages]. -@ProviderFor(messages) -const messagesProvider = MessagesFamily(); - -/// See also [messages]. -class MessagesFamily extends Family>> { - /// See also [messages]. - const MessagesFamily(); - - /// See also [messages]. - MessagesProvider call( - dynamic channel, - ) { - return MessagesProvider( - channel, - ); - } - - @override - MessagesProvider getProviderOverride( - covariant MessagesProvider provider, - ) { - return call( - provider.channel, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'messagesProvider'; -} - -/// See also [messages]. -class MessagesProvider - extends AutoDisposeStreamProvider> { - /// See also [messages]. - MessagesProvider( - this.channel, - ) : super.internal( - (ref) => messages( - ref, - channel, - ), - from: messagesProvider, - name: r'messagesProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$messagesHash, - dependencies: MessagesFamily._dependencies, - allTransitiveDependencies: MessagesFamily._allTransitiveDependencies, - ); - - final dynamic channel; - - @override - bool operator ==(Object other) { - return other is MessagesProvider && other.channel == channel; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, channel.hashCode); - - return _SystemHash.finish(hash); - } -} - -String _$repositoryHash() => r'9735e5fd79954002433a9b2b517fa72087982b00'; +String _$repositoryHash() => r'd3cb0d95aa6bdb749677e8b60896da940e46ce7d'; /// See also [repository]. @ProviderFor(repository) @@ -152,7 +48,7 @@ final repositoryProvider = AutoDisposeFutureProvider.internal( ); typedef RepositoryRef = AutoDisposeFutureProviderRef; -String _$syncedRealmHash() => r'894d981cdc1510e4d50afbf8f059998237daea7a'; +String _$syncedRealmHash() => r'3b57f34e34ceb50022896936a5c0936a1d0b0f05'; /// See also [syncedRealm]. @ProviderFor(syncedRealm) @@ -166,7 +62,7 @@ final syncedRealmProvider = AutoDisposeFutureProvider.internal( ); typedef SyncedRealmRef = AutoDisposeFutureProviderRef; -String _$userHash() => r'b1ca9042230ed76890f0226e50c4d47255094ce4'; +String _$userHash() => r'9affc10569e9ce8c707643d7de439dcd8c2ea500'; /// See also [user]. @ProviderFor(user) diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index c2298eb..42cf2ce 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -8,6 +8,7 @@ extension on MutableSubscriptionSet { void subscribe(Channel channel) => add( name: '${channel.id}', channel.realm.query(r'channelId == $0', [channel.id]), + update: true, ); void unsubscribe(Channel channel) => removeByName('${channel.id}'); } @@ -26,6 +27,7 @@ extension on Stream> { mutableSubscriptions.subscribe(results.elementAt(i)); } } else { + // first time for (final channel in results) { mutableSubscriptions.subscribe(channel); } @@ -44,7 +46,7 @@ class Repository { Repository(this._realm, this.user) : _subscription = user.channels - .asResults() + .asResults() // indexes are off otherwise .changes .updateSubscriptions() .listen((_) {}); @@ -133,6 +135,12 @@ class Repository { [name], ); } + + Future dispose() async { + await _subscription.cancel(); + _realm.syncSession.pause(); + // _realm.close(); // TODO: re-enable once focusedChannel is fixed + } } extension RealmEx on Realm { diff --git a/kilochat/lib/router.dart b/kilochat/lib/router.dart index 021f900..f196301 100644 --- a/kilochat/lib/router.dart +++ b/kilochat/lib/router.dart @@ -1,10 +1,9 @@ import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:realm/realm.dart'; import 'chat_screen.dart'; -import 'providers.dart'; import 'settings.dart'; import 'workspace_view.dart'; @@ -37,12 +36,19 @@ final router = GoRouter( path: Routes.logIn.path, builder: (context, state) { return SignInScreen(actions: [ + AuthCancelledAction((context) { + Routes.chooseWorkspace.go(context); + }), AuthStateChangeAction((context, state) async { - final ref = ProviderScope.containerOf(context); - final firebaseUserController = - ref.read(firebaseUserProvider.notifier); - firebaseUserController.state = state.user; - Routes.chat.go(context); + final app = currentWorkspace?.app; + final jwt = await state.user?.getIdToken(); + if (app != null && jwt != null) { + await app.logIn(Credentials.jwt(jwt)); + } + + if (context.mounted) { + Routes.chat.go(context); + } }), ]); }, @@ -50,19 +56,20 @@ final router = GoRouter( GoRoute( path: Routes.profile.path, builder: (context, state) { - return const Placeholder(); - // return ProfileForm(); + //return const Placeholder(); + return const ProfileScreen(); }, ), ], redirect: (context, state) async { - // always okay to switch workspace - if (state.path == Routes.chat.path) return null; + // always okay to switch workspace selection + if (state.fullPath == Routes.chooseWorkspace.path) return null; // check if workspace or log in is needed - return currentWorkspace == null + final ws = currentWorkspace; + return ws == null ? Routes.chooseWorkspace.path // no workspace, so choose one - : (currentWorkspace!.app.currentUser == null // no user, so log in + : (ws.app.currentUser == null // no user, so log in ? Routes.logIn.path : null); // otherwise, navigation okay }, diff --git a/kilochat/lib/settings.g.dart b/kilochat/lib/settings.g.dart index eeee672..027dc0d 100644 --- a/kilochat/lib/settings.g.dart +++ b/kilochat/lib/settings.g.dart @@ -10,10 +10,12 @@ class Workspace extends _Workspace with RealmEntity, RealmObjectBase, RealmObject { Workspace( String appId, - String name, - ) { + String name, { + String? currentChannelId, + }) { RealmObjectBase.set(this, 'appId', appId); RealmObjectBase.set(this, 'name', name); + RealmObjectBase.set(this, 'currentChannelId', currentChannelId); } Workspace._(); @@ -28,6 +30,13 @@ class Workspace extends _Workspace @override set name(String value) => RealmObjectBase.set(this, 'name', value); + @override + String? get currentChannelId => + RealmObjectBase.get(this, 'currentChannelId') as String?; + @override + set currentChannelId(String? value) => + RealmObjectBase.set(this, 'currentChannelId', value); + @override Stream> get changes => RealmObjectBase.getChanges(this); @@ -42,6 +51,8 @@ class Workspace extends _Workspace return const SchemaObject(ObjectType.realmObject, Workspace, 'Workspace', [ SchemaProperty('appId', RealmPropertyType.string, primaryKey: true), SchemaProperty('name', RealmPropertyType.string), + SchemaProperty('currentChannelId', RealmPropertyType.string, + optional: true), ]); } } diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index 7ba180b..76c4e2d 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -1,7 +1,9 @@ import 'package:animated_emoji/emoji.dart'; import 'package:animated_emoji/emojis.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:markdown/markdown.dart' as md; import 'avatar.dart'; import 'model.dart'; @@ -60,7 +62,21 @@ class MessageTile extends ConsumerWidget { subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - MyTransition(animation: animation, child: Text(message.text)), + MyTransition( + animation: animation, + child: Markdown( + padding: const EdgeInsets.only(bottom: 12), + data: message.text, + shrinkWrap: true, + extensionSet: md.ExtensionSet( + md.ExtensionSet.gitHubFlavored.blockSyntaxes, + [ + md.EmojiSyntax(), + ...md.ExtensionSet.gitHubFlavored.inlineSyntaxes + ], + ), + ), + ), SizedBox( height: 50, child: RealmAnimatedList( diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index e57d43f..1c03e81 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: "direct main" description: name: cancellation_token - sha256: "44891ef71d605bc59ef7974c403630d8e8506fcd897a29c3e38466ef69e5c4eb" + sha256: ad95acf9d4b2f3563e25dc937f63587e46a70ce534e910b65d10e115490f1027 url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "2.0.1" characters: dependency: transitive description: @@ -309,10 +309,10 @@ packages: dependency: transitive description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.0" file: dependency: transitive description: @@ -499,6 +499,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + sha256: "2b206d397dd7836ea60035b2d43825c8a303a76a5098e66f42d55a753e18d431" + url: "https://pub.dev" + source: hosted + version: "0.6.17+1" flutter_riverpod: dependency: "direct main" description: @@ -697,10 +705,18 @@ packages: dependency: transitive description: name: lottie - sha256: "0793a5866062e5cc8a8b24892fa94c3095953ea914a7fdf790f550dd7537fe60" + sha256: b8bdd54b488c54068c57d41ae85d02808da09e2bee8b8dd1f59f441e7efa60cd url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" + markdown: + dependency: "direct main" + description: + name: markdown + sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd + url: "https://pub.dev" + source: hosted + version: "7.1.1" matcher: dependency: transitive description: @@ -840,26 +856,23 @@ packages: realm: dependency: "direct main" description: - name: realm - sha256: "818bd06d65cf736dab3e41111a9f641d129083a6056788544ea6a94697fbac2e" - url: "https://pub.dev" - source: hosted + path: "../../../realm-dart/flutter/realm_flutter" + relative: true + source: path version: "1.3.0" realm_common: dependency: transitive description: - name: realm_common - sha256: b7fefe203362d9afc21094fdc1d97c9ada95c25f80fc5c352a0dd3f3cbf34625 - url: "https://pub.dev" - source: hosted + path: "../../../realm-dart/common" + relative: true + source: path version: "1.3.0" realm_generator: dependency: transitive description: - name: realm_generator - sha256: "22a3b16b3f1830af221d7b07c847e85a44873a82d8b90f67bdd81cc4a2b64f2a" - url: "https://pub.dev" - source: hosted + path: "../../../realm-dart/generator" + relative: true + source: path version: "1.3.0" riverpod: dependency: "direct main" @@ -1078,10 +1091,10 @@ packages: dependency: transitive description: name: vm_service - sha256: ada49637c27973c183dad90beb6bd781eea4c9f5f955d35da172de0af7bd3440 + sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" url: "https://pub.dev" source: hosted - version: "11.8.0" + version: "11.9.0" watcher: dependency: transitive description: @@ -1116,4 +1129,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.0.2 <4.0.0" - flutter: ">=3.10.2" + flutter: ">=3.10.0" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index a40b32d..bbea836 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -13,7 +13,8 @@ dependencies: sdk: flutter animated_emoji: ^2.0.0 - cancellation_token: ^1.5.0 + # cancellation_token: ^1.5.0 + cancellation_token: ^2.0.0 # for realm vNext collection: ^1.17.0 connectivity_plus: ^4.0.1 firebase_auth: ^4.2.10 @@ -23,8 +24,10 @@ dependencies: firebase_ui_oauth_facebook: ^1.0.23 firebase_ui_oauth_google: ^1.0.23 flutter_animate: ^4.0.0 + flutter_markdown: ^0.6.0 flutter_riverpod: ^2.3.0 go_router: ^10.0.0 + markdown: ^7.0.0 path: ^1.0.0 random_avatar: ^0.0.8 realm: ^1.3.0 @@ -47,6 +50,6 @@ flutter: fonts: - asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf -# dependency_overrides: -# realm: -# path: ../../../realm-dart/flutter/realm_flutter +dependency_overrides: + realm: + path: ../../../realm-dart/flutter/realm_flutter From d37a50def79b964c8bd3e2ae9c4af24d4930ab53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 10 Aug 2023 09:46:14 +0200 Subject: [PATCH 20/33] Fix flutter warnings --- kilochat/lib/chat_screen.dart | 4 +- kilochat/lib/display_toast.dart | 58 ++++++++++++---------- kilochat/lib/firebase_user_provider.dart | 8 +++ kilochat/lib/firebase_user_provider.g.dart | 24 +++++++++ kilochat/lib/messages_view.dart | 4 +- kilochat/lib/tiles.dart | 9 ++-- 6 files changed, 74 insertions(+), 33 deletions(-) create mode 100644 kilochat/lib/firebase_user_provider.dart create mode 100644 kilochat/lib/firebase_user_provider.g.dart diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 1e2028b..3114fe9 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -90,7 +90,9 @@ class ChatScreen extends ConsumerWidget { ), drawer: twoPane ? null // no drawer on large screens - : Drawer(child: _buildChannelPane(ref, context)), + : Drawer(child: Builder(builder: (context) { + return _buildChannelPane(ref, context); + })), ); }, ); diff --git a/kilochat/lib/display_toast.dart b/kilochat/lib/display_toast.dart index d8f0a48..403bb6a 100644 --- a/kilochat/lib/display_toast.dart +++ b/kilochat/lib/display_toast.dart @@ -20,42 +20,24 @@ class DisplayToast extends StatefulWidget { class _DisplayToastState extends State> with TickerProviderStateMixin { - late StreamSubscription _subscription; - - late final _controller = AnimationController( - duration: const Duration(milliseconds: 300), - vsync: this, - ); - final _overlayKey = GlobalKey(); + late final AnimationController _controller; + late final StreamSubscription _subscription; @override void initState() { super.initState(); + + _controller = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _subscription = widget.stream.asyncMap((event) async { - final entry = OverlayEntry( - builder: (context) => Positioned( - left: 10, - right: 10, - top: 10, - child: FadeTransition( - opacity: _controller, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: Colors.black), - boxShadow: kElevationToShadow[4], - color: Colors.white, - ), - child: widget.builder(event, _controller), - ), - ), - ), - ); + final entry = _buildOverlayEntry(event); final state = _overlayKey.currentState; if (state != null) { state.insert(entry); - _controller.reset(); await _controller.forward(); await Future.delayed(const Duration(seconds: 3)); await _controller.reverse(); @@ -64,6 +46,28 @@ class _DisplayToastState extends State> }).listen((_) {}); } + OverlayEntry _buildOverlayEntry(event) { + return OverlayEntry( + builder: (context) => Positioned( + left: 10, + right: 10, + top: 10, + child: FadeTransition( + opacity: _controller, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.black), + boxShadow: kElevationToShadow[4], + color: Colors.white, + ), + child: widget.builder(event, _controller), + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { return Stack( diff --git a/kilochat/lib/firebase_user_provider.dart b/kilochat/lib/firebase_user_provider.dart new file mode 100644 index 0000000..2b927bf --- /dev/null +++ b/kilochat/lib/firebase_user_provider.dart @@ -0,0 +1,8 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'firebase_user_provider.g.dart'; + +@riverpod +Stream firebaseUser(FirebaseUserRef ref) => + FirebaseAuth.instance.authStateChanges(); diff --git a/kilochat/lib/firebase_user_provider.g.dart b/kilochat/lib/firebase_user_provider.g.dart new file mode 100644 index 0000000..a393f90 --- /dev/null +++ b/kilochat/lib/firebase_user_provider.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'firebase_user_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$firebaseUserHash() => r'70b3976ab00c6083cb3b1f4c2183d61ee075585a'; + +/// See also [firebaseUser]. +@ProviderFor(firebaseUser) +final firebaseUserProvider = AutoDisposeStreamProvider.internal( + firebaseUser, + name: r'firebaseUserProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$firebaseUserHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef FirebaseUserRef = AutoDisposeStreamProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member diff --git a/kilochat/lib/messages_view.dart b/kilochat/lib/messages_view.dart index abf475f..452e3ea 100644 --- a/kilochat/lib/messages_view.dart +++ b/kilochat/lib/messages_view.dart @@ -17,12 +17,14 @@ class MessagesView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final channel = ref.watch(focusedChannelProvider); + if (channel == null) return const SizedBox.shrink(); + final repository = ref.watch(repositoryProvider); return repository.when( error: buildErrorWidget, loading: buildLoadingWidget, data: (repository) => RealmAnimatedList( - results: repository.messages(channel!), + results: repository.messages(channel), itemBuilder: (context, item, animation) { return MessageTile(message: item, animation: animation); }, diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index 76c4e2d..090b5dd 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -64,10 +64,10 @@ class MessageTile extends ConsumerWidget { children: [ MyTransition( animation: animation, - child: Markdown( - padding: const EdgeInsets.only(bottom: 12), + child: MarkdownBody( + //padding: const EdgeInsets.only(bottom: 12), data: message.text, - shrinkWrap: true, + //shrinkWrap: true, extensionSet: md.ExtensionSet( md.ExtensionSet.gitHubFlavored.blockSyntaxes, [ @@ -77,8 +77,9 @@ class MessageTile extends ConsumerWidget { ), ), ), + const SizedBox(height: 8), SizedBox( - height: 50, + height: 30, child: RealmAnimatedList( results: message.reactions, scrollDirection: Axis.horizontal, From c2a262d445f511653e8d8c8a74ca2d5130da622f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 10 Aug 2023 10:07:39 +0200 Subject: [PATCH 21/33] Simplify/reduce provider use (step 1) --- kilochat/lib/chat_screen.dart | 8 ++------ kilochat/lib/providers.dart | 4 ---- kilochat/lib/providers.g.dart | 14 -------------- kilochat/lib/tiles.dart | 4 ++-- 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 3114fe9..85d49ed 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -24,7 +24,6 @@ class ChatScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final focusedChannel = ref.watch(focusedChannelProvider); final repository = ref.watch(repositoryProvider); - final app = ref.watch(appProvider).value; final twoPane = MediaQuery.of(context).size.width > 700; const menuWidth = 300.0; return repository.when( @@ -37,11 +36,8 @@ class ChatScreen extends ConsumerWidget { title: Text( '${currentWorkspace?.name} / ${focusedChannel?.name ?? ''}'), actions: [ - IconButton( - onPressed: () => app?.reconnect(), - icon: RealmConnectivityIndicator( - changes: repository.connectionStateChanges, - ), + RealmConnectivityIndicator( + changes: repository.connectionStateChanges, ), IconButton( onPressed: () => showSearch( diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index d696982..29345b1 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -37,10 +37,6 @@ Stream app(AppRef ref) async* { } } -@riverpod -Future localRealm(LocalRealmRef ref) async => - await Realm.open(Configuration.local([])); - @riverpod Future repository(RepositoryRef ref) async { final realm = await ref.watch(syncedRealmProvider.future); diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index fb723d6..1ded33b 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -20,20 +20,6 @@ final appProvider = AutoDisposeStreamProvider.internal( ); typedef AppRef = AutoDisposeStreamProviderRef; -String _$localRealmHash() => r'31ba36028bf0e66b3453c3e4a89113e0e6ed5e92'; - -/// See also [localRealm]. -@ProviderFor(localRealm) -final localRealmProvider = AutoDisposeFutureProvider.internal( - localRealm, - name: r'localRealmProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$localRealmHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef LocalRealmRef = AutoDisposeFutureProviderRef; String _$repositoryHash() => r'd3cb0d95aa6bdb749677e8b60896da940e46ce7d'; /// See also [repository]. diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index 090b5dd..2b0e2e0 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -51,8 +51,8 @@ class MessageTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final user = ref.watch(userProvider).value; - final repo = ref.watch(repositoryProvider).value; + final repo = ref.watch(repositoryProvider).requireValue; + final user = repo.user; return AnimatedDismissibleTile( key: ValueKey(message.id), From 684e76914102f1776cb10827c5c73a7cd5218e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 10 Aug 2023 12:02:13 +0200 Subject: [PATCH 22/33] forgot to new realm_ui folder --- .../lib/realm_ui/realm_animated_list.dart | 126 ++++++++++++++++++ .../realm_connectivity_indicator.dart | 36 +++++ .../lib/realm_ui/realm_search_delegate.dart | 44 ++++++ 3 files changed, 206 insertions(+) create mode 100644 kilochat/lib/realm_ui/realm_animated_list.dart create mode 100644 kilochat/lib/realm_ui/realm_connectivity_indicator.dart create mode 100644 kilochat/lib/realm_ui/realm_search_delegate.dart diff --git a/kilochat/lib/realm_ui/realm_animated_list.dart b/kilochat/lib/realm_ui/realm_animated_list.dart new file mode 100644 index 0000000..e5c48c9 --- /dev/null +++ b/kilochat/lib/realm_ui/realm_animated_list.dart @@ -0,0 +1,126 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:realm/realm.dart'; + +typedef ItemWidgetBuilder = Widget Function( + BuildContext context, + E item, + Animation animation, +); + +typedef ErrorWidgetBuilder = Widget Function( + BuildContext context, + Object? error, + StackTrace stackTrace, +); + +extension on Stream> { + Stream> _updateAnimatedList( + GlobalKey listKey, + ItemWidgetBuilder removedItemBuilder, + ) async* { + RealmResults? previous; + await for (final change in this) { + final state = listKey.currentState; + if (previous != null) { + for (final index in change.deleted.reversed) { + final toDie = previous[index]; + state?.removeItem( + index, + (context, animation) => + removedItemBuilder(context, toDie, animation), + ); + } + } + for (final index in change.inserted) { + state?.insertItem(index); + } + final r = change.results; + previous = r.isValid ? r.freeze() : null; + yield change; + } + } +} + +class RealmAnimatedList extends StatefulWidget { + const RealmAnimatedList({ + super.key, + required this.results, + required this.itemBuilder, + ItemWidgetBuilder? removedItemBuilder, + this.loading, + this.error, + this.reverse = false, + this.controller, + this.scrollDirection = Axis.vertical, + }) : removedItemBuilder = removedItemBuilder ?? itemBuilder; + final RealmResults results; + final ItemWidgetBuilder itemBuilder; + final ItemWidgetBuilder removedItemBuilder; + + final WidgetBuilder? loading; + final ErrorWidgetBuilder? error; + + final bool reverse; + final ScrollController? controller; + final Axis scrollDirection; + + @override + State createState() => _RealmAnimatedListState(); +} + +extension on RealmResults { + bool get isLive => isValid && !isFrozen; + Stream> get safeChanges async* { + if (isLive) yield* changes; + } +} + +class _RealmAnimatedListState extends State> { + final _listKey = GlobalKey(); + StreamSubscription? _subscription; + + void _updateSubscription() { + _subscription?.cancel(); + _subscription = widget.results.safeChanges + ._updateAnimatedList(_listKey, widget.removedItemBuilder) + .listen((_) => setState(() {})); + } + + @override + void initState() { + super.initState(); + _updateSubscription(); + } + + @override + void didUpdateWidget(covariant RealmAnimatedList oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.results != oldWidget.results) { + _updateSubscription(); + } + } + + @override + void dispose() { + _subscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final items = widget.results; + return AnimatedList( + key: _listKey, + controller: widget.controller, + scrollDirection: widget.scrollDirection, + initialItemCount: items.length, + itemBuilder: (context, index, animation) => + index < items.length // why is this needed :-/ + ? widget.itemBuilder(context, items[index], animation) + : Container(), + reverse: widget.reverse, + ); + } +} diff --git a/kilochat/lib/realm_ui/realm_connectivity_indicator.dart b/kilochat/lib/realm_ui/realm_connectivity_indicator.dart new file mode 100644 index 0000000..fd9eacb --- /dev/null +++ b/kilochat/lib/realm_ui/realm_connectivity_indicator.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart' hide ConnectionState; +import 'package:realm/realm.dart'; + +class RealmConnectivityIndicator extends StatelessWidget { + const RealmConnectivityIndicator({ + super.key, + this.builder = _defaultBuilder, + required this.changes, + }); + + final Widget Function(ConnectionStateChange connectionState) builder; + final Stream changes; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: changes, + builder: (context, snapshot) { + final data = snapshot.data; + return data != null ? builder(data) : const SizedBox.shrink(); + }, + ); + } + + static Widget _defaultBuilder(ConnectionStateChange connectionState) { + final current = connectionState.current; + final key = ValueKey(current); + return AnimatedSwitcher( + duration: kThemeAnimationDuration, + child: switch (current) { + ConnectionState.disconnected => Icon(key: key, Icons.cloud_off), + ConnectionState.connecting => Icon(key: key, Icons.cloud_sync), + ConnectionState.connected => Icon(key: key, Icons.cloud_done), + }); + } +} diff --git a/kilochat/lib/realm_ui/realm_search_delegate.dart b/kilochat/lib/realm_ui/realm_search_delegate.dart new file mode 100644 index 0000000..e789379 --- /dev/null +++ b/kilochat/lib/realm_ui/realm_search_delegate.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:realm/realm.dart'; + +import 'realm_animated_list.dart'; + +class RealmSearchDelegate extends SearchDelegate { + final RealmResults Function(String query) resultsBuilder; + final ItemWidgetBuilder itemBuilder; + + RealmSearchDelegate(this.resultsBuilder, this.itemBuilder); + + @override + List buildActions(BuildContext context) { + // Create a list of actions to display in the app bar + return [ + IconButton( + icon: const Icon(Icons.clear), + onPressed: () => query = '', + ), + ]; + } + + @override + Widget buildLeading(BuildContext context) { + // Create a widget to display as the leading icon in the app bar + return IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => close(context, null), + ); + } + + @override + Widget buildResults(BuildContext context) { + return RealmAnimatedList( + results: resultsBuilder(query), + itemBuilder: itemBuilder, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + return buildResults(context); + } +} From 4c336c2de8cc40ca81a399d932826c1d6a1dd45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 10 Aug 2023 12:03:59 +0200 Subject: [PATCH 23/33] Move functionality into Repository class. Drop most providers --- kilochat/lib/channels_view.dart | 2 +- kilochat/lib/chat_screen.dart | 2 +- kilochat/lib/providers.dart | 121 +++------------- kilochat/lib/providers.g.dart | 48 ++----- kilochat/lib/repository.dart | 248 +++++++++++++++++++++----------- kilochat/lib/router.dart | 16 ++- kilochat/lib/tiles.dart | 8 +- 7 files changed, 214 insertions(+), 231 deletions(-) diff --git a/kilochat/lib/channels_view.dart b/kilochat/lib/channels_view.dart index 50ea97e..0a8d035 100644 --- a/kilochat/lib/channels_view.dart +++ b/kilochat/lib/channels_view.dart @@ -23,7 +23,7 @@ class ChannelsView extends ConsumerWidget { error: buildErrorWidget, loading: buildLoadingWidget, data: (repository) { - final user = repository.user; + final user = repository.userProfile; final channels = user.channels.asResults(); return ListTileTheme( data: ListTileThemeData( diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 85d49ed..4d5842d 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -30,7 +30,7 @@ class ChatScreen extends ConsumerWidget { error: buildErrorWidget, loading: buildLoadingWidget, data: (repository) { - final user = repository.user; + final user = repository.userProfile; return Scaffold( appBar: AppBar( title: Text( diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 29345b1..ba76abf 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -1,9 +1,5 @@ import 'dart:async'; -import 'dart:io'; -import 'package:cancellation_token/cancellation_token.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:firebase_auth/firebase_auth.dart' as firebase; import 'package:realm/realm.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -14,121 +10,42 @@ import 'settings.dart'; part 'providers.g.dart'; -class FocusedChannel extends Notifier { - @override - Channel? build() => null; - - void focus(Channel channel) => state = channel; -} - -final focusedChannelProvider = - NotifierProvider(FocusedChannel.new); - @riverpod Stream app(AppRef ref) async* { - StreamSubscription? subscription; await for (final ws in workspaceChanges) { - subscription?.cancel(); if (ws == null) continue; - subscription = Connectivity().onConnectivityChanged.listen((_) { - ws.app.reconnect(); - }); yield ws.app; } } -@riverpod -Future repository(RepositoryRef ref) async { - final realm = await ref.watch(syncedRealmProvider.future); - final user = await ref.watch(userProfileProvider.future); - final repo = Repository(realm, user); - ref.onDispose(repo.dispose); - return repo; -} - -@riverpod -Future syncedRealm(SyncedRealmRef ref) async { - final user = await ref.watch(userProvider.future); - var config = Configuration.flexibleSync( - user, - [ - Channel.schema, - Message.schema, - Reaction.schema, - UserProfile.schema, - ], - ); - Realm.logger.level = RealmLogLevel.debug; - late Realm realm; - final ct = TimeoutCancellationToken(const Duration(seconds: 30)); - try { - if (await File(config.path).exists()) { - realm = Realm(config); - } else { - realm = await Realm.open( - config, - cancellationToken: ct, - onProgressCallback: (progress) { - final transferred = progress.transferredBytes; - final transferable = progress.transferableBytes; - print('Sync progress: $transferred / $transferable'); - }, - ); - } - } catch (_) { - realm = Realm(config); - } - try { - realm.subscriptions.update((mutableSubscriptions) { - mutableSubscriptions - ..add(realm.all()) - ..add(realm.query('channelId == null')) - ..add(realm.all()) - ..add(realm.all()); - }); - // await realm.subscriptions.waitForSynchronization(ct); // does not support cancellation yet - await realm.syncSession.waitForDownload(ct); - } on TimeoutException catch (_) {} // ignore and proceed - return realm; -} - @riverpod Stream user(UserRef ref) async* { final app = await ref.watch(appProvider.future); + final firebaseUser = await ref.watch(firebaseUserProvider.future); + final jwt = await firebaseUser?.getIdToken(true); // force refresh var user = app.currentUser; - if (user == null) { - if (firebaseUser != null) { - final jwt = await firebaseUser.getIdToken(true); // force refresh - if (jwt != null) { - user = await app.logIn(Credentials.jwt(jwt)); - } - } + if (jwt != null) { + user = await app.logIn(Credentials.jwt(jwt)); + yield user; } - if (user != null) yield user; } @riverpod -Stream userProfile(UserProfileRef ref) async* { +Future repository(RepositoryRef ref) async { final user = await ref.watch(userProvider.future); - final realm = await ref.watch(syncedRealmProvider.future); - final userProfile = realm.findOrAdd( - user.id, - (id) { - return UserProfile(id, id); - }, - ); - yield userProfile; - await for (final change in userProfile.changes) { - final deactivated = !change.isDeleted && change.object.deactivated; - if (deactivated) { - try { - await user.app.removeUser(user); - await firebase.FirebaseAuth.instance.signOut(); - } finally { - exit(64); - } - } - } + final repo = await Repository.init(user); + ref.onDispose(repo.dispose); + return repo; +} + +class FocusedChannel extends Notifier { + @override + Channel? build() => null; + + void focus(Channel channel) => state = channel; } + +final focusedChannelProvider = + NotifierProvider(FocusedChannel.new); diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index 1ded33b..3c62997 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$appHash() => r'4b65979ca6a10371a96ba49c5ebf78105d4acc3c'; +String _$appHash() => r'f2013a10f035b47580fa754eb5b97b53d77ee6d2'; /// See also [app]. @ProviderFor(app) @@ -20,35 +20,7 @@ final appProvider = AutoDisposeStreamProvider.internal( ); typedef AppRef = AutoDisposeStreamProviderRef; -String _$repositoryHash() => r'd3cb0d95aa6bdb749677e8b60896da940e46ce7d'; - -/// See also [repository]. -@ProviderFor(repository) -final repositoryProvider = AutoDisposeFutureProvider.internal( - repository, - name: r'repositoryProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$repositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef RepositoryRef = AutoDisposeFutureProviderRef; -String _$syncedRealmHash() => r'3b57f34e34ceb50022896936a5c0936a1d0b0f05'; - -/// See also [syncedRealm]. -@ProviderFor(syncedRealm) -final syncedRealmProvider = AutoDisposeFutureProvider.internal( - syncedRealm, - name: r'syncedRealmProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$syncedRealmHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef SyncedRealmRef = AutoDisposeFutureProviderRef; -String _$userHash() => r'9affc10569e9ce8c707643d7de439dcd8c2ea500'; +String _$userHash() => r'9500263a0a8bbe86ba3186a23b7ea55f651338f6'; /// See also [user]. @ProviderFor(user) @@ -62,19 +34,19 @@ final userProvider = AutoDisposeStreamProvider.internal( ); typedef UserRef = AutoDisposeStreamProviderRef; -String _$userProfileHash() => r'33b2c3a07b2745665b33664efb14839edf046050'; +String _$repositoryHash() => r'ba79d3db0c045d0c101373e89a1ce4172155a75a'; -/// See also [userProfile]. -@ProviderFor(userProfile) -final userProfileProvider = AutoDisposeStreamProvider.internal( - userProfile, - name: r'userProfileProvider', +/// See also [repository]. +@ProviderFor(repository) +final repositoryProvider = AutoDisposeFutureProvider.internal( + repository, + name: r'repositoryProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$userProfileHash, + const bool.fromEnvironment('dart.vm.product') ? null : _$repositoryHash, dependencies: null, allTransitiveDependencies: null, ); -typedef UserProfileRef = AutoDisposeStreamProviderRef; +typedef RepositoryRef = AutoDisposeFutureProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index 42cf2ce..60bab2f 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -1,81 +1,151 @@ import 'dart:async'; +import 'dart:io'; +import 'package:cancellation_token/cancellation_token.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:realm/realm.dart'; import 'model.dart'; -extension on MutableSubscriptionSet { - void subscribe(Channel channel) => add( - name: '${channel.id}', - channel.realm.query(r'channelId == $0', [channel.id]), - update: true, - ); - void unsubscribe(Channel channel) => removeByName('${channel.id}'); -} +class Disposable { + final List _disposers = []; -extension on Stream> { - Stream> updateSubscriptions() async* { - RealmResults? previous; - await for (final change in this) { - final results = change.results; - results.realm.subscriptions.update((mutableSubscriptions) { - if (previous != null) { - for (final i in change.deleted.reversed) { - mutableSubscriptions.unsubscribe(previous!.elementAt(i)); - } - for (final i in change.inserted) { - mutableSubscriptions.subscribe(results.elementAt(i)); - } - } else { - // first time - for (final channel in results) { - mutableSubscriptions.subscribe(channel); - } - } - previous = results.freeze(); - }); - yield change; + Future dispose() async { + for (final disposer in _disposers) { + await disposer(); } } + + void onDispose(Function disposer) => _disposers.add(disposer); } -class Repository { +class Repository extends Disposable { final Realm _realm; - final UserProfile user; - final StreamSubscription _subscription; + final User _user; + + late final UserProfile userProfile = + _realm.findOrAdd(_user.id, (id) => UserProfile(id, id)); + + Repository(this._realm, this._user) { + // force logout, and drop local data, user deactivated + userProfile.changes.asyncListen((change) async { + if (change.isDeleted || change.object.deactivated) { + await _user.app.removeUser(_user); + } + }).cancelOnDisposeOf(this); + + // monitor a users selected channels and update subscriptions accordingly, + // on all devices. + userProfile.channels + .asResults() // indexes are off otherwise + .changes + .updateSubscriptions() // add or drop subscriptions + .listen((_) {}) + .cancelOnDisposeOf(this); + + // try to reconnect on any network change, to wake up as early as possible + Connectivity() + .onConnectivityChanged + .listen((_) => _user.app.reconnect()) + .cancelOnDisposeOf(this); + } + + static Future init(User user) async { + return Repository(await _initRealm(user), user); + } - Repository(this._realm, this.user) - : _subscription = user.channels - .asResults() // indexes are off otherwise - .changes - .updateSubscriptions() - .listen((_) {}); + static Future _initRealm(User user) async { + final config = Configuration.flexibleSync( + user, + [ + Channel.schema, + Message.schema, + Reaction.schema, + UserProfile.schema, + ], + ); + late Realm realm; + final ct = TimeoutCancellationToken(const Duration(seconds: 30)); + try { + if (await File(config.path).exists()) { + realm = Realm(config); + } else { + realm = await Realm.open( + config, + cancellationToken: ct, + onProgressCallback: (progress) { + final transferred = progress.transferredBytes; + final transferable = progress.transferableBytes; + print('Sync progress: $transferred / $transferable'); + }, + ); + } + } catch (_) { + realm = Realm(config); + } + try { + realm.subscriptions.update((mutableSubscriptions) { + mutableSubscriptions + ..add(realm.all()) + ..add(realm.query('channelId == null')) + ..add(realm.all()) + ..add(realm.all()); + }); + // await realm.subscriptions.waitForSynchronization(ct); // does not support cancellation yet + await realm.syncSession.waitForDownload(ct); + } on TimeoutException catch (_) {} // ignore and proceed + return realm; + } + + late Stream connectionStateChanges = + _realm.syncSession.connectionStateChanges; late RealmResults allChannels = _realm.query('TRUEPREDICATE SORT(name ASCENDING)'); + RealmResults searchChannel(String name) { + return _realm.query( + r'name CONTAINS $0 SORT(name ASCENDING)', + [name], + ); + } + + Channel createChannel(String name) => _realm + .write(() => _realm.add(Channel(ObjectId(), userProfile.id, name, 0))); + + void subscribeToChannel(Channel channel) => + _realm.write(() => userProfile.channels.add(channel)); + + void unsubscribeFromChannel(Channel channel) => + _realm.write(() => userProfile.channels.remove(channel)); + + void deleteChannel(Channel channel) => + _realm.write(() => _realm.delete(channel)); + late RealmResults allMessages = _realm.query('TRUEPREDICATE SORT(id DESCENDING)'); RealmResults messages(Channel channel) => _realm .query(r'channel == $0 SORT(index DESC, id ASC)', [channel]); + RealmResults searchMessage(String text) { + if (text.isEmpty) return allMessages; + return _realm.query( + r'text TEXT $0 SORT(id DESCENDING)', + [text], + ); + } + late Stream x = allMessages.changes.where((c) => c.inserted.isNotEmpty).map((c) { return c.results[c.inserted.last]; }); - late Stream connectionStateChanges = - _realm.syncSession.connectionStateChanges; - - void updateUserProfile(UserProfile newProfile) => - _realm.write(() => _realm.add(newProfile, update: true)); - void postNewMessage(Channel channel, String text) { // messages are sorted latest first final lastMessage = messages(channel).firstOrNull; if (lastMessage != null && - lastMessage.owner == user && + lastMessage.owner == userProfile && lastMessage.reactions.isEmpty) { // if we own last message, and there are no reactions, then update in place final newText = '${lastMessage.text}\n\n$text'; @@ -87,11 +157,11 @@ class Repository { _realm.add(Message( ObjectId(), (lastMessage?.index ?? 0) + 1, // increment index - user.id, + userProfile.id, channel.id, // not a link text, channel: channel, - owner: user, + owner: userProfile, )); }); } @@ -103,50 +173,68 @@ class Repository { void deleteMessage(Message message) => _realm.write(() => _realm.delete(message)); - RealmResults searchMessage(String text) { - if (text.isEmpty) return allMessages; - return _realm.query( - r'text TEXT $0 SORT(id DESCENDING)', - [text], - ); - } - void addReaction(Message message, String emoji) => - _realm.addOrUpdate(ReactionEx.create(user, message, emoji)); + _realm.addOrUpdate(ReactionEx.create(userProfile, message, emoji)); void deleteReaction(Reaction reaction) => _realm.write(() => _realm.delete(reaction)); - Channel createChannel(String name) => - _realm.write(() => _realm.add(Channel(ObjectId(), user.id, name, 0))); - - void subscribeToChannel(Channel channel) => - _realm.write(() => user.channels.add(channel)); - - void unsubscribeFromChannel(Channel channel) => - _realm.write(() => user.channels.remove(channel)); + void updateUserProfile(UserProfile newProfile) => + _realm.write(() => _realm.add(newProfile, update: true)); +} - void deleteChannel(Channel channel) => - _realm.write(() => _realm.delete(channel)); +extension on MutableSubscriptionSet { + void subscribe(Channel channel) => add( + name: '${channel.id}', + channel.realm.query(r'channelId == $0', [channel.id]), + update: true, + ); + void unsubscribe(Channel channel) => removeByName('${channel.id}'); +} - RealmResults searchChannel(String name) { - return _realm.query( - r'name CONTAINS $0 SORT(name ASCENDING)', - [name], - ); +extension on Stream> { + Stream> updateSubscriptions() async* { + RealmResults? previous; + await for (final change in this) { + final results = change.results; + results.realm.subscriptions.update((mutableSubscriptions) { + if (previous != null) { + for (final i in change.deleted.reversed) { + mutableSubscriptions.unsubscribe(previous!.elementAt(i)); + } + for (final i in change.inserted) { + mutableSubscriptions.subscribe(results.elementAt(i)); + } + } else { + // first time + for (final channel in results) { + mutableSubscriptions.subscribe(channel); + } + } + previous = results.freeze(); + }); + yield change; + } } +} - Future dispose() async { - await _subscription.cancel(); - _realm.syncSession.pause(); - // _realm.close(); // TODO: re-enable once focusedChannel is fixed - } +extension on StreamSubscription { + void cancelOnDisposeOf(Disposable disposable) => disposable.onDispose(cancel); } -extension RealmEx on Realm { - T findOrAdd(I id, T Function(I id) factory) => - write(() => find(id) ?? add(factory(id))); +extension on Stream { + StreamSubscription asyncListen( + Future Function(T) onData, { + Function? onError, + void Function()? onDone, + }) => + asyncMap(onData).listen((_) {}, onError: onError, onDone: onDone); +} +extension RealmEx on Realm { T addOrUpdate(T object) => write(() => add(object, update: true)); + + T findOrAdd(I id, T Function(I id) factory) => + write(() => find(id) ?? add(factory(id))); } diff --git a/kilochat/lib/router.dart b/kilochat/lib/router.dart index f196301..d420ec8 100644 --- a/kilochat/lib/router.dart +++ b/kilochat/lib/router.dart @@ -1,5 +1,6 @@ import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:realm/realm.dart'; @@ -40,10 +41,15 @@ final router = GoRouter( Routes.chooseWorkspace.go(context); }), AuthStateChangeAction((context, state) async { - final app = currentWorkspace?.app; - final jwt = await state.user?.getIdToken(); - if (app != null && jwt != null) { - await app.logIn(Credentials.jwt(jwt)); + // TODO: This sucks!! + try { + final app = currentWorkspace?.app; + final jwt = await state.user?.getIdToken(); + if (app != null && jwt != null) { + await app.logIn(Credentials.jwt(jwt)); + } + } catch (_) { + currentWorkspace = null; } if (context.mounted) { @@ -62,7 +68,7 @@ final router = GoRouter( ), ], redirect: (context, state) async { - // always okay to switch workspace selection + // always okay to switch to workspace selection if (state.fullPath == Routes.chooseWorkspace.path) return null; // check if workspace or log in is needed diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index 2b0e2e0..f9c3bd3 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -52,7 +52,7 @@ class MessageTile extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final repo = ref.watch(repositoryProvider).requireValue; - final user = repo.user; + final user = repo.userProfile; return AnimatedDismissibleTile( key: ValueKey(message.id), @@ -84,7 +84,7 @@ class MessageTile extends ConsumerWidget { results: message.reactions, scrollDirection: Axis.horizontal, itemBuilder: (context, reaction, animation) { - final owner = reaction.owner?.id == user?.id; + final owner = reaction.owner?.id == user.id; return MyTransition( key: ValueKey(reaction.id), axis: Axis.horizontal, @@ -101,7 +101,7 @@ class MessageTile extends ConsumerWidget { errorWidget: Text(reaction.emoji), ), onDeleted: - owner ? () => repo?.deleteReaction(reaction) : null, + owner ? () => repo.deleteReaction(reaction) : null, ), ); }, @@ -117,7 +117,7 @@ class MessageTile extends ConsumerWidget { }); }, ), - onDismissed: message.ownerId != user?.id + onDismissed: message.ownerId != user.id ? null : (direction) async { (await ref.read(repositoryProvider.future)) From 2f75b9ecf5cde1d0b0210b3a1de486b9dee58fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 11 Aug 2023 14:51:54 +0200 Subject: [PATCH 24/33] Better session status indication --- kilochat/lib/chat_screen.dart | 6 +- kilochat/lib/main.dart | 4 + kilochat/lib/providers.dart | 10 +-- .../realm_connectivity_indicator.dart | 36 --------- .../realm_session_state_indicator.dart | 77 +++++++++++++++++++ kilochat/lib/repository.dart | 13 +--- kilochat/lib/router.dart | 1 - kilochat/lib/settings.dart | 6 +- 8 files changed, 94 insertions(+), 59 deletions(-) delete mode 100644 kilochat/lib/realm_ui/realm_connectivity_indicator.dart create mode 100644 kilochat/lib/realm_ui/realm_session_state_indicator.dart diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 4d5842d..8dc0311 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -9,7 +9,7 @@ import 'display_toast.dart'; import 'model.dart'; import 'profile_form.dart'; import 'providers.dart'; -import 'realm_ui/realm_connectivity_indicator.dart'; +import 'realm_ui/realm_session_state_indicator.dart'; import 'realm_ui/realm_search_delegate.dart'; import 'repository.dart'; import 'settings.dart'; @@ -36,9 +36,7 @@ class ChatScreen extends ConsumerWidget { title: Text( '${currentWorkspace?.name} / ${focusedChannel?.name ?? ''}'), actions: [ - RealmConnectivityIndicator( - changes: repository.connectionStateChanges, - ), + RealmSessionStateIndicator(session: repository.session), IconButton( onPressed: () => showSearch( context: context, diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart index 6be29e1..c44b227 100644 --- a/kilochat/lib/main.dart +++ b/kilochat/lib/main.dart @@ -8,6 +8,7 @@ import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:realm/realm.dart'; import 'firebase_options.dart'; import 'router.dart'; @@ -16,8 +17,10 @@ const freedomBlue = Color(0xff0057b7); const energizingYellow = Color(0xffffd700); Future main() async { + Realm.logger.level = RealmLogLevel.debug; Animate.restartOnHotReload = true; WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); FirebaseUIAuth.configureProviders([ EmailAuthProvider(), @@ -25,6 +28,7 @@ Future main() async { FacebookProvider(clientId: ''), GoogleProvider(clientId: ''), ]); + runApp( ProviderScope( child: MaterialApp.router( diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index ba76abf..5797968 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -23,21 +23,21 @@ Stream user(UserRef ref) async* { final app = await ref.watch(appProvider.future); final firebaseUser = await ref.watch(firebaseUserProvider.future); - final jwt = await firebaseUser?.getIdToken(true); // force refresh + final jwt = await firebaseUser?.getIdToken(); var user = app.currentUser; if (jwt != null) { user = await app.logIn(Credentials.jwt(jwt)); - yield user; } + if (user != null) yield user; } @riverpod Future repository(RepositoryRef ref) async { final user = await ref.watch(userProvider.future); - final repo = await Repository.init(user); - ref.onDispose(repo.dispose); - return repo; + final repository = await Repository.init(user); + ref.onDispose(repository.dispose); + return repository; } class FocusedChannel extends Notifier { diff --git a/kilochat/lib/realm_ui/realm_connectivity_indicator.dart b/kilochat/lib/realm_ui/realm_connectivity_indicator.dart deleted file mode 100644 index fd9eacb..0000000 --- a/kilochat/lib/realm_ui/realm_connectivity_indicator.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart' hide ConnectionState; -import 'package:realm/realm.dart'; - -class RealmConnectivityIndicator extends StatelessWidget { - const RealmConnectivityIndicator({ - super.key, - this.builder = _defaultBuilder, - required this.changes, - }); - - final Widget Function(ConnectionStateChange connectionState) builder; - final Stream changes; - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: changes, - builder: (context, snapshot) { - final data = snapshot.data; - return data != null ? builder(data) : const SizedBox.shrink(); - }, - ); - } - - static Widget _defaultBuilder(ConnectionStateChange connectionState) { - final current = connectionState.current; - final key = ValueKey(current); - return AnimatedSwitcher( - duration: kThemeAnimationDuration, - child: switch (current) { - ConnectionState.disconnected => Icon(key: key, Icons.cloud_off), - ConnectionState.connecting => Icon(key: key, Icons.cloud_sync), - ConnectionState.connected => Icon(key: key, Icons.cloud_done), - }); - } -} diff --git a/kilochat/lib/realm_ui/realm_session_state_indicator.dart b/kilochat/lib/realm_ui/realm_session_state_indicator.dart new file mode 100644 index 0000000..2d44de9 --- /dev/null +++ b/kilochat/lib/realm_ui/realm_session_state_indicator.dart @@ -0,0 +1,77 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart' hide ConnectionState; +import 'package:realm/realm.dart'; +import 'package:rxdart/rxdart.dart'; + +typedef SessionStatus = ( + bool connected, + ConnectionState state, + bool downloading, + bool uploading +); + +class RealmSessionStateIndicator extends StatelessWidget { + const RealmSessionStateIndicator({ + super.key, + required this.session, + this.builder = _defaultBuilder, + this.duration = const Duration(seconds: 1), + }); + + final Widget Function(SessionStatus status) builder; + final Session session; + final Duration duration; + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: _stream(), + builder: (context, snapshot) { + final data = snapshot.data; + return AnimatedSwitcher( + duration: duration, + child: data == null ? const SizedBox.shrink() : builder(data), + ); + }, + ); + } + + Stream _stream() async* { + yield* CombineLatestStream.combine4( + Connectivity() + .onConnectivityChanged + .startWith(await Connectivity().checkConnectivity()), + session.connectionStateChanges + .map((c) => c.current) + .startWith(session.connectionState), + session.getProgressStream( + ProgressDirection.download, + ProgressMode.reportIndefinitely, + ), + session.getProgressStream( + ProgressDirection.upload, + ProgressMode.reportIndefinitely, + ), + (connectivity, state, download, upload) => ( + connectivity != ConnectivityResult.none, + state, + download.transferableBytes > download.transferredBytes, + upload.transferredBytes > upload.transferableBytes, + )).distinct(); + } + + static Widget _defaultBuilder(SessionStatus status) { + final iconData = switch (status) { + (false, ConnectionState.disconnected, _, _) => Icons.block, + (false, _, _, _) => Icons.cloud_off, + (_, ConnectionState.disconnected, _, _) => Icons.cloud_off, + (_, ConnectionState.connecting, _, _) => Icons.cloud_queue, + (_, ConnectionState.connected, true, _) => Icons.cloud_download, + (_, ConnectionState.connected, _, true) => Icons.cloud_upload, + (_, ConnectionState.connected, false, false) => Icons.cloud_done, + }; + return Icon(iconData, key: ValueKey(iconData)); + } +} diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index 60bab2f..e7e4226 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -70,15 +70,7 @@ class Repository extends Disposable { if (await File(config.path).exists()) { realm = Realm(config); } else { - realm = await Realm.open( - config, - cancellationToken: ct, - onProgressCallback: (progress) { - final transferred = progress.transferredBytes; - final transferable = progress.transferableBytes; - print('Sync progress: $transferred / $transferable'); - }, - ); + realm = await Realm.open(config, cancellationToken: ct); } } catch (_) { realm = Realm(config); @@ -97,8 +89,7 @@ class Repository extends Disposable { return realm; } - late Stream connectionStateChanges = - _realm.syncSession.connectionStateChanges; + Session get session => _realm.syncSession; late RealmResults allChannels = _realm.query('TRUEPREDICATE SORT(name ASCENDING)'); diff --git a/kilochat/lib/router.dart b/kilochat/lib/router.dart index d420ec8..bfe0b35 100644 --- a/kilochat/lib/router.dart +++ b/kilochat/lib/router.dart @@ -1,6 +1,5 @@ import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:realm/realm.dart'; diff --git a/kilochat/lib/settings.dart b/kilochat/lib/settings.dart index a0eb471..076c1da 100644 --- a/kilochat/lib/settings.dart +++ b/kilochat/lib/settings.dart @@ -10,10 +10,12 @@ class _Workspace { late String name; // for display String? currentChannelId; // current channel - @Ignored() - late final App app = App(AppConfiguration(appId)); + App get app => + _appCache.putIfAbsent(appId, () => App(AppConfiguration(appId))); } +final _appCache = {}; + @RealmModel() class _Settings { _Workspace? workspace; // current workspace From 2c0632068046bdceb5bb1ba921e0b810cd51dd39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 16 Aug 2023 09:28:12 +0200 Subject: [PATCH 25/33] Focus more functionality in Repository --- kilochat/lib/chat_screen.dart | 23 ++++++++++++------- kilochat/lib/chat_widget.dart | 3 +-- kilochat/lib/messages_view.dart | 5 ++-- kilochat/lib/providers.dart | 14 ++++------- kilochat/lib/providers.g.dart | 19 +++++++++++++-- .../realm_session_state_indicator.dart | 22 +++++++++++------- kilochat/lib/repository.dart | 16 ++++++++++--- kilochat/lib/settings.dart | 2 +- kilochat/lib/settings.g.dart | 10 ++++---- kilochat/pubspec.lock | 2 +- kilochat/pubspec.yaml | 1 + 11 files changed, 76 insertions(+), 41 deletions(-) diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 8dc0311..65ec932 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -17,12 +19,14 @@ import 'split_view.dart'; import 'tiles.dart'; import 'widget_builders.dart'; +final platformIsDesktop = + Platform.isMacOS || Platform.isWindows || Platform.isLinux; + class ChatScreen extends ConsumerWidget { const ChatScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final focusedChannel = ref.watch(focusedChannelProvider); final repository = ref.watch(repositoryProvider); final twoPane = MediaQuery.of(context).size.width > 700; const menuWidth = 300.0; @@ -30,8 +34,10 @@ class ChatScreen extends ConsumerWidget { error: buildErrorWidget, loading: buildLoadingWidget, data: (repository) { + final focusedChannel = repository.focusedChannel; final user = repository.userProfile; return Scaffold( + extendBodyBehindAppBar: platformIsDesktop, appBar: AppBar( title: Text( '${currentWorkspace?.name} / ${focusedChannel?.name ?? ''}'), @@ -78,34 +84,35 @@ class ChatScreen extends ConsumerWidget { child: SplitView( showLeft: twoPane, leftWidth: menuWidth, - rightPane: _buildChatPane(focusedChannel, repository), - leftPane: _buildChannelPane(ref, context), + rightPane: _buildChatPane(repository), + leftPane: _buildChannelPane(repository, context), ), ), drawer: twoPane ? null // no drawer on large screens : Drawer(child: Builder(builder: (context) { - return _buildChannelPane(ref, context); + return _buildChannelPane(repository, context); })), ); }, ); } - Widget _buildChannelPane(WidgetRef ref, BuildContext context) { + Widget _buildChannelPane(Repository repository, BuildContext context) { return SafeArea( child: ChannelsView( onTap: (channel) { - ref.read(focusedChannelProvider.notifier).focus(channel); + repository.focusedChannel = channel; Scaffold.of(context).closeDrawer(); }, ), ); } - Widget _buildChatPane(Channel? focusedChannel, Repository repository) { + Widget _buildChatPane(Repository repository) { + final channel = repository.focusedChannel; return const Center(child: Text('Choose a channel')) - .animate(target: focusedChannel == null ? 0 : 1) + .animate(target: channel == null ? 0 : 1) .crossfade(builder: (context) => const ChatWidget()); } } diff --git a/kilochat/lib/chat_widget.dart b/kilochat/lib/chat_widget.dart index 1e852df..3808e23 100644 --- a/kilochat/lib/chat_widget.dart +++ b/kilochat/lib/chat_widget.dart @@ -57,9 +57,8 @@ class _ChatWidgetState extends ConsumerState { void _postNewMessage(String text) async { if (text.isNotEmpty) { - final channel = ref.read(focusedChannelProvider); final repository = ref.read(repositoryProvider).requireValue; - repository.postNewMessage(channel!, text); + repository.postNewMessage(repository.focusedChannel!, text); controller.clear(); await scrollController.animateTo( 0, diff --git a/kilochat/lib/messages_view.dart b/kilochat/lib/messages_view.dart index 452e3ea..42266d6 100644 --- a/kilochat/lib/messages_view.dart +++ b/kilochat/lib/messages_view.dart @@ -16,10 +16,11 @@ class MessagesView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final channel = ref.watch(focusedChannelProvider); + final repository = ref.watch(repositoryProvider); + + final channel = repository.value?.focusedChannel; if (channel == null) return const SizedBox.shrink(); - final repository = ref.watch(repositoryProvider); return repository.when( error: buildErrorWidget, loading: buildLoadingWidget, diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 5797968..5d2aa2d 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -35,17 +35,13 @@ Stream user(UserRef ref) async* { @riverpod Future repository(RepositoryRef ref) async { final user = await ref.watch(userProvider.future); - final repository = await Repository.init(user); + final repository = await Repository.init(user, currentWorkspace!); ref.onDispose(repository.dispose); return repository; } -class FocusedChannel extends Notifier { - @override - Channel? build() => null; - - void focus(Channel channel) => state = channel; +@riverpod +Future focusedChannel(FocusedChannelRef ref) async { + final repository = await ref.watch(repositoryProvider.future); + return repository.focusedChannel; } - -final focusedChannelProvider = - NotifierProvider(FocusedChannel.new); diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index 3c62997..edc1e82 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -20,7 +20,7 @@ final appProvider = AutoDisposeStreamProvider.internal( ); typedef AppRef = AutoDisposeStreamProviderRef; -String _$userHash() => r'9500263a0a8bbe86ba3186a23b7ea55f651338f6'; +String _$userHash() => r'01d81be0d3b467561310f13c6d871e58fd8d8728'; /// See also [user]. @ProviderFor(user) @@ -34,7 +34,7 @@ final userProvider = AutoDisposeStreamProvider.internal( ); typedef UserRef = AutoDisposeStreamProviderRef; -String _$repositoryHash() => r'ba79d3db0c045d0c101373e89a1ce4172155a75a'; +String _$repositoryHash() => r'4e762fc48ddb22c027309c931fd347bf1068b817'; /// See also [repository]. @ProviderFor(repository) @@ -48,5 +48,20 @@ final repositoryProvider = AutoDisposeFutureProvider.internal( ); typedef RepositoryRef = AutoDisposeFutureProviderRef; +String _$focusedChannelHash() => r'891b3f67768c015ba6ca658ce7a6bff9692ddfa9'; + +/// See also [focusedChannel]. +@ProviderFor(focusedChannel) +final focusedChannelProvider = AutoDisposeFutureProvider.internal( + focusedChannel, + name: r'focusedChannelProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$focusedChannelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef FocusedChannelRef = AutoDisposeFutureProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member diff --git a/kilochat/lib/realm_ui/realm_session_state_indicator.dart b/kilochat/lib/realm_ui/realm_session_state_indicator.dart index 2d44de9..f4e7195 100644 --- a/kilochat/lib/realm_ui/realm_session_state_indicator.dart +++ b/kilochat/lib/realm_ui/realm_session_state_indicator.dart @@ -12,6 +12,8 @@ typedef SessionStatus = ( bool uploading ); +const _noProgress = (transferableBytes: 0, transferredBytes: 0); + class RealmSessionStateIndicator extends StatelessWidget { const RealmSessionStateIndicator({ super.key, @@ -46,14 +48,18 @@ class RealmSessionStateIndicator extends StatelessWidget { session.connectionStateChanges .map((c) => c.current) .startWith(session.connectionState), - session.getProgressStream( - ProgressDirection.download, - ProgressMode.reportIndefinitely, - ), - session.getProgressStream( - ProgressDirection.upload, - ProgressMode.reportIndefinitely, - ), + session + .getProgressStream( + ProgressDirection.download, + ProgressMode.reportIndefinitely, + ) + .startWith(_noProgress), + session + .getProgressStream( + ProgressDirection.upload, + ProgressMode.reportIndefinitely, + ) + .startWith(_noProgress), (connectivity, state, download, upload) => ( connectivity != ConnectivityResult.none, state, diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index e7e4226..047d128 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:cancellation_token/cancellation_token.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:kilochat/settings.dart'; import 'package:realm/realm.dart'; import 'model.dart'; @@ -26,7 +27,9 @@ class Repository extends Disposable { late final UserProfile userProfile = _realm.findOrAdd(_user.id, (id) => UserProfile(id, id)); - Repository(this._realm, this._user) { + Repository._(this._realm, this._user, Workspace ws) { + focusedChannel = _realm.find(ws.currentChannelId); + // force logout, and drop local data, user deactivated userProfile.changes.asyncListen((change) async { if (change.isDeleted || change.object.deactivated) { @@ -50,8 +53,8 @@ class Repository extends Disposable { .cancelOnDisposeOf(this); } - static Future init(User user) async { - return Repository(await _initRealm(user), user); + static Future init(User user, Workspace ws) async { + return Repository._(await _initRealm(user), user, ws); } static Future _initRealm(User user) async { @@ -91,6 +94,13 @@ class Repository extends Disposable { Session get session => _realm.syncSession; + Channel? _focusedChannel; + Channel? get focusedChannel => _focusedChannel; + set focusedChannel(Channel? value) { + _focusedChannel = value; + currentWorkspace!.currentChannelId = _focusedChannel?.id; + } + late RealmResults allChannels = _realm.query('TRUEPREDICATE SORT(name ASCENDING)'); diff --git a/kilochat/lib/settings.dart b/kilochat/lib/settings.dart index 076c1da..badd036 100644 --- a/kilochat/lib/settings.dart +++ b/kilochat/lib/settings.dart @@ -8,7 +8,7 @@ class _Workspace { @PrimaryKey() late String appId; // atlas app service id late String name; // for display - String? currentChannelId; // current channel + ObjectId? currentChannelId; // current channel App get app => _appCache.putIfAbsent(appId, () => App(AppConfiguration(appId))); diff --git a/kilochat/lib/settings.g.dart b/kilochat/lib/settings.g.dart index 027dc0d..ce51764 100644 --- a/kilochat/lib/settings.g.dart +++ b/kilochat/lib/settings.g.dart @@ -11,7 +11,7 @@ class Workspace extends _Workspace Workspace( String appId, String name, { - String? currentChannelId, + ObjectId? currentChannelId, }) { RealmObjectBase.set(this, 'appId', appId); RealmObjectBase.set(this, 'name', name); @@ -31,10 +31,10 @@ class Workspace extends _Workspace set name(String value) => RealmObjectBase.set(this, 'name', value); @override - String? get currentChannelId => - RealmObjectBase.get(this, 'currentChannelId') as String?; + ObjectId? get currentChannelId => + RealmObjectBase.get(this, 'currentChannelId') as ObjectId?; @override - set currentChannelId(String? value) => + set currentChannelId(ObjectId? value) => RealmObjectBase.set(this, 'currentChannelId', value); @override @@ -51,7 +51,7 @@ class Workspace extends _Workspace return const SchemaObject(ObjectType.realmObject, Workspace, 'Workspace', [ SchemaProperty('appId', RealmPropertyType.string, primaryKey: true), SchemaProperty('name', RealmPropertyType.string), - SchemaProperty('currentChannelId', RealmPropertyType.string, + SchemaProperty('currentChannelId', RealmPropertyType.objectid, optional: true), ]); } diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 1c03e81..5442273 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -915,7 +915,7 @@ packages: source: hosted version: "1.4.0" rxdart: - dependency: transitive + dependency: "direct main" description: name: rxdart sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index bbea836..67731d6 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: realm: ^1.3.0 riverpod_annotation: ^2.0.0 riverpod: ^2.3.0 + rxdart: ^0.27.0 dev_dependencies: flutter_test: From 3a5e5fc5589a710bb8b2ba47a2ae2d17a507e6fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Wed, 16 Aug 2023 09:35:33 +0200 Subject: [PATCH 26/33] Set version to 0.1.0 --- kilochat/pubspec.lock | 42 +++++++++++++++++++++++++----------------- kilochat/pubspec.yaml | 2 +- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 5442273..1e4c98e 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -205,10 +205,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.1" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: @@ -657,10 +657,10 @@ packages: dependency: transitive description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" io: dependency: transitive description: @@ -721,18 +721,18 @@ packages: dependency: transitive description: name: matcher - sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.15" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.5.0" meta: dependency: transitive description: @@ -963,18 +963,18 @@ packages: dependency: transitive description: name: source_span - sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -987,10 +987,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1027,10 +1027,10 @@ packages: dependency: transitive description: name: test_api - sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.6.1" timing: dependency: transitive description: @@ -1103,6 +1103,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + url: "https://pub.dev" + source: hosted + version: "0.1.4-beta" web_socket_channel: dependency: transitive description: @@ -1128,5 +1136,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.0.2 <4.0.0" + dart: ">=3.1.0-185.0.dev <4.0.0" flutter: ">=3.10.0" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index 67731d6..def4fae 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -3,7 +3,7 @@ description: A chat app built with Flutter, Realm and Atlas. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.0+1 +version: 0.1.0 environment: sdk: ^3.0.0 From fd5de9115efcebb93c16709acce741b184fb5ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 18 Aug 2023 13:38:41 +0200 Subject: [PATCH 27/33] Update realm to 1.4.0 and flutter to 3.13.0.. and fix some broken stuff --- kilochat/lib/channels_view.dart | 2 +- kilochat/lib/chat_screen.dart | 17 +-- kilochat/lib/messages_view.dart | 2 +- kilochat/lib/providers.dart | 8 +- kilochat/lib/providers.g.dart | 8 +- .../realm_session_state_indicator.dart | 2 +- kilochat/lib/repository.dart | 20 ++-- kilochat/pubspec.lock | 113 +++++++++--------- kilochat/pubspec.yaml | 6 +- 9 files changed, 94 insertions(+), 84 deletions(-) diff --git a/kilochat/lib/channels_view.dart b/kilochat/lib/channels_view.dart index 0a8d035..5f42ed2 100644 --- a/kilochat/lib/channels_view.dart +++ b/kilochat/lib/channels_view.dart @@ -17,7 +17,7 @@ class ChannelsView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final focusedChannel = ref.watch(focusedChannelProvider); + final focusedChannel = ref.watch(focusedChannelProvider).value; final repository = ref.watch(repositoryProvider); return repository.when( error: buildErrorWidget, diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 65ec932..7dc48bd 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -8,7 +8,6 @@ import 'avatar.dart'; import 'channels_view.dart'; import 'chat_widget.dart'; import 'display_toast.dart'; -import 'model.dart'; import 'profile_form.dart'; import 'providers.dart'; import 'realm_ui/realm_session_state_indicator.dart'; @@ -28,13 +27,13 @@ class ChatScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final repository = ref.watch(repositoryProvider); + final focusedChannel = ref.watch(focusedChannelProvider).value; final twoPane = MediaQuery.of(context).size.width > 700; const menuWidth = 300.0; return repository.when( error: buildErrorWidget, loading: buildLoadingWidget, data: (repository) { - final focusedChannel = repository.focusedChannel; final user = repository.userProfile; return Scaffold( extendBodyBehindAppBar: platformIsDesktop, @@ -100,12 +99,14 @@ class ChatScreen extends ConsumerWidget { Widget _buildChannelPane(Repository repository, BuildContext context) { return SafeArea( - child: ChannelsView( - onTap: (channel) { - repository.focusedChannel = channel; - Scaffold.of(context).closeDrawer(); - }, - ), + child: Builder(builder: (context) { + return ChannelsView( + onTap: (channel) { + repository.focusedChannel = channel; + Scaffold.of(context).closeDrawer(); + }, + ); + }), ); } diff --git a/kilochat/lib/messages_view.dart b/kilochat/lib/messages_view.dart index 42266d6..24e3209 100644 --- a/kilochat/lib/messages_view.dart +++ b/kilochat/lib/messages_view.dart @@ -18,7 +18,7 @@ class MessagesView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final repository = ref.watch(repositoryProvider); - final channel = repository.value?.focusedChannel; + final channel = ref.watch(focusedChannelProvider).value; if (channel == null) return const SizedBox.shrink(); return repository.when( diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 5d2aa2d..b01f35a 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -35,13 +35,15 @@ Stream user(UserRef ref) async* { @riverpod Future repository(RepositoryRef ref) async { final user = await ref.watch(userProvider.future); - final repository = await Repository.init(user, currentWorkspace!); + final repository = await Repository.init(currentWorkspace!); ref.onDispose(repository.dispose); return repository; } @riverpod -Future focusedChannel(FocusedChannelRef ref) async { +Stream focusedChannel(FocusedChannelRef ref) async* { final repository = await ref.watch(repositoryProvider.future); - return repository.focusedChannel; + await for (final channel in repository.focusedChannelChanges) { + yield channel; + } } diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index edc1e82..44678c7 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -34,7 +34,7 @@ final userProvider = AutoDisposeStreamProvider.internal( ); typedef UserRef = AutoDisposeStreamProviderRef; -String _$repositoryHash() => r'4e762fc48ddb22c027309c931fd347bf1068b817'; +String _$repositoryHash() => r'2275ebf66658e7b0d08ad325b5ea8aacacf4143a'; /// See also [repository]. @ProviderFor(repository) @@ -48,11 +48,11 @@ final repositoryProvider = AutoDisposeFutureProvider.internal( ); typedef RepositoryRef = AutoDisposeFutureProviderRef; -String _$focusedChannelHash() => r'891b3f67768c015ba6ca658ce7a6bff9692ddfa9'; +String _$focusedChannelHash() => r'c467f8b8fbe8c481f8f49a542486227febaa2684'; /// See also [focusedChannel]. @ProviderFor(focusedChannel) -final focusedChannelProvider = AutoDisposeFutureProvider.internal( +final focusedChannelProvider = AutoDisposeStreamProvider.internal( focusedChannel, name: r'focusedChannelProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -62,6 +62,6 @@ final focusedChannelProvider = AutoDisposeFutureProvider.internal( allTransitiveDependencies: null, ); -typedef FocusedChannelRef = AutoDisposeFutureProviderRef; +typedef FocusedChannelRef = AutoDisposeStreamProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member diff --git a/kilochat/lib/realm_ui/realm_session_state_indicator.dart b/kilochat/lib/realm_ui/realm_session_state_indicator.dart index f4e7195..dd0e180 100644 --- a/kilochat/lib/realm_ui/realm_session_state_indicator.dart +++ b/kilochat/lib/realm_ui/realm_session_state_indicator.dart @@ -12,7 +12,7 @@ typedef SessionStatus = ( bool uploading ); -const _noProgress = (transferableBytes: 0, transferredBytes: 0); +const _noProgress = SyncProgress(transferableBytes: 0, transferredBytes: 0); class RealmSessionStateIndicator extends StatelessWidget { const RealmSessionStateIndicator({ diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index 047d128..2f68058 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -21,14 +21,15 @@ class Disposable { } class Repository extends Disposable { + final Workspace _workspace; final Realm _realm; final User _user; late final UserProfile userProfile = _realm.findOrAdd(_user.id, (id) => UserProfile(id, id)); - Repository._(this._realm, this._user, Workspace ws) { - focusedChannel = _realm.find(ws.currentChannelId); + Repository._(this._realm, this._user, this._workspace) { + focusedChannel = _realm.find(_workspace.currentChannelId); // force logout, and drop local data, user deactivated userProfile.changes.asyncListen((change) async { @@ -53,8 +54,9 @@ class Repository extends Disposable { .cancelOnDisposeOf(this); } - static Future init(User user, Workspace ws) async { - return Repository._(await _initRealm(user), user, ws); + static Future init(Workspace workspace) async { + final user = workspace.app.currentUser!; + return Repository._(await _initRealm(user), user, workspace); } static Future _initRealm(User user) async { @@ -94,13 +96,15 @@ class Repository extends Disposable { Session get session => _realm.syncSession; - Channel? _focusedChannel; - Channel? get focusedChannel => _focusedChannel; + Channel? _getChannel(ObjectId? id) => _realm.find(id); + Channel? get focusedChannel => _getChannel(_workspace.currentChannelId); set focusedChannel(Channel? value) { - _focusedChannel = value; - currentWorkspace!.currentChannelId = _focusedChannel?.id; + _workspace.realm.write(() => _workspace.currentChannelId = value?.id); } + Stream get focusedChannelChanges => + _workspace.changes.map((c) => _getChannel(c.object.currentChannelId)); + late RealmResults allChannels = _realm.query('TRUEPREDICATE SORT(name ASCENDING)'); diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 1e4c98e..e317d1c 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: "5dce45a06d386358334eb1689108db6455d90ceb0d75848d5f4819283d4ee2b8" + sha256: "1a5e13736d59235ce0139621b4bbe29bc89839e202409081bc667eb3cd20674c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" analyzer: dependency: transitive description: @@ -205,10 +205,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.2" connectivity_plus: dependency: "direct main" description: @@ -325,34 +325,34 @@ packages: dependency: "direct main" description: name: firebase_auth - sha256: "49fd35ce06f2530dd460e5dc123235731cb61dd7c76b0af4b6e190404880d04d" + sha256: ae8029eee0ed24a0ae4a9bcc94c5ef9c2e0c31066c5132c56575da929dd14a46 url: "https://pub.dev" source: hosted - version: "4.7.2" + version: "4.7.3" firebase_auth_platform_interface: dependency: transitive description: name: firebase_auth_platform_interface - sha256: "817f3ceb84ef5e9adaaf50cf7a19255f6ffcdd12c6f9e9aa4cf00fc7f2eb3cfb" + sha256: cb099fbd3d48f7983c8d7cea45947d98d36174fb7d611e4ff08f802491e8a945 url: "https://pub.dev" source: hosted - version: "6.16.1" + version: "6.16.2" firebase_auth_web: dependency: transitive description: name: firebase_auth_web - sha256: e9044778287f1ff8f9f4cee7e247b03ec87bb8977e0e65ad27dc337e196132e8 + sha256: d35abf4c6c77c9e7be5d7a637e6e07765b70426015800ba031257f382bff5f83 url: "https://pub.dev" source: hosted - version: "5.6.2" + version: "5.6.3" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "2e9324f719e90200dc7d3c4f5d2abc26052f9f2b995d3b6626c47a0dfe1c8192" + sha256: c78132175edda4bc532a71e01a32964e4b4fcf53de7853a422d96dac3725f389 url: "https://pub.dev" source: hosted - version: "2.15.0" + version: "2.15.1" firebase_core_platform_interface: dependency: transitive description: @@ -365,58 +365,58 @@ packages: dependency: transitive description: name: firebase_core_web - sha256: "0fd5c4b228de29b55fac38aed0d9e42514b3d3bd47675de52bf7f8fccaf922fa" + sha256: "4cf4d2161530332ddc3c562f19823fb897ff37a9a774090d28df99f47370e973" url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.7.0" firebase_dynamic_links: dependency: transitive description: name: firebase_dynamic_links - sha256: "4872f4d7e94736041398bc3490c2ddd87ee159d6b051ba01ca2708e5260a7ebe" + sha256: c604cebcc4c2baf978a8c286ab618d8472aaa813497ab4002cb968cd73b03ed8 url: "https://pub.dev" source: hosted - version: "5.3.4" + version: "5.3.5" firebase_dynamic_links_platform_interface: dependency: transitive description: name: firebase_dynamic_links_platform_interface - sha256: "946fccfefb67e26bf63e392f1b3917d79ea031d3071488f0c5e8ab72de8219ab" + sha256: "2a6f77434fcb36c8409d29785fee45a262deffc9d36205a81cbe0093802e72bd" url: "https://pub.dev" source: hosted - version: "0.2.6+4" + version: "0.2.6+5" firebase_ui_auth: dependency: "direct main" description: name: firebase_ui_auth - sha256: e439571fcad7ed48450eed8d64c70b93765526b876327055469806a51101eff0 + sha256: c597e1482ba8f4d3db0f9b97d839754ad76dba514bd1ee94d31a0126d5a3e287 url: "https://pub.dev" source: hosted - version: "1.6.2" + version: "1.6.3" firebase_ui_localizations: dependency: transitive description: name: firebase_ui_localizations - sha256: b13be7432af3eed2ff6f2ed1c55a9afc32ffa7376fcada9e16be055fad6415ed + sha256: "830bd79f3e4ccfb4b8ee83e9928de6be97ce1bc80b1b4c16bce8565eb5b48a37" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" firebase_ui_oauth: dependency: transitive description: name: firebase_ui_oauth - sha256: "2cbe5a8996134f1a57205a5ebfa5863c5d599c03a07aae70867de2ecdfbbde0e" + sha256: bfa054140628dff703c8204080336d6616e4f49bbf51e9cf7d73b7d5970d5f1a url: "https://pub.dev" source: hosted - version: "1.4.7" + version: "1.4.8" firebase_ui_oauth_apple: dependency: "direct main" description: name: firebase_ui_oauth_apple - sha256: "9cd85a9c85bb3d2e43d5a37cb4b4b7efcfc6a718dadbabb36afb91b1413b4de3" + sha256: "29dfad809ff809f711dfc719f5e040cf67a1734821087d1488cefe1a76760ff5" url: "https://pub.dev" source: hosted - version: "1.2.7" + version: "1.2.8" firebase_ui_oauth_facebook: dependency: "direct main" description: @@ -429,10 +429,10 @@ packages: dependency: "direct main" description: name: firebase_ui_oauth_google - sha256: "0e255ced67023871040b3d349c966a281a54ce1a093128733ccbd0a6b00a95d1" + sha256: "47fa590840e6d4ad661f6ae8782c3f14a987b37f524e7c2ce83323cbdeb7ce6b" url: "https://pub.dev" source: hosted - version: "1.2.7" + version: "1.2.8" firebase_ui_shared: dependency: transitive description: @@ -511,10 +511,10 @@ packages: dependency: "direct main" description: name: flutter_riverpod - sha256: b83ac5827baadefd331ea1d85110f34645827ea234ccabf53a655f41901a9bf4 + sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" flutter_svg: dependency: transitive description: @@ -856,32 +856,35 @@ packages: realm: dependency: "direct main" description: - path: "../../../realm-dart/flutter/realm_flutter" - relative: true - source: path - version: "1.3.0" + name: realm + sha256: c25b13cfdadeae582ddda1a97d58cff9e0fc0665a19905ceb5d0707bd90ace6d + url: "https://pub.dev" + source: hosted + version: "1.4.0" realm_common: dependency: transitive description: - path: "../../../realm-dart/common" - relative: true - source: path - version: "1.3.0" + name: realm_common + sha256: "2ee6c3d01a70cf3c7cf8d8550cee6f17e55c0184aed53ececdb5b153ea501b57" + url: "https://pub.dev" + source: hosted + version: "1.4.0" realm_generator: dependency: transitive description: - path: "../../../realm-dart/generator" - relative: true - source: path - version: "1.3.0" + name: realm_generator + sha256: "8ccf72fd6aec2fd8a8863fcfd1e9d24d2c054cfcbe0123808a77bede00285d9c" + url: "https://pub.dev" + source: hosted + version: "1.4.0" riverpod: dependency: "direct main" description: name: riverpod - sha256: "80e48bebc83010d5e67a11c9514af6b44bbac1ec77b4333c8ea65dbc79e2d8ef" + sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "2.3.7" riverpod_analyzer_utils: dependency: transitive description: @@ -894,10 +897,10 @@ packages: dependency: "direct main" description: name: riverpod_annotation - sha256: cedd6a54b6f5764ffd5c05df57b6676bfc8c01978e14ee60a2c16891038820fe + sha256: "8b3f7a54ddd5d53d6ea04bfb4ff77ee1b0816a1b563c0d9d43e73ce94bf2016d" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" riverpod_generator: dependency: "direct dev" description: @@ -971,26 +974,26 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" state_notifier: dependency: transitive description: name: state_notifier - sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb url: "https://pub.dev" source: hosted - version: "0.7.2+1" + version: "1.0.0" stream_channel: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: @@ -1027,10 +1030,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.0" timing: dependency: transitive description: @@ -1137,4 +1140,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.10.2" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index def4fae..d2cfa36 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -51,6 +51,6 @@ flutter: fonts: - asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf -dependency_overrides: - realm: - path: ../../../realm-dart/flutter/realm_flutter +#dependency_overrides: +# realm: +# path: ../../../realm-dart/flutter/realm_flutter From 5da1d54bc1f27d9f92bd4c610c7c571e8f37f64e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Tue, 5 Sep 2023 17:00:08 +0200 Subject: [PATCH 28/33] WIP --- kilochat/README.md | 118 ++++++--- kilochat/lib/chat_screen.dart | 10 +- kilochat/lib/firebase_options.dart | 76 ------ kilochat/lib/firebase_user_provider.dart | 8 - kilochat/lib/firebase_user_provider.g.dart | 24 -- kilochat/lib/main.dart | 14 - kilochat/lib/profile_form.dart | 3 +- kilochat/lib/providers.dart | 4 +- kilochat/lib/router.dart | 26 +- kilochat/pubspec.lock | 293 +++++---------------- kilochat/pubspec.yaml | 12 +- 11 files changed, 159 insertions(+), 429 deletions(-) delete mode 100644 kilochat/lib/firebase_options.dart delete mode 100644 kilochat/lib/firebase_user_provider.dart delete mode 100644 kilochat/lib/firebase_user_provider.g.dart diff --git a/kilochat/README.md b/kilochat/README.md index cf58b23..de24582 100644 --- a/kilochat/README.md +++ b/kilochat/README.md @@ -7,12 +7,12 @@ It aims to give a more thorough and realistic example of how to use Realm and At The app demonstrates how to: 1. Re-use credentials on app restart to allow for _offline login_ -1. Use federated jwt-based authentication with Firebase Auth. -1. React to connectivity changes to quickly recover. +1. Use federated jwt-based authentication with FIDO 2 / WebAuthn passkeys. +1. React to connectivity changes to quickly recover synchronization. 1. Display sync connection state. 1. Handle soft synchronization errors. 1. Handle client resets (hard sync errors). -1. Display a toast when interesting data is synced to device. +1. Display a toast when data is synced to device. 1. Animate list views as changes happen in a realm that impacts a live query. 1. Update flexible sync subscriptions to only sync a subset of data dynamically. 1. Use rule-based permissions to ensure users can only manipulate their own data. @@ -26,50 +26,62 @@ Depending on how you choose to count, it does so in just 1-2K lines of code, hen ## Getting started +The absolute easiest way to check this out is to install the kilochat app from either Apple's App Store or Google's Play Store. + +A shared backend is setup for a fictitious organization [Acme Corporation](https://en.wikipedia.org/wiki/Acme_Corporation) for your convenience. + +If you choose to use the pre-configured backend, then be aware that it is shared with the entire world, and there is no ongoing moderation. Please behave and follow our [code of conduct](https://www.mongodb.com/community-code-of-conduct) or we will have to disable it. + +You will be able to sign-up and login with an email address of yours, and if you device supports it, a biometric passkeys. Otherwise a magic-link will be sent to your address. No password is necessary. + +We hope, that once you have played with it for a while, that you will be tempted to learn how it was build. The rest of this lengthy README will try to explain just that. + ### Prerequisites -You need Flutter 3.10.2 or later. +You need Flutter 3.13.0 or later. -Create the platform projects you are interested in. Only the bare minimum modifications are added in the repo, so you need to create the platform projects on your end. You can choose any platforms except web. +Create the platform projects you are interested in. Only the bare minimum modifications are added in the repo, so you need to create the platform projects on your end. You can choose Android and/or iOS. ```shell -flutter create . --platforms=android,ios,macos,windows,linux # realm does not support web (yet) +flutter create . --platforms=android,ios # realm does not support web (yet), and corbado only support android and ios. flutter pub get ``` -You will need to login with either a google account or email and password. For the latter we have setup 9 different test users (`test1@test.com`, ..., `test9@test.com`), with passwords matching pattern `test!Atlas`. There is no ongoing moderation, so please behave or we will have to disable these shared accounts. +You will need the following CLI tools installed to follow the instructions for setting up a backend: -## Setting up your own backend +- `atlas`, and +- `atlas-app-services-cli`. -To make it easy to get started we have a shared backend setup already, but be aware that it is managed by us at MongoDB and shared with the entire world. +On macos You can get these with: + +```shell +npm install -g atlas-app-services-cli # TODO: is this officially released yet? (or fo we need to use realm-cli?) +brew install mongodb_atlas_cli # TODO: on macos +``` -> :warning: This also means that this repo contains a set of Firebase API keys, which while **not** really [secret](https://firebase.google.com/docs/projects/api-keys) as such, is not something we would recommend in general. -> -> The repo also contains an atlas application identifier (`app_id`) which is **not** a secret, but should not be shared if the app service has developer mode enabled. The reason for this being that developer mode allows clients to do automatic schema changes -To setup your own backend you need to setup both Firebase Auth with Google and Atlas Device Sync with MongoDB. +## Setting up your own backend + +> :warning: This repo contains an atlas application identifier (`app_id`) which is **not** a secret, but should not be shared widely if the app service has developer mode enabled. The reason for this being that developer mode allows clients to do automatic schema changes. + +These instruction only pertain to the setup Atlas Device Sync with MongoDB, and reuse the existing relay party. If you want to setup your own corbado project as well, please consult https://pub.dev/packages/corbado_auth. You will need the following CLI tools installed to follow these instructions: -- `flutterfire`, - `atlas`, and - `atlas-app-services-cli`. You can get these with: ```shell -dart pub global activate flutterfire npm install -g atlas-app-services-cli # TODO: is this officially released yet? (or fo we need to use realm-cli?) brew install mongodb_atlas_cli # TODO: on macos ``` -### Firebase Auth - -You can follow Google's [instructions](https://firebase.google.com/docs/auth/flutter/start) for setting up Firebase Authentication with Flutter. - ### Atlas Device Sync -First you need to setup a cluster. +First, you need to create an organization. +Secondly you need to create a project. Then you need to setup Atlas Device Sync. Then you need to push the schema. Normally during development you would just enable developer mode, and have the client inform the server of the schema implicitly, but this is not how you would go about it in production, so for this sample we will not enable developer mode. @@ -89,7 +101,52 @@ This is similar to how your email client works on your phone. Realm will try to refresh the associated tokens whenever needed. Obviously that will require the device to be online, but as tokens only need to be valid when actually performing requests, this will require the device to be online anyway. For most practical purposes a user only needs to login on a given device once. -In this sample we federate authentication to firebase. Firebase also persists the currently logged in user, so even if `app.currentUser == null` then we may still have a valid jwt token from firebase, so that the user avoid actually performing authentication. +In this sample we federate authentication to corbado. Corbado also persists the currently logged in user, so even if `app.currentUser == null` we may still obtain a valid jwt token, so that the user avoid actually performing authentication. + +## Authentication + +I have a vague recollection that Bill Gates proclaimed the death of passwords back in the early 00's, however we may finally +be on the brink of such a future, due to FIDO2 / WebAuthn passkeys, which are supported on most modern operating systems (Android 9, iOS 16, MacOS Ventura, Windows 10, and later), and with external hardware authenticators such as Yubikey. + +When it comes to Flutter, the support is still a bit limited, but the German startup [Corbado](https://app.corbado.com/) is working hard to change that. So far only Android and iOS is supported, and that is really the reason this project only works on those platforms. Realm itself works on all official platforms, except web. + +## Authorization + +While passkeys allows our users to be securely authenticated, in an UX friendly manner, we need to authorize the users to use the system. For a Atlas App Services this is handled at two levels + +1. Upon user creation. + + When creating a user on Atlas App Services, you can choose to have a user creation callback invoked. This can be used to either deny the creation, or augment the users profile with extra data, or both. + + For kilochat the default is evaluate the + validated user email against a set of regular expressions. This can be used to only allow email addresses from a given company, or maintain an explicit list of allowed users. + +2. Rules and roles + + Once a user has been created, she is authorized to use the app service, but exactly what subset of data she is allowed to read, insert, write, delete, and search, is determined by rules and roles. + +### User creation + + +### Rule-based permissions + +Kilochat is a mostly public system, yet we still wan't to prevent bad actors from impersonating others, and make changes to other peoples data, be it their messages, reactions, or user profile. + +While the app as implemented won't allow such edits, we cannot rely on the app to enforce this. A malicious actor could reverse engineer the app and implement a version without such restrictions. + +Instead it falls on the server to enforce the rules of the system. In our case it is very simple. A user cannot edit objects created by other users, and cannot impersonate others. + +This has some implications on our model classes. First, all model classes has an extra `ownerId` property that must be filled with `user.id` upon creation. The backend will check that `ownerId` on any edit match the user id of the session, or revoke the edit (/creation). If this happens, the perpetrator will receive a compensating write, and undo the invalid change, assuming it was due to a bug and not a malicious intent. + +A more subtle implication is that a message cannot contain a list of reactions, as only the message owner would be able to update this list. Instead a reaction refer to the message it pertains to and the reactions are accessible from the message via a named backlink property. Each reaction is owned by the user who added it. + +The ownership role goes for channels as well. Anyone can create a channel, but only the owner can update or +delete it. The ability to create channels would usually be reserved for admin users, but alas this is after +all just a toy system. + + + + ## Subscriptions @@ -125,21 +182,6 @@ The presence system used here is still rather simple. We refer t [A Scalable Ser ## Leader-board -## Rule-based permissions - -Kilochat is a mostly public system, yet we still wan't to prevent bad actors from impersonating others, and make changes to other peoples data, be it their messages, reactions, or user profile. - -While the app as implemented won't allow such edits, we cannot rely on the app to enforce this. A malicious actor could reverse engineer the app and implement a version without such restrictions. - -Instead it falls on the server to enforce the rules of the system. In our case it is very simple. A user cannot edit objects created by other users, and cannot impersonate others. - -This has some implications on our model classes. First, all model classes has an extra `ownerId` property that must be filled with `user.id` upon creation. The backend will check that `ownerId` on any edit match the user id of the session, or revoke the edit (/creation). If this happens, the perpetrator will receive a compensating write, and undo the invalid change, assuming it was due to a bug and not a malicious intent. - -A more subtle implication is that a message cannot contain a list of reactions, as only the message owner would be able to update this list. Instead a reaction refer to the message it pertains to and the reactions are accessible from the message via a named backlink property. Each reaction is owned by the user who added it. - -The ownership role goes for channels as well. Anyone can create a channel, but only the owner can update or -delete it. The ability to create channels would usually be reserved for admin users, but alas this is after -all just a toy system. # A bit about modelling @@ -180,7 +222,3 @@ Also, we always add the new messages to the end. This means, that if we _believe We still need to maintain the order somehow. To do this each new message will be given a new index that is one larger than max index of any seen message in the channel sofar. There may be duplicates if some peers have have not seen all existing messages when creating a new one. To handle this we define the order as the sort of the indexes, and secondarily the owner id to ensure a stable sort. Note that we may introduce holes in the sequence this way (or by later deletions), but that is alright. Realm will easily find the _n'th_ message in the channel given the sort, irregardless of the actual values of index, and owner id. - -# DISCLAIMER - -This is low priority work in progress. Eventually we hope to make this a well documented example of best practices, but that is _not_ the current state. diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 7dc48bd..8edd19f 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'avatar.dart'; @@ -112,8 +111,11 @@ class ChatScreen extends ConsumerWidget { Widget _buildChatPane(Repository repository) { final channel = repository.focusedChannel; - return const Center(child: Text('Choose a channel')) - .animate(target: channel == null ? 0 : 1) - .crossfade(builder: (context) => const ChatWidget()); + return AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: channel == null + ? const Center(child: Text('Choose a channel')) + : ChatWidget(key: ValueKey(channel.id)), + ); } } diff --git a/kilochat/lib/firebase_options.dart b/kilochat/lib/firebase_options.dart deleted file mode 100644 index c2d8d9f..0000000 --- a/kilochat/lib/firebase_options.dart +++ /dev/null @@ -1,76 +0,0 @@ -// File generated by FlutterFire CLI. -// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members -import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; -import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; - -/// Default [FirebaseOptions] for use with your Firebase apps. -/// -/// Example: -/// ```dart -/// import 'firebase_options.dart'; -/// // ... -/// await Firebase.initializeApp( -/// options: DefaultFirebaseOptions.currentPlatform, -/// ); -/// ``` -class DefaultFirebaseOptions { - static FirebaseOptions get currentPlatform { - if (kIsWeb) { - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for web - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - } - switch (defaultTargetPlatform) { - case TargetPlatform.android: - return android; - case TargetPlatform.iOS: - return ios; - case TargetPlatform.macOS: - return macos; - case TargetPlatform.windows: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for windows - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - case TargetPlatform.linux: - throw UnsupportedError( - 'DefaultFirebaseOptions have not been configured for linux - ' - 'you can reconfigure this by running the FlutterFire CLI again.', - ); - default: - throw UnsupportedError( - 'DefaultFirebaseOptions are not supported for this platform.', - ); - } - } - - static const FirebaseOptions android = FirebaseOptions( - apiKey: 'AIzaSyDm3ZNJi-WewDKmi6m6-pZKCIVNKsPC_eQ', - appId: '1:371214127756:android:1ab8f0af3f604e0b60a1a9', - messagingSenderId: '371214127756', - projectId: 'kilochat-realm', - storageBucket: 'kilochat-realm.appspot.com', - ); - - static const FirebaseOptions ios = FirebaseOptions( - apiKey: 'AIzaSyDJ0UFt4Nrz2OnBQvI3qio72Abw76bcRG4', - appId: '1:371214127756:ios:ba86b9e1d8b4750860a1a9', - messagingSenderId: '371214127756', - projectId: 'kilochat-realm', - storageBucket: 'kilochat-realm.appspot.com', - iosClientId: '371214127756-7hg0fj2a2u8jbo8ur7veael6vfoboan7.apps.googleusercontent.com', - iosBundleId: 'com.example.kilochat', - ); - - static const FirebaseOptions macos = FirebaseOptions( - apiKey: 'AIzaSyDJ0UFt4Nrz2OnBQvI3qio72Abw76bcRG4', - appId: '1:371214127756:ios:672fefd8423f42c560a1a9', - messagingSenderId: '371214127756', - projectId: 'kilochat-realm', - storageBucket: 'kilochat-realm.appspot.com', - iosClientId: '371214127756-qpak0482c07firum3oukirg1kajoe5d7.apps.googleusercontent.com', - iosBundleId: 'com.example.kilochat.RunnerTests', - ); -} diff --git a/kilochat/lib/firebase_user_provider.dart b/kilochat/lib/firebase_user_provider.dart deleted file mode 100644 index 2b927bf..0000000 --- a/kilochat/lib/firebase_user_provider.dart +++ /dev/null @@ -1,8 +0,0 @@ -import 'package:firebase_auth/firebase_auth.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'firebase_user_provider.g.dart'; - -@riverpod -Stream firebaseUser(FirebaseUserRef ref) => - FirebaseAuth.instance.authStateChanges(); diff --git a/kilochat/lib/firebase_user_provider.g.dart b/kilochat/lib/firebase_user_provider.g.dart deleted file mode 100644 index a393f90..0000000 --- a/kilochat/lib/firebase_user_provider.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'firebase_user_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$firebaseUserHash() => r'70b3976ab00c6083cb3b1f4c2183d61ee075585a'; - -/// See also [firebaseUser]. -@ProviderFor(firebaseUser) -final firebaseUserProvider = AutoDisposeStreamProvider.internal( - firebaseUser, - name: r'firebaseUserProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$firebaseUserHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef FirebaseUserRef = AutoDisposeStreamProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart index c44b227..bf78f45 100644 --- a/kilochat/lib/main.dart +++ b/kilochat/lib/main.dart @@ -1,16 +1,10 @@ import 'dart:async'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; -import 'package:firebase_ui_oauth_apple/firebase_ui_oauth_apple.dart'; -import 'package:firebase_ui_oauth_facebook/firebase_ui_oauth_facebook.dart'; -import 'package:firebase_ui_oauth_google/firebase_ui_oauth_google.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:realm/realm.dart'; -import 'firebase_options.dart'; import 'router.dart'; const freedomBlue = Color(0xff0057b7); @@ -21,14 +15,6 @@ Future main() async { Animate.restartOnHotReload = true; WidgetsFlutterBinding.ensureInitialized(); - await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); - FirebaseUIAuth.configureProviders([ - EmailAuthProvider(), - AppleProvider(), - FacebookProvider(clientId: ''), - GoogleProvider(clientId: ''), - ]); - runApp( ProviderScope( child: MaterialApp.router( diff --git a/kilochat/lib/profile_form.dart b/kilochat/lib/profile_form.dart index 63ca8a6..5db61d4 100644 --- a/kilochat/lib/profile_form.dart +++ b/kilochat/lib/profile_form.dart @@ -1,4 +1,3 @@ -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -126,7 +125,7 @@ class _ProfileFormState extends ConsumerState { label: const Text('Save Changes'), ), const Spacer(), - const SignOutButton() + const Placeholder() ]), ] .animate(interval: 100.ms) diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index b01f35a..bf1f799 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:realm/realm.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'firebase_user_provider.dart'; import 'model.dart'; import 'repository.dart'; import 'settings.dart'; @@ -22,8 +21,7 @@ Stream app(AppRef ref) async* { Stream user(UserRef ref) async* { final app = await ref.watch(appProvider.future); - final firebaseUser = await ref.watch(firebaseUserProvider.future); - final jwt = await firebaseUser?.getIdToken(); + final jwt = '' as String?; // todo var user = app.currentUser; if (jwt != null) { diff --git a/kilochat/lib/router.dart b/kilochat/lib/router.dart index bfe0b35..87fa61e 100644 --- a/kilochat/lib/router.dart +++ b/kilochat/lib/router.dart @@ -1,4 +1,3 @@ -import 'package:firebase_ui_auth/firebase_ui_auth.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:realm/realm.dart'; @@ -35,34 +34,13 @@ final router = GoRouter( GoRoute( path: Routes.logIn.path, builder: (context, state) { - return SignInScreen(actions: [ - AuthCancelledAction((context) { - Routes.chooseWorkspace.go(context); - }), - AuthStateChangeAction((context, state) async { - // TODO: This sucks!! - try { - final app = currentWorkspace?.app; - final jwt = await state.user?.getIdToken(); - if (app != null && jwt != null) { - await app.logIn(Credentials.jwt(jwt)); - } - } catch (_) { - currentWorkspace = null; - } - - if (context.mounted) { - Routes.chat.go(context); - } - }), - ]); + return const Placeholder(); }, ), GoRoute( path: Routes.profile.path, builder: (context, state) { - //return const Placeholder(); - return const ProfileScreen(); + return const Placeholder(); }, ), ], diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index e317d1c..863ca8c 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -9,14 +9,6 @@ packages: url: "https://pub.dev" source: hosted version: "60.0.0" - _flutterfire_internals: - dependency: transitive - description: - name: _flutterfire_internals - sha256: "1a5e13736d59235ce0139621b4bbe29bc89839e202409081bc667eb3cd20674c" - url: "https://pub.dev" - source: hosted - version: "1.3.5" analyzer: dependency: transitive description: @@ -233,6 +225,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + corbado_auth: + dependency: "direct main" + description: + name: corbado_auth + sha256: e8e4a8468da51556bd68fad9ebfc345b296767fe86de5ab2532507b6115cd10b + url: "https://pub.dev" + source: hosted + version: "1.0.1" + corbado_frontend_api_client: + dependency: transitive + description: + name: corbado_frontend_api_client + sha256: "2353d291c55e8f7f1fa842198c8ed82477c4129c34742ef74353e72ce7dc5ba8" + url: "https://pub.dev" + source: hosted + version: "1.0.0" crypto: dependency: transitive description: @@ -281,22 +289,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.8" - desktop_webview_auth: - dependency: transitive - description: - name: desktop_webview_auth - sha256: f90d9fa88344fb1446d4f876ee2a12d07f4345f96fdb787d3ce1c76bd9d41feb - url: "https://pub.dev" - source: hosted - version: "0.0.13" - email_validator: - dependency: transitive - description: - name: email_validator - sha256: e9a90f27ab2b915a27d7f9c2a7ddda5dd752d6942616ee83529b686fc086221b - url: "https://pub.dev" - source: hosted - version: "2.1.17" fake_async: dependency: transitive description: @@ -321,126 +313,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - firebase_auth: - dependency: "direct main" - description: - name: firebase_auth - sha256: ae8029eee0ed24a0ae4a9bcc94c5ef9c2e0c31066c5132c56575da929dd14a46 - url: "https://pub.dev" - source: hosted - version: "4.7.3" - firebase_auth_platform_interface: - dependency: transitive - description: - name: firebase_auth_platform_interface - sha256: cb099fbd3d48f7983c8d7cea45947d98d36174fb7d611e4ff08f802491e8a945 - url: "https://pub.dev" - source: hosted - version: "6.16.2" - firebase_auth_web: - dependency: transitive - description: - name: firebase_auth_web - sha256: d35abf4c6c77c9e7be5d7a637e6e07765b70426015800ba031257f382bff5f83 - url: "https://pub.dev" - source: hosted - version: "5.6.3" - firebase_core: - dependency: "direct main" - description: - name: firebase_core - sha256: c78132175edda4bc532a71e01a32964e4b4fcf53de7853a422d96dac3725f389 - url: "https://pub.dev" - source: hosted - version: "2.15.1" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 - url: "https://pub.dev" - source: hosted - version: "4.8.0" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - sha256: "4cf4d2161530332ddc3c562f19823fb897ff37a9a774090d28df99f47370e973" - url: "https://pub.dev" - source: hosted - version: "2.7.0" - firebase_dynamic_links: - dependency: transitive - description: - name: firebase_dynamic_links - sha256: c604cebcc4c2baf978a8c286ab618d8472aaa813497ab4002cb968cd73b03ed8 - url: "https://pub.dev" - source: hosted - version: "5.3.5" - firebase_dynamic_links_platform_interface: - dependency: transitive - description: - name: firebase_dynamic_links_platform_interface - sha256: "2a6f77434fcb36c8409d29785fee45a262deffc9d36205a81cbe0093802e72bd" - url: "https://pub.dev" - source: hosted - version: "0.2.6+5" - firebase_ui_auth: - dependency: "direct main" - description: - name: firebase_ui_auth - sha256: c597e1482ba8f4d3db0f9b97d839754ad76dba514bd1ee94d31a0126d5a3e287 - url: "https://pub.dev" - source: hosted - version: "1.6.3" - firebase_ui_localizations: - dependency: transitive - description: - name: firebase_ui_localizations - sha256: "830bd79f3e4ccfb4b8ee83e9928de6be97ce1bc80b1b4c16bce8565eb5b48a37" - url: "https://pub.dev" - source: hosted - version: "1.5.1" - firebase_ui_oauth: - dependency: transitive - description: - name: firebase_ui_oauth - sha256: bfa054140628dff703c8204080336d6616e4f49bbf51e9cf7d73b7d5970d5f1a - url: "https://pub.dev" - source: hosted - version: "1.4.8" - firebase_ui_oauth_apple: - dependency: "direct main" - description: - name: firebase_ui_oauth_apple - sha256: "29dfad809ff809f711dfc719f5e040cf67a1734821087d1488cefe1a76760ff5" - url: "https://pub.dev" - source: hosted - version: "1.2.8" - firebase_ui_oauth_facebook: - dependency: "direct main" - description: - name: firebase_ui_oauth_facebook - sha256: "146f24e9e4c273488513248907e7f4a15ba3f13409b0e26db2d455056b1f283b" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - firebase_ui_oauth_google: - dependency: "direct main" - description: - name: firebase_ui_oauth_google - sha256: "47fa590840e6d4ad661f6ae8782c3f14a987b37f524e7c2ce83323cbdeb7ce6b" - url: "https://pub.dev" - source: hosted - version: "1.2.8" - firebase_ui_shared: - dependency: transitive - description: - name: firebase_ui_shared - sha256: "6f36f067d955d41591aacf68aafbaec7053571f2f6ed495da8bfa803f7c633b7" - url: "https://pub.dev" - source: hosted - version: "1.3.0" fixnum: dependency: transitive description: @@ -462,30 +334,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0+1" - flutter_facebook_auth: - dependency: transitive - description: - name: flutter_facebook_auth - sha256: "50dc3eef562acbe1e4cfad478053c9c16f9eaac49ad14ec48f00ed9dae1ba0cd" - url: "https://pub.dev" - source: hosted - version: "4.4.1+1" - flutter_facebook_auth_platform_interface: - dependency: transitive - description: - name: flutter_facebook_auth_platform_interface - sha256: "7950f5f8a6f2270c5d29af2a514733987db1191f70838fa777b282e47365f8c8" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - flutter_facebook_auth_web: + flutter_keychain: dependency: transitive description: - name: flutter_facebook_auth_web - sha256: "0f732e968c929a3c11a215ded802557576230ff0a0794c88941a8e92ff07b2eb" + name: flutter_keychain + sha256: f41a276e877453e70afcbf8e77e33203eab6f60a42f8296f9f3994e69fa6214c url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "2.3.0" flutter_lints: dependency: "direct dev" description: @@ -494,11 +350,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - flutter_localizations: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" flutter_markdown: dependency: "direct main" description: @@ -565,54 +416,6 @@ packages: url: "https://pub.dev" source: hosted version: "10.0.0" - google_identity_services_web: - dependency: transitive - description: - name: google_identity_services_web - sha256: "7940fdc3b1035db4d65d387c1bdd6f9574deaa6777411569c05ecc25672efacd" - url: "https://pub.dev" - source: hosted - version: "0.2.1" - google_sign_in: - dependency: transitive - description: - name: google_sign_in - sha256: aab6fdc41374014494f9e9026b9859e7309639d50a0bf4a2a412467a5ae4abc6 - url: "https://pub.dev" - source: hosted - version: "6.1.4" - google_sign_in_android: - dependency: transitive - description: - name: google_sign_in_android - sha256: "8d60a787b29cb7d2bcf29230865f4a91f17323c6ac5b6b9027a6418e48d9ffc3" - url: "https://pub.dev" - source: hosted - version: "6.1.18" - google_sign_in_ios: - dependency: transitive - description: - name: google_sign_in_ios - sha256: "6ec0e13a4c5c646471b9f6a25ceb3ae76d339889d4c0f79b729bf0714215a63e" - url: "https://pub.dev" - source: hosted - version: "5.6.2" - google_sign_in_platform_interface: - dependency: transitive - description: - name: google_sign_in_platform_interface - sha256: e69553c0fc6a76216e9d06a8c3767e291ad9be42171f879aab7ab708569d4393 - url: "https://pub.dev" - source: hosted - version: "2.4.1" - google_sign_in_web: - dependency: transitive - description: - name: google_sign_in_web - sha256: "69b9ce0e760945ff52337921a8b5871592b74c92f85e7632293310701eea68cc" - url: "https://pub.dev" - source: hosted - version: "0.12.0+2" graphs: dependency: transitive description: @@ -685,6 +488,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + jwt_decoder: + dependency: transitive + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -765,6 +576,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + openapi_generator_annotations: + dependency: transitive + description: + name: openapi_generator_annotations + sha256: "9e069bb7e69708f34fe67fca687c8841bd34625206797e623d82a7702e71c323" + url: "https://pub.dev" + source: hosted + version: "4.13.0" package_config: dependency: transitive description: @@ -773,6 +592,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + passkeys: + dependency: transitive + description: + name: passkeys + sha256: "71da8c518bd0d890345ab957abdf01613103725705001c8c9c9a5a189cce2025" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + passkeys_android: + dependency: transitive + description: + name: passkeys_android + sha256: d0bbc591cc85347767ea912f2d6540fdf7dd6d7d8ceb236709ff66a35e59e624 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + passkeys_ios: + dependency: transitive + description: + name: passkeys_ios + sha256: "62866544c45f2bbe90c0ca501f649fbbf62b6256731678a279773e6f451aaaeb" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + passkeys_platform_interface: + dependency: transitive + description: + name: passkeys_platform_interface + sha256: a12a6b2e45e3bbec7d137c73cd99b27ad25ff4a2279ce28d995fe3a223e80e98 + url: "https://pub.dev" + source: hosted + version: "1.0.0" path: dependency: "direct main" description: @@ -837,14 +688,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.3" - quiver: - dependency: transitive - description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 - url: "https://pub.dev" - source: hosted - version: "3.2.1" random_avatar: dependency: "direct main" description: diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index d2cfa36..141932a 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -13,16 +13,10 @@ dependencies: sdk: flutter animated_emoji: ^2.0.0 - # cancellation_token: ^1.5.0 - cancellation_token: ^2.0.0 # for realm vNext + cancellation_token: ^2.0.0 collection: ^1.17.0 connectivity_plus: ^4.0.1 - firebase_auth: ^4.2.10 - firebase_core: ^2.8.0 - firebase_ui_auth: ^1.1.16 - firebase_ui_oauth_apple: ^1.0.23 - firebase_ui_oauth_facebook: ^1.0.23 - firebase_ui_oauth_google: ^1.0.23 + corbado_auth: ^1.0.0 flutter_animate: ^4.0.0 flutter_markdown: ^0.6.0 flutter_riverpod: ^2.3.0 @@ -30,7 +24,7 @@ dependencies: markdown: ^7.0.0 path: ^1.0.0 random_avatar: ^0.0.8 - realm: ^1.3.0 + realm: ^1.4.0 riverpod_annotation: ^2.0.0 riverpod: ^2.3.0 rxdart: ^0.27.0 From c852ec8ba1ebcea29c096750b45973380afdafbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 8 Sep 2023 09:09:51 +0200 Subject: [PATCH 29/33] Replace firebase auth with passkeys --- kilochat/README.md | 2 +- kilochat/android/app/build.gradle | 4 +- kilochat/lib/main.dart | 1 - kilochat/lib/profile_form.dart | 10 ++- kilochat/lib/providers.dart | 37 ++++---- kilochat/lib/providers.g.dart | 40 +++------ kilochat/lib/repository.dart | 2 + kilochat/lib/router.dart | 3 +- kilochat/lib/workspace_view.dart | 143 ++++++++++++++++++++++++++++++ kilochat/pubspec.lock | 33 +------ kilochat/pubspec.yaml | 10 +-- 11 files changed, 197 insertions(+), 88 deletions(-) diff --git a/kilochat/README.md b/kilochat/README.md index de24582..6afdc12 100644 --- a/kilochat/README.md +++ b/kilochat/README.md @@ -32,7 +32,7 @@ A shared backend is setup for a fictitious organization [Acme Corporation](https If you choose to use the pre-configured backend, then be aware that it is shared with the entire world, and there is no ongoing moderation. Please behave and follow our [code of conduct](https://www.mongodb.com/community-code-of-conduct) or we will have to disable it. -You will be able to sign-up and login with an email address of yours, and if you device supports it, a biometric passkeys. Otherwise a magic-link will be sent to your address. No password is necessary. +You will be able to sign-up and login with any email address of yours, and if you device supports it, a biometric passkey. Otherwise a magic-link will be sent to your address. No password is necessary. We hope, that once you have played with it for a while, that you will be tempted to learn how it was build. The rest of this lengthy README will try to explain just that. diff --git a/kilochat/android/app/build.gradle b/kilochat/android/app/build.gradle index 7939b25..6e6f2b5 100644 --- a/kilochat/android/app/build.gradle +++ b/kilochat/android/app/build.gradle @@ -27,7 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { namespace "com.example.kilochat" - compileSdkVersion flutter.compileSdkVersion + compileSdkVersion 34 ndkVersion flutter.ndkVersion compileOptions { @@ -48,7 +48,7 @@ android { applicationId "com.example.kilochat" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 19 // bump to support firebase + minSdkVersion 24 // bump to support passkeys targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart index bf78f45..5715c45 100644 --- a/kilochat/lib/main.dart +++ b/kilochat/lib/main.dart @@ -21,7 +21,6 @@ Future main() async { routerConfig: router, debugShowCheckedModeBanner: false, theme: ThemeData( - //useMaterial3: true, colorScheme: ColorScheme.fromSeed( seedColor: freedomBlue, inversePrimary: energizingYellow, diff --git a/kilochat/lib/profile_form.dart b/kilochat/lib/profile_form.dart index 5db61d4..6207865 100644 --- a/kilochat/lib/profile_form.dart +++ b/kilochat/lib/profile_form.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'model.dart'; import 'providers.dart'; +import 'router.dart'; import 'widget_builders.dart'; class ProfileForm extends ConsumerStatefulWidget { @@ -125,7 +126,14 @@ class _ProfileFormState extends ConsumerState { label: const Text('Save Changes'), ), const Spacer(), - const Placeholder() + TextButton.icon( + onPressed: () { + repository.logoutUser(); + Routes.chooseWorkspace.go(context); + }, + icon: const Icon(Icons.logout), + label: const Text('Logout'), + ), ]), ] .animate(interval: 100.ms) diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index bf1f799..04543af 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -1,7 +1,9 @@ import 'dart:async'; -import 'package:realm/realm.dart'; +import 'package:passkeys/relying_party_server/corbado/corbado_passkey_backend.dart'; +import 'package:passkeys/relying_party_server/corbado/types/shared.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:passkeys/passkey_auth.dart'; import 'model.dart'; import 'repository.dart'; @@ -10,32 +12,27 @@ import 'settings.dart'; part 'providers.g.dart'; @riverpod -Stream app(AppRef ref) async* { - await for (final ws in workspaceChanges) { - if (ws == null) continue; - yield ws.app; +Future> auth(AuthRef ref) async { + final server = CorbadoPasskeyBackend('pro-2636186146982821243'); + final auth = PasskeyAuth(server); + if (await auth.isSupported()) { + return auth; } + throw Exception('Passkeys are not supported on this device!'); // TODO } @riverpod -Stream user(UserRef ref) async* { - final app = await ref.watch(appProvider.future); +Stream repository(RepositoryRef ref) async* { + await for (final ws in workspaceChanges) { + if (ws == null) continue; // no workspace selected, so no repository - final jwt = '' as String?; // todo + final user = ws.app.currentUser; + if (user == null) continue; // no user logged in, so no repository - var user = app.currentUser; - if (jwt != null) { - user = await app.logIn(Credentials.jwt(jwt)); + final repository = await Repository.init(ws); + ref.onDispose(repository.dispose); + yield repository; } - if (user != null) yield user; -} - -@riverpod -Future repository(RepositoryRef ref) async { - final user = await ref.watch(userProvider.future); - final repository = await Repository.init(currentWorkspace!); - ref.onDispose(repository.dispose); - return repository; } @riverpod diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index 44678c7..034c92f 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -6,39 +6,27 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$appHash() => r'f2013a10f035b47580fa754eb5b97b53d77ee6d2'; - -/// See also [app]. -@ProviderFor(app) -final appProvider = AutoDisposeStreamProvider.internal( - app, - name: r'appProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$appHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef AppRef = AutoDisposeStreamProviderRef; -String _$userHash() => r'01d81be0d3b467561310f13c6d871e58fd8d8728'; - -/// See also [user]. -@ProviderFor(user) -final userProvider = AutoDisposeStreamProvider.internal( - user, - name: r'userProvider', +String _$authHash() => r'bb87fc7829de6f1802b23164ef15da1e22abde8d'; + +/// See also [auth]. +@ProviderFor(auth) +final authProvider = + AutoDisposeFutureProvider>.internal( + auth, + name: r'authProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$userHash, + const bool.fromEnvironment('dart.vm.product') ? null : _$authHash, dependencies: null, allTransitiveDependencies: null, ); -typedef UserRef = AutoDisposeStreamProviderRef; -String _$repositoryHash() => r'2275ebf66658e7b0d08ad325b5ea8aacacf4143a'; +typedef AuthRef + = AutoDisposeFutureProviderRef>; +String _$repositoryHash() => r'6fed4ac6e5b8977a4ad0c215b8030845a217b1b3'; /// See also [repository]. @ProviderFor(repository) -final repositoryProvider = AutoDisposeFutureProvider.internal( +final repositoryProvider = AutoDisposeStreamProvider.internal( repository, name: r'repositoryProvider', debugGetCreateSourceHash: @@ -47,7 +35,7 @@ final repositoryProvider = AutoDisposeFutureProvider.internal( allTransitiveDependencies: null, ); -typedef RepositoryRef = AutoDisposeFutureProviderRef; +typedef RepositoryRef = AutoDisposeStreamProviderRef; String _$focusedChannelHash() => r'c467f8b8fbe8c481f8f49a542486227febaa2684'; /// See also [focusedChannel]. diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index 2f68058..f15aa7f 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -186,6 +186,8 @@ class Repository extends Disposable { void updateUserProfile(UserProfile newProfile) => _realm.write(() => _realm.add(newProfile, update: true)); + + Future logoutUser() => _user.logOut(); } extension on MutableSubscriptionSet { diff --git a/kilochat/lib/router.dart b/kilochat/lib/router.dart index 87fa61e..01fa97a 100644 --- a/kilochat/lib/router.dart +++ b/kilochat/lib/router.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:realm/realm.dart'; import 'chat_screen.dart'; import 'settings.dart'; @@ -34,7 +33,7 @@ final router = GoRouter( GoRoute( path: Routes.logIn.path, builder: (context, state) { - return const Placeholder(); + return const LoginScreen(); }, ), GoRoute( diff --git a/kilochat/lib/workspace_view.dart b/kilochat/lib/workspace_view.dart index e811ae9..083a466 100644 --- a/kilochat/lib/workspace_view.dart +++ b/kilochat/lib/workspace_view.dart @@ -1,5 +1,9 @@ +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:kilochat/providers.dart'; +import 'package:passkeys/relying_party_server/corbado/types/shared.dart'; +import 'package:realm/realm.dart'; import 'realm_ui/realm_animated_list.dart'; import 'router.dart'; @@ -142,3 +146,142 @@ class _WorkspaceFormState extends State { ); } } + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +enum PageMode { registration, login, loggedIn } + +class _LoginScreenState extends ConsumerState { + final _emailController = TextEditingController(); + + PageMode _mode = PageMode.registration; + PageMode get mode => _mode; + set mode(PageMode mode) => setState(() => _mode = mode); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(10), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.only(left: 50, top: 10, bottom: 10), + child: Text( + 'Sign in using passkey', + style: TextStyle( + fontSize: 20, + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: TextField( + autofillHints: const [AutofillHints.email], + controller: _emailController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'email address', + ), + ), + ), + SizedBox( + width: double.infinity, + child: _buildButton(), + ), + const SizedBox(height: 16), + _buildSwitchMode() + ], + ), + ), + ), + ); + } + + String _buttonText() { + return switch (mode) { + PageMode.registration => 'sign up', + PageMode.login => 'sign in', + PageMode.loggedIn => 'logout', + }; + } + + Widget _buildButton() => + ElevatedButton(onPressed: _onClick, child: Text(_buttonText())); + + Widget _buildActionSpan(String lead, String action, PageMode newMode) { + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: lead, + style: const TextStyle(color: Colors.black), + ), + TextSpan( + text: action, + style: TextStyle(color: Theme.of(context).primaryColor), + recognizer: TapGestureRecognizer()..onTap = () => mode = newMode, + ), + ], + ), + ); + } + + Widget _buildSwitchMode() { + return switch (mode) { + PageMode.registration => _buildActionSpan( + 'Already have an account? ', + 'Sign in', + PageMode.login, + ), + PageMode.login => _buildActionSpan( + 'First time here? ', + 'Sign up', + PageMode.registration, + ), + PageMode.loggedIn => const Text('You are currently logged in.'), + }; + } + + Future _onClick() async { + final auth = await ref.watch(authProvider.future); + final email = _emailController.value.text; + + try { + final app = currentWorkspace!.app; + if (mode == PageMode.loggedIn) { + app.currentUser?.logOut(); + mode = PageMode.login; + } else { + AuthResponse? response; + if (mode == PageMode.registration) { + response = await auth.registerWithEmail(AuthRequest(email)); + } else if (mode == PageMode.login) { + response = await auth.authenticateWithEmail(AuthRequest(email)); + } + await currentWorkspace?.app.logIn(Credentials.jwt(response!.token)); + mode = PageMode.loggedIn; + + if (mounted) Routes.chat.go(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).primaryColor, + content: Text(e.toString()), + duration: const Duration(seconds: 10), + ), + ); + } + } + } +} diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 863ca8c..1c7b8ea 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -225,14 +225,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" - corbado_auth: - dependency: "direct main" - description: - name: corbado_auth - sha256: e8e4a8468da51556bd68fad9ebfc345b296767fe86de5ab2532507b6115cd10b - url: "https://pub.dev" - source: hosted - version: "1.0.1" corbado_frontend_api_client: dependency: transitive description: @@ -334,14 +326,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.2.0+1" - flutter_keychain: - dependency: transitive - description: - name: flutter_keychain - sha256: f41a276e877453e70afcbf8e77e33203eab6f60a42f8296f9f3994e69fa6214c - url: "https://pub.dev" - source: hosted - version: "2.3.0" flutter_lints: dependency: "direct dev" description: @@ -488,14 +472,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" - jwt_decoder: - dependency: transitive - description: - name: jwt_decoder - sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" - url: "https://pub.dev" - source: hosted - version: "2.0.1" lints: dependency: transitive description: @@ -593,12 +569,11 @@ packages: source: hosted version: "2.1.0" passkeys: - dependency: transitive + dependency: "direct main" description: - name: passkeys - sha256: "71da8c518bd0d890345ab957abdf01613103725705001c8c9c9a5a189cce2025" - url: "https://pub.dev" - source: hosted + path: "../../../../3rd_party/corbado/flutter-passkeys/packages/passkeys/passkeys" + relative: true + source: path version: "1.0.2" passkeys_android: dependency: transitive diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index 141932a..efac1fc 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -16,12 +16,12 @@ dependencies: cancellation_token: ^2.0.0 collection: ^1.17.0 connectivity_plus: ^4.0.1 - corbado_auth: ^1.0.0 flutter_animate: ^4.0.0 flutter_markdown: ^0.6.0 flutter_riverpod: ^2.3.0 go_router: ^10.0.0 markdown: ^7.0.0 + passkeys: ^1.0.0 path: ^1.0.0 random_avatar: ^0.0.8 realm: ^1.4.0 @@ -40,11 +40,9 @@ dev_dependencies: flutter: uses-material-design: true - fonts: - - family: SocialIcons - fonts: - - asset: packages/firebase_ui_auth/fonts/SocialIcons.ttf -#dependency_overrides: +dependency_overrides: + passkeys: + path: ../../../../3rd_party/corbado/flutter-passkeys/packages/passkeys/passkeys # realm: # path: ../../../realm-dart/flutter/realm_flutter From 1ad04aa43229d33f47f1bfb466f7122bc8e9ce99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 22 Feb 2024 08:25:24 +0100 Subject: [PATCH 30/33] Upgrade deps and fix compile errors --- kilochat/lib/model.g.dart | 1 + kilochat/lib/providers.dart | 16 +- kilochat/lib/providers.g.dart | 14 +- kilochat/lib/settings.dart | 6 +- kilochat/lib/settings.g.dart | 1 + kilochat/lib/tiles.dart | 4 +- kilochat/lib/workspace_view.dart | 9 +- kilochat/pubspec.lock | 409 ++++++++++++++++++++----------- kilochat/pubspec.yaml | 23 +- 9 files changed, 305 insertions(+), 178 deletions(-) diff --git a/kilochat/lib/model.g.dart b/kilochat/lib/model.g.dart index 339e27c..6c1ac35 100644 --- a/kilochat/lib/model.g.dart +++ b/kilochat/lib/model.g.dart @@ -6,6 +6,7 @@ part of 'model.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Channel extends _Channel with RealmEntity, RealmObjectBase, RealmObject { Channel( ObjectId id, diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 04543af..b0965c2 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -1,9 +1,7 @@ import 'dart:async'; -import 'package:passkeys/relying_party_server/corbado/corbado_passkey_backend.dart'; -import 'package:passkeys/relying_party_server/corbado/types/shared.dart'; +import 'package:corbado_auth/corbado_auth.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:passkeys/passkey_auth.dart'; import 'model.dart'; import 'repository.dart'; @@ -12,13 +10,11 @@ import 'settings.dart'; part 'providers.g.dart'; @riverpod -Future> auth(AuthRef ref) async { - final server = CorbadoPasskeyBackend('pro-2636186146982821243'); - final auth = PasskeyAuth(server); - if (await auth.isSupported()) { - return auth; - } - throw Exception('Passkeys are not supported on this device!'); // TODO +Future auth(AuthRef ref) async { + const projectId = 'pro-2636186146982821243'; + final auth = CorbadoAuth(); + await auth.init(projectId); + return auth; } @riverpod diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index 034c92f..3b386d8 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -6,12 +6,11 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$authHash() => r'bb87fc7829de6f1802b23164ef15da1e22abde8d'; +String _$authHash() => r'e2d649ccf2244021dd2bb2cc2355b835a9957a3c'; /// See also [auth]. @ProviderFor(auth) -final authProvider = - AutoDisposeFutureProvider>.internal( +final authProvider = AutoDisposeFutureProvider.internal( auth, name: r'authProvider', debugGetCreateSourceHash: @@ -20,8 +19,7 @@ final authProvider = allTransitiveDependencies: null, ); -typedef AuthRef - = AutoDisposeFutureProviderRef>; +typedef AuthRef = AutoDisposeFutureProviderRef; String _$repositoryHash() => r'6fed4ac6e5b8977a4ad0c215b8030845a217b1b3'; /// See also [repository]. @@ -40,7 +38,7 @@ String _$focusedChannelHash() => r'c467f8b8fbe8c481f8f49a542486227febaa2684'; /// See also [focusedChannel]. @ProviderFor(focusedChannel) -final focusedChannelProvider = AutoDisposeStreamProvider.internal( +final focusedChannelProvider = AutoDisposeStreamProvider.internal( focusedChannel, name: r'focusedChannelProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -50,6 +48,6 @@ final focusedChannelProvider = AutoDisposeStreamProvider.internal( allTransitiveDependencies: null, ); -typedef FocusedChannelRef = AutoDisposeStreamProviderRef; +typedef FocusedChannelRef = AutoDisposeStreamProviderRef; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/kilochat/lib/settings.dart b/kilochat/lib/settings.dart index badd036..af1c381 100644 --- a/kilochat/lib/settings.dart +++ b/kilochat/lib/settings.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:path/path.dart' as path; import 'package:realm/realm.dart'; @@ -10,8 +12,8 @@ class _Workspace { late String name; // for display ObjectId? currentChannelId; // current channel - App get app => - _appCache.putIfAbsent(appId, () => App(AppConfiguration(appId))); + App get app => _appCache.putIfAbsent( + appId, () => App(AppConfiguration(appId, httpClient: HttpClient()))); } final _appCache = {}; diff --git a/kilochat/lib/settings.g.dart b/kilochat/lib/settings.g.dart index ce51764..ff7a122 100644 --- a/kilochat/lib/settings.g.dart +++ b/kilochat/lib/settings.g.dart @@ -6,6 +6,7 @@ part of 'settings.dart'; // RealmObjectGenerator // ************************************************************************** +// ignore_for_file: type=lint class Workspace extends _Workspace with RealmEntity, RealmObjectBase, RealmObject { Workspace( diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index f9c3bd3..30b800c 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -1,5 +1,4 @@ -import 'package:animated_emoji/emoji.dart'; -import 'package:animated_emoji/emojis.dart'; +import 'package:animated_emoji/animated_emoji.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -95,6 +94,7 @@ class MessageTile extends ConsumerWidget { label: AnimatedEmoji( AnimatedEmojiData( 'u${reaction.emojiUnicode.toRadixString(16)}', + name: 'n/a', ), repeat: false, size: 20, diff --git a/kilochat/lib/workspace_view.dart b/kilochat/lib/workspace_view.dart index 083a466..fdea20f 100644 --- a/kilochat/lib/workspace_view.dart +++ b/kilochat/lib/workspace_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:kilochat/providers.dart'; -import 'package:passkeys/relying_party_server/corbado/types/shared.dart'; import 'package:realm/realm.dart'; import 'realm_ui/realm_animated_list.dart'; @@ -261,13 +260,13 @@ class _LoginScreenState extends ConsumerState { app.currentUser?.logOut(); mode = PageMode.login; } else { - AuthResponse? response; if (mode == PageMode.registration) { - response = await auth.registerWithEmail(AuthRequest(email)); + await auth.signUpWithPasskey(email: email); } else if (mode == PageMode.login) { - response = await auth.authenticateWithEmail(AuthRequest(email)); + await auth.loginWithPasskey(email: email); } - await currentWorkspace?.app.logIn(Credentials.jwt(response!.token)); + final jwt = (await auth.currentUser)!.idToken; + await currentWorkspace?.app.logIn(Credentials.jwt(jwt)); mode = PageMode.loggedIn; if (mounted) Routes.chat.go(context); diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index 1c7b8ea..c1073af 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -5,42 +5,42 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "405666cd3cf0ee0a48d21ec67e65406aad2c726d9fa58840d3375e7bdcd32a07" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "60.0.0" + version: "67.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "1952250bd005bacb895a01bf1b4dc00e3ba1c526cf47dca54dfe24979c65f5b3" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "6.4.1" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" url: "https://pub.dev" source: hosted - version: "0.11.2" + version: "0.11.3" animated_emoji: dependency: "direct main" description: name: animated_emoji - sha256: "1be7a676dffa68c3d3c4230bf89b064afb461a7b64e7d21fe112519d643d9544" + sha256: "0af5508ce0ccb44caa6d3776d01900be6f6e70676881bfa8ce1a1bb9bf91a6a7" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.1.0" archive: dependency: transitive description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.4.10" args: dependency: transitive description: @@ -93,34 +93,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: transitive description: name: build_runner - sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b" + sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.8" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.3.0" built_collection: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.9.1" cancellation_token: dependency: "direct main" description: @@ -153,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" checked_yaml: dependency: transitive description: @@ -173,10 +181,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -189,26 +197,26 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.10.0" collection: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.18.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - sha256: "77a180d6938f78ca7d2382d2240eb626c0f6a735d0bfdce227d8ffb80f95c48b" + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "5.0.2" connectivity_plus_platform_interface: dependency: transitive description: @@ -225,14 +233,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + corbado_auth: + dependency: "direct main" + description: + name: corbado_auth + sha256: "4bfa23b9d15c7ee2d1ddbc5c932c6dd80e157647e725e7b074a8ea42c667f24c" + url: "https://pub.dev" + source: hosted + version: "2.0.4" corbado_frontend_api_client: dependency: transitive description: name: corbado_frontend_api_client - sha256: "2353d291c55e8f7f1fa842198c8ed82477c4129c34742ef74353e72ce7dc5ba8" + sha256: a6d65fc0da88f2e6a6e95251de0b67735556128c5d96c9b609e7b18010a6f6c1 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" crypto: dependency: transitive description: @@ -241,46 +257,54 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + csslib: + dependency: transitive + description: + name: csslib + sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + url: "https://pub.dev" + source: hosted + version: "1.0.0" custom_lint: dependency: "direct dev" description: name: custom_lint - sha256: "3ce36c04d30c60cde295588c6185b3f9800e6c18f6670a7ffdb3d5eab39bb942" + sha256: "445242371d91d2e24bd7b82e3583a2c05610094ba2d0575262484ad889c8f981" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.6.2" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "73d09c9848e9f6d5c3e0a1809eac841a8d7ea123d0849feefa040e1ad60b6d06" + sha256: "4c0aed2a3491096e91cf1281923ba1b6814993f16dde0fd60f697925225bbbd6" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.6.2" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "9170d9db2daf774aa2251a3bc98e4ba903c7702ab07aa438bc83bd3c9a0de57f" + sha256: ce5d6215f4e143f7780ce53f73dfa6fc503f39d2d30bef76c48be9ac1a09d9a6 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.6.2" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" dbus: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" fake_async: dependency: transitive description: @@ -293,10 +317,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -322,42 +346,58 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "62f346340a96192070e31e3f2a1bd30a28530f1fe8be978821e06cd56b74d6d2" + sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" url: "https://pub.dev" source: hosted - version: "4.2.0+1" + version: "4.5.0" + flutter_keychain: + dependency: transitive + description: + name: flutter_keychain + sha256: de6f2d09ce2a006013db799ad37d6ae49b10530d6841de1c0c0525c4d3ec22a4 + url: "https://pub.dev" + source: hosted + version: "2.4.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" + sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.0.1" flutter_markdown: dependency: "direct main" description: name: flutter_markdown - sha256: "2b206d397dd7836ea60035b2d43825c8a303a76a5098e66f42d55a753e18d431" + sha256: "5b24061317f850af858ef7151dadbb6eb77c1c449c954c7bb064e8a5e0e7d81f" url: "https://pub.dev" source: hosted - version: "0.6.17+1" + version: "0.6.20" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: b6cb0041c6c11cefb2dcb97ef436eba43c6d41287ac6d8ca93e02a497f53a4f3 + sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.4.10" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + url: "https://pub.dev" + source: hosted + version: "0.1.2" flutter_svg: dependency: transitive description: name: flutter_svg - sha256: "8c5d68a82add3ca76d792f058b186a0599414f279f00ece4830b9b231b570338" + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -396,10 +436,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: b3cadd2cd59a4103fd5f6bc572ca75111264698784e927aa471921c3477d5475 + sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "13.2.0" graphs: dependency: transitive description: @@ -412,18 +452,26 @@ packages: dependency: transitive description: name: hotreloader - sha256: "728c0613556c1d153f7e7f4a367cffacc3f5a677d7f6497a1c2b35add4e6dacf" + sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "4.2.0" + html: + dependency: transitive + description: + name: html + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" + source: hosted + version: "0.15.4" http: dependency: transitive description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -472,14 +520,46 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + jwt_decoder: + dependency: transitive + description: + name: jwt_decoder + sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.0.0" logging: dependency: transitive description: @@ -492,50 +572,50 @@ packages: dependency: transitive description: name: lottie - sha256: b8bdd54b488c54068c57d41ae85d02808da09e2bee8b8dd1f59f441e7efa60cd + sha256: ce2bb2605753915080e4ee47f036a64228c88dc7f56f7bc1dbe912d75b55b1e2 url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "3.1.0" markdown: dependency: "direct main" description: name: markdown - sha256: acf35edccc0463a9d7384e437c015a3535772e09714cf60e07eeef3a15870dcd + sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90" url: "https://pub.dev" source: hosted - version: "7.1.1" + version: "7.2.1" matcher: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" nm: dependency: transitive description: @@ -548,18 +628,18 @@ packages: dependency: transitive description: name: objectid - sha256: "22fa972000d3256f10d06323a9dcbf4b564fb03fdb9024399e3a6c1d9902f914" + sha256: fd4a0b9fe07df25c446948b786e7ab2f363b2f461afa78632cab179d7613b9b3 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.0" openapi_generator_annotations: dependency: transitive description: name: openapi_generator_annotations - sha256: "9e069bb7e69708f34fe67fca687c8841bd34625206797e623d82a7702e71c323" + sha256: "46f1fb675029d78e19ce9143e70ce414d738b0f6c45c49c004b4b3afdb405a5c" url: "https://pub.dev" source: hosted - version: "4.13.0" + version: "4.13.1" package_config: dependency: transitive description: @@ -571,42 +651,51 @@ packages: passkeys: dependency: "direct main" description: - path: "../../../../3rd_party/corbado/flutter-passkeys/packages/passkeys/passkeys" - relative: true - source: path - version: "1.0.2" + name: passkeys + sha256: "250330485aaef45db3c8e97e4acc0e4917be653019ab415daec1bda5cb4de231" + url: "https://pub.dev" + source: hosted + version: "2.0.4" passkeys_android: dependency: transitive description: name: passkeys_android - sha256: d0bbc591cc85347767ea912f2d6540fdf7dd6d7d8ceb236709ff66a35e59e624 + sha256: "731c492dbfe1a5d6e36143928d60399cc6dfdb55f20cff8f44ab8c8a24884e73" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.2" passkeys_ios: dependency: transitive description: name: passkeys_ios - sha256: "62866544c45f2bbe90c0ca501f649fbbf62b6256731678a279773e6f451aaaeb" + sha256: "4da97e6670a68a613483b577736d7d45f848a118bb177052c01f9d56cdf90f81" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.2" passkeys_platform_interface: dependency: transitive description: name: passkeys_platform_interface - sha256: a12a6b2e45e3bbec7d137c73cd99b27ad25ff4a2279ce28d995fe3a223e80e98 + sha256: a1f1f5c637049f68350cf43323df9689be2e86fe5822a6e098362e7f6168351e url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "2.0.1" + passkeys_web: + dependency: transitive + description: + name: passkeys_web + sha256: "1c7815020332b9be1af4df67686826a91b6dd29fea53be947d6082654abd6280" + url: "https://pub.dev" + source: hosted + version: "2.0.1" path: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -619,26 +708,26 @@ packages: dependency: transitive description: name: petitparser - sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "6.0.2" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.8" pointycastle: dependency: transitive description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -675,66 +764,74 @@ packages: dependency: "direct main" description: name: realm - sha256: c25b13cfdadeae582ddda1a97d58cff9e0fc0665a19905ceb5d0707bd90ace6d + sha256: a53b6903e2f6603c906bf5aac3691f0b2f45262aa08525a4bd546900602f2f17 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.9.0" realm_common: dependency: transitive description: name: realm_common - sha256: "2ee6c3d01a70cf3c7cf8d8550cee6f17e55c0184aed53ececdb5b153ea501b57" + sha256: "29516a9e43a9e75b2e16226ce24ccd9a549d9377e69be218394d8fb84da11183" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + realm_dart: + dependency: transitive + description: + name: realm_dart + sha256: "55cf02d26b0775e79570cf0ba6e4036a10c66250b99cb8f1ee41d71057206a7f" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.9.0" realm_generator: dependency: transitive description: name: realm_generator - sha256: "8ccf72fd6aec2fd8a8863fcfd1e9d24d2c054cfcbe0123808a77bede00285d9c" + sha256: "6d26ca214aad1b49f37a1a86f3216f19a25657d38d82cefb16551a35aa316ec1" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.9.0" riverpod: dependency: "direct main" description: name: riverpod - sha256: b0657b5b30c81a3184bdaab353045f0a403ebd60bb381591a8b7ad77dcade793 + sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.5.0" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "1b2632a6fc0b659c923a4dcc7cd5da42476f5b3294c70c86c971e63bdd443384" + sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" url: "https://pub.dev" source: hosted - version: "0.3.1" + version: "0.5.1" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: "8b3f7a54ddd5d53d6ea04bfb4ff77ee1b0816a1b563c0d9d43e73ce94bf2016d" + sha256: "77e5d51afa4fa3e67903fb8746f33d368728d7051a0b6c292bcee60aeba46d95" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.3.4" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "691180275664a5420c87d72c1ed26ef8404d32b823807540172bfd1660425376" + sha256: "359068f04879347ae4edbe66c81cc95f83fa1743806d1a0c86e55dd3c33ebb32" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.11" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "17ad319914ac6863c64524e598913c0f17e30688aca8f5b7509e96d6e372d493" + sha256: e9bbd02e9e89e18eecb183bbca556d7b523a0669024da9b8167c08903f442937 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "2.3.9" rxdart: dependency: "direct main" description: @@ -747,10 +844,10 @@ packages: dependency: transitive description: name: sane_uuid - sha256: "615f6d25e6d7e0c59ea0fb5ce0cdf043ce110af15b19b3c35ca1441e5bb06972" + sha256: "5e83f796a7d19d38d3ba3a940642998fdd8c4a4049be135ed25404e37f76a18c" url: "https://pub.dev" source: hosted - version: "1.0.0-alpha.4" + version: "1.0.0-alpha.5" shelf: dependency: transitive description: @@ -776,10 +873,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_span: dependency: transitive description: @@ -788,14 +885,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" state_notifier: dependency: transitive description: @@ -808,10 +913,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -832,10 +937,10 @@ packages: dependency: transitive description: name: tar - sha256: "85ffd53e277f2bac8afa2885e6b195e26937e9c402424c3d88d92fd920b56de9" + sha256: aca91e93ff9ff2dba4462c6eea6bc260b72f0d7010e748e3397c32190529bd6e url: "https://pub.dev" source: hosted - version: "0.5.6" + version: "1.0.4" term_glyph: dependency: transitive description: @@ -848,10 +953,10 @@ packages: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.1" timing: dependency: transitive description: @@ -868,38 +973,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + ua_client_hints: + dependency: transitive + description: + name: ua_client_hints + sha256: "8401d7bec261f61b3d3b61cd877653ddf840de2d9e07bd164f34588572aa0c8b" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + universal_html: + dependency: transitive + description: + name: universal_html + sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971" + url: "https://pub.dev" + source: hosted + version: "2.2.4" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" + source: hosted + version: "2.2.2" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.3.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "670f6e07aca990b4a2bcdc08a784193c4ccdd1932620244c3a86bb72a0eac67f" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "7451721781d967db9933b63f5733b1c4533022c0ba373a01bdd79d1a5457f69f" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "80a13c613c8bde758b1464a1755a7b3a8f2b6cec61fbf0f5a53c94c30f03ba2e" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.7" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -912,10 +1041,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0fae432c85c4ea880b33b497d32824b97795b04cdaa74d270219572a1f50268d" + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.9.0" + version: "13.0.0" watcher: dependency: transitive description: @@ -928,26 +1057,26 @@ packages: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.5.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.4" xml: dependency: transitive description: name: xml - sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.5.0" yaml: dependency: transitive description: @@ -957,5 +1086,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.2" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index efac1fc..8599699 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -12,19 +12,20 @@ dependencies: flutter: sdk: flutter - animated_emoji: ^2.0.0 + animated_emoji: ^3.1.0 cancellation_token: ^2.0.0 collection: ^1.17.0 - connectivity_plus: ^4.0.1 + connectivity_plus: ^5.0.2 + corbado_auth: ^2.0.0 flutter_animate: ^4.0.0 flutter_markdown: ^0.6.0 flutter_riverpod: ^2.3.0 - go_router: ^10.0.0 + go_router: ^13.2.0 markdown: ^7.0.0 - passkeys: ^1.0.0 + passkeys: ^2.0.4 path: ^1.0.0 random_avatar: ^0.0.8 - realm: ^1.4.0 + realm: ^1.9.0 riverpod_annotation: ^2.0.0 riverpod: ^2.3.0 rxdart: ^0.27.0 @@ -33,16 +34,16 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 - custom_lint: ^0.4.0 + flutter_lints: ^3.0.1 + custom_lint: ^0.6.2 riverpod_lint: any riverpod_generator: ^2.0.0 flutter: uses-material-design: true -dependency_overrides: - passkeys: - path: ../../../../3rd_party/corbado/flutter-passkeys/packages/passkeys/passkeys -# realm: +# dependency_overrides: +# passkeys: +# path: ../../../../3rd_party/corbado/flutter-passkeys/packages/passkeys/passkeys +# realm: # path: ../../../realm-dart/flutter/realm_flutter From ff9541182557fa81df0ffdeb4d9a5bf4cd8e2b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Fri, 23 Feb 2024 15:55:11 +0100 Subject: [PATCH 31/33] Brush off a bit --- kilochat/lib/chat_screen.dart | 4 +- kilochat/lib/display_toast.dart | 6 +- kilochat/lib/login_screen.dart | 147 ++++++++++++++++++ kilochat/lib/providers.dart | 10 +- .../lib/realm_ui/realm_animated_list.dart | 1 + kilochat/lib/repository.dart | 13 +- kilochat/lib/router.dart | 1 + kilochat/lib/tiles.dart | 11 +- kilochat/lib/workspace_view.dart | 145 +---------------- .../macos/Runner/DebugProfile.entitlements | 14 -- kilochat/macos/Runner/Release.entitlements | 10 -- kilochat/pubspec.lock | 6 +- kilochat/pubspec.yaml | 2 +- 13 files changed, 181 insertions(+), 189 deletions(-) create mode 100644 kilochat/lib/login_screen.dart delete mode 100644 kilochat/macos/Runner/DebugProfile.entitlements delete mode 100644 kilochat/macos/Runner/Release.entitlements diff --git a/kilochat/lib/chat_screen.dart b/kilochat/lib/chat_screen.dart index 8edd19f..cf09fa9 100644 --- a/kilochat/lib/chat_screen.dart +++ b/kilochat/lib/chat_screen.dart @@ -64,7 +64,7 @@ class ChatScreen extends ConsumerWidget { ], ), body: DisplayToast( - stream: repository.x, + stream: repository.notifications, builder: (message, animation) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -73,7 +73,7 @@ class ChatScreen extends ConsumerWidget { color: Colors.yellow, padding: const EdgeInsets.all(8), width: double.infinity, - child: Text('in channel "${message.channel!.name}"'), + child: Text('in channel "${message.channel?.name ?? 'unknown'}"'), ), MessageTile(message: message, animation: animation), ], diff --git a/kilochat/lib/display_toast.dart b/kilochat/lib/display_toast.dart index 403bb6a..e7442e1 100644 --- a/kilochat/lib/display_toast.dart +++ b/kilochat/lib/display_toast.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; class DisplayToast extends StatefulWidget { const DisplayToast({ - Key? key, + super.key, required this.child, required this.stream, required this.builder, - }) : super(key: key); + }); final Widget child; final Stream stream; @@ -59,7 +59,7 @@ class _DisplayToastState extends State> borderRadius: BorderRadius.circular(10), border: Border.all(color: Colors.black), boxShadow: kElevationToShadow[4], - color: Colors.white, + color: Colors.grey.withAlpha(100), ), child: widget.builder(event, _controller), ), diff --git a/kilochat/lib/login_screen.dart b/kilochat/lib/login_screen.dart new file mode 100644 index 0000000..e079355 --- /dev/null +++ b/kilochat/lib/login_screen.dart @@ -0,0 +1,147 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:realm/realm.dart'; + +import 'providers.dart'; +import 'router.dart'; +import 'settings.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +enum PageMode { registration, login, loggedIn } + +class _LoginScreenState extends ConsumerState { + final _emailController = TextEditingController(); + + PageMode _mode = PageMode.registration; + PageMode get mode => _mode; + set mode(PageMode mode) => setState(() => _mode = mode); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(10), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.only(left: 50, top: 10, bottom: 10), + child: Text( + 'Sign in using passkey', + style: TextStyle( + fontSize: 20, + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: TextField( + autofillHints: const [AutofillHints.email], + controller: _emailController, + decoration: const InputDecoration( + border: OutlineInputBorder(), + hintText: 'email address', + ), + ), + ), + SizedBox( + width: double.infinity, + child: _buildButton(), + ), + const SizedBox(height: 16), + _buildSwitchMode() + ], + ), + ), + ), + ); + } + + String _buttonText() { + return switch (mode) { + PageMode.registration => 'sign up', + PageMode.login => 'sign in', + PageMode.loggedIn => 'logout', + }; + } + + Widget _buildButton() => + ElevatedButton(onPressed: _onClick, child: Text(_buttonText())); + + Widget _buildActionSpan(String lead, String action, PageMode newMode) { + return RichText( + text: TextSpan( + children: [ + TextSpan( + text: lead, + style: const TextStyle(color: Colors.black), + ), + TextSpan( + text: action, + style: TextStyle(color: Theme.of(context).primaryColor), + recognizer: TapGestureRecognizer()..onTap = () => mode = newMode, + ), + ], + ), + ); + } + + Widget _buildSwitchMode() { + return switch (mode) { + PageMode.registration => _buildActionSpan( + 'Already have an account? ', + 'Sign in', + PageMode.login, + ), + PageMode.login => _buildActionSpan( + 'First time here? ', + 'Sign up', + PageMode.registration, + ), + PageMode.loggedIn => const Text('You are currently logged in.'), + }; + } + + Future _onClick() async { + final auth = await ref.watch(authProvider.future); + final email = _emailController.value.text; + + try { + final app = currentWorkspace!.app; + if (mode == PageMode.loggedIn) { + app.currentUser?.logOut(); + mode = PageMode.login; + } else { + if (mode == PageMode.registration) { + await auth.signUpWithPasskey(email: email); + } else if (mode == PageMode.login) { + await auth.loginWithPasskey(email: email); + } + final jwt = (await auth.currentUser)!.idToken; + await currentWorkspace?.app.logIn(Credentials.jwt(jwt)); + mode = PageMode.loggedIn; + + if (mounted) Routes.chat.go(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: Theme.of(context).primaryColor, + content: Text(e.toString()), + duration: const Duration(seconds: 10), + ), + ); + } + } + } +} diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index b0965c2..70e693a 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:corbado_auth/corbado_auth.dart'; +import 'package:realm/realm.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'model.dart'; @@ -11,7 +12,8 @@ part 'providers.g.dart'; @riverpod Future auth(AuthRef ref) async { - const projectId = 'pro-2636186146982821243'; + //const projectId = 'pro-2636186146982821243'; + const projectId = 'pro-9092673961474177526'; final auth = CorbadoAuth(); await auth.init(projectId); return auth; @@ -35,6 +37,10 @@ Stream repository(RepositoryRef ref) async* { Stream focusedChannel(FocusedChannelRef ref) async* { final repository = await ref.watch(repositoryProvider.future); await for (final channel in repository.focusedChannelChanges) { - yield channel; + yield channel?.nullIfInvalid; } } + +extension on T { + T? get nullIfInvalid => isValid ? this : null; +} diff --git a/kilochat/lib/realm_ui/realm_animated_list.dart b/kilochat/lib/realm_ui/realm_animated_list.dart index e5c48c9..7a8b1fb 100644 --- a/kilochat/lib/realm_ui/realm_animated_list.dart +++ b/kilochat/lib/realm_ui/realm_animated_list.dart @@ -105,6 +105,7 @@ class _RealmAnimatedListState extends State> { @override void dispose() { _subscription?.cancel(); + _subscription = null; super.dispose(); } diff --git a/kilochat/lib/repository.dart b/kilochat/lib/repository.dart index f15aa7f..cc6a6c1 100644 --- a/kilochat/lib/repository.dart +++ b/kilochat/lib/repository.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:cancellation_token/cancellation_token.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:kilochat/settings.dart'; import 'package:realm/realm.dart'; @@ -141,10 +140,14 @@ class Repository extends Disposable { ); } - late Stream x = - allMessages.changes.where((c) => c.inserted.isNotEmpty).map((c) { - return c.results[c.inserted.last]; - }); + late Stream notifications = allMessages.changes.asyncExpand((c) async* { + final idx = c.inserted.lastOrNull ?? c.modified.lastOrNull ?? -1; + if (idx >= 0) { // not a delete + final message = c.results[idx]; + if (message.owner != userProfile && message.channel != focusedChannel) { + yield message; + } + }}); void postNewMessage(Channel channel, String text) { // messages are sorted latest first diff --git a/kilochat/lib/router.dart b/kilochat/lib/router.dart index 01fa97a..5e62ad5 100644 --- a/kilochat/lib/router.dart +++ b/kilochat/lib/router.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'chat_screen.dart'; import 'settings.dart'; import 'workspace_view.dart'; +import 'login_screen.dart'; enum Routes { chat, diff --git a/kilochat/lib/tiles.dart b/kilochat/lib/tiles.dart index 30b800c..93df8e1 100644 --- a/kilochat/lib/tiles.dart +++ b/kilochat/lib/tiles.dart @@ -78,7 +78,7 @@ class MessageTile extends ConsumerWidget { ), const SizedBox(height: 8), SizedBox( - height: 30, + height: 32, child: RealmAnimatedList( results: message.reactions, scrollDirection: Axis.horizontal, @@ -92,12 +92,11 @@ class MessageTile extends ConsumerWidget { visualDensity: VisualDensity.compact, avatar: MyAvatar(user: reaction.owner), label: AnimatedEmoji( - AnimatedEmojiData( - 'u${reaction.emojiUnicode.toRadixString(16)}', - name: 'n/a', + AnimatedEmojis.fromId( + reaction.emojiUnicode.toRadixString(16), ), repeat: false, - size: 20, + size: 16, errorWidget: Text(reaction.emoji), ), onDeleted: @@ -113,7 +112,7 @@ class MessageTile extends ConsumerWidget { icon: const Icon(Icons.add), onPressed: () { (ref.read(repositoryProvider.future)).then((repository) { - repository.addReaction(message, '\u{1f605}'); //'👍'); + repository.addReaction(message, '👍'); }); }, ), diff --git a/kilochat/lib/workspace_view.dart b/kilochat/lib/workspace_view.dart index fdea20f..935d47c 100644 --- a/kilochat/lib/workspace_view.dart +++ b/kilochat/lib/workspace_view.dart @@ -1,8 +1,5 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:kilochat/providers.dart'; -import 'package:realm/realm.dart'; import 'realm_ui/realm_animated_list.dart'; import 'router.dart'; @@ -35,7 +32,7 @@ class WorkspaceView extends ConsumerWidget { } class WorkspaceScreen extends StatelessWidget { - const WorkspaceScreen({Key? key}) : super(key: key); + const WorkspaceScreen({super.key}); @override Widget build(BuildContext context) { @@ -79,11 +76,11 @@ class _WorkspaceFormState extends State { @override void initState() { + super.initState(); workspace = Workspace( widget.initialWorkspace?.appId ?? '', widget.initialWorkspace?.name ?? '', ); - super.initState(); } @override @@ -146,141 +143,3 @@ class _WorkspaceFormState extends State { } } -class LoginScreen extends ConsumerStatefulWidget { - const LoginScreen({super.key}); - - @override - ConsumerState createState() => _LoginScreenState(); -} - -enum PageMode { registration, login, loggedIn } - -class _LoginScreenState extends ConsumerState { - final _emailController = TextEditingController(); - - PageMode _mode = PageMode.registration; - PageMode get mode => _mode; - set mode(PageMode mode) => setState(() => _mode = mode); - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Padding( - padding: const EdgeInsets.all(10), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.only(left: 50, top: 10, bottom: 10), - child: Text( - 'Sign in using passkey', - style: TextStyle( - fontSize: 20, - ), - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: TextField( - autofillHints: const [AutofillHints.email], - controller: _emailController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - hintText: 'email address', - ), - ), - ), - SizedBox( - width: double.infinity, - child: _buildButton(), - ), - const SizedBox(height: 16), - _buildSwitchMode() - ], - ), - ), - ), - ); - } - - String _buttonText() { - return switch (mode) { - PageMode.registration => 'sign up', - PageMode.login => 'sign in', - PageMode.loggedIn => 'logout', - }; - } - - Widget _buildButton() => - ElevatedButton(onPressed: _onClick, child: Text(_buttonText())); - - Widget _buildActionSpan(String lead, String action, PageMode newMode) { - return RichText( - text: TextSpan( - children: [ - TextSpan( - text: lead, - style: const TextStyle(color: Colors.black), - ), - TextSpan( - text: action, - style: TextStyle(color: Theme.of(context).primaryColor), - recognizer: TapGestureRecognizer()..onTap = () => mode = newMode, - ), - ], - ), - ); - } - - Widget _buildSwitchMode() { - return switch (mode) { - PageMode.registration => _buildActionSpan( - 'Already have an account? ', - 'Sign in', - PageMode.login, - ), - PageMode.login => _buildActionSpan( - 'First time here? ', - 'Sign up', - PageMode.registration, - ), - PageMode.loggedIn => const Text('You are currently logged in.'), - }; - } - - Future _onClick() async { - final auth = await ref.watch(authProvider.future); - final email = _emailController.value.text; - - try { - final app = currentWorkspace!.app; - if (mode == PageMode.loggedIn) { - app.currentUser?.logOut(); - mode = PageMode.login; - } else { - if (mode == PageMode.registration) { - await auth.signUpWithPasskey(email: email); - } else if (mode == PageMode.login) { - await auth.loginWithPasskey(email: email); - } - final jwt = (await auth.currentUser)!.idToken; - await currentWorkspace?.app.logIn(Credentials.jwt(jwt)); - mode = PageMode.loggedIn; - - if (mounted) Routes.chat.go(context); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: Theme.of(context).primaryColor, - content: Text(e.toString()), - duration: const Duration(seconds: 10), - ), - ); - } - } - } -} diff --git a/kilochat/macos/Runner/DebugProfile.entitlements b/kilochat/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index 08c3ab1..0000000 --- a/kilochat/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - com.apple.security.network.client - - - diff --git a/kilochat/macos/Runner/Release.entitlements b/kilochat/macos/Runner/Release.entitlements deleted file mode 100644 index ee95ab7..0000000 --- a/kilochat/macos/Runner/Release.entitlements +++ /dev/null @@ -1,10 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.network.client - - - diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index c1073af..e252848 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -370,10 +370,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "5b24061317f850af858ef7151dadbb6eb77c1c449c954c7bb064e8a5e0e7d81f" + sha256: a64c5323ac83ed2b7940d2b6288d160aa1753ff271ba9d9b2a86770414aa3eab url: "https://pub.dev" source: hosted - version: "0.6.20" + version: "0.6.20+1" flutter_riverpod: dependency: "direct main" description: @@ -1087,4 +1087,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.3.0 <4.0.0" - flutter: ">=3.16.0" + flutter: ">=3.19.0" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index 8599699..69cd442 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -6,7 +6,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.1.0 environment: - sdk: ^3.0.0 + sdk: ^3.3.0 dependencies: flutter: From e6ee6a46ed7d2ec7b51adc7a7d1feac78b9d7e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Sat, 24 Feb 2024 14:21:17 +0100 Subject: [PATCH 32/33] Update mason brick --- .../__brick__/{{app_name}}/auth/custom_user_data.json | 6 +++++- .../__brick__/{{app_name}}/auth/providers.json | 2 +- .../kilochat_appx/__brick__/{{app_name}}/sync/config.json | 6 +++++- kilochat/bricks/kilochat_appx/brick.yaml | 8 ++++++-- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/custom_user_data.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/custom_user_data.json index a82d0fb..583fcaa 100644 --- a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/custom_user_data.json +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/custom_user_data.json @@ -1,3 +1,7 @@ { - "enabled": false + "enabled": true, + "mongo_service_name": "{{service_name}}", + "database_name": "{{db_name}}", + "collection_name": "UserProfile", + "user_id_field": "ownerId" } diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/providers.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/providers.json index 6fd5fdc..2b8e91e 100644 --- a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/providers.json +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/auth/providers.json @@ -16,7 +16,7 @@ "audience": [ "{{audience}}" ], - "jwkURI": "https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com", + "jwkURI": "https://{{corbado_project_id}}.frontendapi.corbado.io/.well-known/jwks", "requireAnyAudience": false, "signingAlgorithm": "", "useJWKURI": true diff --git a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/sync/config.json b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/sync/config.json index 36a8b33..566a7e7 100644 --- a/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/sync/config.json +++ b/kilochat/bricks/kilochat_appx/__brick__/{{app_name}}/sync/config.json @@ -1,11 +1,15 @@ { "type": "flexible", "state": "enabled", - "development_mode_enabled": true, + "development_mode_enabled": false, "service_name": "{{service_name}}", "database_name": "{{db_name}}", "client_max_offline_days": 30, "is_recovery_mode_disabled": false, + "permissions": { + "rules": {}, + "defaultRoles": [] + }, "queryable_fields_names": [ "owner_id", "channel_id" diff --git a/kilochat/bricks/kilochat_appx/brick.yaml b/kilochat/bricks/kilochat_appx/brick.yaml index 3eb943d..02cedf9 100644 --- a/kilochat/bricks/kilochat_appx/brick.yaml +++ b/kilochat/bricks/kilochat_appx/brick.yaml @@ -51,5 +51,9 @@ vars: audience: type: string description: Firebase jwt provider audience - default: kilochat-realm - prompt: What is your Firebase jwt provider audience? + default: kilochat-audience + prompt: What is your jwt provider audience (aud claim)? + corbado_project_id: + type: string + description: Corbado project id + prompt: What is your Corbado project id? From 7cd46aaa42f8fed20e6fc7b942fd5808ebc7a609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kasper=20Overg=C3=A5rd=20Nielsen?= Date: Thu, 16 May 2024 14:58:02 +0200 Subject: [PATCH 33/33] Update to latest realm and flutter --- kilochat/README.md | 85 ++++--- kilochat/lib/login_screen.dart | 1 + kilochat/lib/main.dart | 5 +- kilochat/lib/model.dart | 2 +- .../lib/{model.g.dart => model.realm.dart} | 214 +++++++++++++++-- kilochat/lib/providers.dart | 3 +- kilochat/lib/providers.g.dart | 2 +- .../realm_session_state_indicator.dart | 14 +- kilochat/lib/settings.dart | 2 +- .../{settings.g.dart => settings.realm.dart} | 76 +++++- kilochat/pubspec.lock | 218 ++++++++++-------- kilochat/pubspec.yaml | 12 +- 12 files changed, 456 insertions(+), 178 deletions(-) rename kilochat/lib/{model.g.dart => model.realm.dart} (68%) rename kilochat/lib/{settings.g.dart => settings.realm.dart} (57%) diff --git a/kilochat/README.md b/kilochat/README.md index 6afdc12..62b472c 100644 --- a/kilochat/README.md +++ b/kilochat/README.md @@ -24,28 +24,38 @@ TODO: It also serves as an example of an app architecture that works well with Realm. Depending on how you choose to count, it does so in just 1-2K lines of code, hence the name. -## Getting started +## Prerequisites -The absolute easiest way to check this out is to install the kilochat app from either Apple's App Store or Google's Play Store. +You will need Flutter 3.22.0 (Dart 3.4.0) or later. -A shared backend is setup for a fictitious organization [Acme Corporation](https://en.wikipedia.org/wiki/Acme_Corporation) for your convenience. +You will need a device/simulator with iOS 17.4+ or a device/emulator with Android XXX. If you are using a simulator/emulator make sure to set up support for biometric authentication, which is needed to use passkeys. -If you choose to use the pre-configured backend, then be aware that it is shared with the entire world, and there is no ongoing moderation. Please behave and follow our [code of conduct](https://www.mongodb.com/community-code-of-conduct) or we will have to disable it. +Create the platform projects you are interested in. You can only choose Android and/or iOS, since we depend on [corbado_auth](https://pub.dev/packages/corbado_auth) for the passkey authentication. Realm itself supports all flutter platforms, except for web. -You will be able to sign-up and login with any email address of yours, and if you device supports it, a biometric passkey. Otherwise a magic-link will be sent to your address. No password is necessary. +```shell +flutter create . --platforms=android,ios # realm does not support web (yet), and corbado only support android and ios. +flutter pub get +``` -We hope, that once you have played with it for a while, that you will be tempted to learn how it was build. The rest of this lengthy README will try to explain just that. +## Passkey authentication -### Prerequisites +We often use email/password authentication, or simply anonymous authentication when we create samples for Atlas Device Sync, since it is easy to set up, and doesn't require external services. -You need Flutter 3.13.0 or later. +However for this sample we will use federated jwt authentication via FIDO 2 / WebAuthn passkeys. -Create the platform projects you are interested in. Only the bare minimum modifications are added in the repo, so you need to create the platform projects on your end. You can choose Android and/or iOS. -```shell -flutter create . --platforms=android,ios # realm does not support web (yet), and corbado only support android and ios. -flutter pub get +The downside is, that there is a bit more setup to do. + + + +Next you need to configure your native app for passkey use. Follow the instruction for [corbada_auth](https://pub.dev/packages/corbado_auth) on pub.dev. +For iOS fx. you need to set minimum OS to 17.4, add the Associated Domains capability, and set an appropriate domain like: ``` +webcredentials:{{project-id}}.frontendapi.corbado.io?mode=developer +``` +replacing `{{project-id}}` with the project id of your personal corbado project. + +## Setting up your own backend You will need the following CLI tools installed to follow the instructions for setting up a backend: @@ -55,30 +65,47 @@ You will need the following CLI tools installed to follow the instructions for s On macos You can get these with: ```shell -npm install -g atlas-app-services-cli # TODO: is this officially released yet? (or fo we need to use realm-cli?) -brew install mongodb_atlas_cli # TODO: on macos +npm install -g atlas-app-services-cli +brew install mongodb_atlas_cli ``` -## Setting up your own backend - -> :warning: This repo contains an atlas application identifier (`app_id`) which is **not** a secret, but should not be shared widely if the app service has developer mode enabled. The reason for this being that developer mode allows clients to do automatic schema changes. - -These instruction only pertain to the setup Atlas Device Sync with MongoDB, and reuse the existing relay party. If you want to setup your own corbado project as well, please consult https://pub.dev/packages/corbado_auth. - -You will need the following CLI tools installed to follow these instructions: +### Atlas Device Sync -- `atlas`, and -- `atlas-app-services-cli`. +``` +atlas auth login +atlas cluster create kilochat-cluster --provider AWS --region EU_CENTRAL_1 --tier=M0 +``` +Be aware that you can only have one free (M0) cluster. If you already have one setup, just use that for the rest of the setup, when prompted for what cluster to use. -You can get these with: +To make it easy to setup the backend, this sample includes +a mason brick that will provision it for you. +You need to have your cluster name, chosen aud claim, and corbado project if ready, before proceeding. +```shell +dart pub global activate mason_cli # activate mason if you havnn't used it before +mason add kilochat_appx --path bricks/kilochat_appx/ # add the custom brick +``` +You need to have your cluster name, chosen aud claim, and corbado project if ready, before proceeding. ```shell -npm install -g atlas-app-services-cli # TODO: is this officially released yet? (or fo we need to use realm-cli?) -brew install mongodb_atlas_cli # TODO: on macos +mason make kilochat_appx # will prompt for cluster, aud claim, and corbado project id +``` +Now you can provision your backend using the `app_service_cli`: +```shell +cd kilochat_app +app-services-cli login +app-services-cli push ``` -### Atlas Device Sync +When running the sample you need to pass the corbada project id with `--dart-define` like: +```shell +flutter run --dart-define=CORBADO_PROJECT_ID= +``` +or simply replace +```dart +const projectId = String.fromEnvironment('CORBADO_PROJECT_ID'); +``` +with you _project id_ in `providers.dart` First, you need to create an organization. Secondly you need to create a project. @@ -108,7 +135,7 @@ In this sample we federate authentication to corbado. Corbado also persists the I have a vague recollection that Bill Gates proclaimed the death of passwords back in the early 00's, however we may finally be on the brink of such a future, due to FIDO2 / WebAuthn passkeys, which are supported on most modern operating systems (Android 9, iOS 16, MacOS Ventura, Windows 10, and later), and with external hardware authenticators such as Yubikey. -When it comes to Flutter, the support is still a bit limited, but the German startup [Corbado](https://app.corbado.com/) is working hard to change that. So far only Android and iOS is supported, and that is really the reason this project only works on those platforms. Realm itself works on all official platforms, except web. +When it comes to Flutter, the support is still a bit limited, but the German startup [Corbado](https://app.corbado.com/) is working to change that. So far only Android and iOS is supported, and that is really the reason this project only works on those platforms. Realm itself works on all official platforms, except web. ## Authorization @@ -178,7 +205,7 @@ not important for a user to know how many time a friend went online/offline whil Realm is build to deliver every update in a reliable manner, but by "resetting" the presence subscription on startup we can indicate it is okay to skip over un-synced data. -The presence system used here is still rather simple. We refer t [A Scalable Server Architecture for Mobile Presence Service in Social Network Applications](https://www.researchgate.net/publication/232657317_A_Scalable_Server_Architecture_for_Mobile_Presence_Service_in_Social_Network_Applications) for the interested reader. +The presence system used here is still rather simple. We refer to [A Scalable Server Architecture for Mobile Presence Service in Social Network Applications](https://www.researchgate.net/publication/232657317_A_Scalable_Server_Architecture_for_Mobile_Presence_Service_in_Social_Network_Applications) for the interested reader. ## Leader-board diff --git a/kilochat/lib/login_screen.dart b/kilochat/lib/login_screen.dart index e079355..7ed2973 100644 --- a/kilochat/lib/login_screen.dart +++ b/kilochat/lib/login_screen.dart @@ -124,6 +124,7 @@ class _LoginScreenState extends ConsumerState { if (mode == PageMode.registration) { await auth.signUpWithPasskey(email: email); } else if (mode == PageMode.login) { + await auth.refreshToken(); await auth.loginWithPasskey(email: email); } final jwt = (await auth.currentUser)!.idToken; diff --git a/kilochat/lib/main.dart b/kilochat/lib/main.dart index 5715c45..6508c30 100644 --- a/kilochat/lib/main.dart +++ b/kilochat/lib/main.dart @@ -11,7 +11,9 @@ const freedomBlue = Color(0xff0057b7); const energizingYellow = Color(0xffffd700); Future main() async { - Realm.logger.level = RealmLogLevel.debug; + Realm.logger.setLogLevel(LogLevel.info); + Realm.logger.onRecord.forEach(print); + Animate.restartOnHotReload = true; WidgetsFlutterBinding.ensureInitialized(); @@ -21,6 +23,7 @@ Future main() async { routerConfig: router, debugShowCheckedModeBanner: false, theme: ThemeData( + useMaterial3: false, colorScheme: ColorScheme.fromSeed( seedColor: freedomBlue, inversePrimary: energizingYellow, diff --git a/kilochat/lib/model.dart b/kilochat/lib/model.dart index 497588c..990507e 100644 --- a/kilochat/lib/model.dart +++ b/kilochat/lib/model.dart @@ -1,6 +1,6 @@ import 'package:realm/realm.dart'; -part 'model.g.dart'; +part 'model.realm.dart'; @RealmModel() class _Channel { diff --git a/kilochat/lib/model.g.dart b/kilochat/lib/model.realm.dart similarity index 68% rename from kilochat/lib/model.g.dart rename to kilochat/lib/model.realm.dart index 6c1ac35..5b9117a 100644 --- a/kilochat/lib/model.g.dart +++ b/kilochat/lib/model.realm.dart @@ -55,14 +55,48 @@ class Channel extends _Channel with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Channel freeze() => RealmObjectBase.freezeObject(this); - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { + EJsonValue toEJson() { + return { + '_id': id.toEJson(), + 'owner_id': ownerId.toEJson(), + 'parent': parent.toEJson(), + 'name': name.toEJson(), + 'count': count.toEJson(), + }; + } + + static EJsonValue _toEJson(Channel value) => value.toEJson(); + static Channel _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + '_id': EJsonValue id, + 'owner_id': EJsonValue ownerId, + 'parent': EJsonValue parent, + 'name': EJsonValue name, + 'count': EJsonValue count, + } => + Channel( + fromEJson(id), + fromEJson(ownerId), + fromEJson(name), + fromEJson(count), + parent: fromEJson(parent), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { RealmObjectBase.registerFactory(Channel._); - return const SchemaObject(ObjectType.realmObject, Channel, 'Channel', [ + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, Channel, 'Channel', [ SchemaProperty('id', RealmPropertyType.objectid, mapTo: '_id', primaryKey: true), SchemaProperty('ownerId', RealmPropertyType.string, mapTo: 'owner_id'), @@ -71,7 +105,10 @@ class Channel extends _Channel with RealmEntity, RealmObjectBase, RealmObject { SchemaProperty('name', RealmPropertyType.string), SchemaProperty('count', RealmPropertyType.int), ]); - } + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } class Message extends _Message with RealmEntity, RealmObjectBase, RealmObject { @@ -153,14 +190,54 @@ class Message extends _Message with RealmEntity, RealmObjectBase, RealmObject { Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Message freeze() => RealmObjectBase.freezeObject(this); - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { + EJsonValue toEJson() { + return { + '_id': id.toEJson(), + 'index': index.toEJson(), + 'owner_id': ownerId.toEJson(), + 'owner': owner.toEJson(), + 'channel_id': channelId.toEJson(), + 'channel': channel.toEJson(), + 'text': text.toEJson(), + }; + } + + static EJsonValue _toEJson(Message value) => value.toEJson(); + static Message _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + '_id': EJsonValue id, + 'index': EJsonValue index, + 'owner_id': EJsonValue ownerId, + 'owner': EJsonValue owner, + 'channel_id': EJsonValue channelId, + 'channel': EJsonValue channel, + 'text': EJsonValue text, + } => + Message( + fromEJson(id), + fromEJson(index), + fromEJson(ownerId), + fromEJson(channelId), + fromEJson(text), + owner: fromEJson(owner), + channel: fromEJson(channel), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { RealmObjectBase.registerFactory(Message._); - return const SchemaObject(ObjectType.realmObject, Message, 'Message', [ + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, Message, 'Message', [ SchemaProperty('id', RealmPropertyType.objectid, mapTo: '_id', primaryKey: true), SchemaProperty('index', RealmPropertyType.int, @@ -179,7 +256,10 @@ class Message extends _Message with RealmEntity, RealmObjectBase, RealmObject { collectionType: RealmCollectionType.list, linkTarget: 'Reaction'), ]); - } + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } class Reaction extends _Reaction @@ -241,14 +321,48 @@ class Reaction extends _Reaction Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Reaction freeze() => RealmObjectBase.freezeObject(this); - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { + EJsonValue toEJson() { + return { + '_id': id.toEJson(), + 'owner_id': ownerId.toEJson(), + 'owner': owner.toEJson(), + 'message': message.toEJson(), + 'emojiUnicode': emojiUnicode.toEJson(), + }; + } + + static EJsonValue _toEJson(Reaction value) => value.toEJson(); + static Reaction _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + '_id': EJsonValue id, + 'owner_id': EJsonValue ownerId, + 'owner': EJsonValue owner, + 'message': EJsonValue message, + 'emojiUnicode': EJsonValue emojiUnicode, + } => + Reaction( + fromEJson(id), + fromEJson(ownerId), + owner: fromEJson(owner), + message: fromEJson(message), + emojiUnicode: fromEJson(emojiUnicode), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { RealmObjectBase.registerFactory(Reaction._); - return const SchemaObject(ObjectType.realmObject, Reaction, 'Reaction', [ + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, Reaction, 'Reaction', [ SchemaProperty('id', RealmPropertyType.objectid, mapTo: '_id', primaryKey: true), SchemaProperty('ownerId', RealmPropertyType.string, mapTo: 'owner_id'), @@ -258,7 +372,10 @@ class Reaction extends _Reaction optional: true, linkTarget: 'Message'), SchemaProperty('emojiUnicode', RealmPropertyType.int), ]); - } + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } class UserProfile extends _UserProfile @@ -369,15 +486,67 @@ class UserProfile extends _UserProfile Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor( + [List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override UserProfile freeze() => RealmObjectBase.freezeObject(this); - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { + EJsonValue toEJson() { + return { + '_id': id.toEJson(), + 'owner_id': ownerId.toEJson(), + 'deactivated': deactivated.toEJson(), + 'name': name.toEJson(), + 'email': email.toEJson(), + 'age': age.toEJson(), + 'gender': genderAsInt.toEJson(), + 'status_emoji': statusEmojiUnicode.toEJson(), + 'typing': typing.toEJson(), + 'channels': channels.toEJson(), + 'bodies': bodies.toEJson(), + }; + } + + static EJsonValue _toEJson(UserProfile value) => value.toEJson(); + static UserProfile _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + '_id': EJsonValue id, + 'owner_id': EJsonValue ownerId, + 'deactivated': EJsonValue deactivated, + 'name': EJsonValue name, + 'email': EJsonValue email, + 'age': EJsonValue age, + 'gender': EJsonValue genderAsInt, + 'status_emoji': EJsonValue statusEmojiUnicode, + 'typing': EJsonValue typing, + 'channels': EJsonValue channels, + 'bodies': EJsonValue bodies, + } => + UserProfile( + fromEJson(id), + fromEJson(ownerId), + deactivated: fromEJson(deactivated), + name: fromEJson(name), + email: fromEJson(email), + age: fromEJson(age), + genderAsInt: fromEJson(genderAsInt), + statusEmojiUnicode: fromEJson(statusEmojiUnicode), + typing: fromEJson(typing), + channels: fromEJson(channels), + bodies: fromEJson(bodies), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { RealmObjectBase.registerFactory(UserProfile._); - return const SchemaObject( - ObjectType.realmObject, UserProfile, 'UserProfile', [ + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, UserProfile, 'UserProfile', [ SchemaProperty('id', RealmPropertyType.string, mapTo: '_id', primaryKey: true), SchemaProperty('ownerId', RealmPropertyType.string, mapTo: 'owner_id'), @@ -394,5 +563,8 @@ class UserProfile extends _UserProfile SchemaProperty('bodies', RealmPropertyType.object, linkTarget: 'UserProfile', collectionType: RealmCollectionType.set), ]); - } + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } diff --git a/kilochat/lib/providers.dart b/kilochat/lib/providers.dart index 70e693a..53bc30c 100644 --- a/kilochat/lib/providers.dart +++ b/kilochat/lib/providers.dart @@ -12,8 +12,7 @@ part 'providers.g.dart'; @riverpod Future auth(AuthRef ref) async { - //const projectId = 'pro-2636186146982821243'; - const projectId = 'pro-9092673961474177526'; + const projectId = 'pro-2636186146982821243'; //String.fromEnvironment('CORBADO_PROJECT_ID'); final auth = CorbadoAuth(); await auth.init(projectId); return auth; diff --git a/kilochat/lib/providers.g.dart b/kilochat/lib/providers.g.dart index 3b386d8..19ff136 100644 --- a/kilochat/lib/providers.g.dart +++ b/kilochat/lib/providers.g.dart @@ -34,7 +34,7 @@ final repositoryProvider = AutoDisposeStreamProvider.internal( ); typedef RepositoryRef = AutoDisposeStreamProviderRef; -String _$focusedChannelHash() => r'c467f8b8fbe8c481f8f49a542486227febaa2684'; +String _$focusedChannelHash() => r'4c82030fe34b65448b7f640cf1009d6518b34993'; /// See also [focusedChannel]. @ProviderFor(focusedChannel) diff --git a/kilochat/lib/realm_ui/realm_session_state_indicator.dart b/kilochat/lib/realm_ui/realm_session_state_indicator.dart index dd0e180..66a5b30 100644 --- a/kilochat/lib/realm_ui/realm_session_state_indicator.dart +++ b/kilochat/lib/realm_ui/realm_session_state_indicator.dart @@ -12,8 +12,6 @@ typedef SessionStatus = ( bool uploading ); -const _noProgress = SyncProgress(transferableBytes: 0, transferredBytes: 0); - class RealmSessionStateIndicator extends StatelessWidget { const RealmSessionStateIndicator({ super.key, @@ -52,19 +50,17 @@ class RealmSessionStateIndicator extends StatelessWidget { .getProgressStream( ProgressDirection.download, ProgressMode.reportIndefinitely, - ) - .startWith(_noProgress), + ), session .getProgressStream( ProgressDirection.upload, ProgressMode.reportIndefinitely, - ) - .startWith(_noProgress), + ), (connectivity, state, download, upload) => ( - connectivity != ConnectivityResult.none, + connectivity.isEmpty, state, - download.transferableBytes > download.transferredBytes, - upload.transferredBytes > upload.transferableBytes, + download.progressEstimate == 1.0, + upload.progressEstimate == 1.0, )).distinct(); } diff --git a/kilochat/lib/settings.dart b/kilochat/lib/settings.dart index af1c381..c0b7199 100644 --- a/kilochat/lib/settings.dart +++ b/kilochat/lib/settings.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:path/path.dart' as path; import 'package:realm/realm.dart'; -part 'settings.g.dart'; +part 'settings.realm.dart'; @RealmModel() class _Workspace { diff --git a/kilochat/lib/settings.g.dart b/kilochat/lib/settings.realm.dart similarity index 57% rename from kilochat/lib/settings.g.dart rename to kilochat/lib/settings.realm.dart index ff7a122..1bd38b5 100644 --- a/kilochat/lib/settings.g.dart +++ b/kilochat/lib/settings.realm.dart @@ -42,20 +42,51 @@ class Workspace extends _Workspace Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Workspace freeze() => RealmObjectBase.freezeObject(this); - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { + EJsonValue toEJson() { + return { + 'appId': appId.toEJson(), + 'name': name.toEJson(), + 'currentChannelId': currentChannelId.toEJson(), + }; + } + + static EJsonValue _toEJson(Workspace value) => value.toEJson(); + static Workspace _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + 'appId': EJsonValue appId, + 'name': EJsonValue name, + 'currentChannelId': EJsonValue currentChannelId, + } => + Workspace( + fromEJson(appId), + fromEJson(name), + currentChannelId: fromEJson(currentChannelId), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { RealmObjectBase.registerFactory(Workspace._); - return const SchemaObject(ObjectType.realmObject, Workspace, 'Workspace', [ + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, Workspace, 'Workspace', [ SchemaProperty('appId', RealmPropertyType.string, primaryKey: true), SchemaProperty('name', RealmPropertyType.string), SchemaProperty('currentChannelId', RealmPropertyType.objectid, optional: true), ]); - } + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } class Settings extends _Settings @@ -79,16 +110,41 @@ class Settings extends _Settings Stream> get changes => RealmObjectBase.getChanges(this); + @override + Stream> changesFor([List? keyPaths]) => + RealmObjectBase.getChangesFor(this, keyPaths); + @override Settings freeze() => RealmObjectBase.freezeObject(this); - static SchemaObject get schema => _schema ??= _initSchema(); - static SchemaObject? _schema; - static SchemaObject _initSchema() { + EJsonValue toEJson() { + return { + 'workspace': workspace.toEJson(), + }; + } + + static EJsonValue _toEJson(Settings value) => value.toEJson(); + static Settings _fromEJson(EJsonValue ejson) { + return switch (ejson) { + { + 'workspace': EJsonValue workspace, + } => + Settings( + workspace: fromEJson(workspace), + ), + _ => raiseInvalidEJson(ejson), + }; + } + + static final schema = () { RealmObjectBase.registerFactory(Settings._); - return const SchemaObject(ObjectType.realmObject, Settings, 'Settings', [ + register(_toEJson, _fromEJson); + return SchemaObject(ObjectType.realmObject, Settings, 'Settings', [ SchemaProperty('workspace', RealmPropertyType.object, optional: true, linkTarget: 'Workspace'), ]); - } + }(); + + @override + SchemaObject get objectSchema => RealmObjectBase.getSchema(this) ?? schema; } diff --git a/kilochat/pubspec.lock b/kilochat/pubspec.lock index e252848..54803b4 100644 --- a/kilochat/pubspec.lock +++ b/kilochat/pubspec.lock @@ -37,18 +37,18 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: ecf4273855368121b1caed0d10d4513c7241dfc813f7d3c8933b36622ae9b265 url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.5.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: transitive description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -109,10 +109,10 @@ packages: dependency: transitive description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.10" build_runner_core: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: built_value - sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.9.1" + version: "8.9.2" cancellation_token: dependency: "direct main" description: @@ -213,18 +213,18 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + sha256: db7a4e143dc72cc3cb2044ef9b052a7ebfe729513e6a82943bc3526f784365b8 url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.3" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + sha256: b6a56efe1e6675be240de39107281d4034b64ac23438026355b4234042a35adb url: "https://pub.dev" source: hosted - version: "1.2.4" + version: "2.0.0" convert: dependency: transitive description: @@ -237,10 +237,10 @@ packages: dependency: "direct main" description: name: corbado_auth - sha256: "4bfa23b9d15c7ee2d1ddbc5c932c6dd80e157647e725e7b074a8ea42c667f24c" + sha256: "63110b01eff8fc186864f795a5f98b3312ac3a9af58febdbd723c0ad46e0fc29" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.7" corbado_frontend_api_client: dependency: transitive description: @@ -269,34 +269,34 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "445242371d91d2e24bd7b82e3583a2c05610094ba2d0575262484ad889c8f981" + sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799" url: "https://pub.dev" source: hosted - version: "0.6.2" + version: "0.6.4" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: "4c0aed2a3491096e91cf1281923ba1b6814993f16dde0fd60f697925225bbbd6" + sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9 url: "https://pub.dev" source: hosted - version: "0.6.2" + version: "0.6.4" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: ce5d6215f4e143f7780ce53f73dfa6fc503f39d2d30bef76c48be9ac1a09d9a6 + sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6 url: "https://pub.dev" source: hosted - version: "0.6.2" + version: "0.6.3" dart_style: dependency: transitive description: name: dart_style - sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.6" dbus: dependency: transitive description: @@ -305,6 +305,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + ejson: + dependency: transitive + description: + name: ejson + sha256: f336c6fb6c5c97db8ae59ba8ed207f542241f1db39cf2ef03776d308de3432ff + url: "https://pub.dev" + source: hosted + version: "0.3.0" + ejson_annotation: + dependency: transitive + description: + name: ejson_annotation + sha256: b265eea722ee340d77d1c36a55a1f963d517a0dabb569b0775664c319a4e3ebf + url: "https://pub.dev" + source: hosted + version: "0.3.0" fake_async: dependency: transitive description: @@ -362,26 +378,26 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: e2a421b7e59244faef694ba7b30562e489c2b489866e505074eb005cd7060db7 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "4.0.0" flutter_markdown: dependency: "direct main" description: name: flutter_markdown - sha256: a64c5323ac83ed2b7940d2b6288d160aa1753ff271ba9d9b2a86770414aa3eab + sha256: "9921f9deda326f8a885e202b1e35237eadfc1345239a0f6f0f1ff287e047547f" url: "https://pub.dev" source: hosted - version: "0.6.20+1" + version: "0.7.1" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098" + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.5.1" flutter_shaders: dependency: transitive description: @@ -420,10 +436,10 @@ packages: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -436,10 +452,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" + sha256: "7685acd06244ba4be60f455c5cafe5790c63dc91fc03f7385b1e922a6b85b17c" url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "14.1.1" graphs: dependency: transitive description: @@ -516,10 +532,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" jwt_decoder: dependency: transitive description: @@ -532,34 +548,34 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" logging: dependency: transitive description: @@ -572,18 +588,18 @@ packages: dependency: transitive description: name: lottie - sha256: ce2bb2605753915080e4ee47f036a64228c88dc7f56f7bc1dbe912d75b55b1e2 + sha256: "46def1e76c4fbfd4643e823980112cfe94a2ba1d9152fe54701c0bf30be4f4cd" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" markdown: dependency: "direct main" description: name: markdown - sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90" + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" matcher: dependency: transitive description: @@ -596,18 +612,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "25dfcaf170a0190f47ca6355bdd4552cb8924b430512ff0cafb8db9bd41fe33b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.14.0" mime: dependency: transitive description: @@ -652,26 +668,26 @@ packages: dependency: "direct main" description: name: passkeys - sha256: "250330485aaef45db3c8e97e4acc0e4917be653019ab415daec1bda5cb4de231" + sha256: "59e50b21746aff90cbc56145174caa3b99523f449e42f7d8aa2199ec09c511cd" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.8" passkeys_android: dependency: transitive description: name: passkeys_android - sha256: "731c492dbfe1a5d6e36143928d60399cc6dfdb55f20cff8f44ab8c8a24884e73" + sha256: "9dc0b84dad03329ff2f3be18bedecf1b8de9309c8e9cda6ef821dc88556a126d" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.4" passkeys_ios: dependency: transitive description: name: passkeys_ios - sha256: "4da97e6670a68a613483b577736d7d45f848a118bb177052c01f9d56cdf90f81" + sha256: "411b10c3cd159c9601426d47b2dc5aa4dd1e34ef69deefaac01ff81712ea6064" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" passkeys_platform_interface: dependency: transitive description: @@ -720,14 +736,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -764,42 +772,42 @@ packages: dependency: "direct main" description: name: realm - sha256: a53b6903e2f6603c906bf5aac3691f0b2f45262aa08525a4bd546900602f2f17 + sha256: "1169e97892d394cfb9bfa13f31797bbbf49d4402c6c9de6ed60aab8a973a6a89" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "2.2.1" realm_common: dependency: transitive description: name: realm_common - sha256: "29516a9e43a9e75b2e16226ce24ccd9a549d9377e69be218394d8fb84da11183" + sha256: "10fa6c3265904ccbb07b2b847ea126de6ca1ddb50dbfbdd54deca97db1201ead" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "2.2.1" realm_dart: dependency: transitive description: name: realm_dart - sha256: "55cf02d26b0775e79570cf0ba6e4036a10c66250b99cb8f1ee41d71057206a7f" + sha256: d8e4d11a0f7a1c3eae5f059135d0be0aac67643b93cf083ab84f4726718d5ad4 url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "2.2.1" realm_generator: dependency: transitive description: name: realm_generator - sha256: "6d26ca214aad1b49f37a1a86f3216f19a25657d38d82cefb16551a35aa316ec1" + sha256: "2292d91558c9f53779261188d67cb1a456664f5b9be72dfdc43c1faae07e6638" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "2.2.1" riverpod: dependency: "direct main" description: name: riverpod - sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589" + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.1" riverpod_analyzer_utils: dependency: transitive description: @@ -812,26 +820,26 @@ packages: dependency: "direct main" description: name: riverpod_annotation - sha256: "77e5d51afa4fa3e67903fb8746f33d368728d7051a0b6c292bcee60aeba46d95" + sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "359068f04879347ae4edbe66c81cc95f83fa1743806d1a0c86e55dd3c33ebb32" + sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22 url: "https://pub.dev" source: hosted - version: "2.3.11" + version: "2.4.0" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: e9bbd02e9e89e18eecb183bbca556d7b523a0669024da9b8167c08903f442937 + sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.3.10" rxdart: dependency: "direct main" description: @@ -860,10 +868,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -937,10 +945,10 @@ packages: dependency: transitive description: name: tar - sha256: aca91e93ff9ff2dba4462c6eea6bc260b72f0d7010e748e3397c32190529bd6e + sha256: "22f67e2d77b51050436620b2a5de521c58ca6f0b75af1d9ab3c8cae2eae58fcd" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" term_glyph: dependency: transitive description: @@ -953,10 +961,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "2419f20b0c8677b2d67c8ac4d1ac7372d862dc6c460cdbb052b40155408cd794" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.1" timing: dependency: transitive description: @@ -965,6 +973,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + type_plus: + dependency: transitive + description: + name: type_plus + sha256: d5d1019471f0d38b91603adb9b5fd4ce7ab903c879d2fbf1a3f80a630a03fcc9 + url: "https://pub.dev" + source: hosted + version: "2.1.1" typed_data: dependency: transitive description: @@ -1001,10 +1017,10 @@ packages: dependency: transitive description: name: uuid - sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" url: "https://pub.dev" source: hosted - version: "4.3.3" + version: "4.4.0" vector_graphics: dependency: transitive description: @@ -1041,10 +1057,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "7475cb4dd713d57b6f7464c0e13f06da0d535d8b2067e188962a59bac2cf280b" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.2" watcher: dependency: transitive description: @@ -1057,18 +1073,26 @@ packages: dependency: transitive description: name: web - sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: bfe704c186c6e32a46f6607f94d079cd0b747b9a489fceeecc93cd3adb98edd5 + url: "https://pub.dev" + source: hosted + version: "0.1.3" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" + sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.0.0" xml: dependency: transitive description: @@ -1086,5 +1110,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.0" diff --git a/kilochat/pubspec.yaml b/kilochat/pubspec.yaml index 69cd442..0a12b21 100644 --- a/kilochat/pubspec.yaml +++ b/kilochat/pubspec.yaml @@ -6,7 +6,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 0.1.0 environment: - sdk: ^3.3.0 + sdk: ^3.4.0 dependencies: flutter: @@ -15,17 +15,17 @@ dependencies: animated_emoji: ^3.1.0 cancellation_token: ^2.0.0 collection: ^1.17.0 - connectivity_plus: ^5.0.2 + connectivity_plus: ^6.0.1 corbado_auth: ^2.0.0 flutter_animate: ^4.0.0 - flutter_markdown: ^0.6.0 + flutter_markdown: ^0.7.1 flutter_riverpod: ^2.3.0 - go_router: ^13.2.0 + go_router: ^14.1.1 markdown: ^7.0.0 passkeys: ^2.0.4 path: ^1.0.0 random_avatar: ^0.0.8 - realm: ^1.9.0 + realm: ^2.0.0 riverpod_annotation: ^2.0.0 riverpod: ^2.3.0 rxdart: ^0.27.0 @@ -34,7 +34,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^3.0.1 + flutter_lints: ^4.0.0 custom_lint: ^0.6.2 riverpod_lint: any riverpod_generator: ^2.0.0