Skip to content

Commit

Permalink
Merge pull request #677 from micahmo/feature/anonymous-instances
Browse files Browse the repository at this point in the history
Account improvements and anonymous instances
  • Loading branch information
hjiangsu authored Sep 21, 2023
2 parents 660d9ef + 3721092 commit 155ac86
Show file tree
Hide file tree
Showing 26 changed files with 834 additions and 250 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- Automatically save drafts for posts and comments - contribution from @micahmo
- Highlight the currently selected page in the navigation drawer - contribution from @micahmo
- Newly created comments get inserted into comment list correctly without losing your scroll position. If comment is top level, the list scrolls to your comment. The comment also gets highlighted - contribution from @ajsosa
- Improved account switching and added anonymous browsing mode for any intance - contribution from @micahmo

### Changed
- Prioritize and label the default accent color - contribution from @micahmo
Expand Down
8 changes: 7 additions & 1 deletion lib/account/bloc/account_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,13 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
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++;
Expand Down
8 changes: 6 additions & 2 deletions lib/account/pages/account_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand All @@ -21,6 +23,7 @@ class _AccountPageState extends State<AccountPage> {

AuthState authState = context.read<AuthBloc>().state;
AccountState accountState = context.read<AccountBloc>().state;
String anonymousInstance = context.watch<ThunderBloc>().state.currentAnonymousInstance;

return MultiBlocListener(
listeners: [
Expand All @@ -46,11 +49,12 @@ class _AccountPageState extends State<AccountPage> {
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),
)
],
Expand Down
165 changes: 98 additions & 67 deletions lib/account/pages/login_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<LoginPage> createState() => _LoginPageState();
Expand Down Expand Up @@ -48,15 +53,15 @@ class _LoginPageState extends State<LoginPage> {
_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);
}
});

_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);
Expand All @@ -69,7 +74,7 @@ class _LoginPageState extends State<LoginPage> {
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);
Expand Down Expand Up @@ -132,7 +137,7 @@ class _LoginPageState extends State<LoginPage> {
});

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<AuthBloc>().state.isLoggedIn) {
context.pop();

showSnackbar(context, AppLocalizations.of(context)!.loginSucceeded);
Expand Down Expand Up @@ -182,74 +187,79 @@ class _LoginPageState extends State<LoginPage> {
errorMaxLines: 2,
),
enableSuggestions: false,
onSubmitted: (_instanceTextEditingController.text.isNotEmpty && widget.anonymous) ? (_) => _addAnonymousInstance() : null,
),
const SizedBox(height: 35.0),
AutofillGroup(
child: Column(
children: <Widget>[
TextField(
textInputAction: TextInputAction.next,
keyboardType: TextInputType.url,
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: <Widget>[
TextField(
textInputAction: TextInputAction.next,
keyboardType: TextInputType.url,
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: <TextInputFormatter>[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: <TextInputFormatter>[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(
Expand All @@ -262,8 +272,11 @@ class _LoginPageState extends State<LoginPage> {
),
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)),
Expand Down Expand Up @@ -292,4 +305,22 @@ class _LoginPageState extends State<LoginPage> {
),
);
}

void _addAnonymousInstance() async {
if (await isLemmyInstance(_instanceTextEditingController.text)) {
final SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences;
List<String> 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<AuthBloc>().add(LogOutOfAllAccounts());
context.read<ThunderBloc>().add(OnAddAnonymousInstance(_instanceTextEditingController.text));
context.read<ThunderBloc>().add(OnSetCurrentAnonymousInstance(_instanceTextEditingController.text));
widget.popRegister();
}
}
}
}
Loading

0 comments on commit 155ac86

Please sign in to comment.