mirror of
https://github.com/nostrlabs-io/zap-stream-flutter.git
synced 2025-06-16 20:08:50 +00:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
77d70e164b
|
|||
026b2eb85c
|
|||
4c800e03e7
|
|||
819a45bc23
|
|||
53794158c0
|
|||
706fb27664
|
16
.github/workflows/build.yml
vendored
16
.github/workflows/build.yml
vendored
@ -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"
|
||||||
|
@ -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),
|
||||||
|
@ -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("/");
|
||||||
}
|
}
|
||||||
|
@ -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("/");
|
||||||
|
@ -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(
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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 = "";
|
||||||
|
@ -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);
|
||||||
|
@ -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"
|
||||||
|
@ -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:
|
||||||
|
Reference in New Issue
Block a user