mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #4185 from vitorpamplona/claude/sharp-keller-7itny0
refactor: Render/Display split, batches 1–3 — shared displays for AccountViewModel-bound composables
This commit is contained in:
+1
-1
@@ -30,7 +30,7 @@ import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.junit4.v2.createComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.vitorpamplona.amethyst.ui.actions.DeferredCrossfade
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.DeferredCrossfade
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
+10
-127
@@ -20,25 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import androidx.collection.mutableScatterMapOf
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.rememberTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
@@ -50,116 +37,12 @@ fun <T> CrossfadeIfEnabled(
|
||||
label: String = "Crossfade",
|
||||
accountViewModel: AccountViewModel,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
if (accountViewModel.settings.isPerformanceMode()) {
|
||||
Box(modifier, contentAlignment) {
|
||||
content(targetState)
|
||||
}
|
||||
} else {
|
||||
DeferredCrossfade(targetState, modifier, contentAlignment, animationSpec, label, content)
|
||||
}
|
||||
}
|
||||
|
||||
/** Latches the first time a crossfade's target moves off the value it was composed with. */
|
||||
private class ChangeLatch {
|
||||
var changed = false
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MyCrossfade] that does not build its [androidx.compose.animation.core.Transition] until there
|
||||
* is something to animate.
|
||||
*
|
||||
* `updateTransition` allocates a transition, its animation list and its seeking state on *first
|
||||
* composition*, even though first composition has nothing to cross-fade — target and initial state
|
||||
* are the same value. In a feed that is waste: every card scrolled in builds a transition per
|
||||
* animated element, and during a scroll essentially none of them run, because the underlying counts
|
||||
* and icons do not change in the second a card is on screen.
|
||||
*
|
||||
* So the plain content renders until the target actually moves. At that point the transition is
|
||||
* built seeded at the *original* value via [MutableTransitionState] and immediately re-targeted at
|
||||
* the new one, so the first real change still animates exactly as before; every later change
|
||||
* animates through the now-live transition normally.
|
||||
*/
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
internal fun <T> DeferredCrossfade(
|
||||
targetState: T,
|
||||
modifier: Modifier,
|
||||
contentAlignment: Alignment,
|
||||
animationSpec: FiniteAnimationSpec<Float>,
|
||||
label: String,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val initial = remember { targetState }
|
||||
val latch = remember { ChangeLatch() }
|
||||
if (targetState != initial) latch.changed = true
|
||||
|
||||
if (!latch.changed) {
|
||||
Box(modifier, contentAlignment) {
|
||||
content(targetState)
|
||||
}
|
||||
} else {
|
||||
val transitionState = remember { MutableTransitionState(initial) }
|
||||
transitionState.targetState = targetState
|
||||
val transition = rememberTransition(transitionState, label)
|
||||
transition.MyCrossfade(modifier, contentAlignment, animationSpec, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun <T> MyCrossfade(
|
||||
targetState: T,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
label: String = "Crossfade",
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val transition = updateTransition(targetState, label)
|
||||
transition.MyCrossfade(modifier, contentAlignment, animationSpec, content = content)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun <T> Transition<T>.MyCrossfade(
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
contentKey: (targetState: T) -> Any? = { it },
|
||||
content: @Composable (targetState: T) -> Unit,
|
||||
) {
|
||||
val currentlyVisible = remember { mutableStateListOf<T>().apply { add(currentState) } }
|
||||
val contentMap = remember { mutableScatterMapOf<T, @Composable () -> Unit>() }
|
||||
// If not animating, just display the current state
|
||||
if (currentState == targetState && (currentlyVisible.size != 1 || currentlyVisible[0] != targetState)) {
|
||||
// Remove all the intermediate items from the list once the animation is finished.
|
||||
currentlyVisible.removeAll { it != targetState }
|
||||
contentMap.clear()
|
||||
}
|
||||
|
||||
if (targetState !in contentMap) {
|
||||
// Replace target with the same key if any
|
||||
val replacementId =
|
||||
currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) }
|
||||
if (replacementId == -1) {
|
||||
currentlyVisible.add(targetState)
|
||||
} else {
|
||||
currentlyVisible[replacementId] = targetState
|
||||
}
|
||||
contentMap.clear()
|
||||
currentlyVisible.fastForEach { stateForContent ->
|
||||
contentMap[stateForContent] = {
|
||||
val alpha by
|
||||
animateFloat(transitionSpec = { animationSpec }) {
|
||||
if (it == stateForContent) 1f else 0f
|
||||
}
|
||||
Box(Modifier.graphicsLayer { this.alpha = alpha }, contentAlignment) { content(stateForContent) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment) {
|
||||
currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() } }
|
||||
}
|
||||
}
|
||||
) = CrossfadeIfEnabled(
|
||||
targetState = targetState,
|
||||
enabled = !accountViewModel.settings.isPerformanceMode(),
|
||||
modifier = modifier,
|
||||
contentAlignment = contentAlignment,
|
||||
animationSpec = animationSpec,
|
||||
label = label,
|
||||
content = content,
|
||||
)
|
||||
|
||||
+4
-69
@@ -20,29 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.elements
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_hashtag_label_confirm
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_hashtag_label_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_hashtag_label_field
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_hashtag_label_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.nip32Labeling.ui.HashtagLabelDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
/**
|
||||
@@ -56,53 +36,8 @@ fun AddHashtagLabelDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var hashtag by remember { mutableStateOf("") }
|
||||
|
||||
// Strip the leading '#', drop whitespace and lowercase so the stored label matches the
|
||||
// hashtag-feed convention. A blank result disables the confirm button.
|
||||
val sanitized =
|
||||
hashtag
|
||||
.trim()
|
||||
.removePrefix("#")
|
||||
.filterNot { it.isWhitespace() }
|
||||
.lowercase()
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringRes(Res.string.add_hashtag_label_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringRes(Res.string.add_hashtag_label_explainer))
|
||||
OutlinedTextField(
|
||||
value = hashtag,
|
||||
onValueChange = { hashtag = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringRes(Res.string.add_hashtag_label_field)) },
|
||||
prefix = { Text("#") },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = sanitized.isNotEmpty(),
|
||||
onClick = {
|
||||
accountViewModel.labelWithHashtag(note, sanitized)
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(Res.string.add_hashtag_label_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringRes(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
HashtagLabelDialog(
|
||||
onConfirm = { hashtag -> accountViewModel.labelWithHashtag(note, hashtag) },
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
+3
-28
@@ -20,24 +20,17 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.elements
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.FollowedHashtagLabel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.hashtags.firstIsTaggedHashes
|
||||
import com.vitorpamplona.quartz.nip22Comments.CommentEvent
|
||||
@@ -68,25 +61,7 @@ fun DisplayFollowingHashtagsInPost(
|
||||
}
|
||||
}
|
||||
|
||||
firstTag?.let {
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) { DisplayTagList(it, accountViewModel, nav) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DisplayTagList(
|
||||
firstTag: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
ClickableTextColor(
|
||||
"#$firstTag",
|
||||
linkColor = MaterialTheme.colorScheme.lessImportantLink,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
) {
|
||||
nav.nav(Route.Hashtag(firstTag.lowercase()))
|
||||
firstTag?.let { tag ->
|
||||
FollowedHashtagLabel(tag) { nav.nav(Route.Hashtag(tag.lowercase())) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,45 +21,23 @@
|
||||
package com.vitorpamplona.amethyst.ui.note.elements
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_info_description
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_settings_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.HeaderPill
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ProofOfWorkPill
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun DisplayPoWPreview() {
|
||||
ThemeComparisonColumn(
|
||||
toPreview = { DisplayPoW(pow = 24, accountViewModel = mockAccountViewModel()) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact pill showing the proof of work a received note carries: a gear plus
|
||||
* the difficulty in leading zero bits. Tapping it explains what the number
|
||||
* means. Sits inline in note headers, so it stays at text height.
|
||||
*/
|
||||
/** Entry: the shared [ProofOfWorkPill], explaining the number in a toast when tapped. */
|
||||
@Composable
|
||||
fun DisplayPoW(
|
||||
pow: Int,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
HeaderPill(
|
||||
symbol = MaterialSymbols.Manufacturing,
|
||||
text = pow.toString(),
|
||||
contentDescription = stringRes(Res.string.pow_settings_title),
|
||||
onClick = {
|
||||
accountViewModel.toastManager.toast(
|
||||
Res.string.pow_settings_title,
|
||||
Res.string.pow_info_description,
|
||||
pow.toString(),
|
||||
)
|
||||
},
|
||||
)
|
||||
ProofOfWorkPill(pow) {
|
||||
accountViewModel.toastManager.toast(
|
||||
Res.string.pow_settings_title,
|
||||
Res.string.pow_info_description,
|
||||
pow.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-56
@@ -20,31 +20,10 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.nip22Comments
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.hashtag_exclusive
|
||||
import com.vitorpamplona.amethyst.commons.nip73ExternalIds.ui.HashtagExternalIdCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip73ExternalIds.topics.HashtagId
|
||||
|
||||
@@ -54,40 +33,7 @@ fun DisplayHashtagExternalId(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
DisplayHashtagExternalId(externalId.topic) {
|
||||
HashtagExternalIdCard(externalId.topic) {
|
||||
nav.nav(Route.Hashtag(externalId.topic.lowercase()))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayHashtagExternalId(
|
||||
topic: String,
|
||||
linkInteractionListener: LinkInteractionListener,
|
||||
) {
|
||||
Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Tag,
|
||||
contentDescription = stringRes(id = Res.string.hashtag_exclusive),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
Spacer(StdHorzSpacer)
|
||||
|
||||
Text(
|
||||
text =
|
||||
buildAnnotatedString {
|
||||
withLink(
|
||||
LinkAnnotation.Clickable("hashtag", null, linkInteractionListener),
|
||||
) {
|
||||
append(topic)
|
||||
}
|
||||
},
|
||||
style =
|
||||
LocalTextStyle.current.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,30 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_going
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_maybe
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_not_going
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.nip52Calendar.ui.CalendarRsvpButtons
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
@@ -90,88 +72,9 @@ fun CalendarRsvpRow(
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
RsvpButton(
|
||||
label = stringRes(Res.string.calendar_rsvp_going),
|
||||
status = RSVPStatusTag.STATUS.ACCEPTED,
|
||||
currentStatus = currentStatus,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onTap,
|
||||
)
|
||||
RsvpButton(
|
||||
label = stringRes(Res.string.calendar_rsvp_maybe),
|
||||
status = RSVPStatusTag.STATUS.TENTATIVE,
|
||||
currentStatus = currentStatus,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onTap,
|
||||
)
|
||||
RsvpButton(
|
||||
label = stringRes(Res.string.calendar_rsvp_not_going),
|
||||
status = RSVPStatusTag.STATUS.DECLINED,
|
||||
currentStatus = currentStatus,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onTap,
|
||||
)
|
||||
}
|
||||
CalendarRsvpButtons(currentStatus, onTap)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RsvpButton(
|
||||
label: String,
|
||||
status: RSVPStatusTag.STATUS,
|
||||
currentStatus: RSVPStatusTag.STATUS?,
|
||||
modifier: Modifier,
|
||||
onClick: (RSVPStatusTag.STATUS) -> Unit,
|
||||
) {
|
||||
val selected = status == currentStatus
|
||||
if (selected) {
|
||||
FilledTonalButton(
|
||||
onClick = { onClick(status) },
|
||||
modifier = modifier,
|
||||
contentPadding = RsvpButtonPadding,
|
||||
colors =
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = colorFor(status),
|
||||
contentColor = Color.White,
|
||||
),
|
||||
) {
|
||||
RsvpButtonLabel(label)
|
||||
}
|
||||
} else {
|
||||
OutlinedButton(
|
||||
onClick = { onClick(status) },
|
||||
modifier = modifier,
|
||||
contentPadding = RsvpButtonPadding,
|
||||
) {
|
||||
RsvpButtonLabel(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val RsvpButtonPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
|
||||
|
||||
@Composable
|
||||
private fun RsvpButtonLabel(label: String) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun colorFor(status: RSVPStatusTag.STATUS) =
|
||||
when (status) {
|
||||
RSVPStatusTag.STATUS.ACCEPTED -> MaterialTheme.colorScheme.primary
|
||||
RSVPStatusTag.STATUS.TENTATIVE -> MaterialTheme.colorScheme.tertiary
|
||||
RSVPStatusTag.STATUS.DECLINED -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
|
||||
/**
|
||||
* Deterministic per-target d-tag so each user's RSVP for a given event is a single addressable.
|
||||
* The format mirrors the a-tag coordinate so it's debuggable (`rsvp:31923:<pubkey>:<dtag>`).
|
||||
|
||||
+2
@@ -41,6 +41,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.nip34Git.ui.CompactButtonHeight
|
||||
import com.vitorpamplona.amethyst.commons.nip34Git.ui.CompactButtonPadding
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_pr_changes_retry
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_pr_loading_changes
|
||||
|
||||
+8
-60
@@ -20,33 +20,14 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.GitStatusIndex
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_status_close
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_status_mark_merged
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_status_reopen
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.nip34Git.ui.GitStatusButtons
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
@@ -60,10 +41,6 @@ import com.vitorpamplona.quartz.nip34Git.status.GitStatusOpenEvent
|
||||
|
||||
private enum class StatusTarget { OPEN, CLOSED, APPLIED }
|
||||
|
||||
/** Compact sizing shared by the small action buttons on git cards. */
|
||||
internal val CompactButtonHeight = Modifier.height(32.dp)
|
||||
internal val CompactButtonPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)
|
||||
|
||||
/**
|
||||
* NIP-34 status controls for an issue, patch or pull request. Visible only to the
|
||||
* people allowed to moderate the thread — the item's author and the repository
|
||||
@@ -88,42 +65,13 @@ fun GitStatusActions(
|
||||
val current = index?.get(note.idHex)
|
||||
val closedOrApplied = current is GitStatusClosedEvent || current is GitStatusAppliedEvent
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
if (closedOrApplied) {
|
||||
FilledTonalButton(
|
||||
onClick = { sendStatus(accountViewModel, note, StatusTarget.OPEN) },
|
||||
modifier = CompactButtonHeight,
|
||||
contentPadding = CompactButtonPadding,
|
||||
) {
|
||||
Icon(MaterialSymbols.RadioButtonChecked, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Text(stringRes(Res.string.git_status_reopen), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
} else {
|
||||
if (isPatchOrPr) {
|
||||
FilledTonalButton(
|
||||
onClick = { sendStatus(accountViewModel, note, StatusTarget.APPLIED) },
|
||||
modifier = CompactButtonHeight,
|
||||
contentPadding = CompactButtonPadding,
|
||||
) {
|
||||
Icon(MaterialSymbols.Check, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Text(stringRes(Res.string.git_status_mark_merged), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { sendStatus(accountViewModel, note, StatusTarget.CLOSED) },
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||
modifier = CompactButtonHeight,
|
||||
contentPadding = CompactButtonPadding,
|
||||
) {
|
||||
Icon(MaterialSymbols.Cancel, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Text(stringRes(Res.string.git_status_close), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
GitStatusButtons(
|
||||
closedOrApplied = closedOrApplied,
|
||||
isPatchOrPr = isPatchOrPr,
|
||||
onReopen = { sendStatus(accountViewModel, note, StatusTarget.OPEN) },
|
||||
onMarkMerged = { sendStatus(accountViewModel, note, StatusTarget.APPLIED) },
|
||||
onClose = { sendStatus(accountViewModel, note, StatusTarget.CLOSED) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun repositoryAddressOf(event: Event): Address? =
|
||||
|
||||
@@ -20,40 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.kinds
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.EmptyNav
|
||||
import com.vitorpamplona.amethyst.commons.nipsOnNostr.ui.NipCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds
|
||||
|
||||
@Composable
|
||||
fun RenderNipContent(
|
||||
@@ -63,133 +35,5 @@ fun RenderNipContent(
|
||||
) {
|
||||
val noteEvent = note.event as? NipTextEvent ?: return
|
||||
|
||||
NipNoteHeader(noteEvent, note, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NipNoteHeader(
|
||||
noteEvent: NipTextEvent,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val kinds = remember(noteEvent) { noteEvent.kinds() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = Size5dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.subtleBorder,
|
||||
QuoteBorder,
|
||||
),
|
||||
verticalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content },
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (kinds.isNotEmpty()) {
|
||||
FlowRow(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
verticalArrangement = SpacedBy5dp,
|
||||
itemVerticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(Res.string.kinds),
|
||||
)
|
||||
kinds.forEach {
|
||||
NoPaddingSuggestionChip(
|
||||
label = it.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun NipNoteHeaderPreview() {
|
||||
val event =
|
||||
NipTextEvent(
|
||||
id = "eb2b05394ff0014bb6a79c2eacfd1c80696821592f4dbf86c950c4bf16614aa0",
|
||||
pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||
createdAt = 1767978398,
|
||||
content = "Trusted Translations\n--------------------\n\n`draft` `optional`\n\nThis NIP allows anyone to post a translation for any event on a `kind:76`.\n\n```js\n{\n \"kind\": 76,\n \"tags\": [\n [\"e\", \"\u003coriginal_event_id\u003e\", \"\u003crelay\u003e\"]\n [\"k\", \"\u003coriginal_event_kind\u003e\"]\n [\"l\", \"\u003ctranslated_to_language_country\u003e\"], // ISO 639-1: \"en\", \"es\", ...\n [\"l\", \"\u003ctranslated_to_language_code\u003e\"], // ISO 639-1: \"en-us\", \"en-br\"\n [\"s\", \"title\", \"translated title tag\"]\n [\"s\", \"summary\", \"translated summary tag\"]\n ],\n \"content\": \"this is a translated version of the original content\",\n // ...other fields\n}\n```\n\n`e` tag points to the event being translated, `k` tag points to the kind of that event.\n\n`l` tags define the language this was translated to in lowercase codes as defined by ISO 639-1. \n\n`s` tags are the translations for tags in the original event. \n\n`.content` contains the translation of the original `.content`\n\nClients SHOULD request translations by `e` and `l` tags spoken by their user. For every tag being rendered, Clients SHOULD look for their translated versions.\n\nProviders SHOULD use the event id in the filter to know which events need translations.\n\nProviders MAY store their translations behind a paid relay with NIP-42 auth.\n\n## Declaring Translation Providers\n\nKind `10041` lists the user's authorized translation providers. Each `p` tag is followed by the `pubkey` of the service publishing kind 76s, and the relay translations can be found. Users can specify these publicly or privately by JSON-stringifying and encrypting the tag list in the `.content` using NIP-44. \n\n```js\n{\n \"kind\": 10041,\n \"tags\": [\n [\"p\", \"4fd5e210530e4f6b2cb083795834bfe5108324f1ed9f00ab73b9e8fcfe5f12fe\", \"wss://translations.nostr.com\"],\n [\"l\", \"\u003ctranslated_to_language_country\u003e\"], // ISO 639-1: \"en\", \"es\", ...\n [\"l\", \"\u003ctranslated_to_language_code\u003e\"], // ISO 639-1: \"en-us\", \"en-br\"\n //...\n}\n```\n\n`l` tags in this event are the languages the user understands and wants translations to.\n\nProviders SHOULD create the `10041` event and post to the user's outbox relay.",
|
||||
sig = "7fa9f1d49c41c7bfbdad6d089d1c6685777c86de8fbda7662a630888658839f0444cb1398385f759461c09191b287fa222eec0ffe22b3fa8cf740f71aab9dc21",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "trusted-translations"),
|
||||
arrayOf("title", "Trusted Translations"),
|
||||
arrayOf("k", "1011"),
|
||||
arrayOf("k", "10041"),
|
||||
arrayOf("k", "1011"),
|
||||
arrayOf("k", "10041"),
|
||||
arrayOf("k", "1011"),
|
||||
arrayOf("k", "10041"),
|
||||
arrayOf("k", "1011"),
|
||||
arrayOf("k", "10041"),
|
||||
arrayOf("client", "nostrhub.io"),
|
||||
),
|
||||
)
|
||||
|
||||
LocalCache.justConsume(event, null, true)
|
||||
val note = LocalCache.getOrCreateNote(event.id)
|
||||
|
||||
ThemeComparisonColumn(
|
||||
toPreview = {
|
||||
NipNoteHeader(
|
||||
noteEvent = event,
|
||||
note = note,
|
||||
accountViewModel = mockAccountViewModel(),
|
||||
nav = EmptyNav(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoPaddingSuggestionChip(
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.extraSmall, // Use a small shape for chip look
|
||||
color = MaterialTheme.colorScheme.secondaryContainer, // Default chip color
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
// Apply desired internal padding to the Text itself
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
}
|
||||
NipCard(noteEvent)
|
||||
}
|
||||
|
||||
+13
-45
@@ -20,30 +20,16 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_to_public_bookmarks
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove_from_public_bookmarks
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.BookmarkToggleButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
/**
|
||||
@@ -56,10 +42,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
* Bookmarked state is read from the public bookmark id/address **sets** (not `List<Note>`
|
||||
* containment) so it reflects reliably for addressable notes and updates the moment the list
|
||||
* changes — the icon flipping filled/outline is the feedback, no toast needed.
|
||||
*
|
||||
* Rendered as a compact clickable glyph (not a Material [androidx.compose.material3.IconButton],
|
||||
* whose 48dp minimum touch target would stand taller than the title it sits beside). [iconSize]
|
||||
* defaults to a single title line so it aligns when placed next to a show/episode title.
|
||||
*/
|
||||
@Composable
|
||||
fun PodcastBookmarkButton(
|
||||
@@ -82,30 +64,16 @@ fun PodcastBookmarkButton(
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
onClick = {
|
||||
if (isBookmarked) {
|
||||
accountViewModel.removePublicBookmark(note)
|
||||
} else {
|
||||
accountViewModel.addPublicBookmark(note)
|
||||
}
|
||||
},
|
||||
).padding(4.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
symbol = if (isBookmarked) MaterialSymbols.Bookmark else MaterialSymbols.BookmarkAdd,
|
||||
contentDescription =
|
||||
stringRes(
|
||||
if (isBookmarked) Res.string.remove_from_public_bookmarks else Res.string.add_to_public_bookmarks,
|
||||
),
|
||||
tint = if (isBookmarked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(iconSize),
|
||||
)
|
||||
}
|
||||
BookmarkToggleButton(
|
||||
isBookmarked = isBookmarked,
|
||||
onToggle = {
|
||||
if (isBookmarked) {
|
||||
accountViewModel.removePublicBookmark(note)
|
||||
} else {
|
||||
accountViewModel.addPublicBookmark(note)
|
||||
}
|
||||
},
|
||||
modifier = modifier,
|
||||
iconSize = iconSize,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,42 +20,22 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_join_request
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_leave_request
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_member_added
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_member_removed
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_members_added
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_members_count
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_members_removed
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_membership_list
|
||||
import com.vitorpamplona.amethyst.commons.nip43RelayMembers.ui.RelayAddMemberCard
|
||||
import com.vitorpamplona.amethyst.commons.nip43RelayMembers.ui.RelayJoinRequestCard
|
||||
import com.vitorpamplona.amethyst.commons.nip43RelayMembers.ui.RelayLeaveRequestCard
|
||||
import com.vitorpamplona.amethyst.commons.nip43RelayMembers.ui.RelayMembershipListCard
|
||||
import com.vitorpamplona.amethyst.commons.nip43RelayMembers.ui.RelayRemoveMemberCard
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent
|
||||
import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent
|
||||
import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent
|
||||
|
||||
// Dispatcher entries for the NIP-43 relay-membership kinds. The cards draw only from the event,
|
||||
// so the entries just decode the note; the rendering is in commonsUI nip43RelayMembers/ui.
|
||||
|
||||
@Composable
|
||||
fun RenderRelayMembershipList(
|
||||
baseNote: Note,
|
||||
@@ -63,15 +43,7 @@ fun RenderRelayMembershipList(
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? RelayMembershipListEvent ?: return
|
||||
val memberCount = remember(noteEvent) { noteEvent.members().size }
|
||||
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.People,
|
||||
title = stringRes(Res.string.relay_membership_list),
|
||||
subtitle = stringRes(Res.string.relay_members_count, memberCount),
|
||||
nav = nav,
|
||||
relayPubKey = noteEvent.pubKey,
|
||||
)
|
||||
RelayMembershipListCard(noteEvent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -81,21 +53,7 @@ fun RenderRelayAddMember(
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? RelayAddMemberEvent ?: return
|
||||
val memberKeys = remember(noteEvent) { noteEvent.memberPubKeys() }
|
||||
val title =
|
||||
if (memberKeys.size == 1) {
|
||||
stringRes(Res.string.relay_member_added)
|
||||
} else {
|
||||
stringRes(Res.string.relay_members_added, memberKeys.size)
|
||||
}
|
||||
val subtitle = remember(memberKeys) { memberKeys.joinToString(", ") { it.take(16) + "..." } }
|
||||
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonAdd,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
relayPubKey = noteEvent.pubKey,
|
||||
)
|
||||
RelayAddMemberCard(noteEvent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -105,21 +63,7 @@ fun RenderRelayRemoveMember(
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = baseNote.event as? RelayRemoveMemberEvent ?: return
|
||||
val memberKeys = remember(noteEvent) { noteEvent.memberPubKeys() }
|
||||
val title =
|
||||
if (memberKeys.size == 1) {
|
||||
stringRes(Res.string.relay_member_removed)
|
||||
} else {
|
||||
stringRes(Res.string.relay_members_removed, memberKeys.size)
|
||||
}
|
||||
val subtitle = remember(memberKeys) { memberKeys.joinToString(", ") { it.take(16) + "..." } }
|
||||
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonRemove,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
relayPubKey = noteEvent.pubKey,
|
||||
)
|
||||
RelayRemoveMemberCard(noteEvent)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -127,129 +71,11 @@ fun RenderRelayJoinRequest(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonAdd,
|
||||
title = stringRes(Res.string.relay_join_request),
|
||||
subtitle = null,
|
||||
nav = nav,
|
||||
relayPubKey = null,
|
||||
)
|
||||
}
|
||||
) = RelayJoinRequestCard()
|
||||
|
||||
@Composable
|
||||
fun RenderRelayLeaveRequest(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.AutoMirrored.ExitToApp,
|
||||
title = stringRes(Res.string.relay_leave_request),
|
||||
subtitle = null,
|
||||
nav = nav,
|
||||
relayPubKey = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayMemberEventCard(
|
||||
icon: MaterialSymbol,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
nav: INav? = null,
|
||||
relayPubKey: String? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
symbol = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
subtitle?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayMembershipListCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.People,
|
||||
title = "Relay membership list",
|
||||
subtitle = "42 members",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayAddMemberCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonAdd,
|
||||
title = "Member added to relay",
|
||||
subtitle = "a1b2c3d4e5f6a7b8...",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayRemoveMemberCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonRemove,
|
||||
title = "Member removed from relay",
|
||||
subtitle = "a1b2c3d4e5f6a7b8...",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayJoinRequestCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonAdd,
|
||||
title = "Relay join request",
|
||||
subtitle = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayLeaveRequestCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.AutoMirrored.ExitToApp,
|
||||
title = "Relay leave request",
|
||||
subtitle = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
) = RelayLeaveRequestCard()
|
||||
|
||||
+20
-131
@@ -20,42 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.header
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.messages_new_message_subject
|
||||
import com.vitorpamplona.amethyst.commons.resources.messages_new_message_subject_caption
|
||||
import com.vitorpamplona.amethyst.commons.resources.messages_new_subject_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.messages_new_subject_message_placeholder
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.buttons.CloseButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.buttons.PostButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.commons.nip17Dm.ui.ChatroomSubjectDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
|
||||
import com.vitorpamplona.quartz.nip17Dm.messages.ChatMessageEvent
|
||||
@@ -67,103 +34,25 @@ fun NewChatroomSubjectDialog(
|
||||
accountViewModel: AccountViewModel,
|
||||
room: ChatroomKey,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
properties =
|
||||
DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
),
|
||||
) {
|
||||
Surface {
|
||||
val groupName =
|
||||
remember {
|
||||
mutableStateOf(
|
||||
accountViewModel.account.chatroomList.rooms
|
||||
.get(room)
|
||||
?.subject
|
||||
?.value ?: "",
|
||||
)
|
||||
}
|
||||
val message = remember { mutableStateOf("") }
|
||||
val scope = rememberCoroutineScope()
|
||||
ChatroomSubjectDialog(
|
||||
initialSubject =
|
||||
accountViewModel.account.chatroomList.rooms
|
||||
.get(room)
|
||||
?.subject
|
||||
?.value ?: "",
|
||||
onPost = { subject, message ->
|
||||
accountViewModel.launchSigner {
|
||||
val template =
|
||||
ChatMessageEvent.build(
|
||||
message,
|
||||
room.users.map { LocalCache.getOrCreateUser(it).toPTag() },
|
||||
) {
|
||||
subject.ifBlank { null }?.let { changeSubject(it) }
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(10.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CloseButton(onPress = { onClose() })
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
accountViewModel.launchSigner {
|
||||
val template =
|
||||
ChatMessageEvent.build(
|
||||
message.value,
|
||||
room.users.map { LocalCache.getOrCreateUser(it).toPTag() },
|
||||
) {
|
||||
groupName.value.ifBlank { null }?.let { changeSubject(it) }
|
||||
}
|
||||
|
||||
accountViewModel.account.sendNip17PrivateMessage(template)
|
||||
}
|
||||
|
||||
onClose()
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(Res.string.messages_new_message_subject)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = groupName.value,
|
||||
onValueChange = { groupName.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(Res.string.messages_new_message_subject_caption),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(Res.string.messages_new_subject_message)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
value = message.value,
|
||||
onValueChange = { message.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(Res.string.messages_new_subject_message_placeholder),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
maxLines = 10,
|
||||
)
|
||||
accountViewModel.account.sendNip17PrivateMessage(template)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onClose = onClose,
|
||||
)
|
||||
}
|
||||
|
||||
+4
-4
@@ -35,6 +35,8 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.JoinChannelButton
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.LeaveChannelButton
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
@@ -47,8 +49,6 @@ import com.vitorpamplona.amethyst.service.relayClient.reqCommand.channel.observe
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.user.observeUserIsFollowingChannel
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.actions.JoinChatButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.actions.LeaveChatButton
|
||||
|
||||
@Composable
|
||||
fun ShortEphemeralChatChannelHeader(
|
||||
@@ -130,8 +130,8 @@ fun JoinEphemeralChatButtonIfNotAlreadyJoined(
|
||||
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel, accountViewModel)
|
||||
|
||||
if (!isFollowing) {
|
||||
JoinChatButton(channel, accountViewModel, nav)
|
||||
JoinChannelButton { accountViewModel.follow(channel) }
|
||||
} else {
|
||||
LeaveChatButton(channel, accountViewModel, nav)
|
||||
LeaveChannelButton { accountViewModel.unfollow(channel) }
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -41,7 +41,10 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.LeaveChannelButton
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.nip28PublicChat.ui.EditChannelButton
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.channel_image
|
||||
import com.vitorpamplona.amethyst.commons.resources.created_at
|
||||
@@ -64,8 +67,6 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.EditButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LeaveChatButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LinkChatButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.MuteChatButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.OpenChatButton
|
||||
@@ -212,7 +213,7 @@ fun EditButtonIfIamCreator(
|
||||
}
|
||||
|
||||
if (isMe) {
|
||||
EditButton(channel, accountViewModel, nav)
|
||||
EditChannelButton { nav.nav(Route.ChannelMetadataEdit(channel.idHex)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,6 +226,6 @@ fun LeaveButtonIfFollowing(
|
||||
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel, accountViewModel)
|
||||
|
||||
if (isFollowing) {
|
||||
LeaveChatButton(channel, accountViewModel, nav)
|
||||
LeaveChannelButton { accountViewModel.unfollow(channel) }
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -37,6 +37,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.JoinChannelButton
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.profile_image
|
||||
@@ -53,7 +54,6 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.JoinChatButton
|
||||
|
||||
@Composable
|
||||
fun ShortPublicChatChannelHeader(
|
||||
@@ -147,6 +147,6 @@ fun JoinChatButtonIfNotAlreadyJoined(
|
||||
val isFollowing by observeUserIsFollowingChannel(accountViewModel.account, channel, accountViewModel)
|
||||
|
||||
if (!isFollowing) {
|
||||
JoinChatButton(channel, accountViewModel, nav)
|
||||
JoinChannelButton { accountViewModel.follow(channel) }
|
||||
}
|
||||
}
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
|
||||
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.join
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HalfHalfHorzModifier
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun JoinChatButton(
|
||||
channel: PublicChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
FilledTonalButton(
|
||||
modifier = HalfHalfHorzModifier,
|
||||
onClick = { accountViewModel.follow(channel) },
|
||||
contentPadding = ButtonPadding,
|
||||
) {
|
||||
Text(text = stringRes(Res.string.join))
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
|
||||
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.leave
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HalfHalfHorzModifier
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun LeaveChatButton(
|
||||
channel: PublicChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
FilledTonalButton(
|
||||
modifier = HalfHalfHorzModifier,
|
||||
onClick = { accountViewModel.unfollow(channel) },
|
||||
contentPadding = ButtonPadding,
|
||||
) {
|
||||
Text(text = stringRes(Res.string.leave))
|
||||
}
|
||||
}
|
||||
+2
-32
@@ -20,23 +20,11 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.chats.ui.MuteChannelButton
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.mute_notifications
|
||||
import com.vitorpamplona.amethyst.commons.resources.unmute_notifications
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
@@ -47,23 +35,5 @@ fun MuteChatButton(
|
||||
val mutedChats by accountViewModel.mutedPublicChatsFlow().collectAsStateWithLifecycle()
|
||||
val isMuted = channel.idHex in mutedChats
|
||||
|
||||
val label =
|
||||
stringRes(
|
||||
if (isMuted) Res.string.unmute_notifications else Res.string.mute_notifications,
|
||||
)
|
||||
|
||||
FilledTonalButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = { accountViewModel.toggleMutedPublicChat(channel.idHex) },
|
||||
contentPadding = ZeroPadding,
|
||||
) {
|
||||
Icon(
|
||||
symbol = if (isMuted) MaterialSymbols.NotificationsOff else MaterialSymbols.Notifications,
|
||||
contentDescription = label,
|
||||
modifier = Size20Modifier,
|
||||
)
|
||||
}
|
||||
MuteChannelButton(isMuted) { accountViewModel.toggleMutedPublicChat(channel.idHex) }
|
||||
}
|
||||
|
||||
+12
-41
@@ -20,25 +20,13 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.buzz.ui.BuzzPinDropdownItem
|
||||
import com.vitorpamplona.amethyst.commons.model.buzz.BuzzChannelStars
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_to_messages
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_pin
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_unpin
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove_from_messages
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.nip29RelayGroups.ui.RelayGroupMessagesDropdownItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip29RelayGroups.GroupId
|
||||
|
||||
@@ -55,21 +43,11 @@ fun BuzzPinDropdownItem(
|
||||
) {
|
||||
val stars = accountViewModel.account.buzzChannelStars
|
||||
val starred by stars.flow.collectAsStateWithLifecycle()
|
||||
val isStarred = groupId.id in starred
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.PushPin,
|
||||
contentDescription = null,
|
||||
tint = if (isStarred) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
},
|
||||
text = { Text(stringRes(if (isStarred) Res.string.buzz_unpin else Res.string.buzz_pin)) },
|
||||
onClick = {
|
||||
closeMenu()
|
||||
stars.toggle(groupId.id)
|
||||
},
|
||||
|
||||
BuzzPinDropdownItem(
|
||||
isPinned = groupId.id in starred,
|
||||
closeMenu = closeMenu,
|
||||
onToggle = { stars.toggle(groupId.id) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -88,18 +66,11 @@ fun RelayGroupMessagesDropdownItem(
|
||||
val joinedGroupIds by accountViewModel.account.relayGroupList.liveRelayGroupIds
|
||||
.collectAsStateWithLifecycle()
|
||||
val onMyList = channel.groupId in joinedGroupIds
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
symbol = if (onMyList) MaterialSymbols.VisibilityOff else MaterialSymbols.Add,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
},
|
||||
text = { Text(stringRes(if (onMyList) Res.string.remove_from_messages else Res.string.add_to_messages)) },
|
||||
onClick = {
|
||||
closeMenu()
|
||||
|
||||
RelayGroupMessagesDropdownItem(
|
||||
onMyList = onMyList,
|
||||
closeMenu = closeMenu,
|
||||
onToggle = {
|
||||
if (onMyList) {
|
||||
accountViewModel.removeRelayGroupFromMessages(channel)
|
||||
} else {
|
||||
|
||||
+6
-51
@@ -20,28 +20,9 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_code_hint
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_code_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_confirm
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.nip29RelayGroups.ui.RelayGroupInviteCodeDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
/**
|
||||
@@ -56,37 +37,11 @@ fun JoinRelayGroupDialog(
|
||||
onJoined: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var code by remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringRes(Res.string.relay_group_join_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringRes(Res.string.relay_group_join_code_hint))
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = { code = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringRes(Res.string.relay_group_join_code_label)) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = code.isNotBlank(),
|
||||
onClick = {
|
||||
accountViewModel.joinRelayGroup(channel, code.trim())
|
||||
onJoined()
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(Res.string.relay_group_join_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringRes(Res.string.cancel)) }
|
||||
RelayGroupInviteCodeDialog(
|
||||
onJoin = { code ->
|
||||
accountViewModel.joinRelayGroup(channel, code)
|
||||
onJoined()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
+4
-54
@@ -34,8 +34,6 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||
@@ -52,10 +50,10 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.DiscoverTab
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.nip23LongContent.ui.NewArticleButton
|
||||
import com.vitorpamplona.amethyst.commons.nip99Classifieds.ui.NewProductButton
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.discover_chat
|
||||
import com.vitorpamplona.amethyst.commons.resources.discover_community_v2
|
||||
@@ -64,8 +62,6 @@ import com.vitorpamplona.amethyst.commons.resources.discover_follows
|
||||
import com.vitorpamplona.amethyst.commons.resources.discover_live_v2
|
||||
import com.vitorpamplona.amethyst.commons.resources.discover_marketplace
|
||||
import com.vitorpamplona.amethyst.commons.resources.discover_reads
|
||||
import com.vitorpamplona.amethyst.commons.resources.new_long_form_post
|
||||
import com.vitorpamplona.amethyst.commons.resources.new_product
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedEmpty
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedError
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.LoadingFeed
|
||||
@@ -79,8 +75,6 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.FeedPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.TabRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.actions.CrossfadeIfEnabled
|
||||
import com.vitorpamplona.amethyst.ui.feeds.PrefetchLoadedFeedMedia
|
||||
@@ -273,11 +267,11 @@ private fun DiscoverPages(
|
||||
if (currentPage >= 0 && currentPage < feedTabs.size) {
|
||||
FabBottomBarPadded(nav) {
|
||||
if (feedTabs[currentPage].resource == Res.string.discover_marketplace) {
|
||||
NewProductButton(accountViewModel, nav)
|
||||
NewProductButton(nav)
|
||||
}
|
||||
|
||||
if (feedTabs[currentPage].resource == Res.string.discover_reads) {
|
||||
NewLongFormMarkdownButton(accountViewModel, nav)
|
||||
NewArticleButton(nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,50 +366,6 @@ private fun RenderDiscoverFeed(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewProductButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
nav.nav(Route.NewProduct())
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Add,
|
||||
contentDescription = stringRes(id = Res.string.new_product),
|
||||
modifier = Size26Modifier,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NewLongFormMarkdownButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
nav.nav(Route.NewLongFormPost())
|
||||
},
|
||||
modifier = Size55Modifier,
|
||||
shape = CircleShape,
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Add,
|
||||
contentDescription = stringRes(id = Res.string.new_long_form_post),
|
||||
modifier = Size26Modifier,
|
||||
tint = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderDiscoverFeed(
|
||||
feedContentState: FeedContentState,
|
||||
|
||||
+3
-27
@@ -22,39 +22,15 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedState
|
||||
import com.vitorpamplona.amethyst.commons.nipCCGeocaching.ui.rememberFoundCacheIds
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nipCCGeocaching.foundLog.GeocacheFoundLogEvent
|
||||
|
||||
/**
|
||||
* The addresses the signed-in user has already logged a find on, as `37516:pubkey:d` strings.
|
||||
*
|
||||
* Derived from the Finds feed rather than scanning the cache directly, so it tracks a new log
|
||||
* the moment the feed does — which is what makes a hunt's progress bar move as you walk it.
|
||||
*/
|
||||
/** The addresses the signed-in user has already logged a find on, from their Finds feed. */
|
||||
@Composable
|
||||
fun rememberMyFoundCacheIds(accountViewModel: AccountViewModel): Set<String> {
|
||||
val finds by accountViewModel.feedStates.geocacheFindsFeed.feedContent
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
return when (val state = finds) {
|
||||
is FeedState.Loaded -> rememberFoundCacheIds(state)
|
||||
else -> emptySet()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the loaded feed's own flow. [FeedState.Loaded] is handed out once and then mutated
|
||||
* through its inner flow, so reading `.value` here would freeze the set at whatever the feed held
|
||||
* when the hunt screen first composed — the progress bar would never move.
|
||||
*/
|
||||
@Composable
|
||||
private fun rememberFoundCacheIds(state: FeedState.Loaded): Set<String> {
|
||||
val loaded by state.feed.collectAsStateWithLifecycle()
|
||||
|
||||
return remember(loaded) {
|
||||
loaded.list.mapNotNullTo(mutableSetOf()) { (it.event as? GeocacheFoundLogEvent)?.geocacheId() }
|
||||
}
|
||||
return rememberFoundCacheIds(finds)
|
||||
}
|
||||
|
||||
+2
-1
@@ -27,6 +27,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.music.ui.NewMusicTrackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
@@ -87,7 +88,7 @@ fun MusicTracksScreen(
|
||||
},
|
||||
floatingButton = {
|
||||
FabBottomBarPadded(nav) {
|
||||
NewMusicTrackButton(accountViewModel, nav)
|
||||
NewMusicTrackButton(nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.nip99Classifieds.ui.NewProductButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
@@ -75,7 +76,7 @@ fun ProductsScreen(
|
||||
},
|
||||
floatingButton = {
|
||||
FabBottomBarPadded(nav) {
|
||||
NewProductButton(accountViewModel, nav)
|
||||
NewProductButton(nav)
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel,
|
||||
|
||||
+2
-2
@@ -67,6 +67,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.profile.ui.RelaysTabHeader
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.profile.UserProfileFilterAssemblerSubscription
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.gallery
|
||||
@@ -109,7 +110,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.mutual.dal.UserProf
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.TabNotesNewThreads
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.newthreads.dal.UserProfileNewThreadsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.pinnedNotes.dal.UserProfilePinnedNotesFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.RelaysTabHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays.TabRelays
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.ReportsTabHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.reports.TabReports
|
||||
@@ -697,7 +697,7 @@ private fun CreateAndRenderTabs(
|
||||
ProfileTab.Bookmarks -> BookmarkTabHeader(baseUser, accountViewModel)
|
||||
ProfileTab.FollowedTags -> FollowedTagsTabHeader(baseUser, accountViewModel)
|
||||
ProfileTab.Reports -> ReportsTabHeader(baseUser, reportsFeedViewModel, accountViewModel)
|
||||
ProfileTab.Relays -> RelaysTabHeader(baseUser, accountViewModel)
|
||||
ProfileTab.Relays -> RelaysTabHeader()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
+8
-195
@@ -20,210 +20,23 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.report
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.block_only
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_additional_reason_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_additional_reason_placeholder
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_block_hide_user_btn
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_blocking_a_user
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_illegal
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_impersonation
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_nudity
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_post_report_btn
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_profanity
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_reminder_public
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_report_btn
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_select_reason_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_select_reason_placeholder
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_spam
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_malware
|
||||
import com.vitorpamplona.amethyst.commons.resources.violence
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.LightRedColor
|
||||
import com.vitorpamplona.amethyst.commons.nip56Reports.ui.ReportDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReportNoteDialog(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val reportTypes =
|
||||
listOf(
|
||||
Pair(ReportType.SPAM, stringRes(Res.string.report_dialog_spam)),
|
||||
Pair(ReportType.PROFANITY, stringRes(Res.string.report_dialog_profanity)),
|
||||
Pair(ReportType.IMPERSONATION, stringRes(Res.string.report_dialog_impersonation)),
|
||||
Pair(ReportType.NUDITY, stringRes(Res.string.report_dialog_nudity)),
|
||||
Pair(ReportType.ILLEGAL, stringRes(Res.string.report_dialog_illegal)),
|
||||
Pair(ReportType.MALWARE, stringRes(Res.string.report_malware)),
|
||||
Pair(ReportType.VIOLENCE, stringRes(Res.string.violence)),
|
||||
)
|
||||
|
||||
val reasonOptions = remember { reportTypes.map { TitleExplainer(it.second) }.toImmutableList() }
|
||||
var additionalReason by remember { mutableStateOf("") }
|
||||
var selectedReason by remember { mutableIntStateOf(-1) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringRes(id = Res.string.report_dialog_title)) },
|
||||
navigationIcon = { IconButton(onClick = onDismiss) { ArrowBackIcon() } },
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.padding(16.dp, pad.calculateTopPadding(), 16.dp, pad.calculateBottomPadding()),
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
SpacerH16()
|
||||
SectionHeader(text = stringRes(id = Res.string.block_only))
|
||||
SpacerH16()
|
||||
Text(
|
||||
text = stringRes(Res.string.report_dialog_blocking_a_user),
|
||||
)
|
||||
SpacerH16()
|
||||
ActionButton(
|
||||
text = stringRes(Res.string.report_dialog_block_hide_user_btn),
|
||||
icon = MaterialSymbols.Block,
|
||||
onClick = {
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
SpacerH16()
|
||||
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.onSurface, thickness = DividerThickness)
|
||||
|
||||
SpacerH16()
|
||||
SectionHeader(text = stringRes(Res.string.report_dialog_report_btn))
|
||||
SpacerH16()
|
||||
Text(stringRes(Res.string.report_dialog_reminder_public))
|
||||
SpacerH16()
|
||||
TextSpinner(
|
||||
label = stringRes(Res.string.report_dialog_select_reason_label),
|
||||
placeholder = stringRes(Res.string.report_dialog_select_reason_placeholder),
|
||||
options = reasonOptions,
|
||||
onSelect = { selectedReason = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SpacerH16()
|
||||
OutlinedTextField(
|
||||
value = additionalReason,
|
||||
onValueChange = { additionalReason = it },
|
||||
placeholder = {
|
||||
Text(text = stringRes(Res.string.report_dialog_additional_reason_placeholder))
|
||||
},
|
||||
label = { Text(stringRes(Res.string.report_dialog_additional_reason_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SpacerH16()
|
||||
|
||||
ActionButton(
|
||||
text = stringRes(Res.string.report_dialog_post_report_btn),
|
||||
icon = MaterialSymbols.Report,
|
||||
enabled = selectedReason in 0..reportTypes.lastIndex,
|
||||
onClick = {
|
||||
accountViewModel.report(
|
||||
note,
|
||||
reportTypes[selectedReason].first,
|
||||
additionalReason,
|
||||
)
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun SpacerH16() = Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(text: String) =
|
||||
Text(
|
||||
text = text,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = 18.sp,
|
||||
ReportDialog(
|
||||
onBlock = { note.author?.let { accountViewModel.hide(it) } },
|
||||
onReport = { reason, additionalReason ->
|
||||
accountViewModel.report(note, reason, additionalReason)
|
||||
note.author?.let { accountViewModel.hide(it) }
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ActionButton(
|
||||
text: String,
|
||||
icon: MaterialSymbol,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
) = Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = LightRedColor),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
symbol = icon,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = text, color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ type (`LazyListState`, `TextFieldValue`, `TextFieldState`) goes to `commonsUI`.
|
||||
| `napplet` | mixed | Protocol/permission logic here; `NappletWebContract` (serves the shell/shim from `composeResources`) in `commonsUI`. |
|
||||
| `favorites`, `nip30CustomEmojis`, `nip34Git`, `nip85TrustedAssertions`, `nip53LiveActivities` | mixed | Logic here; each feature's `ui/` (or the flat `FavoriteAppIcon`, `EmojiSuggestionState`) in `commonsUI`. |
|
||||
| `chats` | yes | Composables shared by every chat kind (DMs, public chats, relay groups, concord): unread badge, divisors, system messages, author line, send button, reply toggle, new-conversation screen. Chat *models* are in `model/chats`. |
|
||||
| `nip17Dm`, `nip23LongContent`, `nip28PublicChat`, `nip29RelayGroups`, `nip51Lists`, `nip52Calendar`, `nip56Reports`, `nip72ModCommunities`, `nipC0CodeSnippets`, `nipCCGeocaching`, `birdstar`, `buzz`, `cashu`, `concord`, `ephemChat`, `music`, `mediaServers`, `browser`, `profile`, `napplet` | yes / mixed | Single-feature UI under `<feature>/ui` (named after the `quartz` package, or its concern name when `quartz` has none). Where the package also has logic (`cashu`, `browser`, `napplet`, `profile`) that part stays here; the `buzz` and `concord` models are in `model/buzz` and `model/concord`. |
|
||||
| `nip17Dm`, `nip23LongContent`, `nip28PublicChat`, `nip29RelayGroups`, `nip32Labeling`, `nip43RelayMembers`, `nip51Lists`, `nip52Calendar`, `nip56Reports`, `nip72ModCommunities`, `nip73ExternalIds`, `nip99Classifieds`, `nipC0CodeSnippets`, `nipCCGeocaching`, `birdstar`, `nipsOnNostr`, `buzz`, `cashu`, `concord`, `music`, `mediaServers`, `browser`, `profile`, `napplet` | yes / mixed | Single-feature UI under `<feature>/ui` (named after the `quartz` package, or its concern name when `quartz` has none). Where the package also has logic (`cashu`, `browser`, `napplet`, `profile`) that part stays here; the `buzz` and `concord` models are in `model/buzz` and `model/concord`. |
|
||||
|
||||
### Mixed (documented debt — see §4)
|
||||
| Package | UI? | Purpose |
|
||||
|
||||
+32
-14
@@ -18,27 +18,45 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ephemChat.ui
|
||||
package com.vitorpamplona.amethyst.commons.buzz.ui
|
||||
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.join
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_pin
|
||||
import com.vitorpamplona.amethyst.commons.resources.buzz_unpin
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
/**
|
||||
* Pin/Unpin overflow-menu item for a Buzz channel. [isPinned] picks the label and tints the pin;
|
||||
* clicking closes the menu, then calls [onToggle].
|
||||
*/
|
||||
@Composable
|
||||
fun JoinButton(
|
||||
onPost: () -> Unit = {},
|
||||
isActive: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
fun BuzzPinDropdownItem(
|
||||
isPinned: Boolean,
|
||||
closeMenu: () -> Unit,
|
||||
onToggle: () -> Unit,
|
||||
) {
|
||||
Button(
|
||||
enabled = isActive,
|
||||
modifier = modifier,
|
||||
onClick = onPost,
|
||||
) {
|
||||
Text(text = stringRes(Res.string.join))
|
||||
}
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.PushPin,
|
||||
contentDescription = null,
|
||||
tint = if (isPinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
},
|
||||
text = { Text(stringRes(if (isPinned) Res.string.buzz_unpin else Res.string.buzz_pin)) },
|
||||
onClick = {
|
||||
closeMenu()
|
||||
onToggle()
|
||||
},
|
||||
)
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.chats.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.join
|
||||
import com.vitorpamplona.amethyst.commons.resources.leave
|
||||
import com.vitorpamplona.amethyst.commons.resources.mute_notifications
|
||||
import com.vitorpamplona.amethyst.commons.resources.unmute_notifications
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HalfHalfHorzModifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding
|
||||
|
||||
// Membership actions shared by every channel header (NIP-28 public chats and ephemeral chats).
|
||||
// They take the action as a callback so the header that knows the account decides what
|
||||
// joining, leaving or muting means.
|
||||
|
||||
@Composable
|
||||
fun JoinChannelButton(onClick: () -> Unit) {
|
||||
FilledTonalButton(
|
||||
modifier = HalfHalfHorzModifier,
|
||||
onClick = onClick,
|
||||
contentPadding = ButtonPadding,
|
||||
) {
|
||||
Text(text = stringRes(Res.string.join))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LeaveChannelButton(onClick: () -> Unit) {
|
||||
FilledTonalButton(
|
||||
modifier = HalfHalfHorzModifier,
|
||||
onClick = onClick,
|
||||
contentPadding = ButtonPadding,
|
||||
) {
|
||||
Text(text = stringRes(Res.string.leave))
|
||||
}
|
||||
}
|
||||
|
||||
/** Bell toggle for a channel's notifications; [isMuted] picks the icon and label. */
|
||||
@Composable
|
||||
fun MuteChannelButton(
|
||||
isMuted: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
) {
|
||||
val label =
|
||||
stringRes(
|
||||
if (isMuted) Res.string.unmute_notifications else Res.string.mute_notifications,
|
||||
)
|
||||
|
||||
FilledTonalButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = onToggle,
|
||||
contentPadding = ZeroPadding,
|
||||
) {
|
||||
Icon(
|
||||
symbol = if (isMuted) MaterialSymbols.NotificationsOff else MaterialSymbols.Notifications,
|
||||
contentDescription = label,
|
||||
modifier = Size20Modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
-8
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.music
|
||||
package com.vitorpamplona.amethyst.commons.music.ui
|
||||
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
@@ -36,14 +36,9 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||
|
||||
@Composable
|
||||
fun NewMusicTrackButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
fun NewMusicTrackButton(nav: INav) {
|
||||
FloatingActionButton(
|
||||
onClick = { nav.nav(Route.NewMusicTrack()) },
|
||||
modifier = Size55Modifier,
|
||||
@@ -63,6 +58,6 @@ fun NewMusicTrackButton(
|
||||
@Composable
|
||||
private fun NewMusicTrackButtonPreview() {
|
||||
ThemeComparisonRow {
|
||||
NewMusicTrackButton(accountViewModel = mockAccountViewModel(), nav = EmptyNav())
|
||||
NewMusicTrackButton(nav = EmptyNav())
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip17Dm.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.messages_new_message_subject
|
||||
import com.vitorpamplona.amethyst.commons.resources.messages_new_message_subject_caption
|
||||
import com.vitorpamplona.amethyst.commons.resources.messages_new_subject_message
|
||||
import com.vitorpamplona.amethyst.commons.resources.messages_new_subject_message_placeholder
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.buttons.CloseButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.buttons.PostButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
|
||||
/**
|
||||
* Rename a private group chat (NIP-17): edit the subject and write the message that carries the
|
||||
* change. [onPost] receives the subject (possibly blank) and the message, then the dialog closes.
|
||||
*/
|
||||
@Composable
|
||||
fun ChatroomSubjectDialog(
|
||||
initialSubject: String,
|
||||
onPost: (subject: String, message: String) -> Unit,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
Dialog(
|
||||
onDismissRequest = { onClose() },
|
||||
properties =
|
||||
DialogProperties(
|
||||
dismissOnClickOutside = false,
|
||||
),
|
||||
) {
|
||||
Surface {
|
||||
val groupName = remember { mutableStateOf(initialSubject) }
|
||||
val message = remember { mutableStateOf("") }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(10.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CloseButton(onPress = { onClose() })
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
onPost(groupName.value, message.value)
|
||||
onClose()
|
||||
},
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(Res.string.messages_new_message_subject)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = groupName.value,
|
||||
onValueChange = { groupName.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(Res.string.messages_new_message_subject_caption),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = stringRes(Res.string.messages_new_subject_message)) },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
value = message.value,
|
||||
onValueChange = { message.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringRes(Res.string.messages_new_subject_message_placeholder),
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.Sentences,
|
||||
),
|
||||
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
maxLines = 10,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-11
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
|
||||
package com.vitorpamplona.amethyst.commons.nip28PublicChat.ui
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -28,27 +28,20 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.edits_the_channel_metadata
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ZeroPadding
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
/** Opens the channel-metadata editor; shown to the channel's owner in the long header. */
|
||||
@Composable
|
||||
fun EditButton(
|
||||
channel: PublicChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
fun EditChannelButton(onClick: () -> Unit) {
|
||||
FilledTonalButton(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(horizontal = 3.dp)
|
||||
.width(50.dp),
|
||||
onClick = { nav.nav(Route.ChannelMetadataEdit(channel.idHex)) },
|
||||
onClick = onClick,
|
||||
contentPadding = ZeroPadding,
|
||||
) {
|
||||
Icon(
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip29RelayGroups.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_code_hint
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_code_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_confirm
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_group_join_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
/**
|
||||
* Invite-code prompt for joining a closed (invite-only) NIP-29 group. Confirm hands the
|
||||
* trimmed code to [onJoin] and then closes the dialog through [onDismiss].
|
||||
*/
|
||||
@Composable
|
||||
fun RelayGroupInviteCodeDialog(
|
||||
onJoin: (code: String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var code by remember { mutableStateOf("") }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringRes(Res.string.relay_group_join_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringRes(Res.string.relay_group_join_code_hint))
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = { code = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringRes(Res.string.relay_group_join_code_label)) },
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = code.isNotBlank(),
|
||||
onClick = {
|
||||
onJoin(code.trim())
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(Res.string.relay_group_join_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) { Text(stringRes(Res.string.cancel)) }
|
||||
},
|
||||
)
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip29RelayGroups.ui
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_to_messages
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove_from_messages
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
/**
|
||||
* Add/Remove overflow-menu item for whether a relay group shows in Messages. [onMyList] picks
|
||||
* the label and icon; clicking closes the menu, then calls [onToggle].
|
||||
*/
|
||||
@Composable
|
||||
fun RelayGroupMessagesDropdownItem(
|
||||
onMyList: Boolean,
|
||||
closeMenu: () -> Unit,
|
||||
onToggle: () -> Unit,
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
symbol = if (onMyList) MaterialSymbols.VisibilityOff else MaterialSymbols.Add,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
},
|
||||
text = { Text(stringRes(if (onMyList) Res.string.remove_from_messages else Res.string.add_to_messages)) },
|
||||
onClick = {
|
||||
closeMenu()
|
||||
onToggle()
|
||||
},
|
||||
)
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip32Labeling.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_hashtag_label_confirm
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_hashtag_label_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_hashtag_label_field
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_hashtag_label_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.cancel
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
/**
|
||||
* Asks for a hashtag to label a post with (NIP-32, `#t` namespace). The typed text is
|
||||
* normalized the way hashtag feeds store tags: leading `#` and whitespace stripped,
|
||||
* lowercased. Confirm hands that value to [onConfirm] and closes through [onDismiss].
|
||||
*/
|
||||
@Composable
|
||||
fun HashtagLabelDialog(
|
||||
onConfirm: (hashtag: String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var hashtag by remember { mutableStateOf("") }
|
||||
|
||||
// Strip the leading '#', drop whitespace and lowercase so the stored label matches the
|
||||
// hashtag-feed convention. A blank result disables the confirm button.
|
||||
val sanitized =
|
||||
hashtag
|
||||
.trim()
|
||||
.removePrefix("#")
|
||||
.filterNot { it.isWhitespace() }
|
||||
.lowercase()
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringRes(Res.string.add_hashtag_label_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(stringRes(Res.string.add_hashtag_label_explainer))
|
||||
OutlinedTextField(
|
||||
value = hashtag,
|
||||
onValueChange = { hashtag = it },
|
||||
singleLine = true,
|
||||
label = { Text(stringRes(Res.string.add_hashtag_label_field)) },
|
||||
prefix = { Text("#") },
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
imeAction = ImeAction.Done,
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = sanitized.isNotEmpty(),
|
||||
onClick = {
|
||||
onConfirm(sanitized)
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text(stringRes(Res.string.add_hashtag_label_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringRes(Res.string.cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip34Git.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_status_close
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_status_mark_merged
|
||||
import com.vitorpamplona.amethyst.commons.resources.git_status_reopen
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
/** Compact sizing shared by the small action buttons on git cards. */
|
||||
val CompactButtonHeight = Modifier.height(32.dp)
|
||||
val CompactButtonPadding = PaddingValues(horizontal = 14.dp, vertical = 4.dp)
|
||||
|
||||
/**
|
||||
* NIP-34 status buttons for an issue, patch or pull request: "Reopen" when it is
|
||||
* [closedOrApplied], otherwise "Mark merged" (patches and PRs only, [isPatchOrPr]) and "Close".
|
||||
* The host decides who may see them and publishes the status event.
|
||||
*/
|
||||
@Composable
|
||||
fun GitStatusButtons(
|
||||
closedOrApplied: Boolean,
|
||||
isPatchOrPr: Boolean,
|
||||
onReopen: () -> Unit,
|
||||
onMarkMerged: () -> Unit,
|
||||
onClose: () -> Unit,
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
if (closedOrApplied) {
|
||||
FilledTonalButton(
|
||||
onClick = onReopen,
|
||||
modifier = CompactButtonHeight,
|
||||
contentPadding = CompactButtonPadding,
|
||||
) {
|
||||
Icon(MaterialSymbols.RadioButtonChecked, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Text(stringRes(Res.string.git_status_reopen), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
} else {
|
||||
if (isPatchOrPr) {
|
||||
FilledTonalButton(
|
||||
onClick = onMarkMerged,
|
||||
modifier = CompactButtonHeight,
|
||||
contentPadding = CompactButtonPadding,
|
||||
) {
|
||||
Icon(MaterialSymbols.Check, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Text(stringRes(Res.string.git_status_mark_merged), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = onClose,
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = MaterialTheme.colorScheme.error),
|
||||
modifier = CompactButtonHeight,
|
||||
contentPadding = CompactButtonPadding,
|
||||
) {
|
||||
Icon(MaterialSymbols.Cancel, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Text(stringRes(Res.string.git_status_close), style = MaterialTheme.typography.labelMedium, modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+224
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip43RelayMembers.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_join_request
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_leave_request
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_member_added
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_member_removed
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_members_added
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_members_count
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_members_removed
|
||||
import com.vitorpamplona.amethyst.commons.resources.relay_membership_list
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.quartz.nip43RelayMembers.addMember.RelayAddMemberEvent
|
||||
import com.vitorpamplona.quartz.nip43RelayMembers.list.RelayMembershipListEvent
|
||||
import com.vitorpamplona.quartz.nip43RelayMembers.removeMember.RelayRemoveMemberEvent
|
||||
|
||||
/** NIP-43 kind 13534: the relay's current member list, shown as a count. */
|
||||
@Composable
|
||||
fun RelayMembershipListCard(event: RelayMembershipListEvent) {
|
||||
val memberCount = remember(event) { event.members().size }
|
||||
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.People,
|
||||
title = stringRes(Res.string.relay_membership_list),
|
||||
subtitle = stringRes(Res.string.relay_members_count, memberCount),
|
||||
)
|
||||
}
|
||||
|
||||
/** NIP-43 kind 8000: members the relay added. */
|
||||
@Composable
|
||||
fun RelayAddMemberCard(event: RelayAddMemberEvent) {
|
||||
val memberKeys = remember(event) { event.memberPubKeys() }
|
||||
val title =
|
||||
if (memberKeys.size == 1) {
|
||||
stringRes(Res.string.relay_member_added)
|
||||
} else {
|
||||
stringRes(Res.string.relay_members_added, memberKeys.size)
|
||||
}
|
||||
val subtitle = remember(memberKeys) { memberKeys.joinToString(", ") { it.take(16) + "..." } }
|
||||
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonAdd,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
)
|
||||
}
|
||||
|
||||
/** NIP-43 kind 8001: members the relay removed. */
|
||||
@Composable
|
||||
fun RelayRemoveMemberCard(event: RelayRemoveMemberEvent) {
|
||||
val memberKeys = remember(event) { event.memberPubKeys() }
|
||||
val title =
|
||||
if (memberKeys.size == 1) {
|
||||
stringRes(Res.string.relay_member_removed)
|
||||
} else {
|
||||
stringRes(Res.string.relay_members_removed, memberKeys.size)
|
||||
}
|
||||
val subtitle = remember(memberKeys) { memberKeys.joinToString(", ") { it.take(16) + "..." } }
|
||||
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonRemove,
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
)
|
||||
}
|
||||
|
||||
/** NIP-43 kind 28934: a request to join the relay. */
|
||||
@Composable
|
||||
fun RelayJoinRequestCard() {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonAdd,
|
||||
title = stringRes(Res.string.relay_join_request),
|
||||
subtitle = null,
|
||||
)
|
||||
}
|
||||
|
||||
/** NIP-43 kind 28936: a request to leave the relay. */
|
||||
@Composable
|
||||
fun RelayLeaveRequestCard() {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.AutoMirrored.ExitToApp,
|
||||
title = stringRes(Res.string.relay_leave_request),
|
||||
subtitle = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RelayMemberEventCard(
|
||||
icon: MaterialSymbol,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(8.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
symbol = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = title,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
subtitle?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayMembershipListCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.People,
|
||||
title = "Relay membership list",
|
||||
subtitle = "42 members",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayAddMemberCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonAdd,
|
||||
title = "Member added to relay",
|
||||
subtitle = "a1b2c3d4e5f6a7b8...",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayRemoveMemberCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonRemove,
|
||||
title = "Member removed from relay",
|
||||
subtitle = "a1b2c3d4e5f6a7b8...",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayJoinRequestCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.PersonAdd,
|
||||
title = "Relay join request",
|
||||
subtitle = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun RelayLeaveRequestCardPreview() {
|
||||
ThemeComparisonColumn {
|
||||
RelayMemberEventCard(
|
||||
icon = MaterialSymbols.AutoMirrored.ExitToApp,
|
||||
title = "Relay leave request",
|
||||
subtitle = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip52Calendar.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_going
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_maybe
|
||||
import com.vitorpamplona.amethyst.commons.resources.calendar_rsvp_not_going
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.tags.RSVPStatusTag
|
||||
|
||||
/**
|
||||
* Going / Maybe / Not going buttons for a NIP-52 calendar event. The one matching
|
||||
* [currentStatus] is filled in its status colour; the others are outlined. [onSelect] publishes
|
||||
* the chosen status.
|
||||
*/
|
||||
@Composable
|
||||
fun CalendarRsvpButtons(
|
||||
currentStatus: RSVPStatusTag.STATUS?,
|
||||
onSelect: (RSVPStatusTag.STATUS) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 12.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
RsvpButton(
|
||||
label = stringRes(Res.string.calendar_rsvp_going),
|
||||
status = RSVPStatusTag.STATUS.ACCEPTED,
|
||||
currentStatus = currentStatus,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onSelect,
|
||||
)
|
||||
RsvpButton(
|
||||
label = stringRes(Res.string.calendar_rsvp_maybe),
|
||||
status = RSVPStatusTag.STATUS.TENTATIVE,
|
||||
currentStatus = currentStatus,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onSelect,
|
||||
)
|
||||
RsvpButton(
|
||||
label = stringRes(Res.string.calendar_rsvp_not_going),
|
||||
status = RSVPStatusTag.STATUS.DECLINED,
|
||||
currentStatus = currentStatus,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = onSelect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RsvpButton(
|
||||
label: String,
|
||||
status: RSVPStatusTag.STATUS,
|
||||
currentStatus: RSVPStatusTag.STATUS?,
|
||||
modifier: Modifier,
|
||||
onClick: (RSVPStatusTag.STATUS) -> Unit,
|
||||
) {
|
||||
val selected = status == currentStatus
|
||||
if (selected) {
|
||||
FilledTonalButton(
|
||||
onClick = { onClick(status) },
|
||||
modifier = modifier,
|
||||
contentPadding = RsvpButtonPadding,
|
||||
colors =
|
||||
ButtonDefaults.filledTonalButtonColors(
|
||||
containerColor = colorFor(status),
|
||||
contentColor = Color.White,
|
||||
),
|
||||
) {
|
||||
RsvpButtonLabel(label)
|
||||
}
|
||||
} else {
|
||||
OutlinedButton(
|
||||
onClick = { onClick(status) },
|
||||
modifier = modifier,
|
||||
contentPadding = RsvpButtonPadding,
|
||||
) {
|
||||
RsvpButtonLabel(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val RsvpButtonPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
|
||||
|
||||
@Composable
|
||||
private fun RsvpButtonLabel(label: String) {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
maxLines = 1,
|
||||
softWrap = false,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun colorFor(status: RSVPStatusTag.STATUS) =
|
||||
when (status) {
|
||||
RSVPStatusTag.STATUS.ACCEPTED -> MaterialTheme.colorScheme.primary
|
||||
RSVPStatusTag.STATUS.TENTATIVE -> MaterialTheme.colorScheme.tertiary
|
||||
RSVPStatusTag.STATUS.DECLINED -> MaterialTheme.colorScheme.error
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip56Reports.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.block_only
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_additional_reason_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_additional_reason_placeholder
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_block_hide_user_btn
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_blocking_a_user
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_illegal
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_impersonation
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_nudity
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_post_report_btn
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_profanity
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_reminder_public
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_report_btn
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_select_reason_label
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_select_reason_placeholder
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_spam
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_dialog_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.report_malware
|
||||
import com.vitorpamplona.amethyst.commons.resources.violence
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.LightRedColor
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
/**
|
||||
* Block-or-report dialog for a post. [onBlock] hides the author; [onReport] files a public
|
||||
* NIP-56 report with the chosen reason and optional text (the host also hides the author).
|
||||
* Both close the dialog through [onDismiss] afterwards.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ReportDialog(
|
||||
onBlock: () -> Unit,
|
||||
onReport: (reason: ReportType, additionalReason: String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val reportTypes =
|
||||
listOf(
|
||||
Pair(ReportType.SPAM, stringRes(Res.string.report_dialog_spam)),
|
||||
Pair(ReportType.PROFANITY, stringRes(Res.string.report_dialog_profanity)),
|
||||
Pair(ReportType.IMPERSONATION, stringRes(Res.string.report_dialog_impersonation)),
|
||||
Pair(ReportType.NUDITY, stringRes(Res.string.report_dialog_nudity)),
|
||||
Pair(ReportType.ILLEGAL, stringRes(Res.string.report_dialog_illegal)),
|
||||
Pair(ReportType.MALWARE, stringRes(Res.string.report_malware)),
|
||||
Pair(ReportType.VIOLENCE, stringRes(Res.string.violence)),
|
||||
)
|
||||
|
||||
val reasonOptions = remember { reportTypes.map { TitleExplainer(it.second) }.toImmutableList() }
|
||||
var additionalReason by remember { mutableStateOf("") }
|
||||
var selectedReason by remember { mutableIntStateOf(-1) }
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(text = stringRes(id = Res.string.report_dialog_title)) },
|
||||
navigationIcon = { IconButton(onClick = onDismiss) { ArrowBackIcon() } },
|
||||
colors =
|
||||
TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
Column(
|
||||
modifier =
|
||||
Modifier.padding(16.dp, pad.calculateTopPadding(), 16.dp, pad.calculateBottomPadding()),
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
SpacerH16()
|
||||
SectionHeader(text = stringRes(id = Res.string.block_only))
|
||||
SpacerH16()
|
||||
Text(
|
||||
text = stringRes(Res.string.report_dialog_blocking_a_user),
|
||||
)
|
||||
SpacerH16()
|
||||
ActionButton(
|
||||
text = stringRes(Res.string.report_dialog_block_hide_user_btn),
|
||||
icon = MaterialSymbols.Block,
|
||||
onClick = {
|
||||
onBlock()
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
SpacerH16()
|
||||
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.onSurface, thickness = DividerThickness)
|
||||
|
||||
SpacerH16()
|
||||
SectionHeader(text = stringRes(Res.string.report_dialog_report_btn))
|
||||
SpacerH16()
|
||||
Text(stringRes(Res.string.report_dialog_reminder_public))
|
||||
SpacerH16()
|
||||
TextSpinner(
|
||||
label = stringRes(Res.string.report_dialog_select_reason_label),
|
||||
placeholder = stringRes(Res.string.report_dialog_select_reason_placeholder),
|
||||
options = reasonOptions,
|
||||
onSelect = { selectedReason = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SpacerH16()
|
||||
OutlinedTextField(
|
||||
value = additionalReason,
|
||||
onValueChange = { additionalReason = it },
|
||||
placeholder = {
|
||||
Text(text = stringRes(Res.string.report_dialog_additional_reason_placeholder))
|
||||
},
|
||||
label = { Text(stringRes(Res.string.report_dialog_additional_reason_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SpacerH16()
|
||||
|
||||
ActionButton(
|
||||
text = stringRes(Res.string.report_dialog_post_report_btn),
|
||||
icon = MaterialSymbols.Report,
|
||||
enabled = selectedReason in 0..reportTypes.lastIndex,
|
||||
onClick = {
|
||||
onReport(reportTypes[selectedReason].first, additionalReason)
|
||||
onDismiss()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable private fun SpacerH16() = Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@Composable
|
||||
private fun SectionHeader(text: String) =
|
||||
Text(
|
||||
text = text,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = 18.sp,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun ActionButton(
|
||||
text: String,
|
||||
icon: MaterialSymbol,
|
||||
enabled: Boolean = true,
|
||||
onClick: () -> Unit,
|
||||
) = Button(
|
||||
onClick = onClick,
|
||||
enabled = enabled,
|
||||
colors = ButtonDefaults.buttonColors(containerColor = LightRedColor),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Icon(
|
||||
symbol = icon,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(text = text, color = Color.White)
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nip73ExternalIds.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.LinkInteractionListener
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.hashtag_exclusive
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
|
||||
|
||||
/** Header card for a NIP-22 comment scoped to a hashtag (NIP-73 `#t` external id): tag icon + the tagged topic as a link. */
|
||||
@Composable
|
||||
fun HashtagExternalIdCard(
|
||||
topic: String,
|
||||
linkInteractionListener: LinkInteractionListener,
|
||||
) {
|
||||
Row(modifier = MaterialTheme.colorScheme.replyModifier.padding(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Tag,
|
||||
contentDescription = stringRes(id = Res.string.hashtag_exclusive),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
|
||||
Spacer(StdHorzSpacer)
|
||||
|
||||
Text(
|
||||
text =
|
||||
buildAnnotatedString {
|
||||
withLink(
|
||||
LinkAnnotation.Clickable("hashtag", null, linkInteractionListener),
|
||||
) {
|
||||
append(topic)
|
||||
}
|
||||
},
|
||||
style =
|
||||
LocalTextStyle.current.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-6
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.products
|
||||
package com.vitorpamplona.amethyst.commons.nip99Classifieds.ui
|
||||
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
@@ -33,13 +33,9 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size26Modifier
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun NewProductButton(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
fun NewProductButton(nav: INav) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
nav.nav(Route.NewProduct())
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nipCCGeocaching.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedState
|
||||
import com.vitorpamplona.quartz.nipCCGeocaching.foundLog.GeocacheFoundLogEvent
|
||||
|
||||
/**
|
||||
* The addresses logged as found in a Finds feed ([feedState]), as `37516:pubkey:d` strings.
|
||||
* Empty until the feed has loaded.
|
||||
*
|
||||
* Deriving it from the feed rather than scanning the cache directly means it tracks a new log
|
||||
* the moment the feed does, which is what makes a hunt's progress bar move as you walk it.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberFoundCacheIds(feedState: FeedState): Set<String> =
|
||||
when (feedState) {
|
||||
is FeedState.Loaded -> rememberLoadedFoundCacheIds(feedState)
|
||||
else -> emptySet()
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects the loaded feed's own flow. [FeedState.Loaded] is handed out once and then mutated
|
||||
* through its inner flow, so reading `.value` here would freeze the set at whatever the feed held
|
||||
* when the hunt screen first composed — the progress bar would never move.
|
||||
*/
|
||||
@Composable
|
||||
private fun rememberLoadedFoundCacheIds(state: FeedState.Loaded): Set<String> {
|
||||
val loaded by state.feed.collectAsStateWithLifecycle()
|
||||
|
||||
return remember(loaded) {
|
||||
loaded.list.mapNotNullTo(mutableSetOf()) { (it.event as? GeocacheFoundLogEvent)?.geocacheId() }
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.nipsOnNostr.ui
|
||||
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.kinds
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.SpacedBy5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.subtleBorder
|
||||
import com.vitorpamplona.quartz.experimental.nipsOnNostr.NipTextEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.kinds.kinds
|
||||
|
||||
/** Card for a NIP published on Nostr ("NIPs on Nostr"): title, summary or opening text, and the kinds it defines. */
|
||||
@Composable
|
||||
fun NipCard(noteEvent: NipTextEvent) {
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val kinds = remember(noteEvent) { noteEvent.kinds() }
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(top = Size5dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colorScheme.subtleBorder,
|
||||
QuoteBorder,
|
||||
),
|
||||
verticalArrangement = SpacedBy5dp,
|
||||
) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content },
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (kinds.isNotEmpty()) {
|
||||
FlowRow(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
horizontalArrangement = SpacedBy5dp,
|
||||
verticalArrangement = SpacedBy5dp,
|
||||
itemVerticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringRes(Res.string.kinds),
|
||||
)
|
||||
kinds.forEach {
|
||||
NoPaddingSuggestionChip(
|
||||
label = it.toString(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun NipCardPreview() {
|
||||
val event =
|
||||
NipTextEvent(
|
||||
id = "eb2b05394ff0014bb6a79c2eacfd1c80696821592f4dbf86c950c4bf16614aa0",
|
||||
pubKey = "460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||
createdAt = 1767978398,
|
||||
content = "Trusted Translations\n--------------------\n\n`draft` `optional`\n\nThis NIP allows anyone to post a translation for any event on a `kind:76`.\n\n```js\n{\n \"kind\": 76,\n \"tags\": [\n [\"e\", \"\u003coriginal_event_id\u003e\", \"\u003crelay\u003e\"]\n [\"k\", \"\u003coriginal_event_kind\u003e\"]\n [\"l\", \"\u003ctranslated_to_language_country\u003e\"], // ISO 639-1: \"en\", \"es\", ...\n [\"l\", \"\u003ctranslated_to_language_code\u003e\"], // ISO 639-1: \"en-us\", \"en-br\"\n [\"s\", \"title\", \"translated title tag\"]\n [\"s\", \"summary\", \"translated summary tag\"]\n ],\n \"content\": \"this is a translated version of the original content\",\n // ...other fields\n}\n```\n\n`e` tag points to the event being translated, `k` tag points to the kind of that event.\n\n`l` tags define the language this was translated to in lowercase codes as defined by ISO 639-1. \n\n`s` tags are the translations for tags in the original event. \n\n`.content` contains the translation of the original `.content`\n\nClients SHOULD request translations by `e` and `l` tags spoken by their user. For every tag being rendered, Clients SHOULD look for their translated versions.\n\nProviders SHOULD use the event id in the filter to know which events need translations.\n\nProviders MAY store their translations behind a paid relay with NIP-42 auth.\n\n## Declaring Translation Providers\n\nKind `10041` lists the user's authorized translation providers. Each `p` tag is followed by the `pubkey` of the service publishing kind 76s, and the relay translations can be found. Users can specify these publicly or privately by JSON-stringifying and encrypting the tag list in the `.content` using NIP-44. \n\n```js\n{\n \"kind\": 10041,\n \"tags\": [\n [\"p\", \"4fd5e210530e4f6b2cb083795834bfe5108324f1ed9f00ab73b9e8fcfe5f12fe\", \"wss://translations.nostr.com\"],\n [\"l\", \"\u003ctranslated_to_language_country\u003e\"], // ISO 639-1: \"en\", \"es\", ...\n [\"l\", \"\u003ctranslated_to_language_code\u003e\"], // ISO 639-1: \"en-us\", \"en-br\"\n //...\n}\n```\n\n`l` tags in this event are the languages the user understands and wants translations to.\n\nProviders SHOULD create the `10041` event and post to the user's outbox relay.",
|
||||
sig = "7fa9f1d49c41c7bfbdad6d089d1c6685777c86de8fbda7662a630888658839f0444cb1398385f759461c09191b287fa222eec0ffe22b3fa8cf740f71aab9dc21",
|
||||
tags =
|
||||
arrayOf(
|
||||
arrayOf("d", "trusted-translations"),
|
||||
arrayOf("title", "Trusted Translations"),
|
||||
arrayOf("k", "1011"),
|
||||
arrayOf("k", "10041"),
|
||||
arrayOf("k", "1011"),
|
||||
arrayOf("k", "10041"),
|
||||
arrayOf("k", "1011"),
|
||||
arrayOf("k", "10041"),
|
||||
arrayOf("k", "1011"),
|
||||
arrayOf("k", "10041"),
|
||||
arrayOf("client", "nostrhub.io"),
|
||||
),
|
||||
)
|
||||
|
||||
ThemeComparisonColumn(
|
||||
toPreview = { NipCard(event) },
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NoPaddingSuggestionChip(
|
||||
label: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.extraSmall, // Use a small shape for chip look
|
||||
color = MaterialTheme.colorScheme.secondaryContainer, // Default chip color
|
||||
modifier = modifier,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
// Apply desired internal padding to the Text itself
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
+2
-7
@@ -18,20 +18,15 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.relays
|
||||
package com.vitorpamplona.amethyst.commons.profile.ui
|
||||
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.relays
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun RelaysTabHeader(
|
||||
baseUser: User,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
fun RelaysTabHeader() {
|
||||
Text(text = stringRes(Res.string.relays))
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
import androidx.collection.mutableScatterMapOf
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.FiniteAnimationSpec
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.Transition
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.rememberTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
|
||||
/**
|
||||
* [Crossfade][MyCrossfade] between [targetState]s when [enabled], a plain swap otherwise (the app
|
||||
* turns it off in performance mode). The animated path defers building its transition until the
|
||||
* target first changes; see [DeferredCrossfade].
|
||||
*/
|
||||
@Composable
|
||||
fun <T> CrossfadeIfEnabled(
|
||||
targetState: T,
|
||||
enabled: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
label: String = "Crossfade",
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
if (enabled) {
|
||||
DeferredCrossfade(targetState, modifier, contentAlignment, animationSpec, label, content)
|
||||
} else {
|
||||
Box(modifier, contentAlignment) {
|
||||
content(targetState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Latches the first time a crossfade's target moves off the value it was composed with. */
|
||||
private class ChangeLatch {
|
||||
var changed = false
|
||||
}
|
||||
|
||||
/**
|
||||
* A [MyCrossfade] that does not build its [androidx.compose.animation.core.Transition] until there
|
||||
* is something to animate.
|
||||
*
|
||||
* `updateTransition` allocates a transition, its animation list and its seeking state on *first
|
||||
* composition*, even though first composition has nothing to cross-fade — target and initial state
|
||||
* are the same value. In a feed that is waste: every card scrolled in builds a transition per
|
||||
* animated element, and during a scroll essentially none of them run, because the underlying counts
|
||||
* and icons do not change in the second a card is on screen.
|
||||
*
|
||||
* So the plain content renders until the target actually moves. At that point the transition is
|
||||
* built seeded at the *original* value via [MutableTransitionState] and immediately re-targeted at
|
||||
* the new one, so the first real change still animates exactly as before; every later change
|
||||
* animates through the now-live transition normally.
|
||||
*/
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun <T> DeferredCrossfade(
|
||||
targetState: T,
|
||||
modifier: Modifier,
|
||||
contentAlignment: Alignment,
|
||||
animationSpec: FiniteAnimationSpec<Float>,
|
||||
label: String,
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val initial = remember { targetState }
|
||||
val latch = remember { ChangeLatch() }
|
||||
if (targetState != initial) latch.changed = true
|
||||
|
||||
if (!latch.changed) {
|
||||
Box(modifier, contentAlignment) {
|
||||
content(targetState)
|
||||
}
|
||||
} else {
|
||||
val transitionState = remember { MutableTransitionState(initial) }
|
||||
transitionState.targetState = targetState
|
||||
val transition = rememberTransition(transitionState, label)
|
||||
transition.MyCrossfade(modifier, contentAlignment, animationSpec, content = content)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun <T> MyCrossfade(
|
||||
targetState: T,
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
label: String = "Crossfade",
|
||||
content: @Composable (T) -> Unit,
|
||||
) {
|
||||
val transition = updateTransition(targetState, label)
|
||||
transition.MyCrossfade(modifier, contentAlignment, animationSpec, content = content)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun <T> Transition<T>.MyCrossfade(
|
||||
modifier: Modifier = Modifier,
|
||||
contentAlignment: Alignment = Alignment.TopStart,
|
||||
animationSpec: FiniteAnimationSpec<Float> = tween(),
|
||||
contentKey: (targetState: T) -> Any? = { it },
|
||||
content: @Composable (targetState: T) -> Unit,
|
||||
) {
|
||||
val currentlyVisible = remember { mutableStateListOf<T>().apply { add(currentState) } }
|
||||
val contentMap = remember { mutableScatterMapOf<T, @Composable () -> Unit>() }
|
||||
// If not animating, just display the current state
|
||||
if (currentState == targetState && (currentlyVisible.size != 1 || currentlyVisible[0] != targetState)) {
|
||||
// Remove all the intermediate items from the list once the animation is finished.
|
||||
currentlyVisible.removeAll { it != targetState }
|
||||
contentMap.clear()
|
||||
}
|
||||
|
||||
if (targetState !in contentMap) {
|
||||
// Replace target with the same key if any
|
||||
val replacementId =
|
||||
currentlyVisible.indexOfFirst { contentKey(it) == contentKey(targetState) }
|
||||
if (replacementId == -1) {
|
||||
currentlyVisible.add(targetState)
|
||||
} else {
|
||||
currentlyVisible[replacementId] = targetState
|
||||
}
|
||||
contentMap.clear()
|
||||
currentlyVisible.fastForEach { stateForContent ->
|
||||
contentMap[stateForContent] = {
|
||||
val alpha by
|
||||
animateFloat(transitionSpec = { animationSpec }) {
|
||||
if (it == stateForContent) 1f else 0f
|
||||
}
|
||||
Box(Modifier.graphicsLayer { this.alpha = alpha }, contentAlignment) { content(stateForContent) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier, contentAlignment) {
|
||||
currentlyVisible.fastForEach { key(contentKey(it)) { contentMap[it]?.invoke() } }
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.add_to_public_bookmarks
|
||||
import com.vitorpamplona.amethyst.commons.resources.remove_from_public_bookmarks
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
|
||||
/**
|
||||
* Round bookmark toggle: a filled, primary-tinted bookmark when [isBookmarked], an outlined
|
||||
* "add" bookmark otherwise. [onToggle] adds or removes the bookmark.
|
||||
*
|
||||
* A compact clickable glyph rather than a Material IconButton, whose 48dp minimum touch target
|
||||
* would stand taller than a title it sits beside; [iconSize] defaults to one title line.
|
||||
*/
|
||||
@Composable
|
||||
fun BookmarkToggleButton(
|
||||
isBookmarked: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
iconSize: Dp = 20.dp,
|
||||
) {
|
||||
Box(
|
||||
modifier =
|
||||
modifier
|
||||
.clip(CircleShape)
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
onClick = onToggle,
|
||||
).padding(4.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
symbol = if (isBookmarked) MaterialSymbols.Bookmark else MaterialSymbols.BookmarkAdd,
|
||||
contentDescription =
|
||||
stringRes(
|
||||
if (isBookmarked) Res.string.remove_from_public_bookmarks else Res.string.add_to_public_bookmarks,
|
||||
),
|
||||
tint = if (isBookmarked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(iconSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
+26
-21
@@ -18,31 +18,36 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.actions
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.leave
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HalfHalfHorzModifier
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.ClickableTextColor
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.lessImportantLink
|
||||
|
||||
/**
|
||||
* The `#tag` link a note header shows when the note carries a hashtag the viewer follows,
|
||||
* explaining why it is in their feed. Single line, ellipsized.
|
||||
*/
|
||||
@Composable
|
||||
fun LeaveChatButton(
|
||||
channel: EphemeralChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
fun FollowedHashtagLabel(
|
||||
tag: String,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
FilledTonalButton(
|
||||
modifier = HalfHalfHorzModifier,
|
||||
onClick = { accountViewModel.unfollow(channel) },
|
||||
contentPadding = ButtonPadding,
|
||||
) {
|
||||
Text(text = stringRes(Res.string.leave))
|
||||
Column(verticalArrangement = Arrangement.Center) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
ClickableTextColor(
|
||||
"#$tag",
|
||||
linkColor = MaterialTheme.colorScheme.lessImportantLink,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
-21
@@ -18,31 +18,38 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ephemChat.header.actions
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.join
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.resources.pow_settings_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ButtonPadding
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.HalfHalfHorzModifier
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
|
||||
|
||||
@Composable
|
||||
fun JoinChatButton(
|
||||
channel: EphemeralChatChannel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
FilledTonalButton(
|
||||
modifier = HalfHalfHorzModifier,
|
||||
onClick = { accountViewModel.follow(channel) },
|
||||
contentPadding = ButtonPadding,
|
||||
) {
|
||||
Text(text = stringRes(Res.string.join))
|
||||
}
|
||||
@Preview
|
||||
fun ProofOfWorkPillPreview() {
|
||||
ThemeComparisonColumn(
|
||||
toPreview = { ProofOfWorkPill(pow = 24, onClick = {}) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact pill showing the proof of work a received note carries: a gear plus
|
||||
* the difficulty in leading zero bits. Sits inline in note headers, so it stays
|
||||
* at text height. [onClick] is where the host explains what the number means.
|
||||
*/
|
||||
@Composable
|
||||
fun ProofOfWorkPill(
|
||||
pow: Int,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
HeaderPill(
|
||||
symbol = MaterialSymbols.Manufacturing,
|
||||
text = pow.toString(),
|
||||
contentDescription = stringRes(Res.string.pow_settings_title),
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user