From 36629f97edbc445179724c4d39e9d11b51768c91 Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Tue, 22 Aug 2023 22:35:31 -0400 Subject: [PATCH 1/6] Account improvements and anonymous instances --- lib/account/bloc/account_bloc.dart | 8 +- lib/account/pages/account_page.dart | 8 +- lib/account/pages/login_page.dart | 163 +++++---- lib/account/widgets/profile_modal_body.dart | 346 ++++++++++++++++---- lib/community/pages/community_page.dart | 21 +- lib/community/widgets/community_drawer.dart | 76 ++++- lib/core/auth/bloc/auth_bloc.dart | 6 + lib/core/auth/bloc/auth_event.dart | 2 + lib/core/auth/helpers/fetch_account.dart | 4 +- lib/core/enums/local_settings.dart | 3 + lib/core/singletons/lemmy_client.dart | 4 +- lib/inbox/bloc/inbox_bloc.dart | 4 +- lib/l10n/app_en.arb | 14 + lib/l10n/app_es.arb | 14 + lib/l10n/app_fi.arb | 14 + lib/l10n/app_pl.arb | 14 + lib/l10n/app_sv.arb | 14 + lib/main.dart | 6 + lib/search/pages/search_page.dart | 12 +- lib/thunder/bloc/thunder_bloc.dart | 49 +++ lib/thunder/bloc/thunder_event.dart | 15 + lib/thunder/bloc/thunder_state.dart | 13 + lib/thunder/pages/thunder_page.dart | 7 + lib/user/pages/user_page.dart | 33 +- lib/user/utils/logout_dialog.dart | 41 +++ 25 files changed, 696 insertions(+), 195 deletions(-) create mode 100644 lib/user/utils/logout_dialog.dart diff --git a/lib/account/bloc/account_bloc.dart b/lib/account/bloc/account_bloc.dart index 2fdaca838..9f9c3ac48 100644 --- a/lib/account/bloc/account_bloc.dart +++ b/lib/account/bloc/account_bloc.dart @@ -67,7 +67,13 @@ class AccountBloc extends Bloc { throw Exception('Error: Timeout when attempting to fetch account details'); }); - return emit(state.copyWith(status: AccountStatus.success, subsciptions: subsciptions, personView: fullPersonView.personView)); + // This eliminates an issue which has plagued me a lot which is that there's a race condition + // with so many calls to GetAccountInformation, we can return success for the new and old account. + if (fullPersonView.personView.person.id == (await fetchActiveProfileAccount())?.userId) { + return emit(state.copyWith(status: AccountStatus.success, subsciptions: subsciptions, personView: fullPersonView.personView)); + } else { + return emit(state.copyWith(status: AccountStatus.success)); + } } catch (e) { exception = e; attemptCount++; diff --git a/lib/account/pages/account_page.dart b/lib/account/pages/account_page.dart index d6f528d48..6e7b13d09 100644 --- a/lib/account/pages/account_page.dart +++ b/lib/account/pages/account_page.dart @@ -5,7 +5,9 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:thunder/account/bloc/account_bloc.dart'; import 'package:thunder/account/utils/profiles.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/user/pages/user_page.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class AccountPage extends StatefulWidget { const AccountPage({super.key}); @@ -21,6 +23,7 @@ class _AccountPageState extends State { AuthState authState = context.read().state; AccountState accountState = context.read().state; + String anonymousInstance = context.watch().state.currentAnonymousInstance; return MultiBlocListener( listeners: [ @@ -46,11 +49,12 @@ class _AccountPageState extends State { children: [ Icon(Icons.people_rounded, size: 100, color: theme.dividerColor), const SizedBox(height: 16), - const Text('Add an account to see your profile', textAlign: TextAlign.center), + Text(AppLocalizations.of(context)!.browsingAnonymously(anonymousInstance), textAlign: TextAlign.center), + Text(AppLocalizations.of(context)!.addAccountToSeeProfile, textAlign: TextAlign.center), const SizedBox(height: 24.0), ElevatedButton( style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(60)), - child: const Text('Manage Accounts'), + child: Text(AppLocalizations.of(context)!.manageAccounts), onPressed: () => showProfileModalSheet(context), ) ], diff --git a/lib/account/pages/login_page.dart b/lib/account/pages/login_page.dart index 3c90577ef..4bb991a73 100644 --- a/lib/account/pages/login_page.dart +++ b/lib/account/pages/login_page.dart @@ -6,17 +6,22 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/core/enums/local_settings.dart'; +import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/shared/snackbar.dart'; +import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/utils/instance.dart'; import 'package:thunder/utils/text_input_formatter.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class LoginPage extends StatefulWidget { final VoidCallback popRegister; + final bool anonymous; - const LoginPage({super.key, required this.popRegister}); + const LoginPage({super.key, required this.popRegister, this.anonymous = false}); @override State createState() => _LoginPageState(); @@ -48,7 +53,7 @@ class _LoginPageState extends State { _instanceTextEditingController = TextEditingController(); _usernameTextEditingController.addListener(() { - if (_usernameTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) { + if (_instanceTextEditingController.text.isNotEmpty && (widget.anonymous || (_usernameTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty))) { setState(() => fieldsFilledIn = true); } else { setState(() => fieldsFilledIn = false); @@ -56,7 +61,7 @@ class _LoginPageState extends State { }); _passwordTextEditingController.addListener(() { - if (_usernameTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) { + if (_instanceTextEditingController.text.isNotEmpty && (widget.anonymous || (_usernameTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty))) { setState(() => fieldsFilledIn = true); } else { setState(() => fieldsFilledIn = false); @@ -69,7 +74,7 @@ class _LoginPageState extends State { currentInstance = _instanceTextEditingController.text; } - if (_usernameTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) { + if (_instanceTextEditingController.text.isNotEmpty && (widget.anonymous || (_usernameTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty))) { setState(() => fieldsFilledIn = true); } else { setState(() => fieldsFilledIn = false); @@ -132,7 +137,7 @@ class _LoginPageState extends State { }); showSnackbar(context, AppLocalizations.of(context)!.loginFailed(state.errorMessage ?? AppLocalizations.of(context)!.missingErrorMessage)); - } else if (state.status == AuthStatus.success) { + } else if (state.status == AuthStatus.success && context.read().state.isLoggedIn) { context.pop(); showSnackbar(context, AppLocalizations.of(context)!.loginSucceeded); @@ -181,73 +186,78 @@ class _LoginPageState extends State { errorMaxLines: 2, ), enableSuggestions: false, + onSubmitted: (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) ? (_) => _addAnonymousInstance() : null, ), - const SizedBox(height: 35.0), - AutofillGroup( - child: Column( - children: [ - TextField( - textInputAction: TextInputAction.next, - autocorrect: false, - controller: _usernameTextEditingController, - autofillHints: const [AutofillHints.username], - decoration: const InputDecoration( - isDense: true, - border: OutlineInputBorder(), - labelText: 'Username', + if (!widget.anonymous) ...[ + const SizedBox(height: 35.0), + AutofillGroup( + child: Column( + children: [ + TextField( + textInputAction: TextInputAction.next, + autocorrect: false, + controller: _usernameTextEditingController, + autofillHints: const [AutofillHints.username], + decoration: const InputDecoration( + isDense: true, + border: OutlineInputBorder(), + labelText: 'Username', + ), + enableSuggestions: false, ), - enableSuggestions: false, - ), - const SizedBox(height: 12.0), - TextField( - onSubmitted: - (!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) - ? (_) => _handleLogin() - : null, - autocorrect: false, - controller: _passwordTextEditingController, - obscureText: !showPassword, - enableSuggestions: false, - maxLength: 60, // This is what lemmy retricts password length to - autofillHints: const [AutofillHints.password], - decoration: InputDecoration( - isDense: true, - border: const OutlineInputBorder(), - labelText: 'Password', - suffixIcon: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: IconButton( - icon: Icon( - showPassword ? Icons.visibility_rounded : Icons.visibility_off_rounded, - semanticLabel: showPassword ? 'Hide Password' : 'Show Password', + const SizedBox(height: 12.0), + TextField( + onSubmitted: + (!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) + ? (_) => _handleLogin() + : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) + ? (_) => _addAnonymousInstance() + : null, + autocorrect: false, + controller: _passwordTextEditingController, + obscureText: !showPassword, + enableSuggestions: false, + maxLength: 60, // This is what lemmy retricts password length to + autofillHints: const [AutofillHints.password], + decoration: InputDecoration( + isDense: true, + border: const OutlineInputBorder(), + labelText: 'Password', + suffixIcon: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: IconButton( + icon: Icon( + showPassword ? Icons.visibility_rounded : Icons.visibility_off_rounded, + semanticLabel: showPassword ? 'Hide Password' : 'Show Password', + ), + onPressed: () { + setState(() { + showPassword = !showPassword; + }); + }, ), - onPressed: () { - setState(() { - showPassword = !showPassword; - }); - }, ), ), ), - ), - ], + ], + ), ), - ), - const SizedBox(height: 12.0), - TextField( - autocorrect: false, - controller: _totpTextEditingController, - maxLength: 6, - keyboardType: TextInputType.number, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], - decoration: const InputDecoration( - isDense: true, - border: OutlineInputBorder(), - labelText: 'TOTP (optional)', - hintText: '000000', + const SizedBox(height: 12.0), + TextField( + autocorrect: false, + controller: _totpTextEditingController, + maxLength: 6, + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + decoration: const InputDecoration( + isDense: true, + border: OutlineInputBorder(), + labelText: 'TOTP (optional)', + hintText: '000000', + ), + enableSuggestions: false, ), - enableSuggestions: false, - ), + ], const SizedBox(height: 12.0), const SizedBox(height: 32.0), ElevatedButton( @@ -260,8 +270,11 @@ class _LoginPageState extends State { ), onPressed: (!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty) ? _handleLogin - : null, - child: Text('Login', style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), + : (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) + ? () => _addAnonymousInstance() + : null, + child: Text(widget.anonymous ? AppLocalizations.of(context)!.add : AppLocalizations.of(context)!.login, + style: theme.textTheme.titleMedium?.copyWith(color: !isLoading && fieldsFilledIn ? theme.colorScheme.onPrimary : theme.colorScheme.primary)), ), TextButton( style: ElevatedButton.styleFrom(minimumSize: const Size.fromHeight(60)), @@ -290,4 +303,22 @@ class _LoginPageState extends State { ), ); } + + void _addAnonymousInstance() async { + if (await isLemmyInstance(_instanceTextEditingController.text)) { + final SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + List anonymousInstances = prefs.getStringList(LocalSettings.anonymousInstances.name) ?? ['lemmy.ml']; + if (anonymousInstances.contains(_instanceTextEditingController.text)) { + setState(() { + instanceValidated = false; + instanceError = AppLocalizations.of(context)!.instanceHasAlreadyBenAdded(currentInstance ?? ''); + }); + } else { + context.read().add(LogOutOfAllAccounts()); + context.read().add(OnAddAnonymousInstance(_instanceTextEditingController.text)); + context.read().add(OnSetCurrentAnonymousInstance(_instanceTextEditingController.text)); + widget.popRegister(); + } + } + } } diff --git a/lib/account/widgets/profile_modal_body.dart b/lib/account/widgets/profile_modal_body.dart index cd9ebb330..30b6e2d3e 100644 --- a/lib/account/widgets/profile_modal_body.dart +++ b/lib/account/widgets/profile_modal_body.dart @@ -8,25 +8,35 @@ import 'package:swipeable_page_route/swipeable_page_route.dart'; import 'package:thunder/account/models/account.dart'; import 'package:thunder/account/pages/login_page.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:thunder/thunder/bloc/thunder_bloc.dart'; +import 'package:thunder/user/utils/logout_dialog.dart'; import 'package:thunder/utils/instance.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -class ProfileModalBody extends StatelessWidget { - const ProfileModalBody({super.key}); +class ProfileModalBody extends StatefulWidget { + final bool anonymous; + + const ProfileModalBody({super.key, this.anonymous = false}); static final GlobalKey shellNavigatorKey = GlobalKey(); - void pushRegister() { - shellNavigatorKey.currentState!.pushNamed("/login"); + @override + State createState() => _ProfileModalBodyState(); +} + +class _ProfileModalBodyState extends State { + void pushRegister({bool anonymous = false}) { + ProfileModalBody.shellNavigatorKey.currentState!.pushNamed("/login", arguments: {'anonymous': anonymous}); } void popRegister() { - shellNavigatorKey.currentState!.pop(); + ProfileModalBody.shellNavigatorKey.currentState!.pop(); } @override Widget build(BuildContext context) { return Navigator( - key: shellNavigatorKey, + key: ProfileModalBody.shellNavigatorKey, onPopPage: (route, result) => false, pages: [MaterialPage(child: ProfileSelect(pushRegister: pushRegister))], onGenerateRoute: _onGenerateRoute, @@ -41,7 +51,7 @@ class ProfileModalBody extends StatelessWidget { break; case '/login': - page = LoginPage(popRegister: popRegister); + page = LoginPage(popRegister: popRegister, anonymous: (settings.arguments as Map)['anonymous']!); break; } return SwipeablePageRoute( @@ -54,88 +64,252 @@ class ProfileModalBody extends StatelessWidget { } class ProfileSelect extends StatefulWidget { - final VoidCallback pushRegister; - const ProfileSelect({Key? key, required this.pushRegister}) : super(key: key); + final void Function({bool anonymous}) pushRegister; + ProfileSelect({Key? key, required this.pushRegister}) : super(key: key); @override State createState() => _ProfileSelectState(); } class _ProfileSelectState extends State { + final GlobalKey _scaffoldMessengerKey = GlobalKey(); List? accounts; + List? anonymousInstances; @override Widget build(BuildContext context) { final theme = Theme.of(context); - String? currentAccountId = context.read().state.account?.id; + String? currentAccountId = context.watch().state.account?.id; + String? currentAnonymousInstance = context.watch().state.currentAnonymousInstance; if (accounts == null) { fetchAccounts(); } - if (accounts != null) { - return ListView.builder( - itemBuilder: (context, index) { - if (index == accounts?.length) { - return Column( - children: [ - if (accounts != null && accounts!.isNotEmpty) const Divider(indent: 16.0, endIndent: 16.0, thickness: 2.0), - ListTile( - leading: const Icon(Icons.add), - title: const Text('Add Account'), - onTap: () => widget.pushRegister(), - ), - ], - ); - } else { - return ListTile( - leading: AnimatedCrossFade( - crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 500), - firstChild: const SizedBox( - width: 40, - child: Icon( - Icons.person, - ), - ), - secondChild: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundImage: accounts![index].instanceIcon == null ? null : CachedNetworkImageProvider(accounts![index].instanceIcon!), - ), - ), - title: Text( - accounts![index].account.username ?? 'N/A', - style: theme.textTheme.titleMedium?.copyWith(), - ), - subtitle: Text(accounts![index].account.instance?.replaceAll('https://', '') ?? 'N/A'), - onTap: (currentAccountId == accounts![index].account.id) - ? null - : () { - context.read().add(SwitchAccount(accountId: accounts![index].account.id)); - context.pop(); - }, - trailing: (currentAccountId == accounts![index].account.id) - ? const InputChip( - label: Text('Active'), - visualDensity: VisualDensity.compact, - ) - : IconButton( - icon: const Icon( - Icons.delete, - semanticLabel: 'Remove Account', - ), - onPressed: () { - context.read().add(RemoveAccount(accountId: accounts![index].account.id)); - context.pop(); - }), - ); - } - }, - itemCount: (accounts?.length ?? 0) + 1, - ); - } else { - return const Center(child: CircularProgressIndicator()); + if (anonymousInstances == null) { + fetchAnonymousInstances(); } + + return BlocListener( + listener: (context, state) {}, + listenWhen: (previous, current) { + if ((previous.anonymousInstances.length != current.anonymousInstances.length) || (previous.currentAnonymousInstance != current.currentAnonymousInstance)) { + anonymousInstances = null; + } + return true; + }, + child: ScaffoldMessenger( + key: _scaffoldMessengerKey, + child: Scaffold( + body: ListView.builder( + itemBuilder: (context, index) { + if (index == (accounts?.length ?? 0) + (anonymousInstances?.length ?? 0)) { + return Column( + children: [ + const Divider(indent: 16.0, endIndent: 16.0, thickness: 2.0), + Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: TextButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(45), + backgroundColor: theme.colorScheme.primaryContainer.withOpacity(0.5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.add), + const SizedBox(width: 8.0), + Text( + AppLocalizations.of(context)!.addAccount, + style: TextStyle(color: theme.colorScheme.onPrimaryContainer), + ), + ], + ), + onPressed: () => widget.pushRegister(), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: TextButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(45), + backgroundColor: theme.colorScheme.secondaryContainer.withOpacity(0.5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.add), + const SizedBox(width: 8.0), + Text( + AppLocalizations.of(context)!.addAnonymousInstance, + style: TextStyle(color: theme.colorScheme.onPrimaryContainer), + ), + ], + ), + onPressed: () => widget.pushRegister(anonymous: true), + ), + ), + ], + ); + } else { + if (index < (accounts?.length ?? 0)) { + return Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: Material( + color: currentAccountId == accounts![index].account.id ? theme.colorScheme.primaryContainer.withOpacity(0.25) : null, + borderRadius: BorderRadius.circular(50), + child: InkWell( + onTap: (currentAccountId == accounts![index].account.id) + ? null + : () { + context.read().add(SwitchAccount(accountId: accounts![index].account.id)); + context.pop(); + }, + borderRadius: BorderRadius.circular(50), + child: ListTile( + leading: AnimatedCrossFade( + crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 500), + firstChild: const SizedBox( + width: 40, + child: Icon( + Icons.person, + ), + ), + secondChild: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: accounts![index].instanceIcon == null ? null : CachedNetworkImageProvider(accounts![index].instanceIcon!), + ), + ), + title: Text( + accounts![index].account.username ?? 'N/A', + style: theme.textTheme.titleMedium?.copyWith(), + ), + subtitle: Text(accounts![index].account.instance?.replaceAll('https://', '') ?? 'N/A'), + trailing: (accounts!.length > 1 || anonymousInstances?.isNotEmpty == true) + ? (currentAccountId == accounts![index].account.id) + ? IconButton( + icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.logOut), + onPressed: () async { + if (await showLogOutDialog(context)) { + await Future.delayed(const Duration(milliseconds: 1500), () { + if ((anonymousInstances?.length ?? 0) > 0) { + context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances!.last.instance)); + } else { + context.read().add(SwitchAccount(accountId: accounts!.lastWhere((account) => account.account.id != currentAccountId).account.id)); + } + setState(() => accounts = null); + }); + } + }, + ) + : IconButton( + icon: Icon( + Icons.delete, + semanticLabel: AppLocalizations.of(context)!.removeAccount, + ), + onPressed: () { + context.read().add(RemoveAccount(accountId: accounts![index].account.id)); + + if ((anonymousInstances?.length ?? 0) > 0) { + context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances!.last.instance)); + } else { + context.read().add(SwitchAccount(accountId: accounts!.lastWhere((account) => account.account.id != currentAccountId).account.id)); + } + setState(() => accounts = null); + }) + : null, + ), + ), + ), + ); + } else { + int realIndex = index - (accounts?.length ?? 0); + return Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: Material( + color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance ? theme.colorScheme.primaryContainer.withOpacity(0.25) : null, + borderRadius: BorderRadius.circular(50), + child: InkWell( + onTap: (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance) + ? null + : () async { + context.read().add(LogOutOfAllAccounts()); + context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances![realIndex].instance)); + context.pop(); + }, + borderRadius: BorderRadius.circular(50), + child: ListTile( + leading: AnimatedCrossFade( + crossFadeState: anonymousInstances![realIndex].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 500), + firstChild: const SizedBox( + width: 40, + child: Icon( + Icons.language, + ), + ), + secondChild: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: anonymousInstances![realIndex].instanceIcon == null ? null : CachedNetworkImageProvider(anonymousInstances![realIndex].instanceIcon!), + ), + ), + title: Row( + children: [ + const Icon( + Icons.person_off_rounded, + size: 15, + ), + const SizedBox(width: 5), + Text( + AppLocalizations.of(context)!.anonymous, + style: theme.textTheme.titleMedium?.copyWith(), + ), + ], + ), + subtitle: Text(anonymousInstances![realIndex].instance), + trailing: ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1) + ? (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance) + ? IconButton( + icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.removeInstance), + onPressed: () async { + context.read().add(OnRemoveAnonymousInstance(anonymousInstances![realIndex].instance)); + + if (anonymousInstances!.length > 1) { + context + .read() + .add(OnSetCurrentAnonymousInstance(anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![realIndex]).instance)); + } else { + context.read().add(SwitchAccount(accountId: accounts!.last.account.id)); + } + + setState(() => anonymousInstances = null); + }, + ) + : IconButton( + icon: Icon( + Icons.delete, + semanticLabel: AppLocalizations.of(context)!.removeInstance, + ), + onPressed: () async { + context.read().add(OnRemoveAnonymousInstance(anonymousInstances![realIndex].instance)); + setState(() { + anonymousInstances = null; + }); + }) + : null, + ), + ), + ), + ); + } + } + }, + itemCount: (accounts?.length ?? 0) + (anonymousInstances?.length ?? 0) + 1, + ), + ), + ), + ); } Future fetchAccounts() async { @@ -160,6 +334,26 @@ class _ProfileSelectState extends State { setState(() => account.instanceIcon = instanceIcon); }); } + + void fetchAnonymousInstances() { + final List anonymousInstances = context.read().state.anonymousInstances.map((instance) => AnonymousInstanceExtended(instance: instance)).toList(); + + fetchAnonymousInstanceIcons(anonymousInstances); + + setState(() { + this.anonymousInstances = anonymousInstances; + }); + } + + Future fetchAnonymousInstanceIcons(List anonymousInstancesExtended) async { + anonymousInstancesExtended.forEach((anonymousInstance) async { + final instanceIcon = await getInstanceIcon(anonymousInstance.instance).timeout( + const Duration(seconds: 3), + onTimeout: () => null, + ); + setState(() => anonymousInstance.instanceIcon = instanceIcon); + }); + } } /// Wrapper class around Account with support for instance icon @@ -170,3 +364,11 @@ class AccountExtended { AccountExtended({required this.account, this.instance, this.instanceIcon}); } + +/// Wrapper class around Account with support for instance icon +class AnonymousInstanceExtended { + String instance; + String? instanceIcon; + + AnonymousInstanceExtended({required this.instance, this.instanceIcon}); +} diff --git a/lib/community/pages/community_page.dart b/lib/community/pages/community_page.dart index 0a0a85edb..64a3add13 100644 --- a/lib/community/pages/community_page.dart +++ b/lib/community/pages/community_page.dart @@ -35,8 +35,9 @@ class CommunityPage extends StatefulWidget { final String? communityName; final GlobalKey? scaffoldKey; final PageController? pageController; + final void Function()? navigateToAccount; - const CommunityPage({super.key, this.communityId, this.communityName, this.scaffoldKey, this.pageController}); + const CommunityPage({super.key, this.communityId, this.communityName, this.scaffoldKey, this.pageController, this.navigateToAccount}); @override State createState() => _CommunityPageState(); @@ -153,7 +154,17 @@ class _CommunityPageState extends State with AutomaticKeepAliveCl return false; }, listener: (context, state) {}, - child: BlocConsumer( + child: BlocListener( + listenWhen: (previous, current) { + if (previous.currentAnonymousInstance != current.currentAnonymousInstance) { + context.read().add(GetCommunityPostsEvent(reset: true, sortType: sortType)); + return true; + } + + return false; + }, + listener: (context, state) {}, + child: BlocConsumer( listener: (c, s) {}, builder: (c, subscriptionsState) { return Scaffold( @@ -248,7 +259,7 @@ class _CommunityPageState extends State with AutomaticKeepAliveCl ) ], ), - drawer: (widget.communityId != null || widget.communityName != null) ? null : const CommunityDrawer(), + drawer: (widget.communityId != null || widget.communityName != null) ? null : CommunityDrawer(navigateToAccount: widget.navigateToAccount), floatingActionButton: enableFab ? AnimatedSwitcher( duration: const Duration(milliseconds: 200), @@ -388,7 +399,9 @@ class _CommunityPageState extends State with AutomaticKeepAliveCl ], ), ); - }), + }, + ), + ), ); }, ), diff --git a/lib/community/widgets/community_drawer.dart b/lib/community/widgets/community_drawer.dart index 379b77eb1..4e99c621b 100644 --- a/lib/community/widgets/community_drawer.dart +++ b/lib/community/widgets/community_drawer.dart @@ -6,11 +6,14 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:thunder/account/bloc/account_bloc.dart'; +import 'package:thunder/account/utils/profiles.dart'; import 'package:thunder/community/bloc/anonymous_subscriptions_bloc.dart'; import 'package:thunder/community/bloc/community_bloc.dart'; import 'package:thunder/core/auth/bloc/auth_bloc.dart'; import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/shared/community_icon.dart'; +import 'package:thunder/shared/user_avatar.dart'; +import 'package:thunder/thunder/bloc/thunder_bloc.dart'; import 'package:thunder/utils/instance.dart'; class Destination { @@ -72,7 +75,9 @@ class DrawerItem extends StatelessWidget { } class CommunityDrawer extends StatefulWidget { - const CommunityDrawer({super.key}); + final void Function()? navigateToAccount; + + const CommunityDrawer({super.key, this.navigateToAccount}); @override State createState() => _CommunityDrawerState(); @@ -95,9 +100,10 @@ class _CommunityDrawerState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); - bool isLoggedIn = context.read().state.isLoggedIn; + bool isLoggedIn = context.watch().state.isLoggedIn; + String anonymousInstance = context.watch().state.currentAnonymousInstance; - AccountStatus status = context.read().state.status; + AccountStatus status = context.watch().state.status; AnonymousSubscriptionsBloc subscriptionsBloc = context.read(); subscriptionsBloc.add(GetSubscribedCommunitiesEvent()); return BlocConsumer( @@ -111,12 +117,62 @@ class _CommunityDrawerState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.fromLTRB(28, 16, 16, 0), - child: Text('Feeds', style: Theme.of(context).textTheme.titleSmall), + padding: const EdgeInsets.fromLTRB(13, 16, 16, 0), + child: TextButton( + style: TextButton.styleFrom( + alignment: Alignment.centerLeft, + minimumSize: const Size.fromHeight(50), + ), + onPressed: () => widget.navigateToAccount?.call(), + child: Row( + children: [ + UserAvatar( + person: isLoggedIn ? context.read().state.personView?.person : null, + radius: 16.0, + ), + const SizedBox(width: 16.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + if (!isLoggedIn) ...[ + const Icon( + Icons.person_off_rounded, + size: 15, + ), + const SizedBox(width: 5), + ], + Text( + isLoggedIn ? context.read().state.personView?.person.name ?? '' : AppLocalizations.of(context)!.anonymous, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + Text( + isLoggedIn ? context.read().state.personView?.instanceHost ?? '' : anonymousInstance, + style: Theme.of(context).textTheme.bodyMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + Expanded( + child: Container(), + ), + IconButton( + icon: const Icon(Icons.more_vert_outlined), + onPressed: () => showProfileModalSheet(context), + ), + ], + ), + ), ), Padding( - padding: const EdgeInsets.fromLTRB(28, 0, 16, 10), - child: Text(LemmyClient.instance.lemmyApiV3.host, style: Theme.of(context).textTheme.bodyMedium), + padding: const EdgeInsets.fromLTRB(28, 16, 16, 0), + child: Text('Feeds', style: Theme.of(context).textTheme.titleSmall), ), Column( children: destinations.map((Destination destination) { @@ -139,16 +195,12 @@ class _CommunityDrawerState extends State { padding: const EdgeInsets.fromLTRB(28, 16, 16, 0), child: Text(AppLocalizations.of(context)!.subscriptions, style: Theme.of(context).textTheme.titleSmall), ), - Padding( - padding: const EdgeInsets.fromLTRB(28, 0, 16, 10), - child: context.read().state.account != null ? Text(context.read().state.account!.username ?? "-", style: Theme.of(context).textTheme.bodyMedium) : Container(), - ), (status != AccountStatus.success && status != AccountStatus.failure) ? const Padding( padding: EdgeInsets.all(16.0), child: Center(child: CircularProgressIndicator()), ) - : (context.read().state.subsciptions.isNotEmpty || subscriptionsBloc.state.subscriptions.isNotEmpty) + : ((isLoggedIn && context.read().state.subsciptions.isNotEmpty) || subscriptionsBloc.state.subscriptions.isNotEmpty) ? (Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 14.0), diff --git a/lib/core/auth/bloc/auth_bloc.dart b/lib/core/auth/bloc/auth_bloc.dart index c4c479a63..fe6ba9a3b 100644 --- a/lib/core/auth/bloc/auth_bloc.dart +++ b/lib/core/auth/bloc/auth_bloc.dart @@ -139,5 +139,11 @@ class AuthBloc extends Bloc { return emit(state.copyWith(status: AuthStatus.failure, account: null, isLoggedIn: false, errorMessage: e.toString())); } }); + + on((event, emit) async { + final SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + prefs.setString('active_profile_id', ''); + return emit(state.copyWith(status: AuthStatus.success, isLoggedIn: false)); + }); } } diff --git a/lib/core/auth/bloc/auth_event.dart b/lib/core/auth/bloc/auth_event.dart index 19bceac8e..e6990ffbd 100644 --- a/lib/core/auth/bloc/auth_event.dart +++ b/lib/core/auth/bloc/auth_event.dart @@ -33,3 +33,5 @@ class SwitchAccount extends AuthEvent { const SwitchAccount({required this.accountId}); } + +class LogOutOfAllAccounts extends AuthEvent {} diff --git a/lib/core/auth/helpers/fetch_account.dart b/lib/core/auth/helpers/fetch_account.dart index 49c1794e5..010058ce4 100644 --- a/lib/core/auth/helpers/fetch_account.dart +++ b/lib/core/auth/helpers/fetch_account.dart @@ -10,7 +10,9 @@ Future fetchActiveProfileAccount() async { Account? account = (accountId != null) ? await Account.fetchAccount(accountId) : null; // Update the baseUrl if account was found - if (account?.instance != null && account!.instance != LemmyClient.instance.lemmyApiV3.host) LemmyClient.instance.changeBaseUrl(account.instance!.replaceAll('https://', '')); + if (account?.instance != null && account!.instance != LemmyClient.instance.lemmyApiV3.host) { + LemmyClient.instance.changeBaseUrl(account.instance!.replaceAll('https://', '')); + } return account; } diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index 309b3b864..8e90e1ab6 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -92,6 +92,9 @@ enum LocalSettings { postFabSinglePressAction(name: 'settings_post_fab_single_press_action', label: ''), postFabLongPressAction(name: 'settings_post_fab_long_press_action', label: ''), enableCommentNavigation(name: 'setting_enable_comment_navigation', label: 'Enable Comment Navigation Buttons'), + + anonymousInstances(name: 'setting_anonymous_instances', label: ''), + currentAnonymousInstance(name: 'setting_current_anonymous_instance', label: ''), ; const LocalSettings({ diff --git a/lib/core/singletons/lemmy_client.dart b/lib/core/singletons/lemmy_client.dart index 969c9983f..51926b41b 100644 --- a/lib/core/singletons/lemmy_client.dart +++ b/lib/core/singletons/lemmy_client.dart @@ -1,9 +1,7 @@ -import 'package:flutter_dotenv/flutter_dotenv.dart'; - import 'package:lemmy_api_client/v3.dart'; class LemmyClient { - LemmyApiV3 lemmyApiV3 = LemmyApiV3(dotenv.env['LEMMY_BASE_URL'] ?? 'lemmy.ml'); + LemmyApiV3 lemmyApiV3 = const LemmyApiV3(''); LemmyClient._initialize(); diff --git a/lib/inbox/bloc/inbox_bloc.dart b/lib/inbox/bloc/inbox_bloc.dart index 413e5b52c..4e3cfef96 100644 --- a/lib/inbox/bloc/inbox_bloc.dart +++ b/lib/inbox/bloc/inbox_bloc.dart @@ -178,9 +178,9 @@ class InboxBloc extends Bloc { } } - emit(state.copyWith(status: InboxStatus.failure, errorMessage: exception.toString())); + emit(state.copyWith(status: InboxStatus.failure, errorMessage: exception.toString(), totalUnreadCount: 0)); } catch (e) { - emit(state.copyWith(status: InboxStatus.failure, errorMessage: e.toString())); + emit(state.copyWith(status: InboxStatus.failure, errorMessage: e.toString(), totalUnreadCount: 0)); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 7f5cfbd8a..fe328d1c4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,4 +1,18 @@ { + "browsingAnonymously": "You are currently browsing {instance} anonymously.", + "addAccountToSeeProfile": "Add an account to see your profile.", + "manageAccounts": "Manage Accounts", + "add": "Add", + "login": "Login", + "instanceHasAlreadyBenAdded": "{instance} has already been added.", + "addAccount": "Add Account", + "addAnonymousInstance": "Add Anonymous Instance", + "removeAccount": "Remove Account", + "anonymous": "Anonymous", + "removeInstance": "Remove instance", + "searchCommunitiesFederatedWith": "Search for communities federated with {instance}", + "confirmLogOut": "Are you sure you want to log out?", + "cancel": "Cancel", "feed": "Feed", "search": "Search", "account": "Account", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 0d7a261ae..16fcdcd8c 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -1,4 +1,18 @@ { + "browsingAnonymously": "You are currently browsing {instance} anonymously.", + "addAccountToSeeProfile": "Add an account to see your profile.", + "manageAccounts": "Manage Accounts", + "add": "Add", + "login": "Login", + "instanceHasAlreadyBenAdded": "{instance} has already been added.", + "addAccount": "Add Account", + "addAnonymousInstance": "Add Anonymous Instance", + "removeAccount": "Remove Account", + "anonymous": "Anonymous", + "removeInstance": "Remove instance", + "searchCommunitiesFederatedWith": "Search for communities federated with {instance}", + "confirmLogOut": "Are you sure you want to log out?", + "cancel": "Cancel", "feed": "Feed", "search": "Buscar", "account": "Cuenta", diff --git a/lib/l10n/app_fi.arb b/lib/l10n/app_fi.arb index c6f532a86..89ef43188 100644 --- a/lib/l10n/app_fi.arb +++ b/lib/l10n/app_fi.arb @@ -1,4 +1,18 @@ { + "browsingAnonymously": "You are currently browsing {instance} anonymously.", + "addAccountToSeeProfile": "Add an account to see your profile.", + "manageAccounts": "Manage Accounts", + "add": "Add", + "login": "Login", + "instanceHasAlreadyBenAdded": "{instance} has already been added.", + "addAccount": "Add Account", + "addAnonymousInstance": "Add Anonymous Instance", + "removeAccount": "Remove Account", + "anonymous": "Anonymous", + "removeInstance": "Remove instance", + "searchCommunitiesFederatedWith": "Search for communities federated with {instance}", + "confirmLogOut": "Are you sure you want to log out?", + "cancel": "Cancel", "feed": "Feed", "search": "Search", "account": "Account", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index ab82212be..2976d0fdf 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -1,4 +1,18 @@ { + "browsingAnonymously": "You are currently browsing {instance} anonymously.", + "addAccountToSeeProfile": "Add an account to see your profile.", + "manageAccounts": "Manage Accounts", + "add": "Add", + "login": "Login", + "instanceHasAlreadyBenAdded": "{instance} has already been added.", + "addAccount": "Add Account", + "addAnonymousInstance": "Add Anonymous Instance", + "removeAccount": "Remove Account", + "anonymous": "Anonymous", + "removeInstance": "Remove instance", + "searchCommunitiesFederatedWith": "Search for communities federated with {instance}", + "confirmLogOut": "Are you sure you want to log out?", + "cancel": "Cancel", "feed": "Feed", "search": "Wyszukaj", "account": "Konto", diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index c6f532a86..89ef43188 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -1,4 +1,18 @@ { + "browsingAnonymously": "You are currently browsing {instance} anonymously.", + "addAccountToSeeProfile": "Add an account to see your profile.", + "manageAccounts": "Manage Accounts", + "add": "Add", + "login": "Login", + "instanceHasAlreadyBenAdded": "{instance} has already been added.", + "addAccount": "Add Account", + "addAnonymousInstance": "Add Anonymous Instance", + "removeAccount": "Remove Account", + "anonymous": "Anonymous", + "removeInstance": "Remove instance", + "searchCommunitiesFederatedWith": "Search for communities federated with {instance}", + "confirmLogOut": "Are you sure you want to log out?", + "cancel": "Cancel", "feed": "Feed", "search": "Search", "account": "Account", diff --git a/lib/main.dart b/lib/main.dart index 273917508..b6e5249b9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,9 @@ import 'package:overlay_support/overlay_support.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:thunder/core/enums/local_settings.dart'; +import 'package:thunder/core/singletons/lemmy_client.dart'; +import 'package:thunder/core/singletons/preferences.dart'; // Internal Packages import 'package:thunder/routes.dart'; @@ -31,6 +34,9 @@ void main() async { // Load up sqlite database await DB.instance.database; + final String initialInstance = (await UserPreferences.instance).sharedPreferences.getString(LocalSettings.currentAnonymousInstance.name) ?? 'lemmy.ml'; + LemmyClient.instance.changeBaseUrl(initialInstance); + runApp(const ThunderApp()); } diff --git a/lib/search/pages/search_page.dart b/lib/search/pages/search_page.dart index 560d4428b..ad138ab08 100644 --- a/lib/search/pages/search_page.dart +++ b/lib/search/pages/search_page.dart @@ -121,6 +121,14 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi _previousUserId = activeProfile?.userId; } }), + BlocListener( + listener: (context, state) { + _controller.clear(); + context.read().add(ResetSearch()); + setState(() {}); + _previousUserId = null; + }, + ), ], child: BlocBuilder( builder: (context, state) { @@ -209,6 +217,8 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi Widget _getSearchBody(BuildContext context, SearchState state) { final theme = Theme.of(context); final bool isUserLoggedIn = context.read().state.isLoggedIn; + final String? accountInstance = context.read().state.account?.instance; + final String currentAnonymousInstance = context.read().state.currentAnonymousInstance; switch (state.status) { case SearchStatus.initial: @@ -223,7 +233,7 @@ class _SearchPageState extends State with AutomaticKeepAliveClientMi Padding( padding: const EdgeInsets.symmetric(horizontal: 32.0), child: Text( - 'Search for communities federated with ${lemmyClient.lemmyApiV3.host}', + AppLocalizations.of(context)!.searchCommunitiesFederatedWith((isUserLoggedIn ? accountInstance : currentAnonymousInstance) ?? ''), textAlign: TextAlign.center, style: theme.textTheme.titleMedium?.copyWith(color: theme.dividerColor), ), diff --git a/lib/thunder/bloc/thunder_bloc.dart b/lib/thunder/bloc/thunder_bloc.dart index 1f8a46d0c..a001bd56c 100644 --- a/lib/thunder/bloc/thunder_bloc.dart +++ b/lib/thunder/bloc/thunder_bloc.dart @@ -5,6 +5,7 @@ import 'package:lemmy_api_client/v3.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:stream_transform/stream_transform.dart'; +import 'package:thunder/account/models/account.dart'; import 'package:thunder/core/enums/custom_theme_type.dart'; import 'package:thunder/core/enums/fab_action.dart'; @@ -14,6 +15,7 @@ import 'package:thunder/core/enums/nested_comment_indicator.dart'; import 'package:thunder/core/enums/swipe_action.dart'; import 'package:thunder/core/enums/theme_type.dart'; import 'package:thunder/core/models/version.dart'; +import 'package:thunder/core/singletons/lemmy_client.dart'; import 'package:thunder/core/singletons/preferences.dart'; import 'package:thunder/core/update/check_github_update.dart'; import 'package:thunder/utils/constants.dart'; @@ -54,6 +56,18 @@ class ThunderBloc extends Bloc { _onFabSummonToggle, transformer: throttleDroppable(throttleDuration), ); + on( + _onAddAnonymousInstance, + transformer: throttleDroppable(throttleDuration), + ); + on( + _onRemoveAnonymousInstance, + transformer: throttleDroppable(throttleDuration), + ); + on( + _onSetCurrentAnonymousInstance, + transformer: throttleDroppable(throttleDuration), + ); } /// This event should be triggered at the start of the app. @@ -184,6 +198,11 @@ class ThunderBloc extends Bloc { bool enableCommentNavigation = prefs.getBool(LocalSettings.enableCommentNavigation.name) ?? true; + List anonymousInstances = prefs.getStringList(LocalSettings.anonymousInstances.name) ?? + // If the user already has some accouts (i.e., an upgrade), we don't want to just throw an anonymous instance at them + ((await Account.accounts()).isNotEmpty ? [] : ['lemmy.ml']); + String currentAnonymousInstance = prefs.getString(LocalSettings.currentAnonymousInstance.name) ?? 'lemmy.ml'; + return emit(state.copyWith( status: ThunderStatus.success, @@ -284,6 +303,9 @@ class ThunderBloc extends Bloc { postFabLongPressAction: postFabLongPressAction, enableCommentNavigation: enableCommentNavigation, + + anonymousInstances: anonymousInstances, + currentAnonymousInstance: currentAnonymousInstance, )); } catch (e) { return emit(state.copyWith(status: ThunderStatus.failure, errorMessage: e.toString())); @@ -305,4 +327,31 @@ class ThunderBloc extends Bloc { void _onFabSummonToggle(OnFabSummonToggle event, Emitter emit) { emit(state.copyWith(isFabSummoned: !state.isFabSummoned)); } + + void _onAddAnonymousInstance(OnAddAnonymousInstance event, Emitter emit) async { + final SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + + prefs.setStringList(LocalSettings.anonymousInstances.name, [...state.anonymousInstances, event.instance]); + + emit(state.copyWith(anonymousInstances: [...state.anonymousInstances, event.instance])); + } + + void _onRemoveAnonymousInstance(OnRemoveAnonymousInstance event, Emitter emit) async { + final List instances = state.anonymousInstances; + instances.remove(event.instance); + + final SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + prefs.setStringList(LocalSettings.anonymousInstances.name, instances); + + emit(state.copyWith(anonymousInstances: instances)); + } + + void _onSetCurrentAnonymousInstance(OnSetCurrentAnonymousInstance event, Emitter emit) async { + LemmyClient.instance.changeBaseUrl(event.instance); + + final SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences; + prefs.setString(LocalSettings.currentAnonymousInstance.name, event.instance); + + emit(state.copyWith(currentAnonymousInstance: event.instance)); + } } diff --git a/lib/thunder/bloc/thunder_event.dart b/lib/thunder/bloc/thunder_event.dart index b67cd48f6..894f052ad 100644 --- a/lib/thunder/bloc/thunder_event.dart +++ b/lib/thunder/bloc/thunder_event.dart @@ -27,3 +27,18 @@ class OnFabSummonToggle extends ThunderEvent { final bool isFabSummoned; const OnFabSummonToggle(this.isFabSummoned); } + +class OnAddAnonymousInstance extends ThunderEvent { + final String instance; + const OnAddAnonymousInstance(this.instance); +} + +class OnRemoveAnonymousInstance extends ThunderEvent { + final String instance; + const OnRemoveAnonymousInstance(this.instance); +} + +class OnSetCurrentAnonymousInstance extends ThunderEvent { + final String instance; + const OnSetCurrentAnonymousInstance(this.instance); +} diff --git a/lib/thunder/bloc/thunder_state.dart b/lib/thunder/bloc/thunder_state.dart index dfdb9fc7a..c12b0b930 100644 --- a/lib/thunder/bloc/thunder_state.dart +++ b/lib/thunder/bloc/thunder_state.dart @@ -104,6 +104,8 @@ class ThunderState extends Equatable { this.postFabSinglePressAction = PostFabAction.replyToPost, this.postFabLongPressAction = PostFabAction.openFab, this.enableCommentNavigation = true, + this.anonymousInstances = const ['lemmy.ml'], + this.currentAnonymousInstance = 'lemmy.ml', /// --------------------------------- UI Events --------------------------------- // Scroll to top event @@ -221,6 +223,9 @@ class ThunderState extends Equatable { final bool enableCommentNavigation; + final List anonymousInstances; + final String currentAnonymousInstance; + /// --------------------------------- UI Events --------------------------------- // Scroll to top event final int scrollToTopId; @@ -332,6 +337,8 @@ class ThunderState extends Equatable { PostFabAction? postFabSinglePressAction, PostFabAction? postFabLongPressAction, bool? enableCommentNavigation, + List? anonymousInstances, + String? currentAnonymousInstance, /// --------------------------------- UI Events --------------------------------- // Scroll to top event @@ -447,6 +454,9 @@ class ThunderState extends Equatable { enableCommentNavigation: enableCommentNavigation ?? this.enableCommentNavigation, + anonymousInstances: anonymousInstances ?? this.anonymousInstances, + currentAnonymousInstance: currentAnonymousInstance ?? this.currentAnonymousInstance, + /// --------------------------------- UI Events --------------------------------- // Scroll to top event scrollToTopId: scrollToTopId ?? this.scrollToTopId, @@ -561,6 +571,9 @@ class ThunderState extends Equatable { enableCommentNavigation, + anonymousInstances, + currentAnonymousInstance, + /// --------------------------------- UI Events --------------------------------- // Scroll to top event scrollToTopId, diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index 9dddd3227..26fc15374 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -208,6 +208,13 @@ class _ThunderState extends State { CommunityPage( scaffoldKey: _feedScaffoldKey, pageController: pageController, + navigateToAccount: () { + _feedScaffoldKey.currentState?.closeDrawer(); + setState(() { + selectedPageIndex = 2; + pageController.animateToPage(2, duration: const Duration(milliseconds: 500), curve: Curves.ease); + }); + }, ), const SearchPage(), const AccountPage(), diff --git a/lib/user/pages/user_page.dart b/lib/user/pages/user_page.dart index a11cdfb17..2455f86ef 100644 --- a/lib/user/pages/user_page.dart +++ b/lib/user/pages/user_page.dart @@ -14,6 +14,7 @@ import 'package:thunder/user/bloc/user_bloc.dart'; import 'package:thunder/user/pages/user_settings_page.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:thunder/user/utils/logout_dialog.dart'; class UserPage extends StatefulWidget { final int? userId; @@ -36,37 +37,7 @@ class _UserPageState extends State { scrolledUnderElevation: 0, leading: widget.isAccountUser ? IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => BlocProvider.value( - value: context.read(), - child: AlertDialog( - title: Text( - 'Are you sure you want to log out?', - style: Theme.of(context).textTheme.bodyLarge, - ), - actions: [ - TextButton( - onPressed: () { - context.pop(); - }, - child: const Text('Cancel')), - const SizedBox( - width: 12, - ), - FilledButton( - onPressed: () { - context.read().add(RemoveAccount( - accountId: context.read().state.account!.id, - )); - context.pop(); - }, - child: const Text('Log out')) - ], - ), - )); - }, + onPressed: () => showLogOutDialog(context), icon: Icon( Icons.logout, semanticLabel: AppLocalizations.of(context)!.logOut, diff --git a/lib/user/utils/logout_dialog.dart b/lib/user/utils/logout_dialog.dart new file mode 100644 index 000000000..d56a2c6f7 --- /dev/null +++ b/lib/user/utils/logout_dialog.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; +import 'package:thunder/core/auth/bloc/auth_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +Future showLogOutDialog(BuildContext context) async { + bool result = false; + await showDialog( + context: context, + builder: (context) => BlocProvider.value( + value: context.read(), + child: AlertDialog( + title: Text( + AppLocalizations.of(context)!.confirmLogOut, + style: Theme.of(context).textTheme.bodyLarge, + ), + actions: [ + TextButton( + onPressed: () { + result = false; + context.pop(); + }, + child: Text(AppLocalizations.of(context)!.cancel)), + const SizedBox( + width: 12, + ), + FilledButton( + onPressed: () { + result = true; + context.read().add(RemoveAccount( + accountId: context.read().state.account!.id, + )); + context.pop(); + }, + child: Text(AppLocalizations.of(context)!.logOut)) + ], + ), + )); + return result; +} From 325ca94c646ed78b4922fed1d511100f68ca141c Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Tue, 22 Aug 2023 22:41:52 -0400 Subject: [PATCH 2/6] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc267ede8..ca5847626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Added access to saved comments from account page - contribution from @CTalvio - Added Polish translation - contribution from @pazdikan - Show default avatar for users without an avatar - contribution from @coslu +- Improved account switching and added anonymous browsing mode for any intance - contribution from @micahmo ### Changed From 453f111c4d97ded10f7937cacd820ee643f89e06 Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Sat, 2 Sep 2023 23:26:45 -0400 Subject: [PATCH 3/6] Fix merge --- lib/account/widgets/profile_modal_body.dart | 416 +++++++++++++++++--- 1 file changed, 359 insertions(+), 57 deletions(-) diff --git a/lib/account/widgets/profile_modal_body.dart b/lib/account/widgets/profile_modal_body.dart index af0d6481f..c85793b30 100644 --- a/lib/account/widgets/profile_modal_body.dart +++ b/lib/account/widgets/profile_modal_body.dart @@ -87,56 +87,344 @@ class _ProfileSelectState extends State { fetchAccounts(); } - if (accounts != null) { - return ListView.builder( - itemBuilder: (context, index) { - if (index == accounts?.length) { - return Column( - children: [ - if (accounts != null && accounts!.isNotEmpty) const Divider(indent: 16.0, endIndent: 16.0, thickness: 2.0), - ListTile( - leading: const Icon(Icons.add), - title: const Text('Add Account'), - onTap: () => widget.pushRegister(), - ), - ], - ); - } else { - return ListTile( - leading: AnimatedCrossFade( - crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: const Duration(milliseconds: 500), - firstChild: const SizedBox( - width: 40, - child: Icon( - Icons.person, - ), - ), - secondChild: CircleAvatar( - backgroundColor: Colors.transparent, - foregroundImage: accounts![index].instanceIcon == null ? null : CachedNetworkImageProvider(accounts![index].instanceIcon!), - ), - ), - title: Text( - accounts![index].account.username ?? 'N/A', - style: theme.textTheme.titleMedium?.copyWith(), - ), - subtitle: Text(accounts![index].account.instance?.replaceAll('https://', '') ?? 'N/A'), - onTap: (currentAccountId == accounts![index].account.id) - ? null - : () { - context.read().add(SwitchAccount(accountId: accounts![index].account.id)); - context.pop(); - }, - trailing: (currentAccountId == accounts![index].account.id) - ? const InputChip( - label: Text('Active'), - visualDensity: VisualDensity.compact, - ) - : IconButton( - icon: const Icon( - Icons.delete, - semanticLabel: 'Remove Account', + if (anonymousInstances == null) { + fetchAnonymousInstances(); + } + + return BlocListener( + listener: (context, state) {}, + listenWhen: (previous, current) { + if ((previous.anonymousInstances.length != current.anonymousInstances.length) || (previous.currentAnonymousInstance != current.currentAnonymousInstance)) { + anonymousInstances = null; + } + return true; + }, + child: ScaffoldMessenger( + key: _scaffoldMessengerKey, + child: Scaffold( + body: ListView.builder( + itemBuilder: (context, index) { + if (index == (accounts?.length ?? 0) + (anonymousInstances?.length ?? 0)) { + return Column( + children: [ + const Divider(indent: 16.0, endIndent: 16.0, thickness: 2.0), + Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: TextButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(45), + backgroundColor: theme.colorScheme.primaryContainer.withOpacity(0.5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.add), + const SizedBox(width: 8.0), + Text( + AppLocalizations.of(context)!.addAccount, + style: TextStyle(color: theme.colorScheme.onPrimaryContainer), + ), + ], + ), + onPressed: () => widget.pushRegister(), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: TextButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(45), + backgroundColor: theme.colorScheme.secondaryContainer.withOpacity(0.5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.add), + const SizedBox(width: 8.0), + Text( + AppLocalizations.of(context)!.addAnonymousInstance, + style: TextStyle(color: theme.colorScheme.onPrimaryContainer), + ), + ], + ), + onPressed: () => widget.pushRegister(anonymous: true), + ), + ), + ], + ); + } else { + if (index < (accounts?.length ?? 0)) { + return Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: Material( + color: currentAccountId == accounts![index].account.id ? HSLColor.fromColor(theme.colorScheme.primaryContainer).withLightness(0.95).toColor() : null, + borderRadius: BorderRadius.circular(50), + child: InkWell( + onTap: (currentAccountId == accounts![index].account.id) + ? null + : () { + context.read().add(SwitchAccount(accountId: accounts![index].account.id)); + context.pop(); + }, + borderRadius: BorderRadius.circular(50), + child: ListTile( + leading: Stack( + children: [ + AnimatedCrossFade( + crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 500), + firstChild: const SizedBox( + width: 40, + child: Padding( + padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), + child: Icon( + Icons.person, + ), + ), + ), + secondChild: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: accounts![index].instanceIcon == null ? null : CachedNetworkImageProvider(accounts![index].instanceIcon!), + maxRadius: 20, + ), + ), + // This widget creates a slight border around the status indicator + Positioned( + right: 0, + bottom: 0, + child: SizedBox( + width: 12, + height: 12, + child: Material( + borderRadius: BorderRadius.circular(10), + color: currentAccountId == accounts![index].account.id ? HSLColor.fromColor(theme.colorScheme.primaryContainer).withLightness(0.95).toColor() : null, + ), + ), + ), + // This is the status indicator + Positioned( + right: 1, + bottom: 1, + child: AnimatedOpacity( + opacity: accounts![index].alive == null ? 0 : 1, + duration: const Duration(milliseconds: 500), + child: Icon( + accounts![index].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, + size: 10, + color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), accounts![index].alive == true ? Colors.green : Colors.red), + ), + ), + ), + ], + ), + title: Text( + accounts![index].account.username ?? 'N/A', + style: theme.textTheme.titleMedium?.copyWith(), + ), + subtitle: Row( + children: [ + Text(accounts![index].account.instance?.replaceAll('https://', '') ?? 'N/A'), + AnimatedOpacity( + opacity: accounts![index].latency == null ? 0 : 1, + duration: const Duration(milliseconds: 500), + child: Row( + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), + ), + const SizedBox(width: 5), + Text( + '${accounts![index].latency?.inMilliseconds}ms', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), + ), + ], + ), + ), + ], + ), + trailing: (accounts!.length > 1 || anonymousInstances?.isNotEmpty == true) + ? (currentAccountId == accounts![index].account.id) + ? IconButton( + icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.logOut), + onPressed: () async { + if (await showLogOutDialog(context)) { + await Future.delayed(const Duration(milliseconds: 1500), () { + if ((anonymousInstances?.length ?? 0) > 0) { + context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances!.last.instance)); + } else { + context.read().add(SwitchAccount(accountId: accounts!.lastWhere((account) => account.account.id != currentAccountId).account.id)); + } + setState(() => accounts = null); + }); + } + }, + ) + : IconButton( + icon: Icon( + Icons.delete, + semanticLabel: AppLocalizations.of(context)!.removeAccount, + ), + onPressed: () { + context.read().add(RemoveAccount(accountId: accounts![index].account.id)); + + if ((anonymousInstances?.length ?? 0) > 0) { + context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances!.last.instance)); + } else { + context.read().add(SwitchAccount(accountId: accounts!.lastWhere((account) => account.account.id != currentAccountId).account.id)); + } + setState(() => accounts = null); + }) + : null, + ), + ), + ), + ); + } else { + int realIndex = index - (accounts?.length ?? 0); + return Padding( + padding: const EdgeInsets.fromLTRB(10, 5, 10, 5), + child: Material( + color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance + ? HSLColor.fromColor(theme.colorScheme.primaryContainer).withLightness(0.95).toColor() + : null, + borderRadius: BorderRadius.circular(50), + child: InkWell( + onTap: (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance) + ? null + : () async { + context.read().add(LogOutOfAllAccounts()); + context.read().add(OnSetCurrentAnonymousInstance(anonymousInstances![realIndex].instance)); + context.pop(); + }, + borderRadius: BorderRadius.circular(50), + child: ListTile( + leading: Stack( + children: [ + AnimatedCrossFade( + crossFadeState: anonymousInstances![realIndex].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 500), + firstChild: const SizedBox( + width: 40, + child: Padding( + padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), + child: Icon( + Icons.language, + ), + ), + ), + secondChild: CircleAvatar( + backgroundColor: Colors.transparent, + foregroundImage: anonymousInstances![realIndex].instanceIcon == null ? null : CachedNetworkImageProvider(anonymousInstances![realIndex].instanceIcon!), + maxRadius: 20, + ), + ), + Positioned( + right: 0, + bottom: 0, + child: SizedBox( + width: 12, + height: 12, + child: Material( + borderRadius: BorderRadius.circular(10), + color: currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance + ? HSLColor.fromColor(theme.colorScheme.primaryContainer).withLightness(0.95).toColor() + : null, + ), + ), + ), + // This is the status indicator + Positioned( + right: 1, + bottom: 1, + child: AnimatedOpacity( + opacity: anonymousInstances![realIndex].alive == null ? 0 : 1, + duration: const Duration(milliseconds: 500), + child: Icon( + anonymousInstances![realIndex].alive == true ? Icons.check_circle_rounded : Icons.remove_circle_rounded, + size: 10, + color: Color.alphaBlend(theme.colorScheme.primaryContainer.withOpacity(0.6), anonymousInstances![realIndex].alive == true ? Colors.green : Colors.red), + ), + ), + ), + ], + ), + title: Row( + children: [ + const Icon( + Icons.person_off_rounded, + size: 15, + ), + const SizedBox(width: 5), + Text( + AppLocalizations.of(context)!.anonymous, + style: theme.textTheme.titleMedium?.copyWith(), + ), + AnimatedOpacity( + opacity: anonymousInstances![realIndex].latency == null ? 0 : 1, + duration: const Duration(milliseconds: 500), + child: Row( + children: [ + const SizedBox(width: 5), + Text( + '•', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), + ), + const SizedBox(width: 5), + Text( + '${anonymousInstances![realIndex].latency?.inMilliseconds}ms', + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryContainer.withOpacity(0.55), + ), + ), + ], + ), + ), + ], + ), + subtitle: Row( + children: [ + Text(anonymousInstances![realIndex].instance), + ], + ), + trailing: ((accounts?.length ?? 0) > 0 || anonymousInstances!.length > 1) + ? (currentAccountId == null && currentAnonymousInstance == anonymousInstances![realIndex].instance) + ? IconButton( + icon: Icon(Icons.logout, semanticLabel: AppLocalizations.of(context)!.removeInstance), + onPressed: () async { + context.read().add(OnRemoveAnonymousInstance(anonymousInstances![realIndex].instance)); + + if (anonymousInstances!.length > 1) { + context + .read() + .add(OnSetCurrentAnonymousInstance(anonymousInstances!.lastWhere((instance) => instance != anonymousInstances![realIndex]).instance)); + } else { + context.read().add(SwitchAccount(accountId: accounts!.last.account.id)); + } + + setState(() => anonymousInstances = null); + }, + ) + : IconButton( + icon: Icon( + Icons.delete, + semanticLabel: AppLocalizations.of(context)!.removeInstance, + ), + onPressed: () async { + context.read().add(OnRemoveAnonymousInstance(anonymousInstances![realIndex].instance)); + setState(() { + anonymousInstances = null; + }); + }) + : null, + ), ), ), ); @@ -170,7 +458,6 @@ class _ProfileSelectState extends State { const Duration(seconds: 3), onTimeout: () => const GetInstanceIconResponse(success: false), ); - setState(() { account.instanceIcon = instanceIconResponse.icon; account.alive = instanceIconResponse.success; @@ -195,19 +482,32 @@ class _ProfileSelectState extends State { final List anonymousInstances = context.read().state.anonymousInstances.map((instance) => AnonymousInstanceExtended(instance: instance)).toList(); fetchAnonymousInstanceIcons(anonymousInstances); + pingAnonymousInstances(anonymousInstances); - setState(() { - this.anonymousInstances = anonymousInstances; - }); + setState(() => this.anonymousInstances = anonymousInstances); } Future fetchAnonymousInstanceIcons(List anonymousInstancesExtended) async { anonymousInstancesExtended.forEach((anonymousInstance) async { - final instanceIcon = await getInstanceIcon(anonymousInstance.instance).timeout( + final GetInstanceIconResponse instanceIconResponse = await getInstanceIcon(anonymousInstance.instance).timeout( const Duration(seconds: 3), - onTimeout: () => null, + onTimeout: () => const GetInstanceIconResponse(success: false), ); - setState(() => anonymousInstance.instanceIcon = instanceIcon); + setState(() { + anonymousInstance.instanceIcon = instanceIconResponse.icon; + anonymousInstance.alive = instanceIconResponse.success; + }); + }); + } + + Future pingAnonymousInstances(List anonymousInstancesExtended) async { + anonymousInstancesExtended.forEach((anonymousInstance) async { + PingData pingData = await Ping( + anonymousInstance.instance, + count: 1, + timeout: 5, + ).stream.first; + setState(() => anonymousInstance.latency = pingData.response?.time); }); } } @@ -227,6 +527,8 @@ class AccountExtended { class AnonymousInstanceExtended { String instance; String? instanceIcon; + Duration? latency; + bool? alive; AnonymousInstanceExtended({required this.instance, this.instanceIcon}); } From d855318b9a276372161f19dbabd36e56992dfac9 Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Sat, 2 Sep 2023 23:29:25 -0400 Subject: [PATCH 4/6] Fix merge --- lib/account/widgets/profile_modal_body.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/account/widgets/profile_modal_body.dart b/lib/account/widgets/profile_modal_body.dart index c85793b30..eaabeb4cc 100644 --- a/lib/account/widgets/profile_modal_body.dart +++ b/lib/account/widgets/profile_modal_body.dart @@ -174,7 +174,6 @@ class _ProfileSelectState extends State { crossFadeState: accounts![index].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: const Duration(milliseconds: 500), firstChild: const SizedBox( - width: 40, child: Padding( padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), child: Icon( @@ -310,7 +309,6 @@ class _ProfileSelectState extends State { crossFadeState: anonymousInstances![realIndex].instanceIcon == null ? CrossFadeState.showFirst : CrossFadeState.showSecond, duration: const Duration(milliseconds: 500), firstChild: const SizedBox( - width: 40, child: Padding( padding: EdgeInsets.only(left: 8, top: 8, right: 8, bottom: 8), child: Icon( From 927cf9f2c7a45acebecd1a9adb914d9f7b55ebcb Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Thu, 21 Sep 2023 13:18:23 -0400 Subject: [PATCH 5/6] Reduce animations for user jump --- lib/thunder/pages/thunder_page.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/thunder/pages/thunder_page.dart b/lib/thunder/pages/thunder_page.dart index dfd826f05..2f1e4bfc1 100644 --- a/lib/thunder/pages/thunder_page.dart +++ b/lib/thunder/pages/thunder_page.dart @@ -223,7 +223,11 @@ class _ThunderState extends State { _feedScaffoldKey.currentState?.closeDrawer(); setState(() { selectedPageIndex = 2; - pageController.animateToPage(2, duration: const Duration(milliseconds: 500), curve: Curves.ease); + if (reduceAnimations) { + pageController.jumpToPage(2); + } else { + pageController.animateToPage(2, duration: const Duration(milliseconds: 500), curve: Curves.ease); + } }); }, ), From 3721092472e793d0a78c09b7c7e0e066ff76f89d Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Thu, 21 Sep 2023 13:23:57 -0400 Subject: [PATCH 6/6] Exclude anonymous instance settings from import/export --- lib/core/enums/local_settings.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/core/enums/local_settings.dart b/lib/core/enums/local_settings.dart index 68b0aa0af..c15b80522 100644 --- a/lib/core/enums/local_settings.dart +++ b/lib/core/enums/local_settings.dart @@ -117,5 +117,9 @@ enum LocalSettings { final String label; /// Defines the settings that are excluded from import/export - static List importExportExcludedSettings = [LocalSettings.draftsCache]; + static List importExportExcludedSettings = [ + LocalSettings.draftsCache, + LocalSettings.anonymousInstances, + LocalSettings.currentAnonymousInstance, + ]; }