Show when a thread is with someone you never added

The app adopts anyone who messages you, because a first message from a
non-contact used to be invisible — no row, no name, just an unread badge over
"Nobody here yet". That fix stays: the message still arrives and is still
readable.

What it also did was decide something for the user. Somebody ends up in your
contacts because they wrote to you, and nothing on screen distinguished that
from a person you went and looked up.

So adoption now sends `auto: true` and leaves the acceptance to the person, and
the thread carries a banner naming what happened with the two useful actions in
it. Nothing is gated: delivery is unchanged, the difference is just visible.

`accepted` defaults to true wherever it is not answered — an older relay says
nothing, and reading silence as "stranger" would label every contact on every
older relay.

The copy states what happened rather than warning about it. Most first messages
are from someone you gave your address to, and opening with an alarm would be
wrong more often than right.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-29 15:47:26 -04:00
co-authored by Claude Opus 4.8
parent 31551f60f2
commit e345d93640
3 changed files with 127 additions and 3 deletions
@@ -44,6 +44,16 @@ class MessengerApi(
val displayName: String,
val handle: String,
val presence: String,
/**
* Whether this person is somebody the user chose, or somebody who
* turned up because they wrote first.
*
* 🔴 **Defaults to true, and that default is the compatibility story.**
* A relay that predates this field says nothing about acceptance, and
* the only safe reading of silence is "these are your contacts" — the
* alternative labels every contact on every older relay a stranger.
*/
val accepted: Boolean = true,
)
data class Notification(
@@ -66,6 +76,7 @@ class MessengerApi(
displayName = b.optString("display_name", ""),
handle = b.optString("screen_name", ""),
presence = b.optString("presence", "offline"),
accepted = b.optBoolean("accepted", true),
)
}
}
@@ -75,8 +86,22 @@ class MessengerApi(
* thing that can turn that into an id, and doing the lookup client-side
* would need a directory this product deliberately does not have.
*/
fun addBuddy(username: String) {
post("buddies", JSONObject().put("username", username))
/**
* @param auto true when the app is adopting somebody who messaged first,
* rather than the user choosing to add them.
*
* 🔴 The distinction is the whole feature. Adopting a stranger keeps their
* first message visible — which is why adoption exists — while leaving the
* decision about whether they are a contact to the person, instead of
* making it for them silently.
*/
fun addBuddy(username: String, auto: Boolean = false) {
post("buddies", JSONObject().put("username", username).put("auto", auto))
}
/** Record that the user has accepted this correspondent. */
fun acceptConversation(userId: Long) {
post("conversation", JSONObject().put("action", "accept").put("buddy", userId))
}
/**