init nostr and add userinfo provider

This commit is contained in:
DASHU
2025-07-22 19:07:51 +08:00
parent 08bb4c63b4
commit 1d918dac20
11 changed files with 930 additions and 88 deletions

View File

@@ -1,5 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:nostr_sdk/nip19/nip19.dart'; import 'package:nostr_sdk/nip19/nip19.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/data/metadata.dart';
import 'package:nowser/provider/userinfo_provider.dart';
import 'package:provider/provider.dart';
class UserNameComponent extends StatefulWidget { class UserNameComponent extends StatefulWidget {
String pubkey; String pubkey;
@@ -17,15 +21,29 @@ class UserNameComponent extends StatefulWidget {
class _UserNameComponent extends State<UserNameComponent> { class _UserNameComponent extends State<UserNameComponent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<UserinfoProvider, Metadata?>(
builder: (context, metadata, child) {
var npub = Nip19.encodePubKey(widget.pubkey); var npub = Nip19.encodePubKey(widget.pubkey);
if (!widget.fullNpubName) { if (!widget.fullNpubName) {
npub = Nip19.encodeSimplePubKey(widget.pubkey); npub = Nip19.encodeSimplePubKey(widget.pubkey);
} }
var name = npub;
if (metadata != null) {
if (StringUtil.isNotBlank(metadata.displayName)) {
name = metadata.displayName!;
} else if (StringUtil.isNotBlank(metadata.name)) {
name = metadata.name!;
}
}
return Text( return Text(
npub, name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
); );
}, selector: (context, _provider) {
return _provider.getMetadata(widget.pubkey);
});
} }
} }

View File

@@ -1,9 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:provider/provider.dart';
import '../../data/metadata.dart';
import '../../provider/userinfo_provider.dart';
import '../image_component.dart';
class UserPicComponent extends StatefulWidget { class UserPicComponent extends StatefulWidget {
String pubkey;
double width; double width;
UserPicComponent({required this.width}); UserPicComponent({
required this.pubkey,
required this.width,
});
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {
@@ -14,11 +25,23 @@ class UserPicComponent extends StatefulWidget {
class _UserPicComponent extends State<UserPicComponent> { class _UserPicComponent extends State<UserPicComponent> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<UserinfoProvider, Metadata?>(
builder: (context, metadata, child) {
Widget innerWidget = Icon( Widget innerWidget = Icon(
Icons.account_circle, Icons.account_circle,
size: widget.width, size: widget.width,
); );
if (metadata != null && StringUtil.isNotBlank(metadata.picture)) {
innerWidget = ImageComponent(
imageUrl: metadata.picture!,
width: widget.width,
height: widget.width,
fit: BoxFit.cover,
placeholder: (context, url) => CircularProgressIndicator(),
);
}
return Container( return Container(
width: widget.width, width: widget.width,
height: widget.width, height: widget.width,
@@ -29,5 +52,10 @@ class _UserPicComponent extends State<UserPicComponent> {
), ),
child: innerWidget, child: innerWidget,
); );
}, selector: (context, _provider) {
if (StringUtil.isNotBlank(widget.pubkey)) {
return _provider.getMetadata(widget.pubkey);
}
});
} }
} }

View File

@@ -0,0 +1,7 @@
class ClientConneccted {
static int UN_CONNECT = -1;
static int CONNECTING = 1;
static int CONNECTED = 2;
}

View File

@@ -4,7 +4,7 @@ import 'package:sqflite/sqflite.dart';
import '../const/base.dart'; import '../const/base.dart';
class DB { class DB {
static const _VERSION = 2; static const _VERSION = 3;
static const _dbName = "nowser.db"; static const _dbName = "nowser.db";
@@ -43,14 +43,28 @@ class DB {
"create table bookmark(id integer not null constraint bookmark_pk primary key autoincrement,title text,url text not null,favicon text,weight integer,added_to_index integer, added_to_qa integer,created_at integer);"); "create table bookmark(id integer not null constraint bookmark_pk primary key autoincrement,title text,url text not null,favicon text,weight integer,added_to_index integer, added_to_qa integer,created_at integer);");
db.execute( db.execute(
"create table browser_history(id integer not null constraint browser_history_pk primary key autoincrement,title text,url text not null,favicon text,created_at integer);"); "create table browser_history(id integer not null constraint browser_history_pk primary key autoincrement,title text,url text not null,favicon text,created_at integer);");
db.execute(
"create table event(id text,pubkey text,created_at integer,kind integer,tags text,content text);");
db.execute("create unique index event_key_index_id_uindex on event (id);");
db.execute("create index event_date_index on event (kind, created_at);");
} }
static Future<void> _onUpgrade( static Future<void> _onUpgrade(
Database db, int oldVersion, int newVersion) async { Database db, int oldVersion, int newVersion) async {
if (oldVersion == 1) { if (oldVersion < 2) {
db.execute( db.execute(
"alter table bookmark add added_to_qa integer after added_to_index"); "alter table bookmark add added_to_qa integer after added_to_index");
} }
if (oldVersion < 3) {
db.execute(
"create table event(id text,pubkey text,created_at integer,kind integer,tags text,content text);");
db.execute(
"create unique index event_key_index_id_uindex on event (id);");
db.execute(
"create index event_date_index on event (kind, created_at);");
}
} }
static Future<Database> getCurrentDatabase() async { static Future<Database> getCurrentDatabase() async {

106
lib/data/event_db.dart Normal file
View File

@@ -0,0 +1,106 @@
import 'dart:convert';
import 'dart:developer';
import 'package:nostr_sdk/event.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:sqflite/sqflite.dart';
import 'db.dart';
class EventDB {
static Future<List<Event>> list(List<int> kinds, int skip, limit,
{DatabaseExecutor? db, List<String>? pubkeys}) async {
db = await DB.getDB(db);
List<Event> l = [];
List<dynamic> args = [];
var sql = "select * from event where kind in(";
for (var kind in kinds) {
sql += "?,";
args.add(kind);
}
sql = sql.substring(0, sql.length - 1);
sql += ")";
if (pubkeys != null && pubkeys.isNotEmpty) {
if (pubkeys.length == 1) {
sql += " and pubkey = ? ";
args.add(pubkeys.first);
} else {
sql += " and pubkey in(";
for (var pubkey in pubkeys) {
sql += "?,";
args.add(pubkey);
}
sql = sql.substring(0, sql.length - 1);
sql += ")";
}
}
sql += " order by created_at desc limit ?, ?";
args.add(skip);
args.add(limit);
List<Map<String, dynamic>> list = await db.rawQuery(sql, args);
for (var listObj in list) {
l.add(loadFromJson(listObj));
}
return l;
}
static Future<int> insert(Event o, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
var jsonObj = o.toJson();
var tags = jsonEncode(o.tags);
jsonObj["tags"] = tags;
jsonObj.remove("sig");
try {
return await db.insert("event", jsonObj);
} catch (e) {
return 0;
}
}
static Future<Event?> get(String id, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
var list = await db.query("event", where: "id = ?", whereArgs: [id]);
if (list.isNotEmpty) {
return Event.fromJson(list[0]);
}
}
static Future<Event?> execute(String sql, List<Object?> arguments,
{DatabaseExecutor? db}) async {
db = await DB.getDB(db);
await db.execute(sql, arguments);
}
static Future<Event?> delete(String id, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
await db.execute("delete from event where id = ?", [id]);
}
static Future<void> deleteAll({DatabaseExecutor? db}) async {
db = await DB.getDB(db);
db.execute("delete from event ", []);
}
static Future update(Event o, {DatabaseExecutor? db}) async {
db = await DB.getDB(db);
var jsonObj = o.toJson();
var tags = jsonEncode(o.tags);
jsonObj["tags"] = tags;
jsonObj.remove("sig");
await db.update("event", jsonObj, where: "id = ?", whereArgs: [o.id]);
}
static Event loadFromJson(Map<String, dynamic> data) {
Map<String, dynamic> m = {};
m.addAll(data);
var tagsStr = data["tags"];
var tagsObj = jsonDecode(tagsStr);
m["tags"] = tagsObj;
m["sig"] = "";
return Event.fromJson(m);
}
}

70
lib/data/metadata.dart Normal file
View File

@@ -0,0 +1,70 @@
class Metadata {
String? pubkey;
String? name;
String? displayName;
String? picture;
String? banner;
String? website;
String? about;
String? nip05;
String? lud16;
String? lud06;
int? updated_at;
int? valid;
Metadata({
this.pubkey,
this.name,
this.displayName,
this.picture,
this.banner,
this.website,
this.about,
this.nip05,
this.lud16,
this.lud06,
this.updated_at,
this.valid,
});
Metadata.fromJson(Map<String, dynamic> json) {
pubkey = json['pub_key'];
name = json['name'];
displayName = json['display_name'];
picture = json['picture'];
banner = json['banner'];
website = json['website'];
about = json['about'];
if (json['nip05'] != null && json['nip05'] is String) {
nip05 = json['nip05'];
}
lud16 = json['lud16'];
lud06 = json['lud06'];
if (json['updated_at'] != null && json['updated_at'] is int) {
updated_at = json['updated_at'];
}
valid = json['valid'];
}
Map<String, dynamic> toFullJson() {
var data = toJson();
data['pub_key'] = this.pubkey;
return data;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['name'] = this.name;
data['display_name'] = this.displayName;
data['picture'] = this.picture;
data['banner'] = this.banner;
data['website'] = this.website;
data['about'] = this.about;
data['nip05'] = this.nip05;
data['lud16'] = this.lud16;
data['lud06'] = this.lud06;
data['updated_at'] = this.updated_at;
data['valid'] = this.valid;
return data;
}
}

View File

@@ -8,6 +8,9 @@ import 'package:flutter/services.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:nostr_sdk/client_utils/keys.dart';
import 'package:nostr_sdk/nostr.dart';
import 'package:nostr_sdk/signer/local_nostr_signer.dart';
import 'package:nostr_sdk/utils/platform_util.dart'; import 'package:nostr_sdk/utils/platform_util.dart';
import 'package:nostr_sdk/utils/string_util.dart'; import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/data/db.dart'; import 'package:nowser/data/db.dart';
@@ -18,6 +21,8 @@ import 'package:nowser/provider/build_in_relay_provider.dart';
import 'package:nowser/provider/download_provider.dart'; import 'package:nowser/provider/download_provider.dart';
import 'package:nowser/provider/key_provider.dart'; import 'package:nowser/provider/key_provider.dart';
import 'package:nowser/provider/permission_check_mixin.dart'; import 'package:nowser/provider/permission_check_mixin.dart';
import 'package:nowser/provider/relay_provider.dart';
import 'package:nowser/provider/userinfo_provider.dart';
import 'package:nowser/provider/web_provider.dart'; import 'package:nowser/provider/web_provider.dart';
import 'package:nowser/router/about_me/about_me_router.dart'; import 'package:nowser/router/about_me/about_me_router.dart';
import 'package:nowser/router/app_detail/app_detail_router.dart'; import 'package:nowser/router/app_detail/app_detail_router.dart';
@@ -75,6 +80,12 @@ BookmarkProvider bookmarkProvider = BookmarkProvider();
late MediaDataCache mediaDataCache; late MediaDataCache mediaDataCache;
late UserinfoProvider userinfoProvider;
late RelayProvider relayProvider;
Nostr? nostr;
Future<void> main() async { Future<void> main() async {
WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
rootIsolateToken = RootIsolateToken.instance!; rootIsolateToken = RootIsolateToken.instance!;
@@ -117,6 +128,11 @@ Future<void> main() async {
await doInit(); await doInit();
relayProvider = RelayProvider.getInstance();
var tempPrivateKey = generatePrivateKey();
nostr = await relayProvider.genNostrWithKey(tempPrivateKey);
userinfoProvider = await UserinfoProvider.getInstance();
mediaDataCache = MediaDataCache(); mediaDataCache = MediaDataCache();
await bookmarkProvider.init(); await bookmarkProvider.init();
@@ -239,6 +255,12 @@ class _MyApp extends State<MyApp> {
ListenableProvider<DownloadProvider>.value( ListenableProvider<DownloadProvider>.value(
value: downloadProvider, value: downloadProvider,
), ),
ListenableProvider<UserinfoProvider>.value(
value: userinfoProvider,
),
ListenableProvider<RelayProvider>.value(
value: relayProvider,
),
], ],
child: MaterialApp( child: MaterialApp(
builder: BotToastInit(), builder: BotToastInit(),

View File

@@ -0,0 +1,350 @@
import 'dart:developer';
import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart';
import 'package:nesigner_adapter/nesigner.dart';
import 'package:nesigner_adapter/nesigner_util.dart';
import 'package:nostr_sdk/event.dart';
import 'package:nostr_sdk/event_kind.dart';
import 'package:nostr_sdk/nip02/nip02.dart';
import 'package:nostr_sdk/nip07/nip07_signer.dart';
import 'package:nostr_sdk/nip19/nip19.dart';
import 'package:nostr_sdk/nip46/nostr_remote_signer.dart';
import 'package:nostr_sdk/nip46/nostr_remote_signer_info.dart';
import 'package:nostr_sdk/nip55/android_nostr_signer.dart';
import 'package:nostr_sdk/nip65/nip65.dart';
import 'package:nostr_sdk/nostr.dart';
import 'package:nostr_sdk/relay/relay.dart';
import 'package:nostr_sdk/relay/relay_base.dart';
import 'package:nostr_sdk/relay/relay_isolate.dart';
import 'package:nostr_sdk/relay/relay_mode.dart';
import 'package:nostr_sdk/relay/relay_status.dart';
import 'package:nostr_sdk/relay/relay_type.dart';
import 'package:nostr_sdk/relay_local/relay_local.dart';
import 'package:nostr_sdk/signer/local_nostr_signer.dart';
import 'package:nostr_sdk/signer/nostr_signer.dart';
import 'package:nostr_sdk/signer/pubkey_only_nostr_signer.dart';
import 'package:nostr_sdk/utils/platform_util.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import '../const/client_connected.dart';
import '../main.dart';
import 'data_util.dart';
class RelayProvider extends ChangeNotifier {
static RelayProvider? _relayProvider;
List<String> relayAddrs = [];
Map<String, RelayStatus> relayStatusMap = {};
RelayStatus? relayStatusLocal;
Map<String, RelayStatus> _tempRelayStatusMap = {};
static RelayProvider getInstance() {
if (_relayProvider == null) {
_relayProvider = RelayProvider();
// _relayProvider!._load();
}
return _relayProvider!;
}
void loadRelayAddrs(String? content) {
var relays = parseRelayAddrs(content);
if (relays.isEmpty) {
relays = [
"wss://nos.lol",
"wss://nostr.wine",
"wss://atlas.nostr.land",
"wss://relay.damus.io",
"wss://nostr-relay.app",
"wss://nostr.oxtr.dev",
"wss://relayable.org",
"wss://relay.primal.net",
"wss://relay.nostr.bg",
"wss://relay.nostr.band",
"wss://yabu.me",
"wss://nostr.mom"
];
}
relayAddrs = relays;
}
List<String> parseRelayAddrs(String? content) {
List<String> relays = [];
if (StringUtil.isBlank(content)) {
return relays;
}
var relayStatuses = NIP02.parseContenToRelays(content!);
for (var relayStatus in relayStatuses) {
var addr = relayStatus.addr;
relays.add(addr);
var oldRelayStatus = relayStatusMap[addr];
if (oldRelayStatus != null) {
oldRelayStatus.readAccess = relayStatus.readAccess;
oldRelayStatus.writeAccess = relayStatus.writeAccess;
} else {
relayStatusMap[addr] = relayStatus;
}
}
return relays;
}
RelayStatus? getRelayStatus(String addr) {
return relayStatusMap[addr];
}
String relayNumStr() {
var normalLength = relayAddrs.length;
int connectedNum = 0;
var it = relayStatusMap.values;
for (var status in it) {
if (status.connected == ClientConneccted.CONNECTED) {
connectedNum++;
}
}
return "$connectedNum / $normalLength";
}
int total() {
return relayAddrs.length;
}
Future<Nostr?> genNostrWithKey(String key) async {
NostrSigner? nostrSigner;
if (Nip19.isPubkey(key)) {
nostrSigner = PubkeyOnlyNostrSigner(Nip19.decode(key));
} else if (AndroidNostrSigner.isAndroidNostrSignerKey(key)) {
var pubkey = AndroidNostrSigner.getPubkeyFromKey(key);
var package = AndroidNostrSigner.getPackageFromKey(key);
nostrSigner = AndroidNostrSigner(pubkey: pubkey, package: package);
} else if (NIP07Signer.isWebNostrSignerKey(key)) {
var pubkey = NIP07Signer.getPubkey(key);
nostrSigner = NIP07Signer(pubkey: pubkey);
} else if (NostrRemoteSignerInfo.isBunkerUrl(key)) {
var info = NostrRemoteSignerInfo.parseBunkerUrl(key);
if (info == null) {
return null;
}
bool hasConnected = false;
if (StringUtil.isNotBlank(info.userPubkey)) {
hasConnected = true;
}
nostrSigner = NostrRemoteSigner(RelayMode.BASE_MODE, info);
await (nostrSigner as NostrRemoteSigner)
.connect(sendConnectRequest: !hasConnected);
if (StringUtil.isBlank(info.userPubkey)) {
await nostrSigner.pullPubkey();
}
if (await nostrSigner.getPublicKey() == null) {
return null;
}
} else if (Nesigner.isNesignerKey(key)) {
var pinCode = Nesigner.getPinCodeFromKey(key);
var pubkey = Nesigner.getPubkeyFromKey(key);
nostrSigner = Nesigner(pinCode, pubkey: pubkey);
try {
if (!(await (nostrSigner as Nesigner).start())) {
return null;
}
} catch (e) {
return null;
}
} else {
try {
nostrSigner = LocalNostrSigner(key);
} catch (e) {}
}
if (nostrSigner == null) {
return null;
}
return await genNostr(nostrSigner);
}
Future<Nostr?> genNostr(NostrSigner signer) async {
var pubkey = await signer.getPublicKey();
if (pubkey == null) {
return null;
}
var _nostr = Nostr(signer, pubkey, [], genTempRelay, onNotice: null);
log("nostr init over");
loadRelayAddrs(null);
for (var relayAddr in relayAddrs) {
log("begin to init $relayAddr");
var custRelay = genRelay(relayAddr);
try {
_nostr.addRelay(custRelay, init: true);
} catch (e) {
log("relay $relayAddr add to pool error ${e.toString()}");
}
}
return _nostr;
}
void onRelayStatusChange() {
notifyListeners();
}
void addRelay(String relayAddr) {
if (!relayAddrs.contains(relayAddr)) {
relayAddrs.add(relayAddr);
_doAddRelay(relayAddr);
}
}
void _doAddRelay(String relayAddr,
{bool init = false, int relayType = RelayType.NORMAL}) {
var custRelay = genRelay(relayAddr, relayType: relayType);
log("begin to init $relayAddr");
nostr!.addRelay(custRelay,
autoSubscribe: true, init: init, relayType: relayType);
}
void removeRelay(String relayAddr) {
if (relayAddrs.contains(relayAddr)) {
relayAddrs.remove(relayAddr);
relayStatusMap.remove(relayAddr);
nostr!.removeRelay(relayAddr);
}
}
List<String> getWritableRelays() {
List<String> list = [];
for (var addr in relayAddrs) {
var relayStatus = relayStatusMap[addr];
if (relayStatus != null && relayStatus.writeAccess) {
list.add(addr);
}
}
return list;
}
List<RelayStatus> _getRelayStatuses() {
List<RelayStatus> relayStatuses = [];
for (var addr in relayAddrs) {
var relayStatus = relayStatusMap[addr];
if (relayStatus != null) {
relayStatuses.add(relayStatus);
}
}
return relayStatuses;
}
Relay genRelay(String relayAddr, {int relayType = RelayType.NORMAL}) {
var relayStatus = relayStatusMap[relayAddr];
if (relayStatus == null) {
relayStatus = RelayStatus(relayAddr, relayType: relayType);
relayStatusMap[relayAddr] = relayStatus;
}
return _doGenRelay(relayStatus);
}
Relay _doGenRelay(RelayStatus relayStatus) {
var relayAddr = relayStatus.addr;
return RelayBase(
relayAddr,
relayStatus,
)..relayStatusCallback = onRelayStatusChange;
}
void relayUpdateByContactListEvent(Event event) {
List<String> oldRelays = []..addAll(relayAddrs);
loadRelayAddrs(event.content);
_updateRelays(oldRelays);
}
void _updateRelays(List<String> oldRelays) {
List<String> needToRemove = [];
List<String> needToAdd = [];
for (var oldRelay in oldRelays) {
if (!relayAddrs.contains(oldRelay)) {
// new addrs don't contain old relay, need to remove
needToRemove.add(oldRelay);
}
}
for (var relayAddr in relayAddrs) {
if (!oldRelays.contains(relayAddr)) {
// old addrs don't contain new relay, need to add
needToAdd.add(relayAddr);
}
}
for (var relayAddr in needToRemove) {
relayStatusMap.remove(relayAddr);
nostr!.removeRelay(relayAddr);
}
for (var relayAddr in needToAdd) {
_doAddRelay(relayAddr);
}
}
void clear() {
// sharedPreferences.remove(DataKey.RELAY_LIST);
relayStatusMap.clear();
loadRelayAddrs(null);
_tempRelayStatusMap.clear();
}
List<RelayStatus> tempRelayStatus() {
List<RelayStatus> list = []..addAll(_tempRelayStatusMap.values);
return list;
}
Relay genTempRelay(String addr) {
var rs = _tempRelayStatusMap[addr];
if (rs == null) {
rs = RelayStatus(addr);
_tempRelayStatusMap[addr] = rs;
}
return _doGenRelay(rs);
}
void cleanTempRelays() {
List<String> needRemoveList = [];
var now = DateTime.now().millisecondsSinceEpoch;
for (var entry in _tempRelayStatusMap.entries) {
var addr = entry.key;
var status = entry.value;
if (now - status.connectTime.millisecondsSinceEpoch > 1000 * 60 * 10 &&
(status.lastNoteTime == null ||
((now - status.lastNoteTime!.millisecondsSinceEpoch) >
1000 * 60 * 10)) &&
(status.lastQueryTime == null ||
((now - status.lastQueryTime!.millisecondsSinceEpoch) >
1000 * 60 * 10))) {
// init time over 10 min
// last note time over 10 min
// last query time over 10 min
needRemoveList.add(addr);
}
}
for (var addr in needRemoveList) {
if (!nostr!.tempRelayHasSubscription(addr)) {
// don't contain subscription, remote!
_tempRelayStatusMap.remove(addr);
nostr!.removeTempRelay(addr);
}
}
}
}

View File

@@ -0,0 +1,227 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:nostr_sdk/event.dart';
import 'package:nostr_sdk/event_kind.dart';
import 'package:nostr_sdk/event_mem_box.dart';
import 'package:nostr_sdk/filter.dart';
import 'package:nostr_sdk/nip02/contact_list.dart';
import 'package:nostr_sdk/nip65/relay_list_metadata.dart';
import 'package:nostr_sdk/utils/later_function.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import '../data/event_db.dart';
import '../data/metadata.dart';
import '../main.dart';
class UserinfoProvider extends ChangeNotifier with LaterFunction {
Map<String, RelayListMetadata> _relayListMetadataCache = {};
Map<String, Metadata> _metadataCache = {};
Map<String, ContactList> _contactListMap = {};
Map<String, int> _handingPubkeys = {};
EventMemBox _penddingEvents = EventMemBox(sortAfterAdd: false);
static UserinfoProvider? _userinfoProvider;
static Future<UserinfoProvider> getInstance() async {
if (_userinfoProvider == null) {
_userinfoProvider = UserinfoProvider();
// lazyTimeMS begin bigger and request less
_userinfoProvider!.laterTimeMS = 2000;
}
return _userinfoProvider!;
}
Metadata? getMetadata(String pubkey, {bool loadData = true}) {
var metadata = _metadataCache[pubkey];
if (metadata != null) {
return metadata;
}
if (loadData) {
_handleDataNotfound(pubkey);
}
return null;
}
List<String> _checkingFromDBPubKeys = [];
List<String> _needUpdatePubKeys = [];
void _handleDataNotfound(String pubkey) {
if (!_checkingFromDBPubKeys.contains(pubkey) &&
!_handingPubkeys.containsKey(pubkey)) {
_checkingFromDBPubKeys.add(pubkey);
EventDB.list(
[
EventKind.METADATA,
EventKind.RELAY_LIST_METADATA,
EventKind.CONTACT_LIST,
],
0,
100,
pubkeys: [pubkey]).then((eventList) {
// print("${eventList.length} metadata find from db.");
_penddingEvents.addList(eventList);
if (eventList.length < 3) {
_needUpdatePubKeys.add(pubkey);
}
_checkingFromDBPubKeys.remove(pubkey);
later(_laterCallback);
});
}
}
void _laterCallback() {
if (_needUpdatePubKeys.isNotEmpty) {
_laterSearch();
}
if (!_penddingEvents.isEmpty()) {
_handlePenddingEvents();
}
}
void _handlePenddingEvents() {
for (var event in _penddingEvents.all()) {
_handingPubkeys.remove(event.pubkey);
if (event.kind == EventKind.METADATA) {
if (StringUtil.isBlank(event.content)) {
continue;
}
// check cache
var oldMetadata = _metadataCache[event.pubkey];
if (oldMetadata == null) {
// insert
EventDB.insert(event);
_eventToMetadataCache(event);
} else if (oldMetadata.updated_at! < event.createdAt) {
// update, remote old event and insert new event
EventDB.execute("delete from event where kind = ? and pubkey = ?",
[EventKind.METADATA, event.pubkey]);
EventDB.insert(event);
_eventToMetadataCache(event);
}
} else if (event.kind == EventKind.RELAY_LIST_METADATA) {
// this is relayInfoMetadata, only set to cache, not update UI
var oldRelayListMetadata = _relayListMetadataCache[event.pubkey];
if (oldRelayListMetadata == null) {
// insert
EventDB.insert(event);
_eventToRelayListCache(event);
} else if (event.createdAt > oldRelayListMetadata.createdAt) {
// update, remote old event and insert new event
EventDB.execute("delete from event where kind = ? and pubkey = ?",
[EventKind.RELAY_LIST_METADATA, event.pubkey]);
EventDB.insert(event);
_eventToRelayListCache(event);
}
} else if (event.kind == EventKind.CONTACT_LIST) {
var oldContactList = _contactListMap[event.pubkey];
if (oldContactList == null) {
// insert
EventDB.insert(event);
_eventToContactList(event);
} else if (event.createdAt > oldContactList.createdAt) {
// update, remote old event and insert new event
EventDB.execute(
"delete from event where key_index = ? and kind = ? and pubkey = ?",
[EventKind.CONTACT_LIST, event.pubkey]);
EventDB.insert(event);
_eventToContactList(event);
}
}
}
_penddingEvents.clear();
notifyListeners();
}
void onEvent(Event event) {
_penddingEvents.add(event);
later(_laterCallback);
}
void _laterSearch() {
if (_needUpdatePubKeys.isEmpty) {
return;
}
// if (!nostr!.readable()) {
// // the nostr isn't readable later handle it again.
// later(_laterCallback, null);
// return;
// }
List<Map<String, dynamic>> filters = [];
for (var pubkey in _needUpdatePubKeys) {
{
var filter = Filter(
kinds: [
EventKind.METADATA,
],
authors: [pubkey],
limit: 1,
);
filters.add(filter.toJson());
}
{
var filter = Filter(
kinds: [
EventKind.RELAY_LIST_METADATA,
],
authors: [pubkey],
limit: 1,
);
filters.add(filter.toJson());
}
{
var filter = Filter(
kinds: [
EventKind.CONTACT_LIST,
],
authors: [pubkey],
limit: 1,
);
filters.add(filter.toJson());
}
if (filters.length > 20) {
nostr!.query(filters, onEvent);
filters = [];
}
}
if (filters.isNotEmpty) {
nostr!.query(filters, onEvent);
}
for (var pubkey in _needUpdatePubKeys) {
_handingPubkeys[pubkey] = 1;
}
_needUpdatePubKeys.clear();
}
void _eventToMetadataCache(Event event) {
var jsonObj = jsonDecode(event.content);
var md = Metadata.fromJson(jsonObj);
md.pubkey = event.pubkey;
md.updated_at = event.createdAt;
_metadataCache[event.pubkey] = md;
}
void _eventToRelayListCache(Event event) {
RelayListMetadata rlm = RelayListMetadata.fromEvent(event);
_relayListMetadataCache[rlm.pubkey] = rlm;
}
void _eventToContactList(Event event) {
var contactList = ContactList.fromJson(event.tags, event.createdAt);
_contactListMap[event.pubkey] = contactList;
}
}

View File

@@ -31,6 +31,7 @@ class _KeysItemComponent extends State<KeysItemComponent> {
right: Base.BASE_PADDING_HALF, right: Base.BASE_PADDING_HALF,
), ),
child: UserPicComponent( child: UserPicComponent(
pubkey: widget.pubkey,
width: 26, width: 26,
), ),
)); ));

View File

@@ -1,5 +1,6 @@
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:nostr_sdk/utils/string_util.dart';
import 'package:nowser/component/cust_state.dart'; import 'package:nowser/component/cust_state.dart';
import 'package:nowser/component/text_input/text_input_dialog.dart'; import 'package:nowser/component/text_input/text_input_dialog.dart';
import 'package:nowser/component/user/user_name_component.dart'; import 'package:nowser/component/user/user_name_component.dart';
@@ -62,6 +63,7 @@ class _MeRouter extends CustState<MeRouter> {
var mediaQueryData = MediaQuery.of(context); var mediaQueryData = MediaQuery.of(context);
var themeData = Theme.of(context); var themeData = Theme.of(context);
var _appProvider = Provider.of<AppProvider>(context); var _appProvider = Provider.of<AppProvider>(context);
var _keyProvider = Provider.of<KeyProvider>(context);
s = S.of(context); s = S.of(context);
var listWidgetMargin = const EdgeInsets.only( var listWidgetMargin = const EdgeInsets.only(
@@ -69,33 +71,31 @@ class _MeRouter extends CustState<MeRouter> {
bottom: Base.BASE_PADDING, bottom: Base.BASE_PADDING,
); );
var pubkeys = _keyProvider.pubkeys;
String defaultPubkey = "";
if (pubkeys.isNotEmpty) {
defaultPubkey = pubkeys.first;
}
List<Widget> defaultUserWidgets = []; List<Widget> defaultUserWidgets = [];
defaultUserWidgets.add(Container( defaultUserWidgets.add(Container(
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
left: Base.BASE_PADDING, left: Base.BASE_PADDING,
), ),
child: UserPicComponent(width: 50), child: UserPicComponent(pubkey: defaultPubkey, width: 50),
)); ));
defaultUserWidgets.add(Container( Widget addOrNameWidget = GestureDetector(
margin: const EdgeInsets.only(left: Base.BASE_PADDING),
child: Selector<KeyProvider, List<String>>(
builder: (context, npubList, child) {
if (npubList.isNotEmpty) {
var pubkey = npubList.first;
return UserNameComponent(pubkey);
}
return GestureDetector(
onTap: () { onTap: () {
KeysRouter.addKey(context); KeysRouter.addKey(context);
}, },
child: Text(s.Click_and_Login), child: Text(s.Click_and_Login),
); );
}, if (StringUtil.isNotBlank(defaultPubkey)) {
selector: (context, _provider) { addOrNameWidget = UserNameComponent(defaultPubkey);
return _provider.pubkeys; }
}, defaultUserWidgets.add(Container(
), margin: const EdgeInsets.only(left: Base.BASE_PADDING),
child: addOrNameWidget,
)); ));
defaultUserWidgets.add(Expanded(child: Container())); defaultUserWidgets.add(Expanded(child: Container()));
defaultUserWidgets.add(Container( defaultUserWidgets.add(Container(
@@ -111,30 +111,33 @@ class _MeRouter extends CustState<MeRouter> {
margin: const EdgeInsets.only( margin: const EdgeInsets.only(
top: Base.BASE_PADDING, top: Base.BASE_PADDING,
), ),
child: GestureDetector(
onTap: () {
RouterUtil.router(context, RouterPath.KEYS);
},
behavior: HitTestBehavior.translucent,
child: Row( child: Row(
children: defaultUserWidgets, children: defaultUserWidgets,
), ),
),
); );
var memberListWidget = Selector<KeyProvider, List<String>>( Widget memberListWidget = Container(
builder: (context, npubList, child) { height: Base.BASE_PADDING,
if (npubList.isEmpty) { );
return Container(); if (pubkeys.length > 1) {
}
List<Widget> memberList = []; List<Widget> memberList = [];
for (var pubkey in npubList) { for (var pubkey in pubkeys) {
memberList.add(Container( memberList.add(Container(
margin: EdgeInsets.only(left: Base.BASE_PADDING_HALF), margin: const EdgeInsets.only(left: Base.BASE_PADDING_HALF),
child: UserPicComponent(width: 30), child: UserPicComponent(pubkey: pubkey, width: 30),
)); ));
} }
memberList.add(Expanded(child: Container())); memberList.add(Expanded(child: Container()));
memberList.add(GestureDetector( memberList.add(GestureDetector(
child: Icon(Icons.chevron_right), child: const Icon(Icons.chevron_right),
)); ));
memberListWidget = Container(
return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: themeData.cardColor, color: themeData.cardColor,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
@@ -142,7 +145,7 @@ class _MeRouter extends CustState<MeRouter> {
), ),
), ),
margin: listWidgetMargin, margin: listWidgetMargin,
padding: EdgeInsets.all(Base.BASE_PADDING), padding: const EdgeInsets.all(Base.BASE_PADDING),
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () {
RouterUtil.router(context, RouterPath.KEYS); RouterUtil.router(context, RouterPath.KEYS);
@@ -153,11 +156,7 @@ class _MeRouter extends CustState<MeRouter> {
), ),
), ),
); );
}, }
selector: (context, _provider) {
return _provider.pubkeys;
},
);
List<Widget> webItemList = []; List<Widget> webItemList = [];
webItemList.add(MeRouterWebItemComponent( webItemList.add(MeRouterWebItemComponent(