Commit graph

17 commits

Author SHA1 Message Date
456cc8154d Merge branch 'claude/festive-einstein-384c45'
Ego-centric trust replaces the global Duniter WoT, plus Wallapop-style
ratings v1 (kind 30778). Conflicts resolved: pubspec assets (kept
seed_saving, dropped trust referents), chat_screen (avatars/no-links
from main + rating strip and simplified TrustCubit from the branch),
strings.g.dart regenerated from merged sources.
2026-07-11 13:25:51 +02:00
7e7340d643 feat(ratings): Wallapop-style ratings in chat and offer detail
- SocialSession grows a ratings transport (fifth interface on the one
  shared channel).
- PeerRatingCubit: count, locale-aware average, and circleCount — how
  many ratings come from your own circle (reuses the ego-centric trust
  rule), the signal that makes strangers' review-stuffing irrelevant.
- Chat: slim rating strip under the trust banner; you can rate only
  someone you've talked with (soft anchor until the signed bilateral
  exchange form lands). Sheet with 5 stars + optional comment,
  pre-filled for editing, with a take-back action.
- Offer detail: author's stars under their name, with 'N from people
  you know' when your circle rated them; nothing shown when unrated.
- i18n ratings.* (en/es/pt/ast); tests for cubit and sheet.
2026-07-11 13:13:43 +02:00
fef174e8c4 refactor(trust): ego-centric trust replaces the global Duniter membership
The global membership rule (curated bootstrap referents + sigQty/stepMax
parameters) solved sybil-proof identity for a UBI — a problem this app
doesn't have — and its screen leaked graph jargon (npub roots, parameter
steppers). Trust is now computed from the user's own position only:
you vouch / vouched by people you know (distance <=2) / vouched by N.

- TrustCubit: drop networkMember tier, referents and params; keep the
  circle rule and the 365-day vouch expiry (renewable, self-pruning).
- Delete TrustReferents, WotSettings, TrustNetworkScreen and the bundled
  referents asset; unwire injector/bootstrap/app/chat.
- New 'Your people' screen (/your-people, from the profile): who you
  vouch for (revocable) and who vouches for you, names via ProfileCache.
- i18n: wot.* removed, yourPeople.* added (en/es/pt/ast); trust.member
  removed. Kind 30777 events on relays stay fully compatible — only the
  interpretation changes.
2026-07-11 13:03:20 +02:00
a461106dbf feat(chat): disallow links in messages
Local-first chat between people whose web of trust is still forming is a
natural phishing/scam vector, so messages may not contain URLs. A
conservative `containsUrl` rule (explicit scheme, www., or a common-TLD
domain — but not "3.5kg" or "12.30") gates it: the composer warns and
keeps the text for editing, and MessagesCubit.send drops any URL as a
backstop. Incoming text was already non-tappable (plain selectable text).
2026-07-11 07:14:58 +02:00
d481e62ff7 feat(chat): lighter bubbles, my own name on my avatar
Feedback: my bubbles were too dark, and avatars showed only a person
glyph, never the profile name. Now both bubbles are light with dark text
(mine a tonal green, the peer's white with a hairline) — the side and the
per-person avatar colour carry the "who", so nothing is heavy to read.
Load my own display name from ProfileStore so my avatar shows my initial
too, not just the peer's; each avatar still falls back to a glyph when no
name is known.
2026-07-11 07:08:54 +02:00
73e96a3ce5 feat(chat): sender avatar on both sides, coloured per person
Feedback: avatars only on the left, all the same colour, so it was hard
to tell who said what. In a 1:1 only the peer had an avatar (one colour).
Now each message carries its sender's avatar on their own side — mine
trailing, the peer's leading — each tinted by a deterministic colour from
their pubkey. Two sides, two colours: who-said-what is obvious at a glance.
2026-07-11 06:59:24 +02:00
68b04ea409 feat(chat): usable 1:1 chat — bottom-anchored, Drift-backed, dated
- Anchor the message list to the bottom (`reverse: true`) so new messages
  stay in view instead of landing below the fold.
- Move chat history from the OS keystore (O(n²) JSON blob, silent 200-msg
  cap) to a separate encrypted Drift/SQLCipher DB (`ChatDatabase`):
  indexed append, uncapped history, dedup as a unique-key invariant. It's
  an ephemeral per-device cache, isolated from the inventory schema, its
  migrations, and its sync. No data migration (pre-release).
- Add day separators (Today/Yesterday/locale date) and a per-bubble time,
  all via ICU (12/24h per locale; Localizations locale maps Asturian →
  Spanish for intl date symbols).
- Add peer avatars (deterministic colour from the pubkey + name initial),
  surface send failures that were previously silent, and make bubble text
  selectable (addresses, links).
- New i18n keys in en/es/pt/ast; tests for grouping, formatting, avatars,
  scroll anchoring, storage and send errors.

Docs: docs/design/chat-storage.md + open-decisions.md.
2026-07-11 06:40:50 +02:00
b9492cb65a refactor(social): one shared relay connection per identity
Every feature (offers, messaging, trust, profile, the inbox listener) used
to open its OWN RelayPool via social.openSession — several sockets to the
same relays, more battery, and reconnection logic living only in the inbox
listener.

Add SocialConnection: ONE shared session per identity, lazily connected and
reused by all. It watches connectivity — dropping the session when the
network goes and reconnecting when it returns, announcing each change on a
sessions stream so the inbox listener re-subscribes automatically. Callers
get the session via connection.session() and never close it (the connection
owns its lifecycle); recreated/disposed on an identity switch.

- InboxService now consumes the shared connection (subscribes on its
  sessions stream) instead of owning its own socket + connectivity code.
- createOffersCubit + the chat/market/profile screens use the shared
  connection; dead createMessagesCubit/createTrustCubit factories removed.
- DI registers SocialConnection per identity; Bootstrap starts it (after
  the inbox subscribes); switchSocialAccount disposes + recreates it.

Tests: SocialConnection lifecycle (reuse, concurrent connect, offline drop +
reconnect, unreachable retry) with a fake opener + online stream; inbox test
updated. nostr added as a dev_dependency for the channel fake.
2026-07-10 21:50:34 +02:00
d2b98af36d Merge branch 'claude/awesome-golick-007e7d': message unread badges + OS notifications
# Conflicts:
#	apps/app_seeds/lib/di/injector.dart
#	apps/app_seeds/lib/i18n/ast.i18n.json
#	apps/app_seeds/lib/i18n/en.i18n.json
#	apps/app_seeds/lib/i18n/es.i18n.json
#	apps/app_seeds/lib/i18n/pt.i18n.json
#	apps/app_seeds/lib/i18n/strings.g.dart
#	apps/app_seeds/lib/i18n/strings_ast.g.dart
#	apps/app_seeds/lib/i18n/strings_en.g.dart
#	apps/app_seeds/lib/i18n/strings_es.g.dart
#	apps/app_seeds/lib/i18n/strings_pt.g.dart
#	apps/app_seeds/lib/ui/chat_screen.dart
2026-07-10 21:15:59 +02:00
d026376993 feat(messages): unread badges + OS notification for private messages
Private messages (NIP-17) arrived in the foreground with no signal: no
unread count, no badge, no OS notification. Hook both into the single
InboxService.ingest choke point.

- UnreadService: per-peer "last read" in the keystore (timestamps+pubkey
  only, no message text), live count via a changes stream, active-peer
  suppression so the open chat never badges or notifies.
- UnreadBadge: reusable, RTL-aware (Flutter Badge) — on the home hamburger,
  the drawer "Chat" item, and per-conversation in the messages list.
- Mark-read on chat open (ChatScreen).
- NotificationService over flutter_local_notifications: generic
  "New message from <name>" (no message text, for privacy), payload = pubkey,
  tap opens /chat/:pubkey. No-op on web/Windows; Android POST_NOTIFICATIONS.
- i18n notifications.newMessageFrom (en/es/pt/ast).

Background/push stays out of scope (foreground-only by design).

Tests: UnreadService, NotificationService (mock plugin), InboxService hooks
(new peer notifies/counts; duplicate, own, and open-chat do not), and a
UnreadBadge widget test. dart analyze clean; commons_core green.
2026-07-10 21:12:00 +02:00
e715b129f6 feat(trust): full Duniter web-of-trust membership (params + referents)
Slice 4 of Block 2. The pure rule was already parameterised; this adds the
policy, cold-start and UI around it.

- commons_core: WotParams (sigQty/stepMax/sigValidity, Duniter defaults) +
  WebOfTrust.membersWith; npubToHex helper (NIP-19 decode) for adding roots.
- WotSettings (keystore): the active parameters, configurable — a young
  network loosens them and tightens as it grows, as Ğ1 did. Defaults Duniter.
- TrustReferents: the bootstrap 'seeds' membership is measured from — a
  bundled asset (empty until real founders are curated, no invented keys)
  unioned with referents the user adds by npub/QR. Honest cold-start.
- TrustCubit: computes the full membership verdict against referents+params
  alongside the personal circle, and exposes a TrustTier (networkMember >
  inYourCircle > vouched > unknown). Certifications issued with the active
  validity (they expire and renew, Duniter rule).
- UI: chat trust badge by tier; a 'Network of trust' screen (manage roots +
  advanced params) reached from the profile. i18n en/es/pt/ast.

Tests: WotParams/membersWith, npubToHex, TrustReferents, WotSettings, and
TrustCubit tiers/membership. Resolves the WoT-parameters decision
(open-decisions §B). Trust net stays empty/undetermined until seeded — by
design; users bootstrap their own roots.
2026-07-10 21:02:08 +02:00
a9c6030dde feat(block2): Ğ1 — publish a Ğ1 address + 'Pay in Ğ1' in chat (levels 1-2)
The optional Ğ1 (free currency) integration, per g1-integration.md.
- Profile: an optional 'Ğ1 address' field, persisted locally and published in
  your NIP-01 kind:0 metadata (separate from the Nostr key).
- commons_core: UserProfile/ProfileTransport carry a 'g1' field (round-trip
  tested).
- Chat: when the peer published a Ğ1 address, a 'Pay in Ğ1' app-bar action opens
  their wallet (url_launcher) so value moves in the wallet, not the app — copies
  the address as a fallback. No payment rails in-app (sharing-model §4.3).

NOTE (vjrj): the wallet deep-link uses the Cesium web wallet as a universal
fallback; swap the URI in chat_screen._payG1 for the Ğ1nkgo/Ğecko scheme you
prefer. Analyzer clean (both packages); profile transport test covers g1.
2026-07-10 13:06:38 +02:00
48228c0bc6 feat(block2): show contact names (resolve peers' published profiles)
Closes the profile loop — peers now appear by name, not a raw key.
- ProfileCache: keystore-backed cache of peers' published display names (works
  offline) + shortPubkey() fallback.
- Chat: the app bar shows the peer's name (cached first, then freshened from
  their kind:0 over the session); falls back to a short key.
- Inbox: each conversation shows the cached name, and missing names are resolved
  over one session and cached.
- Wired via DI + TaneApp(profileCache).

Analyzer clean; ProfileCache covered by a plain test.
2026-07-10 12:47:48 +02:00
7bf80d2031 feat(block2): persist chat history (messages survive leaving the chat)
Messages were in-memory only — gone on leaving the conversation.
- MessageStore: keystore-backed (no plaintext), a capped per-peer JSON list
  (last 200). Simple; the encrypted Drift DB is the eventual home at scale.
- MessagesCubit: loads saved history on start (subscribes FIRST, then loads, so
  a message arriving during the load isn't dropped) and persists every sent and
  received message. Wired via DI + TaneApp(messageStore).

Tests (plain 'test', no hang risk): MessageStore round-trip/per-peer/cap, and a
cubit test that reopens a fresh cubit and sees the saved conversation. Analyzer
clean; run 'flutter test' locally to confirm.
2026-07-10 12:20:25 +02:00
f3a7ad198d feat(block2): 'in your circle' trust — friend-of-a-friend, not a raw count
A network-wide 'N people vouch' count invites spam and means little. Now the
trust banner computes whether the peer is within YOUR circle — you vouch for
them, or someone you vouch for does (friend-of-a-friend) — via the pure
WebOfTrust rule (seeds={you}, threshold 1, distance 2). When known, the banner
turns green with 'In your circle' (else it falls back to the count).

The full member policy (threshold/distance/bootstrap) stays open (network-trust
§2); this is a deliberately loose 'people near you' heuristic.

Analyzer clean; trust cubit test adds a friend-of-a-friend vs stranger case
(run flutter test locally to confirm — the widget suite is too slow to run here).
2026-07-10 12:10:06 +02:00
a85e71dc8d feat(block2): trust UI — vouch for a person in chat (web of trust)
- TrustCubit: reads how many vouch for the peer + whether YOU do; toggleVouch
  certifies/revokes over TrustTransport; never vouches for self; offline-safe.
  Kept simple — the full known-member rule (threshold+distance) and its
  bootstrap set are still-open policy (network-trust §2); a live count + your
  vouch is the honest first step.
- Chat screen now opens ONE session carrying both messaging AND trust (the
  shared-connection shape), and shows a slim vouch banner: 'Vouched for by N' +
  an 'I know this person' / 'You vouch for them' toggle.
- i18n en/es/pt (human words: 'I know this person', not 'certify').

Tests: 4 trust cubit + messaging + chat, 10 green; analyzer clean.
2026-07-10 11:04:07 +02:00
efd64c4da4 feat(block2): messaging UI — private 1:1 chat (NIP-17)
- MessagesCubit: 1:1 conversation over MessageTransport. Filters the shared
  inbox to the peer; tags our own sends with our pubkey (NIP-17 wraps don't come
  back to the sender) so the UI can align bubbles; degrades gracefully offline.
- ChatScreen (mockup 10): message bubbles (mine right / peer left) + a composer;
  opens a session lazily, shows 'set up sharing' when there's no relay.
- Entry point: a 'Message' button on each offer card (hidden on your own
  listings) → /chat/:pubkey.
- Routing + i18n en/es/pt.

Tests: 5 cubit (send/receive/order/filter/offline) + chat offline + market, green.
2026-07-10 10:59:47 +02:00