Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show site content warnings #1485

Merged
merged 1 commit into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 67 additions & 10 deletions lib/account/pages/login_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:go_router/go_router.dart';
import 'package:lemmy_api_client/v3.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/lemmy_client.dart';
import 'package:thunder/core/singletons/preferences.dart';
import 'package:thunder/instances.dart';
import 'package:thunder/shared/dialogs.dart';
import 'package:thunder/shared/snackbar.dart';
import 'package:thunder/thunder/bloc/thunder_bloc.dart';
import 'package:thunder/utils/instance.dart';
Expand Down Expand Up @@ -130,11 +133,13 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix

@override
Widget build(BuildContext context) {
final AppLocalizations l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);

return MultiBlocListener(
listeners: [
BlocListener<AuthBloc, AuthState>(
listener: (listenerContext, state) {
listener: (listenerContext, state) async {
if (state.status == AuthStatus.loading) {
setState(() {
isLoading = true;
Expand All @@ -149,6 +154,31 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
context.pop();

showSnackbar(AppLocalizations.of(context)!.loginSucceeded);
} else if (state.status == AuthStatus.contentWarning) {
bool acceptedContentWarning = false;

await showThunderDialog<void>(
context: context,
title: l10n.contentWarning,
contentText: state.contentWarning,
onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(),
secondaryButtonText: l10n.decline,
onPrimaryButtonPressed: (dialogContext, _) async {
Navigator.of(dialogContext).pop();
acceptedContentWarning = true;
},
primaryButtonText: l10n.accept,
);

if (context.mounted) {
if (acceptedContentWarning) {
// Do another login attempt, this time without the content warning
_handleLogin(showContentWarning: false);
} else {
// Cancel the login
context.read<AuthBloc>().add(const CancelLoginAttempt());
}
}
}
},
),
Expand Down Expand Up @@ -297,7 +327,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
errorMaxLines: 2,
),
enableSuggestions: false,
onSubmitted: (controller.text.isNotEmpty && widget.anonymous) ? (_) => _addAnonymousInstance() : null,
onSubmitted: (controller.text.isNotEmpty && widget.anonymous) ? (_) => _addAnonymousInstance(context) : null,
),
suggestionsCallback: (String pattern) {
if (pattern.isNotEmpty != true) {
Expand Down Expand Up @@ -342,7 +372,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
(!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty)
? (_) => _handleLogin()
: (_instanceTextEditingController.text.isNotEmpty && widget.anonymous)
? (_) => _addAnonymousInstance()
? (_) => _addAnonymousInstance(context)
: null,
autocorrect: false,
controller: _passwordTextEditingController,
Expand Down Expand Up @@ -401,7 +431,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
onPressed: (!isLoading && _passwordTextEditingController.text.isNotEmpty && _passwordTextEditingController.text.isNotEmpty && _instanceTextEditingController.text.isNotEmpty)
? _handleLogin
: (_instanceTextEditingController.text.isNotEmpty && widget.anonymous)
? () => _addAnonymousInstance()
? () => _addAnonymousInstance(context)
: 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)),
Expand All @@ -422,7 +452,7 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
);
}

void _handleLogin() {
void _handleLogin({bool showContentWarning = true}) {
TextInput.finishAutofillContext();
// Perform login authentication
context.read<AuthBloc>().add(
Expand All @@ -431,11 +461,14 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
password: _passwordTextEditingController.text,
instance: _instanceTextEditingController.text.trim(),
totp: _totpTextEditingController.text,
showContentWarning: showContentWarning,
),
);
}

void _addAnonymousInstance() async {
void _addAnonymousInstance(BuildContext context) async {
final AppLocalizations l10n = AppLocalizations.of(context)!;

if (await isLemmyInstance(_instanceTextEditingController.text)) {
final SharedPreferences prefs = (await UserPreferences.instance).sharedPreferences;
List<String> anonymousInstances = prefs.getStringList(LocalSettings.anonymousInstances.name) ?? ['lemmy.ml'];
Expand All @@ -445,10 +478,34 @@ class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMix
instanceError = AppLocalizations.of(context)!.instanceHasAlreadyBenAdded(currentInstance ?? '');
});
} else {
context.read<AuthBloc>().add(const LogOutOfAllAccounts());
context.read<ThunderBloc>().add(OnAddAnonymousInstance(_instanceTextEditingController.text));
context.read<ThunderBloc>().add(OnSetCurrentAnonymousInstance(_instanceTextEditingController.text));
widget.popRegister();
// Check for content warning on anyonmous instance
GetSiteResponse getSiteResponse = await (LemmyClient()..changeBaseUrl(_instanceTextEditingController.text)).lemmyApiV3.run(const GetSite());

bool acceptedContentWarning = true;

if (getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) {
acceptedContentWarning = false;

await showThunderDialog<void>(
context: context,
title: l10n.contentWarning,
contentText: getSiteResponse.siteView.site.contentWarning,
onSecondaryButtonPressed: (dialogContext) => Navigator.of(dialogContext).pop(),
secondaryButtonText: l10n.decline,
onPrimaryButtonPressed: (dialogContext, _) async {
Navigator.of(dialogContext).pop();
acceptedContentWarning = true;
},
primaryButtonText: l10n.accept,
);
}

if (acceptedContentWarning) {
context.read<AuthBloc>().add(const LogOutOfAllAccounts());
context.read<ThunderBloc>().add(OnAddAnonymousInstance(_instanceTextEditingController.text));
context.read<ThunderBloc>().add(OnSetCurrentAnonymousInstance(_instanceTextEditingController.text));
widget.popRegister();
}
}
}
}
Expand Down
11 changes: 10 additions & 1 deletion lib/core/auth/bloc/auth_bloc.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import 'package:bloc/bloc.dart';
import 'package:uuid/uuid.dart';
import 'package:equatable/equatable.dart';
import 'package:lemmy_api_client/v3.dart';
import 'package:collection/collection.dart';
import 'package:bloc_concurrency/bloc_concurrency.dart';
import 'package:stream_transform/stream_transform.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

import 'package:thunder/utils/error_messages.dart';
import 'package:thunder/account/models/account.dart';
import 'package:thunder/core/singletons/preferences.dart';
import 'package:thunder/core/singletons/lemmy_client.dart';
import 'package:thunder/utils/global_context.dart';

part 'auth_event.dart';
part 'auth_state.dart';
Expand Down Expand Up @@ -143,6 +144,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {

GetSiteResponse getSiteResponse = await lemmy.run(GetSite(auth: loginResponse.jwt));

if (event.showContentWarning && getSiteResponse.siteView.site.contentWarning?.isNotEmpty == true) {
return emit(state.copyWith(status: AuthStatus.contentWarning, contentWarning: getSiteResponse.siteView.site.contentWarning));
}

// Create a new account in the database
Account? account = Account(
id: '',
Expand Down Expand Up @@ -178,6 +183,10 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
});

on<CancelLoginAttempt>((event, emit) async {
return emit(state.copyWith(status: AuthStatus.failure, errorMessage: AppLocalizations.of(GlobalContext.context)!.loginAttemptCanceled));
});

/// When we log out of all accounts, clear the instance information
on<LogOutOfAllAccounts>((event, emit) async {
emit(state.copyWith(status: AuthStatus.initial));
Expand Down
8 changes: 7 additions & 1 deletion lib/core/auth/bloc/auth_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@ class LoginAttempt extends AuthEvent {
final String password;
final String instance;
final String totp;
final bool showContentWarning;

const LoginAttempt({required this.username, required this.password, required this.instance, this.totp = ""});
const LoginAttempt({required this.username, required this.password, required this.instance, this.totp = "", this.showContentWarning = true});
}

/// Cancels a login attempt by emitting the `failure` state.
class CancelLoginAttempt extends AuthEvent {
const CancelLoginAttempt();
}

/// TODO: Consolidate logic to have adding accounts (for both authenticated and anonymous accounts) placed here
Expand Down
6 changes: 5 additions & 1 deletion lib/core/auth/bloc/auth_state.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
part of 'auth_bloc.dart';

enum AuthStatus { initial, loading, success, failure, failureCheckingInstance }
enum AuthStatus { initial, loading, success, failure, failureCheckingInstance, contentWarning }

class AuthState extends Equatable {
const AuthState({
Expand All @@ -11,6 +11,7 @@ class AuthState extends Equatable {
this.downvotesEnabled = true,
this.getSiteResponse,
this.reload = true,
this.contentWarning,
});

final AuthStatus status;
Expand All @@ -20,6 +21,7 @@ class AuthState extends Equatable {
final bool downvotesEnabled;
final GetSiteResponse? getSiteResponse;
final bool reload;
final String? contentWarning;

AuthState copyWith({
AuthStatus? status,
Expand All @@ -29,6 +31,7 @@ class AuthState extends Equatable {
bool? downvotesEnabled,
GetSiteResponse? getSiteResponse,
bool? reload,
String? contentWarning,
}) {
return AuthState(
status: status ?? this.status,
Expand All @@ -38,6 +41,7 @@ class AuthState extends Equatable {
downvotesEnabled: downvotesEnabled ?? this.downvotesEnabled,
getSiteResponse: getSiteResponse ?? this.getSiteResponse,
reload: reload ?? this.reload,
contentWarning: contentWarning,
);
}

Expand Down
22 changes: 20 additions & 2 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
"@about": {
"description": "About settings category."
},
"accept": "Accept",
"@accept": {
"description": "The ability to accept an option"
},
"accessibility": "Accessibility",
"@accessibility": {},
"accessibilityProfilesDescription": "Accessibility profiles allows applying several settings at once to accommodate a particular accessibility requirement.",
Expand Down Expand Up @@ -453,6 +457,10 @@
"@contentManagement": {
"description": "Setting for content management (languages/blocking)"
},
"contentWarning": "Content Warning",
"@contentWarning": {
"description": "Heading for content warning dialog"
},
"controversial": "Controversial",
"@controversial": {},
"copiedToClipboard": "Copied to clipboard",
Expand Down Expand Up @@ -595,6 +603,10 @@
"@debugNotificationsDescription": {
"description": "A description for the notification debugging section"
},
"decline": "Decline",
"@decline": {
"description": "The ability to decline an option"
},
"defaultColor": "Default",
"@defaultColor": {
"description": "Default setting value (e.g., color)"
Expand Down Expand Up @@ -1167,8 +1179,14 @@
"@logOut": {},
"login": "Log in",
"@login": {},
"loginFailed": "Could not log in. Please try again:({errorMessage})",
"@loginFailed": {},
"loginAttemptCanceled": "Login attempt canceled.",
"@loginAttemptCanceled": {
"description": "Message shown to user when they cancel a login attempt"
},
"loginFailed": "Could not log in. Please try again. (Error: {errorMessage})",
"@loginFailed": {
"description": "Message shown to user when their login attempt failed"
},
"loginSucceeded": "Logged in.",
"@loginSucceeded": {},
"loginToPerformAction": "You need to be logged in to carry out this task.",
Expand Down
4 changes: 4 additions & 0 deletions lib/shared/media_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ class _MediaViewState extends State<MediaView> with SingleTickerProviderStateMix
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;

// TODO: If this site has a content warning, we don't need to blur previews.
// (This can be implemented once the web UI does the same.)
final blurNSFWPreviews = widget.hideNsfwPreviews && widget.postViewMedia.postView.post.nsfw;

return InkWell(
Expand Down Expand Up @@ -237,6 +239,8 @@ class _MediaViewState extends State<MediaView> with SingleTickerProviderStateMix
final theme = Theme.of(context);
final l10n = AppLocalizations.of(context)!;

// TODO: If this site has a content warning, we don't need to blur previews.
// (This can be implemented once the web UI does the same.)
final blurNSFWPreviews = widget.hideNsfwPreviews && widget.postViewMedia.postView.post.nsfw;

return InkWell(
Expand Down
1 change: 1 addition & 0 deletions lib/thunder/pages/thunder_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,7 @@ class _ThunderState extends State<Thunder> {
child: Container(),
),
);
case AuthStatus.contentWarning:
case AuthStatus.success:
Version? version = thunderBlocState.version;
bool showInAppUpdateNotification = thunderBlocState.showInAppUpdateNotification;
Expand Down
Loading