frosty/lib/widgets/user_actions_modal.dart
Francis Lavoie b11a0f4407
feat(chat): moderator actions (delete / timeout / ban / unban)
Add a reusable ModerationActions widget shown to channel moderators (and
the broadcaster) from both the message long-press menu and the user
panel. Supports delete message, timeout (10 min), ban, and remove
ban/timeout — the recovery action appears only when the target is
currently banned or timed out (tracked via CLEARCHAT). Surfaces Twitch's
own error reason when an action is rejected, and forces an immediate
chat re-render so a deleted/banned message reflects instantly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:53:10 -04:00

162 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:frosty/screens/settings/stores/auth_store.dart';
import 'package:frosty/screens/settings/stores/settings_store.dart';
import 'package:frosty/widgets/blurred_container.dart';
import 'package:provider/provider.dart';
import 'package:webview_flutter/webview_flutter.dart';
class UserActionsModal extends StatelessWidget {
final AuthStore authStore;
final String name;
final String userLogin;
final String userId;
final bool showPinOption;
final bool? isPinned;
/// 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.moderationActions,
});
@override
Widget build(BuildContext context) {
return ListView(
primary: false,
shrinkWrap: true,
children: [
?moderationActions,
if (showPinOption)
ListTile(
leading: const Icon(Icons.push_pin_outlined),
title: Text('${isPinned == true ? 'Unpin' : 'Pin'} $name'),
onTap: () {
if (isPinned == true) {
context.read<SettingsStore>().pinnedChannelIds = [
...context.read<SettingsStore>().pinnedChannelIds
..remove(userId),
];
} else {
context.read<SettingsStore>().pinnedChannelIds = [
...context.read<SettingsStore>().pinnedChannelIds,
userId,
];
}
Navigator.pop(context);
},
),
if (authStore.isLoggedIn)
ListTile(
leading: const Icon(Icons.block_rounded),
onTap: () => authStore
.showBlockDialog(
context,
targetUser: name,
targetUserId: userId,
)
.then((_) {
if (context.mounted) {
Navigator.pop(context);
}
}),
title: Text('Block $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 $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/$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 $name'),
),
],
);
}
}