Merge branch 'spike/block2-derisking'
This commit is contained in:
commit
552f1737bf
17 changed files with 428 additions and 108 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../i18n/strings.g.dart';
|
||||
import '../services/message_store.dart';
|
||||
|
|
@ -45,6 +47,7 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
TrustCubit? _trust;
|
||||
bool _loading = true;
|
||||
String? _peerName;
|
||||
String? _peerG1;
|
||||
final _input = TextEditingController();
|
||||
|
||||
@override
|
||||
|
|
@ -94,9 +97,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
unawaited(() async {
|
||||
try {
|
||||
final profile = await live.profile.fetch(widget.peerPubkey);
|
||||
if (profile != null && profile.name.isNotEmpty && mounted) {
|
||||
await widget.profileCache!.setName(widget.peerPubkey, profile.name);
|
||||
setState(() => _peerName = profile.name);
|
||||
if (profile != null && mounted) {
|
||||
if (profile.name.isNotEmpty) {
|
||||
await widget.profileCache!
|
||||
.setName(widget.peerPubkey, profile.name);
|
||||
}
|
||||
setState(() {
|
||||
if (profile.name.isNotEmpty) _peerName = profile.name;
|
||||
if (profile.g1.isNotEmpty) _peerG1 = profile.g1;
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
// best effort
|
||||
|
|
@ -105,6 +114,30 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Opens the peer's Ğ1 wallet so you can pay them (Ğ1 is separate from the
|
||||
/// Nostr key — this uses the address they published in their profile). Falls
|
||||
/// back to copying the address if no wallet handles the link.
|
||||
///
|
||||
/// NOTE: confirm the deep-link scheme for the target wallet (Ğ1nkgo / Ğecko /
|
||||
/// Cesium) — this uses the Cesium web wallet as a universal fallback.
|
||||
Future<void> _payG1() async {
|
||||
final g1 = _peerG1;
|
||||
if (g1 == null) return;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final t = context.t;
|
||||
final uri = Uri.parse('https://demo.cesium.app/#/app/wallet/$g1/');
|
||||
var launched = false;
|
||||
try {
|
||||
launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {
|
||||
launched = false;
|
||||
}
|
||||
if (!launched && mounted) {
|
||||
await Clipboard.setData(ClipboardData(text: g1));
|
||||
messenger.showSnackBar(SnackBar(content: Text(t.chat.g1Copied)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _send() async {
|
||||
final cubit = _messages;
|
||||
if (cubit == null || _input.text.trim().isEmpty) return;
|
||||
|
|
@ -126,9 +159,19 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||
Widget build(BuildContext context) {
|
||||
final messages = _messages;
|
||||
final trust = _trust;
|
||||
final t = context.t;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(_peerName ?? shortPubkey(widget.peerPubkey)),
|
||||
actions: [
|
||||
if (_peerG1 != null)
|
||||
IconButton(
|
||||
key: const Key('chat.payG1'),
|
||||
icon: const Icon(Icons.payments_outlined),
|
||||
tooltip: t.chat.payG1,
|
||||
onPressed: _payG1,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: _loading || messages == null || trust == null
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ class MarketScreen extends StatefulWidget {
|
|||
class _MarketScreenState extends State<MarketScreen> {
|
||||
OffersCubit? _cubit;
|
||||
bool _loading = true;
|
||||
bool _hasArea = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
|
@ -64,6 +65,7 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
final previous = _cubit;
|
||||
setState(() {
|
||||
_cubit = cubit;
|
||||
_hasArea = area.isNotEmpty;
|
||||
_loading = false;
|
||||
});
|
||||
await previous?.close();
|
||||
|
|
@ -171,6 +173,8 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
value: cubit,
|
||||
child: MarketBody(
|
||||
onConfigure: _openConfig,
|
||||
onRetry: _init,
|
||||
hasArea: _hasArea,
|
||||
selfPubkey: widget.social.publicKeyHex,
|
||||
),
|
||||
),
|
||||
|
|
@ -181,12 +185,20 @@ class _MarketScreenState extends State<MarketScreen> {
|
|||
class MarketBody extends StatelessWidget {
|
||||
const MarketBody({
|
||||
required this.onConfigure,
|
||||
this.onRetry,
|
||||
this.hasArea = true,
|
||||
this.selfPubkey,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final VoidCallback onConfigure;
|
||||
|
||||
/// Re-opens the connection (for the "can't reach servers" retry).
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
/// Whether the user has set their coarse area yet.
|
||||
final bool hasArea;
|
||||
|
||||
/// This user's own pubkey, so we don't offer to message our own listings.
|
||||
final String? selfPubkey;
|
||||
|
||||
|
|
@ -196,10 +208,21 @@ class MarketBody extends StatelessWidget {
|
|||
return BlocBuilder<OffersCubit, OffersState>(
|
||||
builder: (context, state) {
|
||||
if (!context.read<OffersCubit>().isOnline) {
|
||||
// Default community servers exist, so "offline" means unreachable —
|
||||
// offer a retry rather than "set up sharing".
|
||||
return _EmptyState(
|
||||
icon: Icons.wifi_tethering_off_outlined,
|
||||
title: t.market.notSetUp,
|
||||
body: t.market.notSetUpBody,
|
||||
title: t.market.cantReach,
|
||||
body: t.market.cantReachBody,
|
||||
actionLabel: onRetry != null ? t.market.retry : null,
|
||||
onAction: onRetry,
|
||||
);
|
||||
}
|
||||
if (!hasArea) {
|
||||
return _EmptyState(
|
||||
icon: Icons.place_outlined,
|
||||
title: t.market.setArea,
|
||||
body: t.market.setAreaBody,
|
||||
actionLabel: t.market.setUp,
|
||||
onAction: onConfigure,
|
||||
);
|
||||
|
|
@ -431,6 +454,7 @@ class _ConfigSheet extends StatefulWidget {
|
|||
|
||||
class _ConfigSheetState extends State<_ConfigSheet> {
|
||||
final _area = TextEditingController();
|
||||
final _servers = TextEditingController();
|
||||
bool _loading = true;
|
||||
bool _locationBusy = false;
|
||||
String? _locationError;
|
||||
|
|
@ -443,13 +467,13 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
|
||||
Future<void> _load() async {
|
||||
_area.text = await widget.settings.areaGeohash();
|
||||
_servers.text = (await widget.settings.relayUrls()).join('\n');
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
// Relays are managed automatically (default community servers, dead ones
|
||||
// skipped by the pool) — the user only sets their area here.
|
||||
await widget.settings.setAreaGeohash(_area.text);
|
||||
await widget.settings.setRelayUrls(_servers.text.split('\n'));
|
||||
if (mounted) Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
|
|
@ -477,12 +501,14 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
@override
|
||||
void dispose() {
|
||||
_area.dispose();
|
||||
_servers.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.t;
|
||||
final hasArea = _area.text.trim().isNotEmpty;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: 20,
|
||||
|
|
@ -491,40 +517,34 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
|
||||
),
|
||||
child: _loading
|
||||
? const SizedBox(height: 120, child: Center(child: CircularProgressIndicator()))
|
||||
? const SizedBox(
|
||||
height: 120, child: Center(child: CircularProgressIndicator()))
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.market.configTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: seedTitle,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
t.market.configTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: seedTitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
t.market.setupIntro,
|
||||
style: const TextStyle(color: seedMuted, fontSize: 13, height: 1.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
key: const Key('market.area'),
|
||||
controller: _area,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.market.areaLabel,
|
||||
helperText: t.market.areaHelp,
|
||||
helperMaxLines: 3,
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
t.market.setupIntro,
|
||||
style: const TextStyle(
|
||||
color: seedMuted, fontSize: 13, height: 1.4),
|
||||
),
|
||||
),
|
||||
if (widget.location != null)
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton.icon(
|
||||
const SizedBox(height: 18),
|
||||
// Setting your area from device location is the human path;
|
||||
// typing a code is the advanced fallback.
|
||||
if (widget.location != null)
|
||||
FilledButton.tonalIcon(
|
||||
key: const Key('market.useLocation'),
|
||||
onPressed: _locationBusy ? null : _useLocation,
|
||||
icon: _locationBusy
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
|
|
@ -533,26 +553,85 @@ class _ConfigSheetState extends State<_ConfigSheet> {
|
|||
)
|
||||
: const Icon(Icons.my_location, size: 18),
|
||||
label: Text(t.market.useLocation),
|
||||
onPressed: _locationBusy ? null : _useLocation,
|
||||
),
|
||||
if (_locationError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6),
|
||||
child: Text(
|
||||
_locationError!,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFFB3261E), fontSize: 12),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
hasArea
|
||||
? Icons.check_circle
|
||||
: Icons.pending_outlined,
|
||||
size: 18,
|
||||
color: hasArea ? seedGreen : seedMuted,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
hasArea ? t.market.areaSet : t.market.areaNotSet,
|
||||
style: TextStyle(
|
||||
color: hasArea ? seedOnSurface : seedMuted,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Theme(
|
||||
data: Theme.of(context)
|
||||
.copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
key: const Key('market.advanced'),
|
||||
tilePadding: EdgeInsets.zero,
|
||||
childrenPadding: const EdgeInsets.only(bottom: 8),
|
||||
title: Text(
|
||||
t.market.advanced,
|
||||
style: const TextStyle(fontSize: 13, color: seedMuted),
|
||||
),
|
||||
children: [
|
||||
TextField(
|
||||
key: const Key('market.area'),
|
||||
controller: _area,
|
||||
onChanged: (_) => setState(() {}),
|
||||
decoration: InputDecoration(
|
||||
labelText: t.market.areaCodeLabel,
|
||||
helperText: t.market.areaCodeHint,
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextField(
|
||||
key: const Key('market.servers'),
|
||||
controller: _servers,
|
||||
minLines: 1,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.market.serversLabel,
|
||||
helperText: t.market.serversHelp,
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_locationError != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
_locationError!,
|
||||
style: const TextStyle(color: Color(0xFFB3261E), fontSize: 12),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton(
|
||||
key: const Key('market.save'),
|
||||
onPressed: _save,
|
||||
child: Text(t.market.save),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton(
|
||||
key: const Key('market.save'),
|
||||
onPressed: _save,
|
||||
child: Text(t.market.save),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class ProfileScreen extends StatefulWidget {
|
|||
class _ProfileScreenState extends State<ProfileScreen> {
|
||||
final _name = TextEditingController();
|
||||
final _about = TextEditingController();
|
||||
final _g1 = TextEditingController();
|
||||
bool _loading = true;
|
||||
bool _saving = false;
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
Future<void> _load() async {
|
||||
_name.text = await widget.profileStore.name();
|
||||
_about.text = await widget.profileStore.about();
|
||||
_g1.text = await widget.profileStore.g1();
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +59,8 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
final messenger = ScaffoldMessenger.of(context);
|
||||
final t = context.t;
|
||||
setState(() => _saving = true);
|
||||
await widget.profileStore.save(name: _name.text, about: _about.text);
|
||||
await widget.profileStore
|
||||
.save(name: _name.text, about: _about.text, g1: _g1.text);
|
||||
|
||||
// Best-effort publish to the network so peers see the name.
|
||||
final relays = await widget.settings.relayUrls();
|
||||
|
|
@ -67,6 +70,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
await session.profile.publish(
|
||||
name: _name.text.trim(),
|
||||
about: _about.text.trim(),
|
||||
g1: _g1.text.trim(),
|
||||
);
|
||||
await session.close();
|
||||
} catch (_) {
|
||||
|
|
@ -82,6 +86,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
void dispose() {
|
||||
_name.dispose();
|
||||
_about.dispose();
|
||||
_g1.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -116,6 +121,16 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
|||
helperText: t.profile.aboutHint,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
key: const Key('profile.g1'),
|
||||
controller: _g1,
|
||||
decoration: InputDecoration(
|
||||
labelText: t.profile.g1,
|
||||
helperText: t.profile.g1Hint,
|
||||
helperMaxLines: 2,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
key: const Key('profile.save'),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue