6 Commits

Author SHA1 Message Date
77d70e164b fix: replace Nip19.decode to bech32ToHex (TLV decode) 2025-05-12 15:58:13 +01:00
026b2eb85c chore: build only from main push 2025-05-12 15:22:36 +01:00
4c800e03e7 chore: bump version 2025-05-12 15:19:44 +01:00
819a45bc23 fix: chat state (again) 2025-05-12 15:18:43 +01:00
53794158c0 chore: bump version 2025-05-12 15:06:40 +01:00
706fb27664 fix: write chat state 2025-05-12 15:06:22 +01:00
10 changed files with 116 additions and 18 deletions

View File

@ -1,5 +1,7 @@
name: build name: build
on: push on:
push:
branches: ["main"]
jobs: jobs:
android: android:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -12,7 +14,19 @@ jobs:
channel: stable channel: stable
- run: flutter pub get - run: flutter pub get
- run: flutter build appbundle - run: flutter build appbundle
env:
KEYSTORE: ${{ secrets.KEYSTORE }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEYSTORE_SHA256: ${{ secrets.KEYSTORE_SHA256 }}
- run: flutter build apk --split-per-abi - run: flutter build apk --split-per-abi
env:
KEYSTORE: ${{ secrets.KEYSTORE }}
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
KEYSTORE_SHA256: ${{ secrets.KEYSTORE_SHA256 }}
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
with: with:
name: "release.aab" name: "release.aab"

View File

@ -6,6 +6,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:ndk/domain_layer/entities/account.dart'; import 'package:ndk/domain_layer/entities/account.dart';
import 'package:ndk/shared/nips/nip01/bip340.dart'; import 'package:ndk/shared/nips/nip01/bip340.dart';
import 'package:ndk/shared/nips/nip19/nip19.dart'; import 'package:ndk/shared/nips/nip19/nip19.dart';
import 'package:zap_stream_flutter/utils.dart';
class LoginAccount { class LoginAccount {
final AccountType type; final AccountType type;
@ -15,12 +16,15 @@ class LoginAccount {
LoginAccount._({required this.type, required this.pubkey, this.privateKey}); LoginAccount._({required this.type, required this.pubkey, this.privateKey});
static LoginAccount nip19(String key) { static LoginAccount nip19(String key) {
final keyData = Nip19.decode(key); final keyData = bech32ToHex(key);
final pubkey = final pubkey =
Nip19.isKey("nsec", key) ? Bip340.getPublicKey(keyData) : keyData; Nip19.isKey("nsec", key) ? Bip340.getPublicKey(keyData) : keyData;
final privateKey = Nip19.isKey("npub", key) ? null : keyData; final privateKey = Nip19.isKey("npub", key) ? null : keyData;
return LoginAccount._( return LoginAccount._(
type: Nip19.isKey("npub", key) ? AccountType.publicKey : AccountType.privateKey, type:
Nip19.isKey("npub", key)
? AccountType.publicKey
: AccountType.privateKey,
pubkey: pubkey, pubkey: pubkey,
privateKey: privateKey, privateKey: privateKey,
); );
@ -46,6 +50,13 @@ class LoginAccount {
static LoginAccount? fromJson(Map<String, dynamic> json) { static LoginAccount? fromJson(Map<String, dynamic> json) {
if (json.length > 2 && json.containsKey("pubKey")) { if (json.length > 2 && json.containsKey("pubKey")) {
if ((json["pubKey"] as String).length != 64) {
throw "Invalid pubkey, length != 64";
}
if (json.containsKey("privateKey") &&
(json["privateKey"] as String).length != 64) {
throw "Invalid privateKey, length != 64";
}
return LoginAccount._( return LoginAccount._(
type: AccountType.values.firstWhere( type: AccountType.values.firstWhere(
(v) => v.toString().endsWith(json["type"] as String), (v) => v.toString().endsWith(json["type"] as String),

View File

@ -1,10 +1,10 @@
import 'package:amberflutter/amberflutter.dart'; import 'package:amberflutter/amberflutter.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:ndk/shared/nips/nip19/nip19.dart';
import 'package:zap_stream_flutter/login.dart'; import 'package:zap_stream_flutter/login.dart';
import 'package:zap_stream_flutter/main.dart'; import 'package:zap_stream_flutter/main.dart';
import 'package:zap_stream_flutter/theme.dart'; 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/button.dart';
class LoginPage extends StatelessWidget { class LoginPage extends StatelessWidget {
@ -24,7 +24,7 @@ class LoginPage extends StatelessWidget {
final amber = Amberflutter(); final amber = Amberflutter();
final result = await amber.getPublicKey(); final result = await amber.getPublicKey();
if (result['signature'] != null) { if (result['signature'] != null) {
final key = Nip19.decode(result['signature']); final key = bech32ToHex(result['signature']);
loginData.value = LoginAccount.externalPublicKeyHex(key); loginData.value = LoginAccount.externalPublicKeyHex(key);
ctx.go("/"); ctx.go("/");
} }

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:ndk/shared/nips/nip19/nip19.dart';
import 'package:zap_stream_flutter/login.dart'; import 'package:zap_stream_flutter/login.dart';
import 'package:zap_stream_flutter/main.dart'; import 'package:zap_stream_flutter/main.dart';
import 'package:zap_stream_flutter/theme.dart'; 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/button.dart';
class LoginInputPage extends StatefulWidget { class LoginInputPage extends StatefulWidget {
@ -30,7 +30,7 @@ class _LoginInputPage extends State<LoginInputPage> {
"Login", "Login",
onTap: () async { onTap: () async {
try { try {
final keyData = Nip19.decode(_controller.text); final keyData = bech32ToHex(_controller.text);
if (keyData.isNotEmpty) { if (keyData.isNotEmpty) {
loginData.value = LoginAccount.nip19(_controller.text); loginData.value = LoginAccount.nip19(_controller.text);
context.go("/"); context.go("/");

View File

@ -2,11 +2,11 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:ndk/ndk.dart'; import 'package:ndk/ndk.dart';
import 'package:ndk/shared/nips/nip19/nip19.dart';
import 'package:zap_stream_flutter/imgproxy.dart'; import 'package:zap_stream_flutter/imgproxy.dart';
import 'package:zap_stream_flutter/main.dart'; import 'package:zap_stream_flutter/main.dart';
import 'package:zap_stream_flutter/rx_filter.dart'; import 'package:zap_stream_flutter/rx_filter.dart';
import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/utils.dart';
import 'package:zap_stream_flutter/widgets/avatar.dart'; import 'package:zap_stream_flutter/widgets/avatar.dart';
import 'package:zap_stream_flutter/widgets/button.dart'; import 'package:zap_stream_flutter/widgets/button.dart';
import 'package:zap_stream_flutter/widgets/header.dart'; import 'package:zap_stream_flutter/widgets/header.dart';
@ -20,7 +20,7 @@ class ProfilePage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final hexPubkey = Nip19.decode(pubkey); final hexPubkey = bech32ToHex(pubkey);
return ProfileLoaderWidget(hexPubkey, (ctx, state) { return ProfileLoaderWidget(hexPubkey, (ctx, state) {
final profile = state.data ?? Metadata(pubKey: hexPubkey); final profile = state.data ?? Metadata(pubKey: hexPubkey);
return SingleChildScrollView( return SingleChildScrollView(

View File

@ -1,5 +1,8 @@
import 'package:bech32/bech32.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:convert/convert.dart';
import 'package:ndk/ndk.dart'; import 'package:ndk/ndk.dart';
import 'package:ndk/shared/nips/nip19/nip19.dart';
/// Container class over event and stream info /// Container class over event and stream info
class StreamEvent { class StreamEvent {
@ -238,3 +241,56 @@ String zapSum(List<Nip01Event> zaps) {
.fold(0, (acc, v) => acc + (v.amountSats ?? 0)); .fold(0, (acc, v) => acc + (v.amountSats ?? 0));
return formatSats(total); return formatSats(total);
} }
String bech32ToHex(String bech32) {
final decoder = Bech32Decoder();
final data = decoder.convert(bech32, 10_000);
final data8bit = Nip19.convertBits(data.data, 5, 8, false);
if (data.hrp == "nevent" || data.hrp == "naddr" || data.hrp == "nprofile") {
final tlv = parseTLV(data8bit);
return hex.encode(tlv.firstWhere((v) => v.type == 0).value);
} else {
return hex.encode(data8bit);
}
}
class TLV {
final int type;
final int length;
final List<int> value;
TLV(this.type, this.length, this.value);
}
List<TLV> parseTLV(List<int> data) {
List<TLV> result = [];
int index = 0;
while (index < data.length) {
// Check if we have enough bytes for type and length
if (index + 2 > data.length) {
throw FormatException('Incomplete TLV data');
}
// Read type (1 byte)
int type = data[index];
index++;
// Read length (1 byte)
int length = data[index];
index++;
// Check if we have enough bytes for value
if (index + length > data.length) {
throw FormatException('TLV value length exceeds available data');
}
// Read value
List<int> value = data.sublist(index, index + length);
index += length;
result.add(TLV(type, length, value));
}
return result;
}

View File

@ -347,12 +347,23 @@ class _ChatMessageWidget extends StatelessWidget {
} }
} }
class _WriteMessageWidget extends StatelessWidget { class _WriteMessageWidget extends StatefulWidget {
final StreamEvent stream; final StreamEvent stream;
_WriteMessageWidget({required this.stream}); const _WriteMessageWidget({required this.stream});
final TextEditingController _controller = TextEditingController(); @override
State<StatefulWidget> createState() => __WriteMessageWidget();
}
class __WriteMessageWidget extends State<_WriteMessageWidget> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
Future<void> _sendMessage() async { Future<void> _sendMessage() async {
final login = ndk.accounts.getLoggedAccount(); final login = ndk.accounts.getLoggedAccount();
@ -363,10 +374,9 @@ class _WriteMessageWidget extends StatelessWidget {
kind: 1311, kind: 1311,
content: _controller.text, content: _controller.text,
tags: [ tags: [
["a", stream.aTag], ["a", widget.stream.aTag],
], ],
); );
developer.log(chatMsg.toString());
final res = ndk.broadcast.broadcast(nostrEvent: chatMsg); final res = ndk.broadcast.broadcast(nostrEvent: chatMsg);
await res.broadcastDoneFuture; await res.broadcastDoneFuture;
_controller.text = ""; _controller.text = "";

View File

@ -5,6 +5,7 @@ import 'package:ndk/ndk.dart';
import 'package:ndk/shared/nips/nip19/nip19.dart'; import 'package:ndk/shared/nips/nip19/nip19.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:zap_stream_flutter/theme.dart'; import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/utils.dart';
import 'package:zap_stream_flutter/widgets/profile.dart'; import 'package:zap_stream_flutter/widgets/profile.dart';
/// Converts a nostr note text containing links /// Converts a nostr note text containing links
@ -62,10 +63,15 @@ InlineSpan _buildProfileOrNoteSpan(String word) {
cleanedWord.startsWith('note') || cleanedWord.startsWith('nevent'); cleanedWord.startsWith('note') || cleanedWord.startsWith('nevent');
if (isProfile) { if (isProfile) {
return _inlineMention(Nip19.decode(cleanedWord)); final hexKey = bech32ToHex(cleanedWord);
if (hexKey.isNotEmpty) {
return _inlineMention(hexKey);
} else {
return TextSpan(text: "@$cleanedWord");
}
} }
if (isNote) { if (isNote) {
final eventId = Nip19.decode(cleanedWord); final eventId = bech32ToHex(cleanedWord);
return TextSpan(text: eventId, style: TextStyle(color: PRIMARY_1)); return TextSpan(text: eventId, style: TextStyle(color: PRIMARY_1));
} else { } else {
return TextSpan(text: word); return TextSpan(text: word);

View File

@ -26,7 +26,7 @@ packages:
source: hosted source: hosted
version: "2.12.0" version: "2.12.0"
bech32: bech32:
dependency: transitive dependency: "direct main"
description: description:
name: bech32 name: bech32
sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7" sha256: "156cbace936f7720c79a79d16a03efad343b1ef17106716e04b8b8e39f99f7f7"

View File

@ -1,7 +1,7 @@
name: zap_stream_flutter name: zap_stream_flutter
description: "zap.stream" description: "zap.stream"
publish_to: 'none' publish_to: 'none'
version: 0.2.0+2 version: 0.2.2+4
environment: environment:
sdk: ^3.7.2 sdk: ^3.7.2
@ -30,6 +30,7 @@ dependencies:
chewie: ^1.11.3 chewie: ^1.11.3
image_picker: ^1.1.2 image_picker: ^1.1.2
emoji_picker_flutter: ^4.3.0 emoji_picker_flutter: ^4.3.0
bech32: ^0.2.2
dependency_overrides: dependency_overrides:
ndk: ndk: