From 1d918dac205dbba8fb22c0e8ea8961e8e3f92d37 Mon Sep 17 00:00:00 2001 From: DASHU <385321165@qq.com> Date: Tue, 22 Jul 2025 19:07:51 +0800 Subject: [PATCH] init nostr and add userinfo provider --- lib/component/user/user_name_component.dart | 36 +- lib/component/user/user_pic_component.dart | 58 +++- lib/const/client_connected.dart | 7 + lib/data/db.dart | 18 +- lib/data/event_db.dart | 106 ++++++ lib/data/metadata.dart | 70 ++++ lib/main.dart | 22 ++ lib/provider/relay_provider.dart | 350 ++++++++++++++++++++ lib/provider/userinfo_provider.dart | 227 +++++++++++++ lib/router/keys/keys_item_component.dart | 1 + lib/router/me/me_router.dart | 123 ++++--- 11 files changed, 930 insertions(+), 88 deletions(-) create mode 100644 lib/const/client_connected.dart create mode 100644 lib/data/event_db.dart create mode 100644 lib/data/metadata.dart create mode 100644 lib/provider/relay_provider.dart create mode 100644 lib/provider/userinfo_provider.dart diff --git a/lib/component/user/user_name_component.dart b/lib/component/user/user_name_component.dart index 8bea54b..d9decf9 100644 --- a/lib/component/user/user_name_component.dart +++ b/lib/component/user/user_name_component.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.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 { String pubkey; @@ -17,15 +21,29 @@ class UserNameComponent extends StatefulWidget { class _UserNameComponent extends State { @override Widget build(BuildContext context) { - var npub = Nip19.encodePubKey(widget.pubkey); - if (!widget.fullNpubName) { - npub = Nip19.encodeSimplePubKey(widget.pubkey); - } + return Selector( + builder: (context, metadata, child) { + var npub = Nip19.encodePubKey(widget.pubkey); + if (!widget.fullNpubName) { + npub = Nip19.encodeSimplePubKey(widget.pubkey); + } + var name = npub; - return Text( - npub, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ); + if (metadata != null) { + if (StringUtil.isNotBlank(metadata.displayName)) { + name = metadata.displayName!; + } else if (StringUtil.isNotBlank(metadata.name)) { + name = metadata.name!; + } + } + + return Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }, selector: (context, _provider) { + return _provider.getMetadata(widget.pubkey); + }); } } diff --git a/lib/component/user/user_pic_component.dart b/lib/component/user/user_pic_component.dart index b4ef201..e583c79 100644 --- a/lib/component/user/user_pic_component.dart +++ b/lib/component/user/user_pic_component.dart @@ -1,9 +1,20 @@ 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 { + String pubkey; + double width; - UserPicComponent({required this.width}); + UserPicComponent({ + required this.pubkey, + required this.width, + }); @override State createState() { @@ -14,20 +25,37 @@ class UserPicComponent extends StatefulWidget { class _UserPicComponent extends State { @override Widget build(BuildContext context) { - Widget innerWidget = Icon( - Icons.account_circle, - size: widget.width, - ); + return Selector( + builder: (context, metadata, child) { + Widget innerWidget = Icon( + Icons.account_circle, + size: widget.width, + ); - return Container( - width: widget.width, - height: widget.width, - clipBehavior: Clip.hardEdge, - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(widget.width / 2), - ), - child: innerWidget, - ); + 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( + width: widget.width, + height: widget.width, + clipBehavior: Clip.hardEdge, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(widget.width / 2), + ), + child: innerWidget, + ); + }, selector: (context, _provider) { + if (StringUtil.isNotBlank(widget.pubkey)) { + return _provider.getMetadata(widget.pubkey); + } + }); } } diff --git a/lib/const/client_connected.dart b/lib/const/client_connected.dart new file mode 100644 index 0000000..658df01 --- /dev/null +++ b/lib/const/client_connected.dart @@ -0,0 +1,7 @@ +class ClientConneccted { + static int UN_CONNECT = -1; + + static int CONNECTING = 1; + + static int CONNECTED = 2; +} diff --git a/lib/data/db.dart b/lib/data/db.dart index 7402e0a..4a34752 100644 --- a/lib/data/db.dart +++ b/lib/data/db.dart @@ -4,7 +4,7 @@ import 'package:sqflite/sqflite.dart'; import '../const/base.dart'; class DB { - static const _VERSION = 2; + static const _VERSION = 3; 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);"); 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);"); + + 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 _onUpgrade( Database db, int oldVersion, int newVersion) async { - if (oldVersion == 1) { + if (oldVersion < 2) { db.execute( "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 getCurrentDatabase() async { diff --git a/lib/data/event_db.dart b/lib/data/event_db.dart new file mode 100644 index 0000000..c3cd361 --- /dev/null +++ b/lib/data/event_db.dart @@ -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(List kinds, int skip, limit, + {DatabaseExecutor? db, List? pubkeys}) async { + db = await DB.getDB(db); + List l = []; + List 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> list = await db.rawQuery(sql, args); + for (var listObj in list) { + l.add(loadFromJson(listObj)); + } + return l; + } + + static Future 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 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 execute(String sql, List arguments, + {DatabaseExecutor? db}) async { + db = await DB.getDB(db); + await db.execute(sql, arguments); + } + + static Future delete(String id, {DatabaseExecutor? db}) async { + db = await DB.getDB(db); + await db.execute("delete from event where id = ?", [id]); + } + + static Future 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 data) { + Map m = {}; + m.addAll(data); + + var tagsStr = data["tags"]; + var tagsObj = jsonDecode(tagsStr); + m["tags"] = tagsObj; + m["sig"] = ""; + return Event.fromJson(m); + } +} diff --git a/lib/data/metadata.dart b/lib/data/metadata.dart new file mode 100644 index 0000000..c222c34 --- /dev/null +++ b/lib/data/metadata.dart @@ -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 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 toFullJson() { + var data = toJson(); + data['pub_key'] = this.pubkey; + return data; + } + + Map toJson() { + final Map data = new Map(); + 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; + } +} diff --git a/lib/main.dart b/lib/main.dart index 5b2c1fc..8a23f94 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,6 +8,9 @@ import 'package:flutter/services.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:flutter_localizations/flutter_localizations.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/string_util.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/key_provider.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/router/about_me/about_me_router.dart'; import 'package:nowser/router/app_detail/app_detail_router.dart'; @@ -75,6 +80,12 @@ BookmarkProvider bookmarkProvider = BookmarkProvider(); late MediaDataCache mediaDataCache; +late UserinfoProvider userinfoProvider; + +late RelayProvider relayProvider; + +Nostr? nostr; + Future main() async { WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); rootIsolateToken = RootIsolateToken.instance!; @@ -117,6 +128,11 @@ Future main() async { await doInit(); + relayProvider = RelayProvider.getInstance(); + var tempPrivateKey = generatePrivateKey(); + nostr = await relayProvider.genNostrWithKey(tempPrivateKey); + userinfoProvider = await UserinfoProvider.getInstance(); + mediaDataCache = MediaDataCache(); await bookmarkProvider.init(); @@ -239,6 +255,12 @@ class _MyApp extends State { ListenableProvider.value( value: downloadProvider, ), + ListenableProvider.value( + value: userinfoProvider, + ), + ListenableProvider.value( + value: relayProvider, + ), ], child: MaterialApp( builder: BotToastInit(), diff --git a/lib/provider/relay_provider.dart b/lib/provider/relay_provider.dart new file mode 100644 index 0000000..f643370 --- /dev/null +++ b/lib/provider/relay_provider.dart @@ -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 relayAddrs = []; + + Map relayStatusMap = {}; + + RelayStatus? relayStatusLocal; + + Map _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 parseRelayAddrs(String? content) { + List 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 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 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 getWritableRelays() { + List list = []; + for (var addr in relayAddrs) { + var relayStatus = relayStatusMap[addr]; + if (relayStatus != null && relayStatus.writeAccess) { + list.add(addr); + } + } + return list; + } + + List _getRelayStatuses() { + List 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 oldRelays = []..addAll(relayAddrs); + loadRelayAddrs(event.content); + _updateRelays(oldRelays); + } + + void _updateRelays(List oldRelays) { + List needToRemove = []; + List 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 tempRelayStatus() { + List 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 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); + } + } + } +} diff --git a/lib/provider/userinfo_provider.dart b/lib/provider/userinfo_provider.dart new file mode 100644 index 0000000..93a4c70 --- /dev/null +++ b/lib/provider/userinfo_provider.dart @@ -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 _relayListMetadataCache = {}; + + Map _metadataCache = {}; + + Map _contactListMap = {}; + + Map _handingPubkeys = {}; + + EventMemBox _penddingEvents = EventMemBox(sortAfterAdd: false); + + static UserinfoProvider? _userinfoProvider; + + static Future 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 _checkingFromDBPubKeys = []; + + List _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> 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; + } +} diff --git a/lib/router/keys/keys_item_component.dart b/lib/router/keys/keys_item_component.dart index 36a5062..3480f35 100644 --- a/lib/router/keys/keys_item_component.dart +++ b/lib/router/keys/keys_item_component.dart @@ -31,6 +31,7 @@ class _KeysItemComponent extends State { right: Base.BASE_PADDING_HALF, ), child: UserPicComponent( + pubkey: widget.pubkey, width: 26, ), )); diff --git a/lib/router/me/me_router.dart b/lib/router/me/me_router.dart index b0abf7e..08c0f82 100644 --- a/lib/router/me/me_router.dart +++ b/lib/router/me/me_router.dart @@ -1,5 +1,6 @@ import 'package:bot_toast/bot_toast.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/text_input/text_input_dialog.dart'; import 'package:nowser/component/user/user_name_component.dart'; @@ -62,6 +63,7 @@ class _MeRouter extends CustState { var mediaQueryData = MediaQuery.of(context); var themeData = Theme.of(context); var _appProvider = Provider.of(context); + var _keyProvider = Provider.of(context); s = S.of(context); var listWidgetMargin = const EdgeInsets.only( @@ -69,33 +71,31 @@ class _MeRouter extends CustState { bottom: Base.BASE_PADDING, ); + var pubkeys = _keyProvider.pubkeys; + String defaultPubkey = ""; + if (pubkeys.isNotEmpty) { + defaultPubkey = pubkeys.first; + } + List defaultUserWidgets = []; defaultUserWidgets.add(Container( margin: const EdgeInsets.only( left: Base.BASE_PADDING, ), - child: UserPicComponent(width: 50), + child: UserPicComponent(pubkey: defaultPubkey, width: 50), )); + Widget addOrNameWidget = GestureDetector( + onTap: () { + KeysRouter.addKey(context); + }, + child: Text(s.Click_and_Login), + ); + if (StringUtil.isNotBlank(defaultPubkey)) { + addOrNameWidget = UserNameComponent(defaultPubkey); + } defaultUserWidgets.add(Container( margin: const EdgeInsets.only(left: Base.BASE_PADDING), - child: Selector>( - builder: (context, npubList, child) { - if (npubList.isNotEmpty) { - var pubkey = npubList.first; - return UserNameComponent(pubkey); - } - - return GestureDetector( - onTap: () { - KeysRouter.addKey(context); - }, - child: Text(s.Click_and_Login), - ); - }, - selector: (context, _provider) { - return _provider.pubkeys; - }, - ), + child: addOrNameWidget, )); defaultUserWidgets.add(Expanded(child: Container())); defaultUserWidgets.add(Container( @@ -111,53 +111,52 @@ class _MeRouter extends CustState { margin: const EdgeInsets.only( top: Base.BASE_PADDING, ), - child: Row( - children: defaultUserWidgets, + child: GestureDetector( + onTap: () { + RouterUtil.router(context, RouterPath.KEYS); + }, + behavior: HitTestBehavior.translucent, + child: Row( + children: defaultUserWidgets, + ), ), ); - var memberListWidget = Selector>( - builder: (context, npubList, child) { - if (npubList.isEmpty) { - return Container(); - } - - List memberList = []; - for (var pubkey in npubList) { - memberList.add(Container( - margin: EdgeInsets.only(left: Base.BASE_PADDING_HALF), - child: UserPicComponent(width: 30), - )); - } - memberList.add(Expanded(child: Container())); - memberList.add(GestureDetector( - child: Icon(Icons.chevron_right), - )); - - return Container( - decoration: BoxDecoration( - color: themeData.cardColor, - borderRadius: BorderRadius.circular( - Base.BASE_PADDING, - ), - ), - margin: listWidgetMargin, - padding: EdgeInsets.all(Base.BASE_PADDING), - child: GestureDetector( - onTap: () { - RouterUtil.router(context, RouterPath.KEYS); - }, - behavior: HitTestBehavior.translucent, - child: Row( - children: memberList, - ), - ), - ); - }, - selector: (context, _provider) { - return _provider.pubkeys; - }, + Widget memberListWidget = Container( + height: Base.BASE_PADDING, ); + if (pubkeys.length > 1) { + List memberList = []; + for (var pubkey in pubkeys) { + memberList.add(Container( + margin: const EdgeInsets.only(left: Base.BASE_PADDING_HALF), + child: UserPicComponent(pubkey: pubkey, width: 30), + )); + } + memberList.add(Expanded(child: Container())); + memberList.add(GestureDetector( + child: const Icon(Icons.chevron_right), + )); + memberListWidget = Container( + decoration: BoxDecoration( + color: themeData.cardColor, + borderRadius: BorderRadius.circular( + Base.BASE_PADDING, + ), + ), + margin: listWidgetMargin, + padding: const EdgeInsets.all(Base.BASE_PADDING), + child: GestureDetector( + onTap: () { + RouterUtil.router(context, RouterPath.KEYS); + }, + behavior: HitTestBehavior.translucent, + child: Row( + children: memberList, + ), + ), + ); + } List webItemList = []; webItemList.add(MeRouterWebItemComponent(