import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:gap/gap.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:provider/provider.dart'; import 'package:responsive_framework/responsive_framework.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:surface/providers/notification.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/providers/userinfo.dart'; import 'package:surface/screens/notification.dart'; import 'package:surface/types/notification.dart'; import 'package:surface/widgets/dialog.dart'; import 'package:surface/widgets/universal_image.dart'; import 'markdown_content.dart'; class NotifyIndicator extends StatefulWidget { const NotifyIndicator({super.key}); @override State createState() => _NotifyIndicatorState(); } class _NotifyIndicatorState extends State with SingleTickerProviderStateMixin { late final AnimationController _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); void _markOneAsRead(SnNotification notification) async { final ua = context.read(); if (!ua.isAuthorized) return; if (notification.id == 0) return; if (notification.readAt != null) return; try { final sn = context.read(); await sn.client.put('/cgi/id/notifications/read/${notification.id}'); if (!mounted) return; context.showSnackbar( 'notificationMarkOneReadPrompt'.tr(args: ['#${notification.id}']), ); } catch (err) { if (!mounted) return; context.showErrorDialog(err); } } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final sn = context.read(); final ua = context.read(); final nty = context.watch(); final isMobile = ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE); final show = nty.showingCount > 0 && ua.isAuthorized; if (show) { _animationController.animateTo(1); } else { _animationController.animateTo(0); } return ListenableBuilder( listenable: nty, builder: (context, _) { final current = nty.notifications.lastOrNull; return IgnorePointer( ignoring: !show, child: GestureDetector( child: Animate( autoPlay: false, controller: _animationController, effects: [ SlideEffect( begin: isMobile ? Offset(0, -1) : Offset(1, 0), end: Offset(0, 0), duration: Duration(milliseconds: 300), curve: Curves.fastEaseInToSlowEaseOut, ), FadeEffect( begin: 0.0, end: 1.0, duration: Duration(milliseconds: 300), curve: Curves.easeInOut, ), ], child: Container( padding: const EdgeInsets.symmetric(vertical: 16), width: double.infinity, constraints: BoxConstraints( maxWidth: isMobile ? MediaQuery.of(context).size.width - 16 : 360, ), child: Material( elevation: 2, borderRadius: BorderRadius.circular(8), color: Theme.of(context).colorScheme.surfaceContainer, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (current?.metadata['avatar'] != null) CircleAvatar( radius: 14, backgroundImage: UniversalImage.provider( sn.getAttachmentUrl(current!.metadata['avatar']), ), ) else Icon(kNotificationTopicIcons[current?.topic] ?? Symbols.notifications), const Gap(16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( current?.title ?? 'Notification', style: Theme.of(context) .textTheme .bodyMedium! .copyWith( fontWeight: FontWeight.bold, ), ), if (current?.subtitle?.isNotEmpty ?? false) Text( current!.subtitle!, style: Theme.of(context) .textTheme .bodyMedium! .copyWith( fontWeight: FontWeight.bold, ), ), MarkdownTextContent( content: current?.body ?? '', isAutoWarp: true, ), ], ), ), const Gap(16), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( DateFormat('HH:mm').format( (current?.createdAt ?? DateTime.now()) .millisecondsSinceEpoch > 0 ? (current?.createdAt ?? DateTime.now()) : DateTime.now()), ).fontSize(12).padding(right: 2), const Gap(6), if (current?.metadata['image'] != null) SizedBox( width: 40, height: 40, child: ClipRRect( borderRadius: const BorderRadius.all( Radius.circular(8)), child: AutoResizeUniversalImage( sn.getAttachmentUrl( current?.metadata['image']), fit: BoxFit.cover, ), ), ), ], ), ], ).padding(horizontal: 16, vertical: 12), ), ), ), onTap: () { nty.clear(); if (current != null) { _markOneAsRead(current); } }, ), ); }); } }