fix: give every tappable element a real target, and every icon a decided description

Phase 3, second step, of docs/material-design-conformance.md. Two accessibility rules the
tree had no way to hold: M3's 48x48dp touch target and 44x44dp pointer target, and its
requirement that a decorative visual be *annotated* as decorative rather than merely left
undescribed.

**Nineteen `.clickable` chains had no minimum size, and three were text-sized.**
`ArticleCard` and `LiveStreamCardContent` each make an author's name tappable -- a
`labelMedium`, around 16dp tall -- and `LinkPreview` does the same to a `bodyLarge` url
with 2dp of vertical padding. The other sixteen are cards, rows and full-screen boxes that
are already far larger.

`minimumInteractiveComponentSize()` is applied to all nineteen rather than to the three,
because it is a no-op on anything already 48dp and that makes the rule checkable by a
script instead of by measuring. Worth being precise about what it does, since the modifier
is easy to describe wrongly: it reserves 48x48dp of **layout**, not of touch handling --
touch expansion happens at the input layer regardless. Layout is what keeps adjacent
targets from overlapping, what satisfies M3's 8dp separation, and what a mouse pointer on
the desktop build actually has to land on.

**`Clickable.kt` had it built in and moved house.** The vendored ACINQ helper defaults to
`RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as its
content -- and its call sites wrap a 20dp emoji and a row of wallet text. It now applies
the modifier unconditionally, before `.padding(internalPadding)`, since a size modifier
after it would re-impose the smaller constraint.

It also stopped declaring `package com.machankura.compose.ui.composable.widgets.buttons`
while living under `press/mantra/`. That is the second of the three package namespaces the
UI was spread across; `Type.kt` was the first.

**Eighteen `contentDescription = null` were indistinguishable from eighteen oversights.**
`null` is the *correct* API -- M3 asks that decorative visuals be "annotated as decorative
in order to hide them in code", and null is how that annotation is spelled in Compose. The
problem is that it reads identically whether somebody decided or never looked.

So `Decorative` is introduced -- a `String?` that is null -- and fifteen sites now say
`contentDescription = Decorative`. Same bytes, same behaviour, and the difference between
a decision and a gap is now visible in the source and countable by the audit. Each of the
fifteen has adjacent text saying what the icon says: a lock beside "Private to Ada", a
check beside "The group has a shared key.", an icon inside a button whose label is right
there.

**Three were not decorative and now carry their state.**

  - `DkgRitualScreen`'s participant list -- a filled or empty circle beside each member.
    The name says who; only the icon says whether they have contributed. Now "Contributed"
    / "Not yet contributed".
  - `DkgRitualScreen`'s round header -- the title says which round and the count says how
    far along; only the icon says whether it finished. Now "Complete" / "In progress".
  - `ProposalListScreen`'s leading icon, which is the one this commit could not have left
    alone: the previous commit took the red away from the failure state on the highlighted
    card, because `error` is 2.67:1 there. The shape is now the only cue a sighted user
    gets and the description is the only cue anyone else gets. Now "Awaiting your
    signature" / "Signed" / "Failed" / "Waiting on others".

Descriptions follow M3's rule -- name the purpose, not the picture, and never the role.
"Contributed", not "green check", and never "Contributed icon", since the role is added
automatically and a screen reader would say it twice.

**Two new checks, replacing one that was asking the wrong question.**
`docs/scripts/m3-touch-targets.py` finds `.clickable` chains with no minimum size,
including chains broken across two lines. The audit used to count `.clickable` outright,
which is not a defect count: a clickable `Card` is fine and a clickable `Text` is not, and
only the modifier tells them apart. The audit also now separates `contentDescription =
null` (untriaged, budget 0) from `Decorative` (decided, reported at 15).

Both budgets ratcheted to 0, dated in the file.

**Tests.** 944 pass, 595 jvm over 72 classes and 349 android over 44, unchanged -- these
are layout and semantics properties, and this repo has no Compose UI test infrastructure to
assert them against a running composition. What stands in for it is the two scripts, which
check the property that *can* be checked statically: that the modifier and the decision are
present at every site. `:composeApp:compileDebugKotlinAndroid` builds, `m3-audit.sh
--check` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kgothatso Ngako
2026-09-08 00:52:15 +02:00
parent 4fe47c7d46
commit 831d1c0ad4
26 changed files with 195 additions and 35 deletions

View File

@@ -57,6 +57,7 @@ import press.mantra.compose.ui.view.model.ChatRoomMessagingViewModel
import press.mantra.compose.ui.view.state.ChatRoomMessagingUIState
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import press.mantra.compose.ui.theme.spacing
import press.mantra.compose.ui.composable.widgets.Decorative
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
@@ -222,7 +223,7 @@ fun ChatRoomMessagingScreen(
) {
Icon(
Icons.Default.Lock,
contentDescription = null,
contentDescription = Decorative,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)

View File

@@ -38,6 +38,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey
import fr.acinq.phoenix.data.ActiveWallet
import kotlinx.coroutines.flow.StateFlow
import press.mantra.compose.ui.theme.spacing
import press.mantra.compose.ui.composable.widgets.Decorative
/**
* The frame every ChillDKG approval screen sits in: the view model, the states
@@ -137,7 +138,7 @@ internal fun DkgApprovalScaffold(
Spacer(modifier = Modifier.height(MaterialTheme.spacing.space500))
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
contentDescription = Decorative,
modifier = Modifier.size(40.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)

View File

@@ -76,6 +76,7 @@ import fr.acinq.phoenix.data.ActiveWallet
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import press.mantra.compose.ui.theme.spacing
import press.mantra.compose.ui.composable.widgets.Decorative
/**
* The shared-key ceremony: a ChillDKG ritual run across the group's NIP-17
@@ -165,7 +166,7 @@ fun DkgRitualScreen(
)
}
) {
Icon(Icons.Default.Key, contentDescription = null)
Icon(Icons.Default.Key, contentDescription = Decorative)
Text(
text = when (pending) {
DkgApprovalStep.HOST_KEY -> "Review and join"
@@ -333,7 +334,7 @@ private fun RitualProgress(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.ErrorOutline, contentDescription = null)
Icon(Icons.Default.ErrorOutline, contentDescription = Decorative)
Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.relatedGap)) {
Text("The ceremony was abandoned.", style = MaterialTheme.typography.titleSmall)
session.failureReason?.let {
@@ -404,7 +405,7 @@ private fun RitualProgress(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.CheckCircle, contentDescription = null)
Icon(Icons.Default.CheckCircle, contentDescription = Decorative)
Text("The group has a shared key.", style = MaterialTheme.typography.titleSmall)
}
@@ -473,7 +474,7 @@ private fun RitualProgress(
if (isActionPending) {
CircularProgressIndicator(modifier = Modifier.size(20.dp))
} else {
Icon(Icons.Default.Groups, contentDescription = null)
Icon(Icons.Default.Groups, contentDescription = Decorative)
Spacer(modifier = Modifier.width(MaterialTheme.spacing.space100))
Text(text = "Create the #admins group")
}
@@ -526,13 +527,19 @@ private fun RitualRoster(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
verticalAlignment = Alignment.CenterVertically
) {
// Not decorative: the name beside it says who, and only the
// icon says whether they have contributed.
Icon(
imageVector = if (publicKey in hostKeyParticipants) {
Icons.Default.CheckCircle
} else {
Icons.Default.RadioButtonUnchecked
},
contentDescription = null
contentDescription = if (publicKey in hostKeyParticipants) {
"Contributed"
} else {
"Not yet contributed"
}
)
Text(
@@ -572,9 +579,11 @@ private fun RitualStep(
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
verticalAlignment = Alignment.CenterVertically
) {
// Not decorative: the title says which round, the count says how far
// along, and only the icon says whether it has finished.
Icon(
imageVector = if (isDone) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked,
contentDescription = null
contentDescription = if (isDone) "Complete" else "In progress"
)
Text(text = title, style = MaterialTheme.typography.titleSmall)
Spacer(modifier = Modifier.weight(1f))

View File

@@ -57,6 +57,7 @@ import press.mantra.compose.ui.theme.TorchTheme
import press.mantra.compose.ui.view.model.FrostSigningViewModel
import press.mantra.compose.ui.view.state.FrostSigningUIState
import press.mantra.compose.ui.theme.spacing
import press.mantra.compose.ui.composable.widgets.Decorative
/**
* One signing session, and the member's decision about it.
@@ -275,7 +276,7 @@ fun FrostSigningScreen(
!frostSigningViewModel.isActionPending.value,
onClick = { frostSigningViewModel.approve(onNavigateBack) }
) {
Icon(Icons.Default.Draw, contentDescription = null)
Icon(Icons.Default.Draw, contentDescription = Decorative)
Spacer(modifier = Modifier.width(MaterialTheme.spacing.space125))
Text("Sign")
}

View File

@@ -270,7 +270,14 @@ private fun ProposalCard(
Icons.Default.ErrorOutline
else -> Icons.Default.HourglassEmpty
},
contentDescription = null,
// Carries the stage on its own, and carries it alone on the
// highlighted card where the failure state has no colour to spare.
contentDescription = when {
proposal.awaitsYou -> "Awaiting your signature"
proposal.session.stage == FrostSigningStage.COMPLETE -> "Signed"
proposal.session.stage == FrostSigningStage.FAILED -> "Failed"
else -> "Waiting on others"
},
tint = when {
// `error` is 2.67:1 on the highlighted card, so on that one the
// failure is carried by the icon shape and the supporting line

View File

@@ -35,6 +35,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -176,6 +177,7 @@ fun SearchScreen(
}
},
modifier = Modifier
.minimumInteractiveComponentSize()
.clickable {
onNavigateToSearchResult.invoke(
SearchResultRoute(
@@ -218,6 +220,7 @@ fun SearchScreen(
}
},
modifier = Modifier
.minimumInteractiveComponentSize()
.clickable {
onNavigateToSearchResult.invoke(
SearchResultRoute(
@@ -257,6 +260,7 @@ fun SearchScreen(
)
},
modifier = Modifier
.minimumInteractiveComponentSize()
.clickable {
onNavigateToProfile.invoke(
NostrEventDetailRoute(
@@ -362,6 +366,7 @@ fun SearchScreen(
)
},
modifier = Modifier
.minimumInteractiveComponentSize()
.clickable {
onNavigateToSearchResult.invoke(
SearchResultRoute(

View File

@@ -56,6 +56,7 @@ import fr.acinq.phoenix.data.ActiveWallet
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import press.mantra.compose.ui.theme.spacing
import press.mantra.compose.ui.composable.widgets.Decorative
/**
* Last step of group creation: convenient (one admin) or robust (everyone
@@ -345,7 +346,7 @@ private fun ChatRoomTypeCard(
Icon(
imageVector = icon,
contentDescription = null
contentDescription = Decorative
)
Text(

View File

@@ -27,6 +27,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.VerticalDivider
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@@ -192,7 +193,8 @@ private fun ChunkTranslationRow(
// there is no translation yet, the original text is shown greyed out as
// a placeholder. It stays plain text, laid out like the original cell
// beside it, rather than a button with its own shape and padding.
rightModifier = Modifier.clickable(onClick = onClick),
rightModifier = Modifier.minimumInteractiveComponentSize()
.clickable(onClick = onClick),
right = {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(

View File

@@ -0,0 +1,26 @@
package press.mantra.compose.ui.composable.widgets
/**
* An icon or image that adds nothing a screen reader user would miss.
*
* `contentDescription = null` is the correct Compose API for this -- M3 asks that
* decorative visuals be "annotated as decorative in order to hide them in code", and null
* is how that annotation is spelled. The problem with `null` is not what it does; it is
* that it looks identical whether somebody decided the icon was decorative or never
* thought about it, and 18 of them in this tree were indistinguishable.
*
* `contentDescription = Decorative` compiles to the same null and says which it was. It is
* also greppable, so docs/scripts/m3-audit.sh can count the ones still to triage.
*
* **Use it when the adjacent text already says what the icon says** -- a lock beside
* "Private to Ada", a check beside "The group has a shared key.", an icon inside a button
* whose label is right there.
*
* **Do not use it for an icon carrying state the text does not repeat**: a filled-versus-
* empty circle beside a member's name, a status icon in a list row. Those get a real
* description, and M3's rule for writing one is to name the purpose rather than the
* picture, and never to include the role -- "Contributed", not "green check", and never
* "Contributed icon".
*/
@Suppress("MayBeConstant") // a `const` cannot be nullable, and null is the point
val Decorative: String? = null

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package com.machankura.compose.ui.composable.widgets.buttons
package press.mantra.compose.ui.composable.widgets.buttons
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Indication
@@ -26,6 +26,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -38,6 +39,21 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import press.mantra.compose.ui.theme.spacing
/**
* A tappable surface with no chrome of its own -- vendored from ACINQ's phoenix app.
*
* Two things were changed when it moved into `press.mantra` from the `com.machankura`
* package it was still declaring:
*
* - **`minimumInteractiveComponentSize()` is applied unconditionally.** Its defaults are
* `RectangleShape` and `PaddingValues(0.dp)`, so a `Clickable` is exactly as big as
* whatever is inside it, and the call sites here wrap a 20dp emoji and a row of wallet
* text. The modifier reserves 48x48dp of *layout* -- touch expansion happens at the
* input layer regardless -- which is what keeps adjacent targets from overlapping and
* is what a pointer on desktop has to hit.
* - The modifier ordering. It comes before `.padding(internalPadding)`, since a size
* modifier after it would re-impose the smaller constraint.
*/
@Composable
fun Clickable(
onClick: () -> Unit,
@@ -61,6 +77,7 @@ fun Clickable(
border = border,
contentColor = contentColor,
modifier = modifier
.minimumInteractiveComponentSize()
.clip(shape)
.combinedClickable(
onClick = onClick,

View File

@@ -14,6 +14,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -53,7 +54,8 @@ private fun ArticleCard(
.fillMaxWidth()
.padding(vertical = MaterialTheme.spacing.space75)
.then(
if (onArticleClick != null) Modifier.clickable { onArticleClick(30023, author, dTag) }
if (onArticleClick != null) Modifier.minimumInteractiveComponentSize()
.clickable { onArticleClick(30023, author, dTag) }
else Modifier
)
) {
@@ -141,7 +143,8 @@ private fun ArticleCard(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = if (onProfileClick != null) {
Modifier.clickable { onProfileClick(author) }
Modifier.minimumInteractiveComponentSize()
.clickable { onProfileClick(author) }
} else Modifier
)
if (publishedAt != null) {

View File

@@ -38,6 +38,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil3.compose.AsyncImage
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.minimumInteractiveComponentSize
import press.mantra.compose.ui.theme.spacing
@Composable
@@ -64,6 +65,7 @@ fun FullScreenImageViewer(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.scrim)
.minimumInteractiveComponentSize()
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,

View File

@@ -19,6 +19,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -49,6 +50,7 @@ internal fun ImageWithContextMenu(meta: MediaMeta, onFullScreen: () -> Unit) {
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp))
.minimumInteractiveComponentSize()
.clickable { loaded = true },
color = MaterialTheme.colorScheme.surfaceVariant
) {

View File

@@ -34,6 +34,7 @@ import fr.acinq.lightning.payment.Bolt11Invoice
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import press.mantra.compose.ui.theme.spacing
import press.mantra.compose.ui.composable.widgets.Decorative
@Composable
internal fun LightningInvoiceCard(
@@ -109,7 +110,7 @@ internal fun LightningInvoiceCard(
) {
Icon(
Icons.Default.Bolt,
contentDescription = null,
contentDescription = Decorative,
tint = primary,
modifier = Modifier.size(20.dp)
)

View File

@@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -25,6 +26,7 @@ import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import io.ktor.http.Url
import press.mantra.compose.ui.theme.spacing
import press.mantra.compose.ui.composable.widgets.Decorative
@Composable
internal fun LinkPreview(url: String) {
@@ -49,13 +51,14 @@ internal fun LinkPreview(url: String) {
modifier = Modifier
.fillMaxWidth()
.padding(vertical = MaterialTheme.spacing.space50)
.minimumInteractiveComponentSize()
.clickable { uriHandler.openUri(url) }
) {
Column {
data.image?.let { imageUrl ->
AsyncImage(
model = imageUrl,
contentDescription = null,
contentDescription = Decorative,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
@@ -120,6 +123,7 @@ internal fun LinkPreview(url: String) {
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(vertical = MaterialTheme.spacing.space25)
.minimumInteractiveComponentSize()
.clickable { uriHandler.openUri(url) }
)
}

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -47,7 +48,8 @@ private fun LiveStreamCard(
.fillMaxWidth()
.padding(vertical = MaterialTheme.spacing.space75)
.then(
if (onLiveStreamClick != null) Modifier.clickable {
if (onLiveStreamClick != null) Modifier.minimumInteractiveComponentSize()
.clickable {
onLiveStreamClick(author, dTag, segmentRelayHints.firstOrNull())
}
else Modifier

View File

@@ -12,6 +12,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -149,7 +150,8 @@ internal fun LiveStreamCardContent(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = if (onProfileClick != null) {
Modifier.clickable { onProfileClick(author) }
Modifier.minimumInteractiveComponentSize()
.clickable { onProfileClick(author) }
} else Modifier
)
}

View File

@@ -13,6 +13,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -73,7 +74,8 @@ fun QuotedNote(
modifier = Modifier
.fillMaxWidth()
.then(
Modifier.clickable { effectiveNoteClick(localQuotedNostrEvent.nostrEvent.id) }
Modifier.minimumInteractiveComponentSize()
.clickable { effectiveNoteClick(localQuotedNostrEvent.nostrEvent.id) }
)
) {
localQuotedNostrEvent.RenderNotePreview(

View File

@@ -35,6 +35,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
@@ -103,6 +104,7 @@ fun TextNoteEventDetail(
Surface(
modifier = Modifier
.fillMaxWidth()
.minimumInteractiveComponentSize()
.clickable {
onNavigateToWriteAReply.invoke(
localNostrEvent.nostrEvent.id

View File

@@ -13,6 +13,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -41,7 +42,8 @@ fun press.mantra.compose.database.model.intermdiate.LocalNostrEvent.ListView(
.fillMaxWidth()
.padding(
horizontal = MaterialTheme.spacing.space125
).clickable(
).minimumInteractiveComponentSize()
.clickable(
enabled = true,
onClick = {
onNavigateToEvent.invoke(profile?.nostrEventId ?: nostrEvent.id)

View File

@@ -15,6 +15,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SuggestionChip
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Alignment
@@ -54,7 +55,8 @@ internal fun TextNoteFeedItem(
)
Text(
modifier = Modifier.clickable(
modifier = Modifier.minimumInteractiveComponentSize()
.clickable(
onClick = {
onNavigateToEvent.invoke(localInReplyToNostrEvent.inReplyToRelation.inReplyToNostrEventId)
}
@@ -81,7 +83,8 @@ internal fun TextNoteFeedItem(
) {
if (profile != null) {
Row(
modifier = Modifier.fillMaxWidth().clickable(
modifier = Modifier.fillMaxWidth().minimumInteractiveComponentSize()
.clickable(
onClick = {
onNavigateToEvent.invoke(profile.nostrEventId)
}

View File

@@ -44,7 +44,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.machankura.compose.ui.composable.widgets.buttons.Clickable
import press.mantra.compose.ui.composable.widgets.buttons.Clickable
import press.mantra.compose.ui.composable.widgets.dialogs.ModalBottomSheet
import press.mantra.compose.ui.theme.spacing

View File

@@ -43,7 +43,7 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.machankura.compose.ui.composable.widgets.buttons.Clickable
import press.mantra.compose.ui.composable.widgets.buttons.Clickable
import fr.acinq.phoenix.data.UserWallet
import fr.acinq.phoenix.data.WalletId
import fr.acinq.phoenix.utils.preferences.GlobalPrefs

View File

@@ -47,6 +47,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
@@ -92,6 +93,7 @@ import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import press.mantra.compose.ui.theme.spacing
import press.mantra.compose.ui.composable.widgets.Decorative
class ChatMessageListViewModel(
initialChatMessageListUIState: ChatMessageListUIState,
@@ -650,7 +652,7 @@ class ChatMessageListViewModel(
) {
Icon(
Icons.Default.Lock,
contentDescription = null,
contentDescription = Decorative,
modifier = Modifier.size(12.dp),
tint = MaterialTheme.colorScheme.primary
)
@@ -724,7 +726,7 @@ class ChatMessageListViewModel(
Text("Reply privately to ${nameFor(localChatMessage.chatMessage.senderPublicKey)}")
},
leadingIcon = {
Icon(Icons.Default.Lock, contentDescription = null)
Icon(Icons.Default.Lock, contentDescription = Decorative)
},
onClick = {
participantFor(localChatMessage.chatMessage.senderPublicKey)
@@ -864,7 +866,7 @@ private fun ProposalsAwaitingYouNotice(
) {
Icon(
Icons.Default.Draw,
contentDescription = null
contentDescription = Decorative
)
Column(
@@ -930,7 +932,7 @@ private fun PrivateMessageNotice(
) {
Icon(
Icons.Default.Lock,
contentDescription = null,
contentDescription = Decorative,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -1029,6 +1031,7 @@ private fun RitualNotice(
Row(
modifier = Modifier
.fillMaxWidth()
.minimumInteractiveComponentSize()
.clickable(onClick = onClick)
.padding(horizontal = MaterialTheme.spacing.space250, vertical = MaterialTheme.spacing.space125),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.spacing.space125),
@@ -1036,7 +1039,7 @@ private fun RitualNotice(
) {
Icon(
imageVector = icon,
contentDescription = null,
contentDescription = Decorative,
tint = tint
)