Compare commits
3 Commits
4b5b001739
...
b8dcdb2315
Author | SHA1 | Date | |
---|---|---|---|
b8dcdb2315 | |||
b7b921f1f4 | |||
319d5c7d7f |
26
api/Passport/Developer Notify One User.bru
Normal file
26
api/Passport/Developer Notify One User.bru
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
meta {
|
||||||
|
name: Developer Notify One User
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
url: {{endpoint}}/cgi/id/dev/notify/1
|
||||||
|
body: json
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
body:json {
|
||||||
|
{
|
||||||
|
"client_id": "{{third_client_id}}",
|
||||||
|
"client_secret":"{{third_client_tk}}",
|
||||||
|
"type": "general",
|
||||||
|
"subject": "测试",
|
||||||
|
"subtitle": "Alphabot です",
|
||||||
|
"content": "全新通知动画",
|
||||||
|
"metadata": {
|
||||||
|
"image": "D2EDbcrsTugs3xk5"
|
||||||
|
},
|
||||||
|
"priority": 10
|
||||||
|
}
|
||||||
|
}
|
@ -71,22 +71,29 @@ class NotificationProvider extends ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int showingCount = 0;
|
||||||
List<SnNotification> notifications = List.empty(growable: true);
|
List<SnNotification> notifications = List.empty(growable: true);
|
||||||
|
|
||||||
void listen() {
|
void listen() {
|
||||||
_ws.stream.stream.listen((event) {
|
_ws.stream.stream.listen((event) {
|
||||||
if (event.method == 'notifications.new') {
|
if (event.method == 'notifications.new') {
|
||||||
final notification = SnNotification.fromJson(event.payload!);
|
final notification = SnNotification.fromJson(event.payload!);
|
||||||
|
if (showingCount < 0) showingCount = 0;
|
||||||
|
showingCount++;
|
||||||
notifications.add(notification);
|
notifications.add(notification);
|
||||||
|
Future.delayed(const Duration(seconds: 3), () {
|
||||||
|
if (showingCount >= 0) showingCount--;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
final doHaptic = _cfg.prefs.getBool(kAppNotifyWithHaptic) ?? true;
|
||||||
if (doHaptic) HapticFeedback.lightImpact();
|
if (doHaptic) HapticFeedback.mediumImpact();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear() {
|
void clear() {
|
||||||
notifications.clear();
|
showingCount = 0;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,19 @@ class AccountScreen extends StatelessWidget {
|
|||||||
return AppScaffold(
|
return AppScaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: AutoAppBarLeading(),
|
leading: AutoAppBarLeading(),
|
||||||
title: Text("screenAccount").tr(),
|
title: Text(
|
||||||
|
"screenAccount",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
offset: Offset(1, 1),
|
||||||
|
blurRadius: 5.0,
|
||||||
|
color: Color.fromARGB(255, 0, 0, 0),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
).tr(),
|
||||||
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
flexibleSpace: ua.user != null && ua.user!.banner.isNotEmpty
|
||||||
? Stack(
|
? Stack(
|
||||||
fit: StackFit.expand,
|
fit: StackFit.expand,
|
||||||
|
@ -288,6 +288,7 @@ class _HomeDashTodayNewsState extends State<_HomeDashTodayNews> {
|
|||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
@ -21,6 +21,16 @@ import 'package:very_good_infinite_list/very_good_infinite_list.dart';
|
|||||||
import '../providers/userinfo.dart';
|
import '../providers/userinfo.dart';
|
||||||
import '../widgets/unauthorized_hint.dart';
|
import '../widgets/unauthorized_hint.dart';
|
||||||
|
|
||||||
|
const Map<String, IconData> kNotificationTopicIcons = {
|
||||||
|
'general': Symbols.notifications,
|
||||||
|
'passport.security.alert': Symbols.gpp_maybe,
|
||||||
|
'passport.security.otp': Symbols.password,
|
||||||
|
'interactive.subscription': Symbols.subscriptions,
|
||||||
|
'interactive.feedback': Symbols.add_reaction,
|
||||||
|
'messaging.callStart': Symbols.call_received,
|
||||||
|
'wallet.transaction.new': Symbols.receipt,
|
||||||
|
};
|
||||||
|
|
||||||
class NotificationScreen extends StatefulWidget {
|
class NotificationScreen extends StatefulWidget {
|
||||||
const NotificationScreen({super.key});
|
const NotificationScreen({super.key});
|
||||||
|
|
||||||
@ -36,15 +46,6 @@ class _NotificationScreenState extends State<NotificationScreen> {
|
|||||||
final List<SnNotification> _notifications = List.empty(growable: true);
|
final List<SnNotification> _notifications = List.empty(growable: true);
|
||||||
int? _totalCount;
|
int? _totalCount;
|
||||||
|
|
||||||
static const Map<String, IconData> kNotificationTopicIcons = {
|
|
||||||
'passport.security.alert': Symbols.gpp_maybe,
|
|
||||||
'passport.security.otp': Symbols.password,
|
|
||||||
'interactive.subscription': Symbols.subscriptions,
|
|
||||||
'interactive.feedback': Symbols.add_reaction,
|
|
||||||
'messaging.callStart': Symbols.call_received,
|
|
||||||
'wallet.transaction.new': Symbols.receipt,
|
|
||||||
};
|
|
||||||
|
|
||||||
Future<void> _fetchNotifications() async {
|
Future<void> _fetchNotifications() async {
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
if (!ua.isAuthorized) return;
|
if (!ua.isAuthorized) return;
|
||||||
|
@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/symbols.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/userinfo.dart';
|
import 'package:surface/providers/userinfo.dart';
|
||||||
import 'package:surface/providers/websocket.dart';
|
import 'package:surface/providers/websocket.dart';
|
||||||
|
|
||||||
@ -13,6 +14,9 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final ws = context.watch<WebSocketProvider>();
|
final ws = context.watch<WebSocketProvider>();
|
||||||
|
final cfg = context.watch<ConfigProvider>();
|
||||||
|
|
||||||
|
final marginLeft = cfg.drawerIsCollapsed ? 0.0 : cfg.drawerIsExpanded ? 304.0 : 80.0;
|
||||||
|
|
||||||
return ListenableBuilder(
|
return ListenableBuilder(
|
||||||
listenable: ws,
|
listenable: ws,
|
||||||
@ -22,6 +26,7 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !show,
|
ignoring: !show,
|
||||||
|
child: Center(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Material(
|
child: Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
@ -29,13 +34,16 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||||
child: ua.isAuthorized
|
child: ua.isAuthorized
|
||||||
? Row(
|
? Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
if (ws.isBusy)
|
if (ws.isBusy)
|
||||||
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
Text('serverConnecting').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||||
else if (!ws.isConnected)
|
else if (!ws.isConnected)
|
||||||
Text('serverDisconnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
Text('serverDisconnected')
|
||||||
|
.tr()
|
||||||
|
.textColor(Theme.of(context).colorScheme.onSecondaryContainer)
|
||||||
else
|
else
|
||||||
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
Text('serverConnected').tr().textColor(Theme.of(context).colorScheme.onSecondaryContainer),
|
||||||
const Gap(8),
|
const Gap(8),
|
||||||
@ -61,6 +69,7 @@ class ConnectionIndicator extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
).padding(left: marginLeft),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -28,7 +28,7 @@ class ContextMenuArea extends StatelessWidget {
|
|||||||
// Leave padding for side navigation
|
// Leave padding for side navigation
|
||||||
mousePosition = cfg.drawerIsExpanded
|
mousePosition = cfg.drawerIsExpanded
|
||||||
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
? mousePosition.copyWith(dx: mousePosition.dx - 304 * 2)
|
||||||
: mousePosition.copyWith(dx: mousePosition.dx - 72 * 2);
|
: mousePosition.copyWith(dx: mousePosition.dx - 80 * 2);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
|
@ -20,6 +20,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
final bool isAutoWarp;
|
final bool isAutoWarp;
|
||||||
final bool isEnlargeSticker;
|
final bool isEnlargeSticker;
|
||||||
final TextScaler? textScaler;
|
final TextScaler? textScaler;
|
||||||
|
final Color? textColor;
|
||||||
final List<SnAttachment?>? attachments;
|
final List<SnAttachment?>? attachments;
|
||||||
|
|
||||||
const MarkdownTextContent({
|
const MarkdownTextContent({
|
||||||
@ -28,6 +29,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
this.isAutoWarp = false,
|
this.isAutoWarp = false,
|
||||||
this.isEnlargeSticker = false,
|
this.isEnlargeSticker = false,
|
||||||
this.textScaler,
|
this.textScaler,
|
||||||
|
this.textColor,
|
||||||
this.attachments,
|
this.attachments,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -42,6 +44,7 @@ class MarkdownTextContent extends StatelessWidget {
|
|||||||
Theme.of(context),
|
Theme.of(context),
|
||||||
).copyWith(
|
).copyWith(
|
||||||
textScaler: textScaler,
|
textScaler: textScaler,
|
||||||
|
p: textColor != null ? Theme.of(context).textTheme.bodyMedium!.copyWith(color: textColor) : null,
|
||||||
blockquote: TextStyle(
|
blockquote: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
@ -31,7 +31,9 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
|||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
final destinations = nav.destinations.where((ele) => ele.isPinned).toList();
|
||||||
|
|
||||||
return NavigationRail(
|
return SizedBox(
|
||||||
|
width: 80,
|
||||||
|
child: NavigationRail(
|
||||||
selectedIndex:
|
selectedIndex:
|
||||||
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
nav.currentIndex != null && nav.currentIndex! < nav.pinnedDestinationCount ? nav.currentIndex : null,
|
||||||
destinations: [
|
destinations: [
|
||||||
@ -59,6 +61,7 @@ class _AppRailNavigationState extends State<AppRailNavigation> {
|
|||||||
nav.setIndex(idx);
|
nav.setIndex(idx);
|
||||||
GoRouter.of(context).goNamed(destinations[idx].screen);
|
GoRouter.of(context).goNamed(destinations[idx].screen);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -140,6 +140,7 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
final safeTop = MediaQuery.of(context).padding.top;
|
final safeTop = MediaQuery.of(context).padding.top;
|
||||||
|
final safeBottom = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
key: globalRootScaffoldKey,
|
key: globalRootScaffoldKey,
|
||||||
@ -191,7 +192,10 @@ class AppRootScaffold extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
Positioned(top: safeTop > 0 ? safeTop : 16, right: 8, child: NotifyIndicator()),
|
||||||
Positioned(top: safeTop > 0 ? safeTop : 16, left: 8, child: ConnectionIndicator()),
|
if (ResponsiveBreakpoints.of(context).smallerOrEqualTo(MOBILE))
|
||||||
|
Positioned(bottom: safeBottom > 0 ? safeBottom : 16, left: 0, right: 0, child: ConnectionIndicator())
|
||||||
|
else
|
||||||
|
Positioned(top: safeTop > 0 ? safeTop : 16, left: 0, right: 0, child: ConnectionIndicator()),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
drawer: !isExpandedDrawer ? AppNavigationDrawer() : null,
|
||||||
|
@ -1,60 +1,184 @@
|
|||||||
|
import 'dart:math' show min;
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:material_symbols_icons/symbols.dart';
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:responsive_framework/responsive_framework.dart';
|
||||||
import 'package:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.dart';
|
||||||
|
import 'package:surface/providers/config.dart';
|
||||||
import 'package:surface/providers/notification.dart';
|
import 'package:surface/providers/notification.dart';
|
||||||
|
import 'package:surface/providers/sn_network.dart';
|
||||||
import 'package:surface/providers/userinfo.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';
|
||||||
|
|
||||||
class NotifyIndicator extends StatelessWidget {
|
import 'markdown_content.dart';
|
||||||
|
|
||||||
|
class NotifyIndicator extends StatefulWidget {
|
||||||
const NotifyIndicator({super.key});
|
const NotifyIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NotifyIndicator> createState() => _NotifyIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotifyIndicatorState extends State<NotifyIndicator> with SingleTickerProviderStateMixin {
|
||||||
|
late final AnimationController _animationController = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
);
|
||||||
|
|
||||||
|
void _markOneAsRead(SnNotification notification) async {
|
||||||
|
final ua = context.read<UserProvider>();
|
||||||
|
if (!ua.isAuthorized) return;
|
||||||
|
|
||||||
|
if (notification.id == 0) return;
|
||||||
|
if (notification.readAt != null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final sn = context.read<SnNetworkProvider>();
|
||||||
final ua = context.read<UserProvider>();
|
final ua = context.read<UserProvider>();
|
||||||
final nty = context.watch<NotificationProvider>();
|
final nty = context.watch<NotificationProvider>();
|
||||||
|
|
||||||
final show = nty.notifications.isNotEmpty && ua.isAuthorized;
|
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(
|
return ListenableBuilder(
|
||||||
listenable: nty,
|
listenable: nty,
|
||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
|
final current = nty.notifications.lastOrNull;
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !show,
|
ignoring: !show,
|
||||||
child: GestureDetector(
|
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(
|
child: Material(
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
borderRadius: BorderRadius.circular(8),
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||||
child: ua.isAuthorized
|
child: Row(
|
||||||
? Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
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: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
nty.notifications.lastOrNull?.title ??
|
current?.title ?? 'Notification',
|
||||||
'notificationUnreadCount'.plural(nty.notifications.length),
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
maxLines: 1,
|
fontWeight: FontWeight.bold,
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
),
|
||||||
if (nty.notifications.lastOrNull?.body != null)
|
),
|
||||||
|
if (current?.subtitle?.isNotEmpty ?? false)
|
||||||
Text(
|
Text(
|
||||||
nty.notifications.lastOrNull!.body,
|
current!.subtitle!,
|
||||||
maxLines: 1,
|
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
overflow: TextOverflow.ellipsis,
|
fontWeight: FontWeight.bold,
|
||||||
).padding(left: 4),
|
),
|
||||||
const Gap(8),
|
),
|
||||||
const Icon(Symbols.notifications_unread, size: 18),
|
MarkdownTextContent(
|
||||||
|
content: current?.body ?? '',
|
||||||
|
isAutoWarp: true,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
).padding(horizontal: 8, vertical: 4)
|
),
|
||||||
: const SizedBox.shrink(),
|
),
|
||||||
).opacity(show ? 1 : 0, animate: true).animate(
|
const Gap(16),
|
||||||
const Duration(milliseconds: 300),
|
Column(
|
||||||
Curves.easeInOut,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(DateFormat('HH:mm').format(current?.createdAt.toLocal() ?? 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: () {
|
onTap: () {
|
||||||
nty.clear();
|
nty.clear();
|
||||||
|
if (current != null) {
|
||||||
|
_markOneAsRead(current);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import 'dart:developer';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ class _PostReactionPopupState extends State<PostReactionPopup> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.heavyImpact();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// ignore: use_build_context_synchronously
|
// ignore: use_build_context_synchronously
|
||||||
if (context.mounted) context.showErrorDialog(err);
|
if (context.mounted) context.showErrorDialog(err);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user