diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt index 64e2af8..95d8999 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt @@ -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() } diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/NetworkSection.kt b/app/src/main/kotlin/io/flamenet/messenger/app/NetworkSection.kt index 200c4cb..6db595f 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/NetworkSection.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/NetworkSection.kt @@ -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 = 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()) } @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(