mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-16 20:08:50 +00:00
feat: notify keys
This commit is contained in:
@ -1,6 +1,9 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:convert/convert.dart';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:firebase_core/firebase_core.dart';
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
import 'package:firebase_messaging/firebase_messaging.dart';
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
@ -19,10 +22,60 @@ class Notepush {
|
|||||||
final pubkey = signer.getPublicKey();
|
final pubkey = signer.getPublicKey();
|
||||||
final url =
|
final url =
|
||||||
"$base/user-info/$pubkey/${Uri.encodeComponent(token)}?backend=fcm";
|
"$base/user-info/$pubkey/${Uri.encodeComponent(token)}?backend=fcm";
|
||||||
developer.log(url);
|
final rsp = await _sendPutRequest(url);
|
||||||
final auth = await _makeAuth("PUT", url);
|
return rsp.body;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<String>> getWatchedKeys() async {
|
||||||
|
final pubkey = signer.getPublicKey();
|
||||||
|
final url = "$base/user-info/$pubkey/notify";
|
||||||
|
final rsp = await _sendGetRequest(url);
|
||||||
|
final List<dynamic> obj = JsonCodec().decode(rsp.body);
|
||||||
|
return List<String>.from(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> watchPubkey(String target, List<int> kinds) async {
|
||||||
|
final pubkey = signer.getPublicKey();
|
||||||
|
final url = "$base/user-info/$pubkey/notify/$target";
|
||||||
|
await _sendPutRequest(url, body: {"kinds": kinds});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> removeWatchPubkey(String target) async {
|
||||||
|
final pubkey = signer.getPublicKey();
|
||||||
|
final url = "$base/user-info/$pubkey/notify/$target";
|
||||||
|
await _sendDeleteRequest(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setNotificationSettings(String token, List<int> kinds) async {
|
||||||
|
final pubkey = signer.getPublicKey();
|
||||||
|
final url =
|
||||||
|
"$base/user-info/$pubkey/${Uri.encodeComponent(token)}/preference";
|
||||||
|
await _sendPutRequest(url, body: {"kinds": kinds});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<http.Response> _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
|
final rsp = await http
|
||||||
.put(
|
.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<http.Response> _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),
|
Uri.parse(url),
|
||||||
headers: {
|
headers: {
|
||||||
"authorization": "Nostr $auth",
|
"authorization": "Nostr $auth",
|
||||||
@ -32,18 +85,40 @@ class Notepush {
|
|||||||
)
|
)
|
||||||
.timeout(Duration(seconds: 10));
|
.timeout(Duration(seconds: 10));
|
||||||
developer.log(rsp.body);
|
developer.log(rsp.body);
|
||||||
return rsp.body;
|
return rsp;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> _makeAuth(String method, String url) async {
|
Future<http.Response> _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<String> _makeAuth(String method, String url, {String? body}) async {
|
||||||
final pubkey = signer.getPublicKey();
|
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(
|
final authEvent = Nip01Event(
|
||||||
pubKey: pubkey,
|
pubKey: pubkey,
|
||||||
kind: 27235,
|
kind: 27235,
|
||||||
tags: [
|
tags: tags,
|
||||||
["u", url],
|
|
||||||
["method", "PUT"],
|
|
||||||
],
|
|
||||||
content: "",
|
content: "",
|
||||||
);
|
);
|
||||||
await signer.sign(authEvent);
|
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<void> setupNotifications() async {
|
Future<void> setupNotifications() async {
|
||||||
await Firebase.initializeApp();
|
await Firebase.initializeApp();
|
||||||
|
|
||||||
@ -62,7 +144,7 @@ Future<void> setupNotifications() async {
|
|||||||
developer.log(msg.notification?.body ?? "");
|
developer.log(msg.notification?.body ?? "");
|
||||||
final notification = msg.notification;
|
final notification = msg.notification;
|
||||||
if (notification != null && notification.android != null) {
|
if (notification != null && notification.android != null) {
|
||||||
/*FlutterLocalNotificationsPlugin().show(
|
FlutterLocalNotificationsPlugin().show(
|
||||||
notification.hashCode,
|
notification.hashCode,
|
||||||
notification.title,
|
notification.title,
|
||||||
notification.body,
|
notification.body,
|
||||||
@ -72,8 +154,13 @@ Future<void> setupNotifications() async {
|
|||||||
"fcm",
|
"fcm",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);*/
|
);
|
||||||
// TODO: foreground notification
|
}
|
||||||
|
});
|
||||||
|
FirebaseMessaging.onMessageOpenedApp.listen((msg) {
|
||||||
|
final notification = msg.notification;
|
||||||
|
if (notification != null) {
|
||||||
|
// TODO: redirect to stream
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
await fbase.setAutoInitEnabled(true);
|
await fbase.setAutoInitEnabled(true);
|
||||||
@ -92,6 +179,7 @@ Future<void> setupNotifications() async {
|
|||||||
fbase.onTokenRefresh.listen((token) async {
|
fbase.onTokenRefresh.listen((token) async {
|
||||||
developer.log("NEW TOKEN: $token");
|
developer.log("NEW TOKEN: $token");
|
||||||
await pusher.register(token);
|
await pusher.register(token);
|
||||||
|
await pusher.setNotificationSettings(token, [30_311]);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (Platform.isIOS) {
|
if (Platform.isIOS) {
|
||||||
@ -105,5 +193,6 @@ Future<void> setupNotifications() async {
|
|||||||
throw "Push token is null";
|
throw "Push token is null";
|
||||||
}
|
}
|
||||||
await pusher.register(fcmToken);
|
await pusher.register(fcmToken);
|
||||||
|
await pusher.setNotificationSettings(fcmToken, [30_311]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import 'package:zap_stream_flutter/theme.dart';
|
|||||||
import 'package:zap_stream_flutter/utils.dart';
|
import 'package:zap_stream_flutter/utils.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/button.dart';
|
import 'package:zap_stream_flutter/widgets/button.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/chat.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/pill.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/profile.dart';
|
import 'package:zap_stream_flutter/widgets/profile.dart';
|
||||||
import 'package:zap_stream_flutter/widgets/stream_info.dart';
|
import 'package:zap_stream_flutter/widgets/stream_info.dart';
|
||||||
@ -157,6 +158,7 @@ class _StreamPage extends State<StreamPage> with RouteAware {
|
|||||||
ProfileWidget.pubkey(
|
ProfileWidget.pubkey(
|
||||||
stream.info.host,
|
stream.info.host,
|
||||||
children: [
|
children: [
|
||||||
|
NotificationsButtonWidget(stream: widget.stream),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
BasicButton(
|
BasicButton(
|
||||||
Row(
|
Row(
|
||||||
|
57
lib/widgets/notifications_button.dart
Normal file
57
lib/widgets/notifications_button.dart
Normal file
@ -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<StatefulWidget> createState() => _NotificationsButtonWidget();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NotificationsButtonWidget extends State<NotificationsButtonWidget> {
|
||||||
|
@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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user