Give sharing a bridge a way in
`shareBridge` shipped in the last change with nothing calling it. The receiving half was complete — an offer arrives, is shown, is accepted by a button — and the sending half existed only as a method. Half a feature, and the half that was missing is the one the whole argument rests on: a bridge nobody can hand out is a bridge nobody has. The Account screen now lists contacts under the bridge, with a Send beside each, and only when a bridge is actually configured. Sharing from a screen with nothing set would have posted an empty line onto somebody else's settings screen. ⚠️ The button says "Sent" afterwards but does not disable. An operator rotates a bridge and the same person needs the new line; a control that never re-arms would make that impossible from here. And "Sent" is not persisted — whether somebody still has a working bridge is not something this phone can know, so it is a confirmation for this sitting rather than a record. The copy says who learns what, because neither party is nothing: whoever runs the bridge sees one more person connecting, and the contact learns you use it. 1175 tests, 0 failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4d915a237d
commit
9a20c06eda
@@ -805,7 +805,7 @@ fun ColumnScope.AccountScreen(
|
||||
// from it — filing it as privacy would make the claim the section itself
|
||||
// is careful not to make.
|
||||
SectionBand()
|
||||
SettingsSection { NetworkSection(model) }
|
||||
SettingsSection { NetworkSection(model, phase.buddies) }
|
||||
|
||||
SectionBand()
|
||||
SettingsSection { ParentalControlsSection() }
|
||||
|
||||
@@ -2,6 +2,8 @@ package io.flamenet.messenger.app
|
||||
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -42,13 +44,20 @@ import io.flamenet.obfs4.Obfs4Stats
|
||||
* bridge then proxies to the relay, which sees exactly what it saw before.
|
||||
*/
|
||||
@Composable
|
||||
fun ColumnScope.NetworkSection(model: MessengerViewModel? = null) {
|
||||
fun ColumnScope.NetworkSection(
|
||||
model: MessengerViewModel? = null,
|
||||
buddies: List<io.flamenet.client.MessengerApi.Buddy> = emptyList(),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val settings = remember { Obfs4Settings(context) }
|
||||
var draft by remember { mutableStateOf(settings.bridgeLine ?: "") }
|
||||
var stored by remember { mutableStateOf(settings.bridgeLine) }
|
||||
// Bumped on save/remove so the live status below re-reads.
|
||||
var tick by remember { mutableIntStateOf(0) }
|
||||
// Who has been handed the line this sitting. A confirmation, not a record:
|
||||
// it is not persisted, because whether somebody still has a working bridge
|
||||
// is not something this phone can know.
|
||||
var sharedWith by remember { mutableStateOf(setOf<Long>()) }
|
||||
|
||||
@Suppress("UNUSED_EXPRESSION") tick
|
||||
|
||||
@@ -209,6 +218,51 @@ fun ColumnScope.NetworkSection(model: MessengerViewModel? = null) {
|
||||
)
|
||||
}
|
||||
|
||||
// 🔴 **The other half of receiving one.** A bridge is only useful while
|
||||
// whoever wants to block it does not have the line, and every public way of
|
||||
// handing one out is somewhere a censor can go and collect them. Passing it
|
||||
// to somebody you already talk to is the one route that does not scale for
|
||||
// them: enumerating bridges becomes infiltrating friendships one at a time.
|
||||
//
|
||||
// ⚠️ Only when a bridge is actually configured. "Share" on a screen with
|
||||
// nothing set would send an empty line and put an un-actionable row on
|
||||
// somebody else's settings screen.
|
||||
if (stored != null && buddies.isNotEmpty()) {
|
||||
Spacer(Modifier.height(Space.md))
|
||||
Text("Share it with", fontWeight = FontWeight.Medium, fontSize = 15.sp)
|
||||
Text(
|
||||
// Says who learns what. The bridge operator sees a new client; the
|
||||
// contact learns you use this bridge. Neither is nothing.
|
||||
"They get the line and decide for themselves. Whoever runs the bridge will " +
|
||||
"see one more person connecting, and your contact will know you use it.",
|
||||
fontSize = 12.sp,
|
||||
color = Palette.SecondaryText,
|
||||
)
|
||||
for (buddy in buddies) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = Space.xs),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
buddy.displayName.ifBlank { buddy.handle },
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
KitSecondaryButton(
|
||||
text = if (sharedWith.contains(buddy.userId)) "Sent" else "Send",
|
||||
onClick = {
|
||||
model?.shareBridge(buddy.userId)
|
||||
sharedWith = sharedWith + buddy.userId
|
||||
},
|
||||
// ⚠️ Not disabled after sending. An operator rotates a
|
||||
// bridge and the same person needs the new line; a button
|
||||
// that never re-arms would make that impossible from here.
|
||||
tag = "obfs4-share-${buddy.userId}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(Space.sm))
|
||||
|
||||
Text(
|
||||
|
||||
Reference in New Issue
Block a user