import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:frosty/apis/base_api_client.dart'; import 'package:frosty/screens/onboarding/login_webview.dart'; import 'package:frosty/screens/settings/stores/auth_store.dart'; import 'package:frosty/screens/settings/stores/settings_store.dart'; import 'package:frosty/widgets/alert_message.dart'; import 'package:frosty/widgets/blurred_container.dart'; import 'package:frosty/widgets/frosty_dialog.dart'; import 'package:frosty/widgets/integrity_webview_dialog.dart'; import 'package:provider/provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; class UserActionsModal extends StatefulWidget { final AuthStore authStore; final String name; final String userLogin; final String userId; final bool showPinOption; final bool? isPinned; /// When set (e.g. Following tab), skip the Helix lookup and show this state. final bool? initialIsFollowing; /// Called after a successful follow (`true`) or unfollow (`false`). final ValueChanged? onFollowChanged; /// Optional moderation actions (shown at the top) for chat contexts. Kept as /// an injected widget so this generic modal stays chat-agnostic. final Widget? moderationActions; const UserActionsModal({ super.key, required this.authStore, required this.name, required this.userLogin, required this.userId, this.showPinOption = false, this.isPinned, this.initialIsFollowing, this.onFollowChanged, this.moderationActions, }); @override State createState() => _UserActionsModalState(); } class _UserActionsModalState extends State { bool? _isFollowing; bool _followLoading = false; bool _followActionInProgress = false; bool get _isSelf => widget.authStore.user.details?.id != null && widget.authStore.user.details!.id == widget.userId; bool get _canFollow => widget.authStore.isLoggedIn && !_isSelf; @override void initState() { super.initState(); if (!_canFollow) return; if (widget.initialIsFollowing != null) { _isFollowing = widget.initialIsFollowing; } else { _loadFollowState(); } } Future _loadFollowState() async { final cached = widget.authStore.user.cachedIsFollowing(widget.userId); if (cached != null) { setState(() => _isFollowing = cached); return; } setState(() => _followLoading = true); try { final following = await widget.authStore.user.isFollowingChannel( widget.userId, ); if (mounted) { setState(() { _isFollowing = following; _followLoading = false; }); } } catch (_) { if (mounted) { setState(() => _followLoading = false); } } } Future _toggleFollow() async { if (_followActionInProgress || _isFollowing == null) return; final gqlToken = widget.authStore.gqlToken; if (gqlToken == null) { await _promptLinkWebSession(); return; } final following = _isFollowing!; setState(() => _followActionInProgress = true); try { await _runFollowMutation(following: following, gqlToken: gqlToken); } on IntegrityCheckException { if (!mounted) return; // Dio /integrity is almost always bad_bot — mint via WebView like Xtra. final credentials = await showIntegrityWebViewDialog( context: context, gqlToken: gqlToken, ); if (!mounted) return; if (credentials == null) { setState(() => _followActionInProgress = false); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: AlertMessage( message: 'Follow cancelled — Twitch verification required', centered: false, ), ), ); return; } try { await widget.authStore.user.applyWebIntegrity(credentials); await _runFollowMutation(following: following, gqlToken: gqlToken); } on ApiException catch (e) { if (!mounted) return; setState(() => _followActionInProgress = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: AlertMessage(message: e.message, centered: false), ), ); return; } catch (_) { if (!mounted) return; setState(() => _followActionInProgress = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: AlertMessage( message: following ? 'Could not unfollow ${widget.name}' : 'Could not follow ${widget.name}', centered: false, ), ), ); return; } } on ApiException catch (e) { if (!mounted) return; setState(() => _followActionInProgress = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: AlertMessage(message: e.message, centered: false), ), ); return; } catch (_) { if (!mounted) return; setState(() => _followActionInProgress = false); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: AlertMessage( message: following ? 'Could not unfollow ${widget.name}' : 'Could not follow ${widget.name}', centered: false, ), ), ); return; } if (!mounted) return; final nowFollowing = !following; setState(() { _isFollowing = nowFollowing; _followActionInProgress = false; }); widget.onFollowChanged?.call(nowFollowing); final messenger = ScaffoldMessenger.maybeOf(context); Navigator.pop(context); messenger?.showSnackBar( SnackBar( content: AlertMessage( message: nowFollowing ? 'Followed ${widget.name}' : 'Unfollowed ${widget.name}', centered: false, ), ), ); } Future _runFollowMutation({ required bool following, required String gqlToken, }) async { if (following) { await widget.authStore.user.unfollow( targetId: widget.userId, gqlToken: gqlToken, ); } else { await widget.authStore.user.follow( targetId: widget.userId, gqlToken: gqlToken, ); } } Future _promptLinkWebSession() { return showDialog( context: context, builder: (dialogContext) => FrostyDialog( title: 'Web session required', message: 'Following channels needs your Twitch web session linked. Log in again to link it, then try follow/unfollow.', actions: [ TextButton( onPressed: Navigator.of(dialogContext).pop, child: const Text('Cancel'), ), FilledButton( onPressed: () { Navigator.of(dialogContext).pop(); Navigator.of(context).push( MaterialPageRoute(builder: (context) => LoginWebView()), ); }, child: const Text('Log in'), ), ], ), ); } @override Widget build(BuildContext context) { final showFollowTile = _canFollow && (_followLoading || _isFollowing != null); return ListView( primary: false, shrinkWrap: true, children: [ ?widget.moderationActions, if (widget.showPinOption) ListTile( leading: const Icon(Icons.push_pin_outlined), title: Text( '${widget.isPinned == true ? 'Unpin' : 'Pin'} ${widget.name}', ), onTap: () { if (widget.isPinned == true) { context.read().pinnedChannelIds = [ ...context.read().pinnedChannelIds ..remove(widget.userId), ]; } else { context.read().pinnedChannelIds = [ ...context.read().pinnedChannelIds, widget.userId, ]; } Navigator.pop(context); }, ), if (showFollowTile) ListTile( leading: _followLoading || _followActionInProgress ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ) : Icon( _isFollowing == true ? Icons.heart_broken_rounded : Icons.favorite_rounded, ), title: Text( _isFollowing == true ? 'Unfollow ${widget.name}' : 'Follow ${widget.name}', ), enabled: !_followLoading && !_followActionInProgress, onTap: _toggleFollow, ), if (widget.authStore.isLoggedIn) ListTile( leading: const Icon(Icons.block_rounded), onTap: () => widget.authStore .showBlockDialog( context, targetUser: widget.name, targetUserId: widget.userId, ) .then((_) { if (context.mounted) { Navigator.pop(context); } }), title: Text('Block ${widget.name}'), ), ListTile( leading: const Icon(Icons.outlined_flag_rounded), onTap: () => Navigator.push( context, MaterialPageRoute( builder: (context) { final theme = Theme.of(context); return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, extendBody: true, extendBodyBehindAppBar: true, appBar: AppBar( centerTitle: false, elevation: 0, backgroundColor: Colors.transparent, surfaceTintColor: Colors.transparent, systemOverlayStyle: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: theme.brightness == Brightness.dark ? Brightness.light : Brightness.dark, ), leading: IconButton( tooltip: 'Back', icon: Icon(Icons.adaptive.arrow_back_rounded), onPressed: Navigator.of(context).pop, ), title: Text('Report ${widget.name}'), ), body: Stack( children: [ // WebView content Positioned.fill( child: Padding( padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top + kToolbarHeight, ), child: WebViewWidget( controller: WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..loadRequest( Uri.parse( 'https://www.twitch.tv/${widget.userLogin}/report', ), ) ..setNavigationDelegate( NavigationDelegate( onWebResourceError: (error) { debugPrint( 'WebView error: ${error.description}', ); }, ), ), ), ), ), // Blurred app bar overlay Positioned( top: 0, left: 0, right: 0, child: BlurredContainer( gradientDirection: GradientDirection.up, padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top, left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right, ), child: const SizedBox(height: kToolbarHeight), ), ), ], ), ); }, ), ), title: Text('Report ${widget.name}'), ), ], ); } }