refactor: minor profile loader improvement

This commit is contained in:
2025-05-28 11:40:53 +01:00
parent c6b76bc64d
commit 2ec17c6c41
4 changed files with 157 additions and 145 deletions

View File

@ -6,6 +6,7 @@ import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/theme.dart';
import 'package:zap_stream_flutter/widgets/avatar_upload.dart';
import 'package:zap_stream_flutter/widgets/button.dart';
import 'package:zap_stream_flutter/widgets/profile.dart';
class SettingsProfilePage extends StatelessWidget {
final TextEditingController _picture = TextEditingController();
@ -22,97 +23,94 @@ class SettingsProfilePage extends StatelessWidget {
final pubkey = ndk.accounts.getPublicKey();
if (pubkey == null) return Text(t.settings.profile.error.logged_out);
return FutureBuilder(
future: ndk.metadata.loadMetadata(pubkey),
builder: (context, state) {
if (state.hasData) {
_name.text = state.data!.name ?? "";
_about.text = state.data!.about ?? "";
_nip5.text = state.data!.nip05 ?? "";
_lud16.text = state.data!.lud16 ?? "";
_picture.text = state.data!.picture ?? "";
}
return ValueListenableBuilder(
valueListenable: _loading,
builder: (context, v, _) {
return Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: AvatarUpload(
key: Key("avatar:${_picture.text}"),
avatar: _picture.text.isEmpty ? null : _picture.text,
onUpload: (i) {
_picture.text = i;
},
),
),
TextField(
controller: _name,
readOnly: v,
decoration: InputDecoration(
labelText: t.settings.profile.display_name,
fillColor: LAYER_1,
filled: true,
),
),
TextField(
controller: _about,
readOnly: v,
decoration: InputDecoration(
labelText: t.settings.profile.about,
fillColor: LAYER_1,
filled: true,
),
),
TextField(
controller: _nip5,
readOnly: v,
decoration: InputDecoration(
labelText: t.settings.profile.nip05,
fillColor: LAYER_1,
filled: true,
),
),
TextField(
controller: _lud16,
readOnly: v,
decoration: InputDecoration(
labelText: t.settings.profile.lud16,
fillColor: LAYER_1,
filled: true,
),
),
BasicButton.text(
t.button.save,
disabled: v,
onTap: (context) async {
_loading.value = true;
try {
final newMeta = Metadata(
pubKey: pubkey,
name: _name.text.isEmpty ? null : _name.text,
about: _about.text.isEmpty ? null : _about.text,
picture: _picture.text.isEmpty ? null : _picture.text,
nip05: _nip5.text.isEmpty ? null : _nip5.text,
lud16: _lud16.text.isEmpty ? null : _lud16.text,
);
await ndk.metadata.broadcastMetadata(newMeta);
if (context.mounted) {
context.pop();
}
} finally {
_loading.value = false;
}
return ProfileLoaderWidget(pubkey, (context, state) {
if (state.hasData) {
_name.text = state.data!.name ?? "";
_about.text = state.data!.about ?? "";
_nip5.text = state.data!.nip05 ?? "";
_lud16.text = state.data!.lud16 ?? "";
_picture.text = state.data!.picture ?? "";
}
return ValueListenableBuilder(
valueListenable: _loading,
builder: (context, v, _) {
return Column(
spacing: 16,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: AvatarUpload(
key: Key("avatar:${_picture.text}"),
avatar: _picture.text.isEmpty ? null : _picture.text,
onUpload: (i) {
_picture.text = i;
},
),
if (v) Center(child: CircularProgressIndicator()),
],
);
},
);
},
);
),
TextField(
controller: _name,
readOnly: v,
decoration: InputDecoration(
labelText: t.settings.profile.display_name,
fillColor: LAYER_1,
filled: true,
),
),
TextField(
controller: _about,
readOnly: v,
decoration: InputDecoration(
labelText: t.settings.profile.about,
fillColor: LAYER_1,
filled: true,
),
),
TextField(
controller: _nip5,
readOnly: v,
decoration: InputDecoration(
labelText: t.settings.profile.nip05,
fillColor: LAYER_1,
filled: true,
),
),
TextField(
controller: _lud16,
readOnly: v,
decoration: InputDecoration(
labelText: t.settings.profile.lud16,
fillColor: LAYER_1,
filled: true,
),
),
BasicButton.text(
t.button.save,
disabled: v,
onTap: (context) async {
_loading.value = true;
try {
final newMeta = Metadata(
pubKey: pubkey,
name: _name.text.isEmpty ? null : _name.text,
about: _about.text.isEmpty ? null : _about.text,
picture: _picture.text.isEmpty ? null : _picture.text,
nip05: _nip5.text.isEmpty ? null : _nip5.text,
lud16: _lud16.text.isEmpty ? null : _lud16.text,
);
await ndk.metadata.broadcastMetadata(newMeta);
if (context.mounted) {
context.pop();
}
} finally {
_loading.value = false;
}
},
),
if (v) Center(child: CircularProgressIndicator()),
],
);
},
);
});
}
}

View File

@ -23,33 +23,31 @@ class ChatMessageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: ndk.metadata.loadMetadata(msg.pubKey),
builder: (ctx, state) {
final profile = state.data ?? Metadata(pubKey: msg.pubKey);
return GestureDetector(
onLongPress: () {
if (ndk.accounts.canSign) {
showModalBottomSheet(
context: context,
constraints: BoxConstraints.expand(),
builder:
(ctx) => ChatModalWidget(
profile: profile,
event: msg,
stream: stream,
),
);
}
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
child: Column(
spacing: 2,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_chatText(profile),
/*RxFilter<Nip01Event>(
return ProfileLoaderWidget(msg.pubKey, (ctx, state) {
final profile = state.data ?? Metadata(pubKey: msg.pubKey);
return GestureDetector(
onLongPress: () {
if (ndk.accounts.canSign) {
showModalBottomSheet(
context: context,
constraints: BoxConstraints.expand(),
builder:
(ctx) => ChatModalWidget(
profile: profile,
event: msg,
stream: stream,
),
);
}
},
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 2, vertical: 4),
child: Column(
spacing: 2,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_chatText(profile),
/*RxFilter<Nip01Event>(
Key(msg.id),
filters: [
Filter(kinds: [9735, 7], eTags: [msg.id]),
@ -58,12 +56,11 @@ class ChatMessageWidget extends StatelessWidget {
return ChatReactions(msg: msg, events: state ?? []);
},
),*/
],
),
],
),
);
},
);
),
);
});
}
Widget _chatText(Metadata profile) {

View File

@ -5,6 +5,9 @@ import 'package:ndk/shared/nips/nip19/nip19.dart';
import 'package:zap_stream_flutter/const.dart';
import 'package:zap_stream_flutter/widgets/avatar.dart';
// create simple sync cache to avoid extra re-draw
final Map<String, Metadata> syncProfileCache = {};
class ProfileLoaderWidget extends StatelessWidget {
final String pubkey;
final AsyncWidgetBuilder<Metadata?> builder;
@ -14,8 +17,18 @@ class ProfileLoaderWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return FutureBuilder(
key: super.key ?? Key("profile-loader:$pubkey"),
future: ndk.metadata.loadMetadata(pubkey),
key: super.key,
initialData:
syncProfileCache.containsKey(pubkey)
? syncProfileCache[pubkey]
: null,
future: () async {
final profile = await ndk.metadata.loadMetadata(pubkey);
if (profile != null) {
syncProfileCache[pubkey] = profile;
}
return profile;
}(),
builder: builder,
);
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:ndk/ndk.dart';
import 'package:zap_stream_flutter/i18n/strings.g.dart';
import 'package:zap_stream_flutter/imgproxy.dart';
import 'package:zap_stream_flutter/theme.dart';
@ -79,28 +80,31 @@ class StreamTileWidget extends StatelessWidget {
),
),
),
Row(
spacing: 8,
children: [
AvatarWidget.pubkey(stream.info.host),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stream.info.title ?? "",
overflow: TextOverflow.clip,
style: TextStyle(fontWeight: FontWeight.w500),
),
ProfileNameWidget.pubkey(
stream.info.host,
style: TextStyle(color: LAYER_4),
),
],
ProfileLoaderWidget(stream.info.host, (context, state) {
final profile = state.data ?? Metadata(pubKey: stream.info.host);
return Row(
spacing: 8,
children: [
AvatarWidget(profile: profile),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
stream.info.title ?? "",
overflow: TextOverflow.clip,
style: TextStyle(fontWeight: FontWeight.w500),
),
ProfileNameWidget(
profile: profile,
style: TextStyle(color: LAYER_4),
),
],
),
),
),
],
),
],
);
}),
],
),
);