♻️ Refactor notification indicator
This commit is contained in:
parent
4b5b001739
commit
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,28 @@ 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!);
|
||||||
|
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,
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
@ -1,60 +1,179 @@
|
|||||||
|
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:styled_widget/styled_widget.dart';
|
import 'package:styled_widget/styled_widget.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 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: Material(
|
child: Animate(
|
||||||
elevation: 2,
|
controller: _animationController,
|
||||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
|
effects: [
|
||||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
SlideEffect(
|
||||||
child: ua.isAuthorized
|
begin: Offset(0, -1),
|
||||||
? Row(
|
end: Offset(0, 0),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
duration: Duration(milliseconds: 300),
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
curve: Curves.fastEaseInToSlowEaseOut,
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
nty.notifications.lastOrNull?.title ??
|
|
||||||
'notificationUnreadCount'.plural(nty.notifications.length),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
if (nty.notifications.lastOrNull?.body != null)
|
|
||||||
Text(
|
|
||||||
nty.notifications.lastOrNull!.body,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
).padding(left: 4),
|
|
||||||
const Gap(8),
|
|
||||||
const Icon(Symbols.notifications_unread, size: 18),
|
|
||||||
],
|
|
||||||
).padding(horizontal: 8, vertical: 4)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
).opacity(show ? 1 : 0, animate: true).animate(
|
|
||||||
const Duration(milliseconds: 300),
|
|
||||||
Curves.easeInOut,
|
|
||||||
),
|
),
|
||||||
|
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: min(480, MediaQuery.of(context).size.width - 16),
|
||||||
|
),
|
||||||
|
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.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