feat(private-replies): hide noisy actions and chain privacy
- ActionBar gates React / Repost / Quote / Zap behind !isPrivate, leaving Reply and Bookmark on private replies. Avoids leaking the rumor id via a public e-tag on kind 7 / 6 / 9735 attached to the rumor. - ComposeViewModel auto-enables and locks the private toggle when the parent being replied to is itself a private reply. Sending publicly would attach an e-tag to the rumor id on public relays and leak the thread structure, so the toggle is forced on and ignores clicks.
This commit is contained in:
@@ -84,6 +84,7 @@ fun ActionBar(
|
|||||||
resolvedEmojis: Map<String, String> = emptyMap(),
|
resolvedEmojis: Map<String, String> = emptyMap(),
|
||||||
unicodeEmojis: List<String> = emptyList(),
|
unicodeEmojis: List<String> = emptyList(),
|
||||||
onOpenEmojiLibrary: (() -> Unit)? = null,
|
onOpenEmojiLibrary: (() -> Unit)? = null,
|
||||||
|
isPrivate: Boolean = false,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
@@ -107,6 +108,9 @@ fun ActionBar(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
|
// Private replies hide React / Repost / Quote / Zap to avoid leaking the rumor id
|
||||||
|
// on public relays. Reply (above) and Bookmark (below) remain available.
|
||||||
|
if (!isPrivate) {
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Box {
|
Box {
|
||||||
Box(
|
Box(
|
||||||
@@ -243,6 +247,7 @@ fun ActionBar(
|
|||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
} // end !isPrivate
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
IconButton(onClick = onAddToList) {
|
IconButton(onClick = onAddToList) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -806,6 +806,7 @@ fun PostCard(
|
|||||||
resolvedEmojis = resolvedEmojis,
|
resolvedEmojis = resolvedEmojis,
|
||||||
unicodeEmojis = unicodeEmojis,
|
unicodeEmojis = unicodeEmojis,
|
||||||
onOpenEmojiLibrary = onOpenEmojiLibrary,
|
onOpenEmojiLibrary = onOpenEmojiLibrary,
|
||||||
|
isPrivate = isPrivate,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
Icon(
|
Icon(
|
||||||
|
|||||||
@@ -182,6 +182,11 @@ fun ComposeScreen(
|
|||||||
val scheduleEnabled by viewModel.scheduleEnabled.collectAsState()
|
val scheduleEnabled by viewModel.scheduleEnabled.collectAsState()
|
||||||
val scheduleTimestamp by viewModel.scheduleTimestamp.collectAsState()
|
val scheduleTimestamp by viewModel.scheduleTimestamp.collectAsState()
|
||||||
val privateReply by viewModel.privateReply.collectAsState()
|
val privateReply by viewModel.privateReply.collectAsState()
|
||||||
|
val privateReplyLocked by viewModel.privateReplyLocked.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(replyTo) {
|
||||||
|
viewModel.configureForReply(replyTo)
|
||||||
|
}
|
||||||
val powStatus = powManager?.status?.collectAsState()?.value ?: PowStatus.Idle
|
val powStatus = powManager?.status?.collectAsState()?.value ?: PowStatus.Idle
|
||||||
val isMiningBusy = powStatus is PowStatus.Mining
|
val isMiningBusy = powStatus is PowStatus.Mining
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|||||||
@@ -123,10 +123,24 @@ class ComposeViewModel(app: Application, private val savedStateHandle: SavedStat
|
|||||||
private val _privateReply = MutableStateFlow(false)
|
private val _privateReply = MutableStateFlow(false)
|
||||||
val privateReply: StateFlow<Boolean> = _privateReply
|
val privateReply: StateFlow<Boolean> = _privateReply
|
||||||
|
|
||||||
|
// Locked = the user is replying to a private reply, so the new reply must also be private
|
||||||
|
// (sending publicly would attach an e-tag to the rumor id on public relays, leaking metadata).
|
||||||
|
private val _privateReplyLocked = MutableStateFlow(false)
|
||||||
|
val privateReplyLocked: StateFlow<Boolean> = _privateReplyLocked
|
||||||
|
|
||||||
fun togglePrivateReply() {
|
fun togglePrivateReply() {
|
||||||
|
if (_privateReplyLocked.value) return
|
||||||
_privateReply.value = !_privateReply.value
|
_privateReply.value = !_privateReply.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Called by ComposeScreen when the screen mounts with [replyTo]; auto-enables
|
||||||
|
* + locks the private toggle if [replyTo] is itself a private reply we received. */
|
||||||
|
fun configureForReply(replyTo: NostrEvent?) {
|
||||||
|
val isReplyingToPrivate = replyTo != null && eventRepo?.isPrivateReply(replyTo.id) == true
|
||||||
|
_privateReplyLocked.value = isReplyingToPrivate
|
||||||
|
if (isReplyingToPrivate) _privateReply.value = true
|
||||||
|
}
|
||||||
|
|
||||||
private val _powEnabled = MutableStateFlow(false)
|
private val _powEnabled = MutableStateFlow(false)
|
||||||
val powEnabled: StateFlow<Boolean> = _powEnabled
|
val powEnabled: StateFlow<Boolean> = _powEnabled
|
||||||
|
|
||||||
@@ -1203,6 +1217,7 @@ class ComposeViewModel(app: Application, private val savedStateHandle: SavedStat
|
|||||||
_hashtags.value = emptyList()
|
_hashtags.value = emptyList()
|
||||||
_powEnabled.value = false
|
_powEnabled.value = false
|
||||||
_privateReply.value = false
|
_privateReply.value = false
|
||||||
|
_privateReplyLocked.value = false
|
||||||
_galleryMode.value = false
|
_galleryMode.value = false
|
||||||
_galleryHasVideo.value = false
|
_galleryHasVideo.value = false
|
||||||
_uploadedMediaMeta.clear()
|
_uploadedMediaMeta.clear()
|
||||||
|
|||||||
Reference in New Issue
Block a user