Skip to content

Commit 7dcf3d1

Browse files
Adam Fiskclaude
authored andcommitted
SmC: real per-peer geo, on-globe heart burst, arc reversal
- Geo: peerLookup switched from geo.getiantem.org/<ip> (returns 404 for arbitrary IPs — every peer collapsed to the IR-fallback center) to ipwho.is (HTTPS, no auth, city-level lat/lon + country name + flag emoji). PeerLookup now returns PeerGeo with a real, unique location per peer. - Event model: UnboundedConnectionEvent carries country name, flag emoji, coords, and an isReplay flag. - Notifier: ref-counts streams per TCP peer so the arc persists until the peer's last H2 stream closes (samizdat multiplexes many streams over one conn); resolves geo async then emits enriched events; replayCurrentPeers() seeds the globe with existing peers when the user navigates to SmC mid-stream; emits synthetic -1's on toggle-off so arcs don't orphan when peer.Client.Stop suppresses the box.Close cascade. - Globe: arcs linger 5s past last -1 so brief URL-test probes still register; coords jittered ±2° per workerIdx hash so multiple peers in the same city fan out instead of overlapping; arc direction reversed (censored user → uncensored peer) so the dash animation reads as traffic arriving at us. - Heart burst: on-globe animation anchored at peer coords via Point.labelBuilder (lib projects 3D→2D for us). Uses the actual assets from getlantern/unbounded — explosion.json Lottie + the inline FF5A79 heart SVG path via CustomPainter. 4.6s burst + 4.2s fading country label below. - StatusCard: small info_outline tooltip explaining that most events are short URL-test liveness probes (601 of ~700 CONNECTs in a measured session were to api.iantem.io — clients probing peer reachability before sending real traffic). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 71342de commit 7dcf3d1

6 files changed

Lines changed: 477 additions & 50 deletions

File tree

assets/unbounded/explosion.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

lib/core/models/unbounded_connection_event.dart

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
1+
import 'package:flutter_earth_globe/globe_coordinates.dart';
2+
13
/// Represents a consumer connection change from the broflake widget proxy.
24
class UnboundedConnectionEvent {
35
final int state; // 1 = connected, -1 = disconnected
46
final int workerIdx;
57
final String addr; // IP address
8+
// Geo fields are populated by ShareNotifier after peer lookup. Empty on
9+
// legacy events and on -1 frames (where only workerIdx matters for the
10+
// globe to remove the arc).
11+
final String countryName;
12+
final String countryCode;
13+
final String flagEmoji;
14+
final GlobeCoordinates? coordinates;
15+
// True for synthetic events the notifier emits to seed a newly-mounted
16+
// globe with peers that connected before the screen opened. Lets the UI
17+
// suppress the "new connection from <country>" burst for replays.
18+
final bool isReplay;
619

720
UnboundedConnectionEvent({
821
required this.state,
922
required this.workerIdx,
1023
required this.addr,
24+
this.countryName = '',
25+
this.countryCode = '',
26+
this.flagEmoji = '',
27+
this.coordinates,
28+
this.isReplay = false,
1129
});
1230

1331
factory UnboundedConnectionEvent.fromJson(Map<String, dynamic> json) {

lib/core/services/geo_lookup_service.dart

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,38 @@ import 'dart:convert';
33
import 'package:flutter_earth_globe/globe_coordinates.dart';
44
import 'package:http/http.dart' as http;
55

6+
/// Result of a peer geo lookup. Country/flag default to empty when the
7+
/// lookup fails; coordinates default to a centre-of-the-globe sentinel.
8+
class PeerGeo {
9+
const PeerGeo({
10+
required this.coordinates,
11+
required this.countryName,
12+
required this.countryCode,
13+
required this.flagEmoji,
14+
});
15+
16+
final GlobeCoordinates coordinates;
17+
final String countryName;
18+
final String countryCode;
19+
final String flagEmoji;
20+
21+
static const unknown = PeerGeo(
22+
coordinates: GlobeCoordinates(0, 0),
23+
countryName: '',
24+
countryCode: '',
25+
flagEmoji: '',
26+
);
27+
}
28+
629
class GeoLookupService {
7-
static const _geoUrl = 'https://geo.getiantem.org';
30+
static const _selfUrl = 'https://geo.getiantem.org';
31+
// ipwho.is: HTTPS, no auth, 10k req/month free. Returns country + lat/lon
32+
// + flag emoji in one shot.
33+
static const _peerUrl = 'https://ipwho.is';
834

9-
// ISO country code → approximate centre coordinates
10-
static const _countries = {
35+
// ISO country code → approximate centre coordinates. Used as a fallback
36+
// when ipwho.is doesn't return city-level coords.
37+
static const _countryCenters = {
1138
'AF': (lat: 33.0, lng: 65.0),
1239
'AL': (lat: 41.0, lng: 20.0),
1340
'DZ': (lat: 28.0, lng: 3.0),
@@ -133,15 +160,15 @@ class GeoLookupService {
133160
};
134161

135162
static GlobeCoordinates _isoToCoords(String iso) {
136-
final c = _countries[iso] ?? _countries['US']!;
163+
final c = _countryCenters[iso] ?? _countryCenters['US']!;
137164
return GlobeCoordinates(c.lat, c.lng);
138165
}
139166

140167
/// Looks up the current device's location (no IP argument).
141168
static Future<GlobeCoordinates> selfLookup() async {
142169
try {
143170
final response = await http
144-
.get(Uri.parse('$_geoUrl/'))
171+
.get(Uri.parse('$_selfUrl/'))
145172
.timeout(const Duration(seconds: 5));
146173
if (response.statusCode == 200) {
147174
final data = jsonDecode(response.body) as Map<String, dynamic>;
@@ -154,20 +181,31 @@ class GeoLookupService {
154181
return _isoToCoords('US');
155182
}
156183

157-
/// Looks up the country for a peer [ip] address.
158-
static Future<GlobeCoordinates> peerLookup(String ip) async {
184+
/// Looks up country, flag, and coordinates for a peer [ip] address.
185+
/// Returns [PeerGeo.unknown] on any failure so callers can suppress the
186+
/// arc / banner rather than displaying a wrong country.
187+
static Future<PeerGeo> peerLookup(String ip) async {
159188
try {
160189
final response = await http
161-
.get(Uri.parse('$_geoUrl/$ip'))
190+
.get(Uri.parse('$_peerUrl/$ip'))
162191
.timeout(const Duration(seconds: 5));
163-
if (response.statusCode == 200) {
164-
final data = jsonDecode(response.body) as Map<String, dynamic>;
165-
final iso =
166-
(data['Country'] as Map<String, dynamic>?)?['IsoCode'] as String? ??
167-
'IR';
168-
return _isoToCoords(iso);
169-
}
192+
if (response.statusCode != 200) return PeerGeo.unknown;
193+
final data = jsonDecode(response.body) as Map<String, dynamic>;
194+
if (data['success'] != true) return PeerGeo.unknown;
195+
final iso = (data['country_code'] as String?) ?? '';
196+
final lat = (data['latitude'] as num?)?.toDouble();
197+
final lng = (data['longitude'] as num?)?.toDouble();
198+
final coords = (lat != null && lng != null)
199+
? GlobeCoordinates(lat, lng)
200+
: _isoToCoords(iso);
201+
final flagObj = data['flag'] as Map<String, dynamic>?;
202+
return PeerGeo(
203+
coordinates: coords,
204+
countryName: (data['country'] as String?) ?? '',
205+
countryCode: iso,
206+
flagEmoji: (flagObj?['emoji'] as String?) ?? '',
207+
);
170208
} catch (_) {}
171-
return _isoToCoords('IR');
209+
return PeerGeo.unknown;
172210
}
173211
}

0 commit comments

Comments
 (0)