Compare commits

..

3 Commits

Author SHA1 Message Date
Vitor Pamplona
f42881a48e v1.06.3 2026-03-24 20:55:04 -04:00
Vitor Pamplona
6fc72f47bb Highlights when arriving at the reply message 2026-03-24 20:53:22 -04:00
Vitor Pamplona
041c788e21 Improvements to anonymous replies 2026-03-24 20:42:06 -04:00
13 changed files with 109 additions and 42 deletions

View File

@@ -745,8 +745,8 @@ android {
applicationId = "com.vitorpamplona.amethyst"
minSdk = 26 // Android 8.0 (Oreo)
targetSdk = 36 // Android 15
versionCode = 434
versionName = "1.06.2"
versionCode = 435
versionName = "1.06.3"
vectorDrawables {
useSupportLibrary = true

View File

@@ -7,7 +7,7 @@ description: Integration guide for using the Quartz Nostr KMP library in externa
Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr KMP projects.
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.06.2` (Maven Central)
**Published artifact**: `com.vitorpamplona.quartz:quartz:1.06.3` (Maven Central)
**Targets**: JVM 21+, Android (minSdk 21+), iOS (XCFramework `quartz-kmpKit`)
**License**: MIT
@@ -19,7 +19,7 @@ Reference for integrating `com.vitorpamplona.quartz:quartz` into external Nostr
```toml
[versions]
quartz = "1.06.2"
quartz = "1.06.3"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
@@ -41,7 +41,7 @@ kotlin {
```kotlin
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
implementation("com.vitorpamplona.quartz:quartz:1.06.3")
}
```

View File

@@ -3,7 +3,7 @@
## Current version
```
com.vitorpamplona.quartz:quartz:1.06.2
com.vitorpamplona.quartz:quartz:1.06.3
```
Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/quartz
@@ -16,7 +16,7 @@ Check latest: https://central.sonatype.com/artifact/com.vitorpamplona.quartz/qua
```toml
[versions]
quartz = "1.06.2"
quartz = "1.06.3"
[libraries]
quartz = { module = "com.vitorpamplona.quartz:quartz", version.ref = "quartz" }
@@ -55,7 +55,7 @@ kotlin {
```kotlin
// build.gradle.kts (app module)
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
implementation("com.vitorpamplona.quartz:quartz:1.06.3")
}
```
@@ -70,7 +70,7 @@ plugins {
}
dependencies {
implementation("com.vitorpamplona.quartz:quartz:1.06.2")
implementation("com.vitorpamplona.quartz:quartz:1.06.3")
// JNA needed for libsodium (NIP-44) on JVM
implementation("net.java.dev.jna:jna:5.18.1")
}

View File

@@ -17,7 +17,7 @@ The Quartz library was successfully converted from Android-only to full KMP supp
## Current artifact
```
com.vitorpamplona.quartz:quartz:1.06.2
com.vitorpamplona.quartz:quartz:1.06.3
```
See `.claude/skills/quartz-integration/SKILL.md` for full integration guide.

View File

@@ -54,8 +54,8 @@ android {
applicationId = "com.vitorpamplona.amethyst"
minSdk = libs.versions.android.minSdk.get().toInteger()
targetSdk = libs.versions.android.targetSdk.get().toInteger()
versionCode = 434
versionName = generateVersionName("1.06.2")
versionCode = 435
versionName = generateVersionName("1.06.3")
buildConfigField "String", "RELEASE_NOTES_ID", "\"0b6af7660b44215b0edf9c39a1c9c0b4aafba7aba1ae28665ffcecb1a9717195\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -1300,9 +1300,18 @@ class Account(
val anonymousSigner = NostrSignerInternal(KeyPair())
val event = anonymousSigner.sign(template)
val relayList = nip65RelayList.outboxFlow.value.toSet()
cache.justConsumeMyOwnEvent(event)
val note =
if (event is AddressableEvent) {
cache.getOrCreateAddressableNote(event.address())
} else {
cache.getOrCreateNote(event.id)
}
val relayList = computeRelayListToBroadcast(note)
client.send(event, relayList)
broadcast.forEach { client.send(it, relayList) }
return event

View File

@@ -194,6 +194,8 @@ open class CommentPostViewModel :
var wantsZapraiser by mutableStateOf(false)
override val zapRaiserAmount = mutableStateOf<Long?>(null)
var wantsAnonymousPost by mutableStateOf(false)
fun lnAddress(): String? = account.userProfile().lnAddress()
fun hasLnAddress(): Boolean = account.userProfile().lnAddress() != null
@@ -342,10 +344,15 @@ open class CommentPostViewModel :
}
val version = draftTag.current
val anonymous = wantsAnonymousPost
cancel()
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
if (anonymous) {
accountViewModel.account.signAnonymouslyAndBroadcast(template, extraNotesToBroadcast)
} else {
accountViewModel.account.signAndComputeBroadcast(template, extraNotesToBroadcast)
}
accountViewModel.viewModelScope.launch(Dispatchers.IO) {
accountViewModel.account.deleteDraftIgnoreErrors(version)
}
@@ -588,6 +595,7 @@ open class CommentPostViewModel :
contentWarningDescription = ""
wantsToAddGeoHash = false
wantsSecretEmoji = false
wantsAnonymousPost = false
forwardZapTo.value = SplitBuilder()
forwardZapToEditting.value = TextFieldValue("")

View File

@@ -22,7 +22,9 @@ package com.vitorpamplona.amethyst.ui.note.nip22Comments
import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -35,6 +37,8 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
@@ -79,9 +83,12 @@ import com.vitorpamplona.amethyst.ui.note.creators.zapraiser.ZapRaiserRequest
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapTo
import com.vitorpamplona.amethyst.ui.note.creators.zapsplits.ForwardZapToButton
import com.vitorpamplona.amethyst.ui.note.types.ReplyRenderType
import com.vitorpamplona.amethyst.ui.painterRes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.SuggestionListDefaultHeightPage
@@ -241,11 +248,32 @@ private fun GenericCommentPostBody(
Row(
modifier = Modifier.padding(vertical = Size10dp),
) {
BaseUserPicture(
accountViewModel.userProfile(),
Size35dp,
accountViewModel = accountViewModel,
)
if (postViewModel.wantsAnonymousPost) {
IconButton(
modifier = Size35Modifier,
onClick = { postViewModel.wantsAnonymousPost = false },
) {
Icon(
painter = painterRes(resourceId = R.drawable.incognito, 1),
contentDescription = stringRes(R.string.post_anonymously),
modifier = Size30Modifier,
tint = MaterialTheme.colorScheme.onBackground,
)
}
} else {
Box(
modifier =
Modifier.clickable {
postViewModel.wantsAnonymousPost = true
},
) {
BaseUserPicture(
accountViewModel.userProfile(),
Size35dp,
accountViewModel = accountViewModel,
)
}
}
MessageField(
R.string.what_s_on_your_mind,
postViewModel,

View File

@@ -28,6 +28,8 @@ import androidx.compose.foundation.lazy.itemsIndexed
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.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -134,11 +136,13 @@ fun ChatFeedLoaded(
}
val scope = rememberCoroutineScope()
val highlightedNoteId = remember { mutableStateOf<String?>(null) }
val onScrollToNote: (Note) -> Unit = { note ->
val index = items.list.indexOfFirst { it.idHex == note.idHex }
if (index >= 0) {
scope.launch {
listState.animateScrollToItem(index)
highlightedNoteId.value = note.idHex
}
}
}
@@ -160,6 +164,8 @@ fun ChatFeedLoaded(
onWantsToReply = onWantsToReply,
onWantsToEditDraft = onWantsToEditDraft,
onScrollToNote = onScrollToNote,
shouldHighlight = highlightedNoteId.value == item.idHex,
onHighlightFinished = { highlightedNoteId.value = null },
)
NewDateOrSubjectDivisor(items.list.getOrNull(index + 1), item)

View File

@@ -96,6 +96,8 @@ fun ChatroomMessageCompose(
onWantsToReply: (Note) -> Unit,
onWantsToEditDraft: (Note) -> Unit,
onScrollToNote: ((Note) -> Unit)? = null,
shouldHighlight: Boolean = false,
onHighlightFinished: (() -> Unit)? = null,
) {
WatchNoteEvent(baseNote = baseNote, accountViewModel = accountViewModel, nav) {
WatchBlockAndReport(
@@ -116,6 +118,8 @@ fun ChatroomMessageCompose(
onWantsToReply,
onWantsToEditDraft,
onScrollToNote,
shouldHighlight,
onHighlightFinished,
)
}
}
@@ -133,6 +137,8 @@ fun NormalChatNote(
onWantsToReply: (Note) -> Unit,
onWantsToEditDraft: (Note) -> Unit,
onScrollToNote: ((Note) -> Unit)? = null,
shouldHighlight: Boolean = false,
onHighlightFinished: (() -> Unit)? = null,
) {
val isLoggedInUser =
remember(note.author) {
@@ -170,6 +176,8 @@ fun NormalChatNote(
hasDetailsToShow = note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty(),
drawAuthorInfo = drawAuthorInfo,
parentBackgroundColor = parentBackgroundColor,
shouldHighlight = shouldHighlight,
onHighlightFinished = onHighlightFinished,
onClick = {
if (note.event is ChannelCreateEvent) {
nav.nav(Route.PublicChatChannel(note.idHex))

View File

@@ -20,6 +20,8 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.layouts
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -36,7 +38,9 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -58,6 +62,7 @@ import com.vitorpamplona.amethyst.ui.theme.chatBackground
import com.vitorpamplona.amethyst.ui.theme.chatDraftBackground
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.messageBubbleLimits
import kotlinx.coroutines.delay
private const val RELAYS_AND_ACTIONS_TEXT = "Relays and Actions"
@@ -71,6 +76,8 @@ fun ChatBubbleLayout(
hasDetailsToShow: Boolean,
drawAuthorInfo: Boolean,
parentBackgroundColor: MutableState<Color>? = null,
shouldHighlight: Boolean = false,
onHighlightFinished: (() -> Unit)? = null,
onClick: () -> Boolean,
onAuthorClick: () -> Unit,
actionMenu: @Composable (onDismiss: () -> Unit) -> Unit,
@@ -100,6 +107,24 @@ fun ChatBubbleLayout(
}
}
val highlightActive = remember { mutableStateOf(false) }
LaunchedEffect(shouldHighlight) {
if (shouldHighlight) {
highlightActive.value = true
delay(1500)
highlightActive.value = false
onHighlightFinished?.invoke()
}
}
val highlightColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.3f)
val animatedColor by animateColorAsState(
targetValue = if (highlightActive.value) highlightColor.compositeOver(bgColor.value) else bgColor.value,
animationSpec = tween(durationMillis = if (highlightActive.value) 300 else 800),
label = "highlightAnimation",
)
Row(
modifier = if (innerQuote) ChatPaddingInnerQuoteModifier else ChatPaddingModifier,
horizontalArrangement = if (isLoggedInUser) Arrangement.End else Arrangement.Start,
@@ -136,7 +161,7 @@ fun ChatBubbleLayout(
modifier = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier,
) {
Surface(
color = bgColor.value,
color = animatedColor,
shape = if (isLoggedInUser) ChatBubbleShapeMe else ChatBubbleShapeThem,
modifier = clickableModifier,
) {

View File

@@ -25,7 +25,6 @@ import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Box
@@ -49,7 +48,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -110,6 +108,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size19Modifier
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -288,23 +287,6 @@ private fun NewPostScreenBody(
}
}
if (postViewModel.wantsAnonymousPost) {
Row(
modifier =
Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.errorContainer)
.padding(horizontal = Size10dp, vertical = 8.dp),
verticalAlignment = CenterVertically,
) {
Text(
text = stringRes(R.string.anonymous_reply_warning),
color = MaterialTheme.colorScheme.onErrorContainer,
style = MaterialTheme.typography.bodySmall,
)
}
}
// Only show text input if no voice message is being posted
if (postViewModel.voiceMetadata == null && postViewModel.voiceRecording == null) {
Row(
@@ -312,12 +294,13 @@ private fun NewPostScreenBody(
) {
if (postViewModel.wantsAnonymousPost) {
IconButton(
modifier = Size35Modifier,
onClick = { postViewModel.wantsAnonymousPost = false },
) {
Icon(
painter = painterRes(resourceId = R.drawable.incognito, 1),
contentDescription = stringRes(R.string.post_anonymously),
modifier = Size35Modifier,
modifier = Size30Modifier,
tint = MaterialTheme.colorScheme.onBackground,
)
}

View File

@@ -350,7 +350,7 @@ mavenPublishing {
coordinates(
groupId = "com.vitorpamplona.quartz",
artifactId = "quartz",
version = "1.06.2",
version = "1.06.3",
)
// Configure publishing to Maven Central