mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 01:07:46 +00:00
fix(buzz): don't offer a canvas on a DM unless one already exists
The Canvas icon showed on every Buzz channel including DMs, and its edit FAB offered to create one there. Buzz itself never does: its canvas entry needs `hasCanvas || canEditNarrative`, and `canEditNarrative` is `canManageChannel && selfMember !== null && channelType !== "dm"` — so a DM can only ever *display* a canvas that already exists, never write one (ChannelManagementSheet.tsx). We were advertising "start a shared document" in a two-person conversation, and a canvas created there would have been ours alone to see. Mirror both halves: a DM gets the icon only when a canvas is already in cache, and the canvas screen drops its edit FAB for DMs so what remains is read-only. Non-DM Buzz channels are unchanged — icon always, editing always. The has-a-canvas check reads the same BuzzWorkspaceStates registry the canvas screen renders from and recomposes on `canvasUpdates`, so the icon appears when the document lands rather than on the next visit. Verified on emulator-5554: the Buzz DM with Vitor (no canvas) shows only the overflow, where it used to show Canvas + overflow; `general` on the same relay still shows Canvas + overflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7d4e4f1cc9
commit
3acec7ee87
+16
-6
@@ -63,6 +63,7 @@ import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarExtensibleWithBack
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.buzz.stream.CanvasEvent
|
||||
import com.vitorpamplona.quartz.buzz.workspace.isBuzzDm
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.RelayUrlNormalizer
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -95,14 +96,21 @@ fun BuzzCanvasScreen(
|
||||
val canvas = remember(version) { state.canvasNote }
|
||||
val content = canvas?.event?.content
|
||||
|
||||
// The channel this canvas belongs to, for the subtitle. Null until its kind-39000 lands, which
|
||||
// only costs the subtitle — the canvas itself is keyed by the raw channel id.
|
||||
val channelName =
|
||||
// The channel this canvas belongs to, for the subtitle and the edit gate. Null until its
|
||||
// kind-39000 lands, which only costs the subtitle — the canvas itself is keyed by the raw id.
|
||||
val channel =
|
||||
remember(channelId, relayUrl) {
|
||||
RelayUrlNormalizer.normalizeOrNull(relayUrl)?.let { relay ->
|
||||
LocalCache.getRelayGroupChannelIfExists(GroupId(channelId, relay))?.toBestDisplayName()
|
||||
LocalCache.getRelayGroupChannelIfExists(GroupId(channelId, relay))
|
||||
}
|
||||
}
|
||||
val channelName = channel?.toBestDisplayName()
|
||||
|
||||
// Buzz never lets a DM's canvas be written: its editor's `canEdit` is `canEditNarrative`, which
|
||||
// excludes `channelType === "dm"` outright. A DM reaching this screen at all means a canvas
|
||||
// already exists (see the top bar's gate), so render it read-only rather than offering an edit
|
||||
// that Buzz's own client would never show.
|
||||
val canEdit = channel?.event?.isBuzzDm() != true
|
||||
|
||||
var editing by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -145,8 +153,10 @@ fun BuzzCanvasScreen(
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { editing = true }) {
|
||||
Icon(symbol = MaterialSymbols.Edit, contentDescription = stringRes(R.string.buzz_canvas_edit))
|
||||
if (canEdit) {
|
||||
FloatingActionButton(onClick = { editing = true }) {
|
||||
Icon(symbol = MaterialSymbols.Edit, contentDescription = stringRes(R.string.buzz_canvas_edit))
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
|
||||
+29
-5
@@ -37,6 +37,7 @@ import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -47,10 +48,12 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzRelayDialect
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzWorkspaceStates
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupMembership
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -170,11 +173,17 @@ fun RelayGroupTopBar(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
// Buzz workspace canvas (kind 40100): shown on any Buzz-dialect relay so a member can
|
||||
// open the shared markdown doc — or create one when the channel has none yet. The only
|
||||
// affordance that stays an icon: it is this channel's shared document, i.e. content, while
|
||||
// Threads and Share are navigation the reader needs once in a while.
|
||||
if (BuzzRelayDialect.isBuzz(channel.groupId.relayUrl)) {
|
||||
// Buzz canvas (kind 40100): shown on any Buzz-dialect relay so a member can open the
|
||||
// channel's shared markdown doc — or create one when it has none yet. The only affordance
|
||||
// that stays an icon: it is this channel's shared document, i.e. content, while Threads and
|
||||
// Share are navigation the reader needs once in a while.
|
||||
//
|
||||
// Except on a DM, where Buzz itself never offers to *write* one: its canvas entry needs
|
||||
// `hasCanvas || canEditNarrative`, and `canEditNarrative` excludes `channelType === "dm"`
|
||||
// outright. So a DM shows the icon only when a canvas already exists — which is also what
|
||||
// keeps us from advertising "start a shared doc" in a two-person conversation.
|
||||
val hasCanvas by observeBuzzCanvas(channel.groupId.id)
|
||||
if (BuzzRelayDialect.isBuzz(channel.groupId.relayUrl) && (!isDm || hasCanvas)) {
|
||||
IconButton(onClick = { nav.nav(Route.BuzzCanvas(channel.groupId.id, channel.groupId.relayUrl.url)) }) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Dashboard,
|
||||
@@ -372,3 +381,18 @@ private fun RoleBadge(membership: RelayGroupMembership) {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this Buzz channel has a canvas (kind 40100) in cache, recomposing when one lands.
|
||||
*
|
||||
* Only the top bar's DM case needs this: a DM gets the canvas affordance solely when a document
|
||||
* already exists, mirroring Buzz's own `hasCanvas || canEditNarrative` where `canEditNarrative`
|
||||
* excludes DMs. Reads the same [BuzzWorkspaceStates] registry the canvas screen renders from, so the
|
||||
* icon appears the moment the document arrives rather than on the next visit.
|
||||
*/
|
||||
@Composable
|
||||
private fun observeBuzzCanvas(channelId: String): State<Boolean> {
|
||||
val state = remember(channelId) { BuzzWorkspaceStates.getOrCreate(channelId) }
|
||||
val version by state.canvasUpdates.collectAsStateWithLifecycle()
|
||||
return remember(channelId, version) { mutableStateOf(state.canvasNote != null) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user