diff --git a/lib/notifications.dart b/lib/notifications.dart index 3e26438..31cbc1a 100644 --- a/lib/notifications.dart +++ b/lib/notifications.dart @@ -1,6 +1,9 @@ +import 'dart:convert'; import 'dart:developer' as developer; import 'dart:io'; +import 'package:convert/convert.dart'; +import 'package:crypto/crypto.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -19,10 +22,60 @@ class Notepush { final pubkey = signer.getPublicKey(); final url = "$base/user-info/$pubkey/${Uri.encodeComponent(token)}?backend=fcm"; - developer.log(url); - final auth = await _makeAuth("PUT", url); + final rsp = await _sendPutRequest(url); + return rsp.body; + } + + Future> getWatchedKeys() async { + final pubkey = signer.getPublicKey(); + final url = "$base/user-info/$pubkey/notify"; + final rsp = await _sendGetRequest(url); + final List obj = JsonCodec().decode(rsp.body); + return List.from(obj); + } + + Future watchPubkey(String target, List kinds) async { + final pubkey = signer.getPublicKey(); + final url = "$base/user-info/$pubkey/notify/$target"; + await _sendPutRequest(url, body: {"kinds": kinds}); + } + + Future removeWatchPubkey(String target) async { + final pubkey = signer.getPublicKey(); + final url = "$base/user-info/$pubkey/notify/$target"; + await _sendDeleteRequest(url); + } + + Future setNotificationSettings(String token, List kinds) async { + final pubkey = signer.getPublicKey(); + final url = + "$base/user-info/$pubkey/${Uri.encodeComponent(token)}/preference"; + await _sendPutRequest(url, body: {"kinds": kinds}); + } + + Future _sendPutRequest(String url, {Object? body}) async { + final jsonBody = body != null ? JsonCodec().encode(body) : null; + final auth = await _makeAuth("PUT", url, body: jsonBody); final rsp = await http .put( + Uri.parse(url), + body: jsonBody, + headers: { + "authorization": "Nostr $auth", + "accept": "application/json", + "content-type": "application/json", + }, + ) + .timeout(Duration(seconds: 10)); + developer.log(rsp.body); + return rsp; + } + + Future _sendGetRequest(String url, {Object? body}) async { + final jsonBody = body != null ? JsonCodec().encode(body) : null; + final auth = await _makeAuth("GET", url, body: jsonBody); + final rsp = await http + .get( Uri.parse(url), headers: { "authorization": "Nostr $auth", @@ -32,18 +85,40 @@ class Notepush { ) .timeout(Duration(seconds: 10)); developer.log(rsp.body); - return rsp.body; + return rsp; } - Future _makeAuth(String method, String url) async { + Future _sendDeleteRequest(String url, {Object? body}) async { + final jsonBody = body != null ? JsonCodec().encode(body) : null; + final auth = await _makeAuth("DELETE", url, body: jsonBody); + final rsp = await http + .delete( + Uri.parse(url), + headers: { + "authorization": "Nostr $auth", + "accept": "application/json", + "content-type": "application/json", + }, + ) + .timeout(Duration(seconds: 10)); + developer.log(rsp.body); + return rsp; + } + + Future _makeAuth(String method, String url, {String? body}) async { final pubkey = signer.getPublicKey(); + var tags = [ + ["u", url], + ["method", method], + ]; + if (body != null) { + final hash = hex.encode(sha256.convert(utf8.encode(body)).bytes); + tags.add(["payload", hash]); + } final authEvent = Nip01Event( pubKey: pubkey, kind: 27235, - tags: [ - ["u", url], - ["method", "PUT"], - ], + tags: tags, content: "", ); await signer.sign(authEvent); @@ -51,6 +126,13 @@ class Notepush { } } +Notepush? getNotificationService() { + final signer = ndk.accounts.getLoggedAccount()?.signer; + return signer != null + ? Notepush(dotenv.env["NOTEPUSH_URL"]!, signer: signer) + : null; +} + Future setupNotifications() async { await Firebase.initializeApp(); @@ -62,7 +144,7 @@ Future setupNotifications() async { developer.log(msg.notification?.body ?? ""); final notification = msg.notification; if (notification != null && notification.android != null) { - /*FlutterLocalNotificationsPlugin().show( + FlutterLocalNotificationsPlugin().show( notification.hashCode, notification.title, notification.body, @@ -72,8 +154,13 @@ Future setupNotifications() async { "fcm", ), ), - );*/ - // TODO: foreground notification + ); + } + }); + FirebaseMessaging.onMessageOpenedApp.listen((msg) { + final notification = msg.notification; + if (notification != null) { + // TODO: redirect to stream } }); await fbase.setAutoInitEnabled(true); @@ -92,6 +179,7 @@ Future setupNotifications() async { fbase.onTokenRefresh.listen((token) async { developer.log("NEW TOKEN: $token"); await pusher.register(token); + await pusher.setNotificationSettings(token, [30_311]); }); if (Platform.isIOS) { @@ -105,5 +193,6 @@ Future setupNotifications() async { throw "Push token is null"; } await pusher.register(fcmToken); + await pusher.setNotificationSettings(fcmToken, [30_311]); } } diff --git a/lib/pages/stream.dart b/lib/pages/stream.dart index f11c14c..173612f 100644 --- a/lib/pages/stream.dart +++ b/lib/pages/stream.dart @@ -13,6 +13,7 @@ import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/utils.dart'; import 'package:zap_stream_flutter/widgets/button.dart'; import 'package:zap_stream_flutter/widgets/chat.dart'; +import 'package:zap_stream_flutter/widgets/notifications_button.dart'; import 'package:zap_stream_flutter/widgets/pill.dart'; import 'package:zap_stream_flutter/widgets/profile.dart'; import 'package:zap_stream_flutter/widgets/stream_info.dart'; @@ -157,6 +158,7 @@ class _StreamPage extends State with RouteAware { ProfileWidget.pubkey( stream.info.host, children: [ + NotificationsButtonWidget(stream: widget.stream), Spacer(), BasicButton( Row( diff --git a/lib/widgets/notifications_button.dart b/lib/widgets/notifications_button.dart new file mode 100644 index 0000000..7cc9166 --- /dev/null +++ b/lib/widgets/notifications_button.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:zap_stream_flutter/notifications.dart'; +import 'package:zap_stream_flutter/theme.dart'; +import 'package:zap_stream_flutter/utils.dart'; + +class NotificationsButtonWidget extends StatefulWidget { + final StreamEvent stream; + + const NotificationsButtonWidget({super.key, required this.stream}); + + @override + State createState() => _NotificationsButtonWidget(); +} + +class _NotificationsButtonWidget extends State { + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: () async { + final n = getNotificationService(); + return await n?.getWatchedKeys(); + }(), + builder: (context, state) { + if (state.data == null) { + return SizedBox.shrink(); + } else { + final isNotified = (state.data ?? []).contains( + widget.stream.info.host, + ); + return IconButton( + iconSize: 20, + onPressed: () async { + final n = getNotificationService(); + if (n == null) return; + + if (isNotified) { + await n.removeWatchPubkey(widget.stream.info.host); + } else { + await n.watchPubkey(widget.stream.info.host, [30311]); + } + setState(() { + // reload widget + }); + }, + style: ButtonStyle( + padding: WidgetStatePropertyAll(EdgeInsets.all(0)), + backgroundColor: WidgetStateColor.resolveWith((_) => LAYER_2), + ), + icon: Icon( + isNotified ? Icons.notifications_off : Icons.notification_add, + ), + ); + } + }, + ); + } +}