diff --git a/lib/lib/model/store.dart b/lib/lib/model/store.dart new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lib/model/store.dart b/lib/model/store.dart index a5d288fe02..d92d3e4c5b 100644 --- a/lib/model/store.dart +++ b/lib/model/store.dart @@ -45,7 +45,7 @@ export 'database.dart' show Account, AccountsCompanion, AccountAlreadyExistsExce /// we use outside of tests. abstract class GlobalStore extends ChangeNotifier { GlobalStore({required Iterable accounts}) - : _accounts = Map.fromEntries(accounts.map((a) => MapEntry(a.id, a))); + : _accounts = Map.fromEntries(accounts.map((a) => MapEntry(a.id, a))); /// A cache of the [Accounts] table in the underlying data store. final Map _accounts; @@ -67,8 +67,8 @@ abstract class GlobalStore extends ChangeNotifier { ApiConnection apiConnectionFromAccount(Account account) { return apiConnection( - realmUrl: account.realmUrl, zulipFeatureLevel: account.zulipFeatureLevel, - email: account.email, apiKey: account.apiKey); + realmUrl: account.realmUrl, zulipFeatureLevel: account.zulipFeatureLevel, + email: account.email, apiKey: account.apiKey); } final Map _perAccountStores = {}; @@ -186,6 +186,7 @@ class PerAccountStore extends ChangeNotifier with StreamStore { /// to `globalStore.apiConnectionFromAccount(account)`. /// When present, it should be a connection that came from that method call, /// but it may have already been used for other requests. + bool isstale=true; // Drives the connecting snackbar factory PerAccountStore.fromInitialSnapshot({ required GlobalStore globalStore, required int accountId, @@ -208,10 +209,10 @@ class PerAccountStore extends ChangeNotifier with StreamStore { selfUserId: account.userId, userSettings: initialSnapshot.userSettings, users: Map.fromEntries( - initialSnapshot.realmUsers - .followedBy(initialSnapshot.realmNonActiveUsers) - .followedBy(initialSnapshot.crossRealmBots) - .map((user) => MapEntry(user.userId, user))), + initialSnapshot.realmUsers + .followedBy(initialSnapshot.realmNonActiveUsers) + .followedBy(initialSnapshot.crossRealmBots) + .map((user) => MapEntry(user.userId, user))), streams: streams, unreads: Unreads( initial: initialSnapshot.unreadMsgs, @@ -219,7 +220,7 @@ class PerAccountStore extends ChangeNotifier with StreamStore { streamStore: streams, ), recentDmConversationsView: RecentDmConversationsView( - initial: initialSnapshot.recentPrivateConversations, selfUserId: account.userId), + initial: initialSnapshot.recentPrivateConversations, selfUserId: account.userId), ); } @@ -240,10 +241,10 @@ class PerAccountStore extends ChangeNotifier with StreamStore { required this.unreads, required this.recentDmConversationsView, }) : assert(selfUserId == globalStore.getAccount(accountId)!.userId), - assert(realmUrl == globalStore.getAccount(accountId)!.realmUrl), - assert(realmUrl == connection.realmUrl), - _globalStore = globalStore, - _streams = streams; + assert(realmUrl == globalStore.getAccount(accountId)!.realmUrl), + assert(realmUrl == connection.realmUrl), + _globalStore = globalStore, + _streams = streams; //////////////////////////////////////////////////////////////// // Data. @@ -298,7 +299,7 @@ class PerAccountStore extends ChangeNotifier with StreamStore { Map get subscriptions => _streams.subscriptions; @override UserTopicVisibilityPolicy topicVisibilityPolicy(int streamId, String topic) => - _streams.topicVisibilityPolicy(streamId, topic); + _streams.topicVisibilityPolicy(streamId, topic); final StreamStoreImpl _streams; @@ -356,6 +357,7 @@ class PerAccountStore extends ChangeNotifier with StreamStore { void handleEvent(Event event) { if (event is HeartbeatEvent) { assert(debugLog("server event: heartbeat")); + isstale=false; // Dismiss the connecting snackbar } else if (event is RealmEmojiUpdateEvent) { assert(debugLog("server event: realm_emoji/update")); realmEmoji = event.realmEmoji; @@ -526,11 +528,11 @@ class LiveGlobalStore extends GlobalStore { @override ApiConnection apiConnection({ - required Uri realmUrl, required int? zulipFeatureLevel, - String? email, String? apiKey}) { + required Uri realmUrl, required int? zulipFeatureLevel, + String? email, String? apiKey}) { return ApiConnection.live( - realmUrl: realmUrl, zulipFeatureLevel: zulipFeatureLevel, - email: email, apiKey: apiKey); + realmUrl: realmUrl, zulipFeatureLevel: zulipFeatureLevel, + email: email, apiKey: apiKey); } // We keep the API simple and synchronous for the bulk of the app's code @@ -591,11 +593,11 @@ class UpdateMachine { required this.store, required InitialSnapshot initialSnapshot, }) : queueId = initialSnapshot.queueId ?? (() { - // The queueId is optional in the type, but should only be missing in the - // case of unauthenticated access to a web-public realm. We authenticated. - throw Exception("bad initial snapshot: missing queueId"); - })(), - lastEventId = initialSnapshot.lastEventId; + // The queueId is optional in the type, but should only be missing in the + // case of unauthenticated access to a web-public realm. We authenticated. + throw Exception("bad initial snapshot: missing queueId"); + })(), + lastEventId = initialSnapshot.lastEventId; /// Load the user's data from the server, and start an event queue going. /// @@ -616,7 +618,7 @@ class UpdateMachine { initialSnapshot: initialSnapshot, ); final updateMachine = UpdateMachine.fromInitialSnapshot( - store: store, initialSnapshot: initialSnapshot); + store: store, initialSnapshot: initialSnapshot); updateMachine.poll(); // TODO do registerNotificationToken before registerQueue: // https://github.com/zulip/zulip-flutter/pull/325#discussion_r1365982807 @@ -636,7 +638,7 @@ class UpdateMachine { return await registerQueue(connection); } catch (e) { assert(debugLog('Error fetching initial snapshot: $e\n' - 'Backing off, then will retry…')); + 'Backing off, then will retry…')); // TODO tell user if initial-fetch errors persist, or look non-transient await (backoffMachine ??= BackoffMachine()).wait(); assert(debugLog('… Backoff wait complete, retrying initial fetch.')); @@ -680,10 +682,11 @@ class UpdateMachine { final GetEventsResult result; try { result = await getEvents(store.connection, - queueId: queueId, lastEventId: lastEventId); + queueId: queueId, lastEventId: lastEventId); } catch (e) { switch (e) { case ZulipApiException(code: 'BAD_EVENT_QUEUE_ID'): + assert(debugLog('Lost event queue for $store. Replacing…')); await store._globalStore._reloadPerAccount(store.accountId); dispose(); @@ -693,6 +696,7 @@ class UpdateMachine { case Server5xxException() || NetworkException(): assert(debugLog('Transient error polling event queue for $store: $e\n' 'Backing off, then will retry…')); + store.isstale=true; // drives the connecting snackbar // TODO tell user if transient polling errors persist // TODO reset to short backoff eventually await backoffMachine.wait(); diff --git a/lib/widgets/app.dart b/lib/widgets/app.dart index 49b8f202e2..63e8c071f7 100644 --- a/lib/widgets/app.dart +++ b/lib/widgets/app.dart @@ -1,3 +1,4 @@ +// @formatter:off import 'dart:async'; import 'package:flutter/foundation.dart'; @@ -16,6 +17,7 @@ import 'recent_dm_conversations.dart'; import 'store.dart'; import 'subscription_list.dart'; import 'text.dart'; +import 'snackbar.dart'; class ZulipApp extends StatefulWidget { const ZulipApp({super.key, this.navigatorObservers}); @@ -87,7 +89,7 @@ class _ZulipAppState extends State with WidgetsBindingObserver { @override Future didPushRouteInformation(routeInformation) async { if (routeInformation case RouteInformation( - uri: Uri(scheme: 'zulip', host: 'login') && var url) + uri: Uri(scheme: 'zulip', host: 'login') && var url) ) { await LoginPage.handleWebAuthUrl(url); return true; @@ -141,45 +143,45 @@ class _ZulipAppState extends State with WidgetsBindingObserver { ); return GlobalStoreWidget( - child: Builder(builder: (context) { - final globalStore = GlobalStoreWidget.of(context); - // TODO(#524) choose initial account as last one used - final initialAccountId = globalStore.accounts.firstOrNull?.id; - return MaterialApp( - title: 'Zulip', - localizationsDelegates: ZulipLocalizations.localizationsDelegates, - supportedLocales: ZulipLocalizations.supportedLocales, - theme: theme, - - navigatorKey: ZulipApp.navigatorKey, - navigatorObservers: widget.navigatorObservers ?? const [], - builder: (BuildContext context, Widget? child) { - if (!ZulipApp.ready.value) { - SchedulerBinding.instance.addPostFrameCallback( - (_) => widget._declareReady()); - } - GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context); - return child!; - }, - - // We use onGenerateInitialRoutes for the real work of specifying the - // initial nav state. To do that we need [MaterialApp] to decide to - // build a [Navigator]... which means specifying either `home`, `routes`, - // `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`. - // It never actually gets called, though: `onGenerateInitialRoutes` - // handles startup, and then we always push whole routes with methods - // like [Navigator.push], never mere names as with [Navigator.pushNamed]. - onGenerateRoute: (_) => null, - - onGenerateInitialRoutes: (_) { - return [ - MaterialWidgetRoute(page: const ChooseAccountPage()), - if (initialAccountId != null) ...[ - HomePage.buildRoute(accountId: initialAccountId), - InboxPage.buildRoute(accountId: initialAccountId), - ], - ]; - }); + child: Builder(builder: (context) { + final globalStore = GlobalStoreWidget.of(context); + // TODO(#524) choose initial account as last one used + final initialAccountId = globalStore.accounts.firstOrNull?.id; + return MaterialApp( + title: 'Zulip', + localizationsDelegates: ZulipLocalizations.localizationsDelegates, + supportedLocales: ZulipLocalizations.supportedLocales, + theme: theme, + + navigatorKey: ZulipApp.navigatorKey, + navigatorObservers: widget.navigatorObservers ?? const [], + builder: (BuildContext context, Widget? child) { + if (!ZulipApp.ready.value) { + SchedulerBinding.instance.addPostFrameCallback( + (_) => widget._declareReady()); + } + GlobalLocalizations.zulipLocalizations = ZulipLocalizations.of(context); + return child!; + }, + + // We use onGenerateInitialRoutes for the real work of specifying the + // initial nav state. To do that we need [MaterialApp] to decide to + // build a [Navigator]... which means specifying either `home`, `routes`, + // `onGenerateRoute`, or `onUnknownRoute`. Make it `onGenerateRoute`. + // It never actually gets called, though: `onGenerateInitialRoutes` + // handles startup, and then we always push whole routes with methods + // like [Navigator.push], never mere names as with [Navigator.pushNamed]. + onGenerateRoute: (_) => null, + + onGenerateInitialRoutes: (_) { + return [ + MaterialWidgetRoute(page: const ChooseAccountPage()), + if (initialAccountId != null) ...[ + HomePage.buildRoute(accountId: initialAccountId), + InboxPage.buildRoute(accountId: initialAccountId), + ], + ]; + }); })); } } @@ -194,18 +196,18 @@ class ChooseAccountPage extends StatelessWidget { const ChooseAccountPage({super.key}); Widget _buildAccountItem( - BuildContext context, { - required int accountId, - required Widget title, - Widget? subtitle, - }) { + BuildContext context, { + required int accountId, + required Widget title, + Widget? subtitle, + }) { return Card( - clipBehavior: Clip.hardEdge, - child: ListTile( - title: title, - subtitle: subtitle, - onTap: () => Navigator.push(context, - HomePage.buildRoute(accountId: accountId)))); + clipBehavior: Clip.hardEdge, + child: ListTile( + title: title, + subtitle: subtitle, + onTap: () => Navigator.push(context, + HomePage.buildRoute(accountId: accountId)))); } @override @@ -214,31 +216,31 @@ class ChooseAccountPage extends StatelessWidget { assert(!PerAccountStoreWidget.debugExistsOf(context)); final globalStore = GlobalStoreWidget.of(context); return Scaffold( - appBar: AppBar( - title: Text(zulipLocalizations.chooseAccountPageTitle), - actions: const [ChooseAccountPageOverflowButton()]), - body: SafeArea( - minimum: const EdgeInsets.fromLTRB(8, 0, 8, 8), - child: Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 400), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Flexible(child: SingleChildScrollView( - padding: const EdgeInsets.only(top: 8), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - for (final (:accountId, :account) in globalStore.accountEntries) - _buildAccountItem(context, - accountId: accountId, - title: Text(account.realmUrl.toString()), - subtitle: Text(account.email)), - ]))), - const SizedBox(height: 12), - ElevatedButton( - onPressed: () => Navigator.push(context, - AddAccountPage.buildRoute()), - child: Text(zulipLocalizations.chooseAccountButtonAddAnAccount)), - ]))), - )); + appBar: AppBar( + title: Text(zulipLocalizations.chooseAccountPageTitle), + actions: const [ChooseAccountPageOverflowButton()]), + body: SafeArea( + minimum: const EdgeInsets.fromLTRB(8, 0, 8, 8), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 400), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + Flexible(child: SingleChildScrollView( + padding: const EdgeInsets.only(top: 8), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + for (final (:accountId, :account) in globalStore.accountEntries) + _buildAccountItem(context, + accountId: accountId, + title: Text(account.realmUrl.toString()), + subtitle: Text(account.email)), + ]))), + const SizedBox(height: 12), + ElevatedButton( + onPressed: () => Navigator.push(context, + AddAccountPage.buildRoute()), + child: Text(zulipLocalizations.chooseAccountButtonAddAnAccount)), + ]))), + )); } } @@ -250,17 +252,17 @@ class ChooseAccountPageOverflowButton extends StatelessWidget { @override Widget build(BuildContext context) { return PopupMenuButton( - itemBuilder: (BuildContext context) => const [ - PopupMenuItem( - value: ChooseAccountPageOverflowMenuItem.aboutZulip, - child: Text('About Zulip')), - ], - onSelected: (item) { - switch (item) { - case ChooseAccountPageOverflowMenuItem.aboutZulip: - Navigator.push(context, AboutZulipPage.buildRoute(context)); - } - }); + itemBuilder: (BuildContext context) => const [ + PopupMenuItem( + value: ChooseAccountPageOverflowMenuItem.aboutZulip, + child: Text('About Zulip')), + ], + onSelected: (item) { + switch (item) { + case ChooseAccountPageOverflowMenuItem.aboutZulip: + Navigator.push(context, AboutZulipPage.buildRoute(context)); + } + }); } } @@ -269,7 +271,7 @@ class HomePage extends StatelessWidget { static Route buildRoute({required int accountId}) { return MaterialAccountWidgetRoute(accountId: accountId, - page: const HomePage()); + page: const HomePage()); } @override @@ -278,7 +280,7 @@ class HomePage extends StatelessWidget { final zulipLocalizations = ZulipLocalizations.of(context); InlineSpan bold(String text) => TextSpan( - text: text, style: const TextStyle(fontWeight: FontWeight.bold)); + text: text, style: const TextStyle(fontWeight: FontWeight.bold)); int? testStreamId; if (store.connection.realmUrl.origin == 'https://chat.zulip.org') { @@ -286,52 +288,56 @@ class HomePage extends StatelessWidget { } return Scaffold( - appBar: AppBar(title: const Text("Home")), - body: Center( - child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ - DefaultTextStyle.merge( - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 18), - child: Column(children: [ - const Text('🚧 Under construction 🚧'), - const SizedBox(height: 12), - Text.rich(TextSpan( - text: 'Connected to: ', - children: [bold(store.realmUrl.toString())])), - Text.rich(TextSpan( - text: 'Zulip server version: ', - children: [bold(store.zulipVersion)])), - Text(zulipLocalizations.subscribedToNStreams(store.subscriptions.length)), - ])), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - MessageListPage.buildRoute(context: context, - narrow: const AllMessagesNarrow())), - child: Text(zulipLocalizations.allMessagesPageTitle)), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - InboxPage.buildRoute(context: context)), - child: const Text("Inbox")), // TODO(i18n) - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - SubscriptionListPage.buildRoute(context: context)), - child: const Text("Subscribed streams")), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - RecentDmConversationsPage.buildRoute(context: context)), - child: Text(zulipLocalizations.recentDmConversationsPageTitle)), - if (testStreamId != null) ...[ - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => Navigator.push(context, - MessageListPage.buildRoute(context: context, - narrow: StreamNarrow(testStreamId!))), - child: const Text("#test here")), // scaffolding hack, see above - ], - ]))); + appBar: AppBar(title: const Text("Home")), + body: Center( + child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + DefaultTextStyle.merge( + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 18), + child: Column(children: [ + const Text('🚧 Under construction 🚧'), + const SizedBox(height: 12), + Text.rich(TextSpan( + text: 'Connected to: ', + children: [bold(store.realmUrl.toString())])), + Text.rich(TextSpan( + text: 'Zulip server version: ', + children: [bold(store.zulipVersion)])), + Text(zulipLocalizations.subscribedToNStreams(store.subscriptions.length)), + ])), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: const AllMessagesNarrow())), + child: Text(zulipLocalizations.allMessagesPageTitle)), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + InboxPage.buildRoute(context: context)), + child: const Text("Inbox")), // TODO(i18n) + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + SubscriptionListPage.buildRoute(context: context)), + child: const Text("Subscribed streams")), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + RecentDmConversationsPage.buildRoute(context: context)), + child: Text(zulipLocalizations.recentDmConversationsPageTitle)), + if (testStreamId != null) ...[ + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.push(context, + MessageListPage.buildRoute(context: context, + narrow: StreamNarrow(testStreamId!))), + child: const Text("#test here")), + SizedBox( + height:40, + child:SnackBarPage(isStale:store.isstale), + )// scaffolding hack, see above + ]]))); } } +// @formatter:off \ No newline at end of file diff --git a/lib/widgets/snackbar.dart b/lib/widgets/snackbar.dart new file mode 100644 index 0000000000..3c3e694605 --- /dev/null +++ b/lib/widgets/snackbar.dart @@ -0,0 +1,94 @@ +// @formatter:off +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; + +class SnackBarPage extends StatefulWidget { + final bool isStale; + const SnackBarPage({super.key, required this.isStale}); + @override + SnackBarPageState createState() => SnackBarPageState(); +} + +class SnackBarPageState extends State { + late ConnectivityResult _connectivityResult; // ignore: unused_field + late StreamSubscription _connectivitySubscription; + + @override + void initState() { + super.initState(); + _initConnectivity(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _connectivitySubscription = Connectivity().onConnectivityChanged.listen((result) { + setState(() { + _connectivityResult = result; + }); + showSnackBar(result); + }); + } + + @override + void dispose() { + _connectivitySubscription.cancel(); + super.dispose(); + } + + Future _initConnectivity() async { + final ConnectivityResult connectivityResult = await Connectivity().checkConnectivity(); + setState(() { + _connectivityResult = connectivityResult; + }); + showSnackBar(connectivityResult); + } + + void showSnackBar(ConnectivityResult connectivityResult) { + final bool isConnected = connectivityResult != ConnectivityResult.none; + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + + if (!isConnected) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Row( + children: [ + Icon( + Icons.error_outline, + color: Colors.white, + ), + SizedBox(width: 8), + Text( + 'No Internet Connection', + style: TextStyle(color: Colors.white)),],), + duration: Duration(days: 365), + )); + } + else if (widget.isStale) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Row( + children: [ + Icon( + Icons.sync, + color: Colors.white, + ), + SizedBox(width: 8), + Text( + 'Connecting', + style: TextStyle(color: Colors.white), + )]), + duration: Duration(seconds: 20), + )); + } + } + + @override + Widget build(BuildContext context) { + return const Scaffold( + body: Center( + child: Text(''), + )); + } +} // @formatter:off \ No newline at end of file diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d9af90d6c9..c8f7172f4c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import connectivity_plus import device_info_plus import file_selector_macos import firebase_core @@ -17,6 +18,7 @@ import sqlite3_flutter_libs import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 9610f2458c..a4ae0b5c8d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -201,6 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + 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: "direct main" description: @@ -724,6 +740,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" node_preamble: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 56aa501a8f..b5e24fd067 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + connectivity_plus: ^5.0.0 app_settings: ^5.0.0 collection: ^1.17.2 diff --git a/test/widgets/snackbar_test.dart b/test/widgets/snackbar_test.dart new file mode 100644 index 0000000000..3e938b93c4 --- /dev/null +++ b/test/widgets/snackbar_test.dart @@ -0,0 +1,30 @@ +//@formatter:off +import 'package:flutter/material.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:zulip/widgets/snackbar.dart'; + +void main() { + testWidgets('SnackBarPage displays correct SnackBar based on connectivity', (WidgetTester tester) async { + // connection restored with isStale = true + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (BuildContext context) { + return const SnackBarPage(isStale: true); + })), + ); + + // state of SnackBarPage + final snackBarPageState = tester.state(find.byType(SnackBarPage)); + snackBarPageState.showSnackBar(ConnectivityResult.wifi); + await tester.pump(); + expect(find.text('Connecting'), findsOneWidget); + // showSnackBar with ConnectivityResult.none + snackBarPageState.showSnackBar(ConnectivityResult.none); + // Wait for the widget to rebuild with the new SnackBar + await tester.pump(); + // Verify that the 'Connecting' SnackBar is shown + expect(find.text('No Internet Connection'), findsOneWidget); + }); +} //@formatter:off \ No newline at end of file diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0d4b4d65c2..a44504aafd 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseCorePluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 4a4d9be3e7..c2ced0f6aa 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus file_selector_windows firebase_core share_plus