feat: Add NIP-75 Zap Goal rendering and creation screen
- Add GoalEvent renderer (Goal.kt) with progress bar showing zap funding status, goal image, summary, and closed status - Add NoteCompose integration so GoalEvents render properly in feeds - Add NewGoalScreen with form for description, amount, summary, image URL, website URL, and optional deadline - Add NewGoalViewModel handling event creation via GoalEvent.build() - Add Route.NewGoal and navigation registration - Add string resources for goal UI https://claude.ai/code/session_012dS1srrK9LjA4vEZ8WKDEk
This commit is contained in:
@@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.hashtag.HashtagScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.lists.PeopleListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.display.packs.FollowPackScreen
|
||||
@@ -322,6 +323,13 @@ fun AppNavigation(
|
||||
)
|
||||
}
|
||||
|
||||
composableFromBottom<Route.NewGoal> {
|
||||
NewGoalScreen(
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
)
|
||||
}
|
||||
|
||||
composableFromBottomArgs<Route.NewLongFormPost> {
|
||||
LongFormPostScreen(
|
||||
draftId = it.draft,
|
||||
|
||||
@@ -290,6 +290,8 @@ sealed class Route {
|
||||
val draft: String? = null,
|
||||
) : Route()
|
||||
|
||||
@Serializable data object NewGoal : Route()
|
||||
|
||||
@Serializable
|
||||
data class NewLongFormPost(
|
||||
val draft: String? = null,
|
||||
|
||||
@@ -123,6 +123,7 @@ import com.vitorpamplona.amethyst.ui.note.types.RenderFhirResource
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitIssueEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitPatchEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGitRepositoryEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderGoal
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderHighlight
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderInteractiveStory
|
||||
import com.vitorpamplona.amethyst.ui.note.types.RenderLiveActivityChatMessage
|
||||
@@ -250,6 +251,7 @@ import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprov
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.communityAddress
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.definition.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.quartz.nip72ModCommunities.isACommunityPost
|
||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||
import com.vitorpamplona.quartz.nip84Highlights.HighlightEvent
|
||||
import com.vitorpamplona.quartz.nip88Polls.poll.PollEvent
|
||||
import com.vitorpamplona.quartz.nip89AppHandlers.definition.AppDefinitionEvent
|
||||
@@ -1017,6 +1019,10 @@ private fun RenderNoteRow(
|
||||
RenderCalendarDateSlotEvent(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is GoalEvent -> {
|
||||
RenderGoal(baseNote, accountViewModel, nav)
|
||||
}
|
||||
|
||||
is HighlightEvent -> {
|
||||
RenderHighlight(
|
||||
baseNote,
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 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.note.types
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
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.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps
|
||||
import com.vitorpamplona.amethyst.ui.components.MyAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapraiserStatus
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.elements.DefaultImageHeaderBackground
|
||||
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.fundraiserProgressColor
|
||||
import com.vitorpamplona.amethyst.ui.theme.replyModifier
|
||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.math.BigDecimal
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun RenderGoal(
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val noteEvent = note.event as? GoalEvent ?: return
|
||||
|
||||
GoalHeader(noteEvent, note, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GoalHeader(
|
||||
noteEvent: GoalEvent,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val image = noteEvent.image()
|
||||
val summary =
|
||||
remember(noteEvent) {
|
||||
noteEvent.summary()?.ifBlank { null } ?: noteEvent.content.take(200).ifBlank { null }
|
||||
}
|
||||
val goalAmountMillisats = noteEvent.amount() ?: 0L
|
||||
val goalAmountSats = goalAmountMillisats / 1000
|
||||
val closedAt = noteEvent.closedAt()
|
||||
val isClosed = closedAt != null && closedAt < TimeUtils.now()
|
||||
|
||||
Column(MaterialTheme.colorScheme.replyModifier) {
|
||||
image?.let {
|
||||
Box {
|
||||
MyAsyncImage(
|
||||
imageUrl = it,
|
||||
contentDescription = stringRes(R.string.preview_card_image_for, it),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
mainImageModifier = Modifier.fillMaxWidth(),
|
||||
loadedImageModifier = Modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
onLoadingBackground = { DefaultImageHeaderBackground(note, accountViewModel) },
|
||||
onError = { DefaultImageHeader(note, accountViewModel) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.padding(10.dp)) {
|
||||
summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (goalAmountSats > 0) {
|
||||
GoalProgressBar(
|
||||
note = note,
|
||||
goalAmountSats = goalAmountSats,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
}
|
||||
|
||||
if (isClosed) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringRes(R.string.goal_closed),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GoalProgressBar(
|
||||
note: Note,
|
||||
goalAmountSats: Long,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val zapsState by observeNoteZaps(note, accountViewModel)
|
||||
|
||||
var zapraiserStatus by
|
||||
remember { mutableStateOf(ZapraiserStatus(0F, showAmount(goalAmountSats.toBigDecimal()))) }
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
zapsState?.note?.let {
|
||||
val newZapAmount = accountViewModel.account.calculateZappedAmount(note)
|
||||
var percentage = newZapAmount.div(goalAmountSats.toBigDecimal()).toFloat()
|
||||
if (percentage > 1) percentage = 1f
|
||||
|
||||
val left =
|
||||
if (percentage > 0.99) {
|
||||
"0"
|
||||
} else {
|
||||
showAmount(
|
||||
goalAmountSats.toBigDecimal() * BigDecimal(1.0 - percentage),
|
||||
)
|
||||
}
|
||||
zapraiserStatus = ZapraiserStatus(percentage, left)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
LinearProgressIndicator(
|
||||
modifier = Modifier.fillMaxWidth().height(24.dp),
|
||||
color = MaterialTheme.colorScheme.fundraiserProgressColor,
|
||||
progress = { zapraiserStatus.progress },
|
||||
gapSize = 0.dp,
|
||||
strokeCap = StrokeCap.Square,
|
||||
drawStopIndicator = {},
|
||||
)
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||
) {
|
||||
val totalPercentage by
|
||||
remember(zapraiserStatus) {
|
||||
derivedStateOf { "${(zapraiserStatus.progress * 100).roundToInt()}%" }
|
||||
}
|
||||
|
||||
Text(
|
||||
text =
|
||||
stringRes(
|
||||
R.string.goal_progress,
|
||||
totalPercentage,
|
||||
showAmount(goalAmountSats.toBigDecimal()),
|
||||
),
|
||||
fontSize = Font14SP,
|
||||
maxLines = 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.home.nip75Goals
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
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.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.Nav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.PostingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewGoalScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: Nav,
|
||||
) {
|
||||
val goalViewModel: NewGoalViewModel = viewModel()
|
||||
goalViewModel.init(accountViewModel)
|
||||
|
||||
LaunchedEffect(goalViewModel, accountViewModel) {
|
||||
// no-op for now, could load drafts
|
||||
}
|
||||
|
||||
NewGoalScreen(
|
||||
goalViewModel,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewGoalScreen(
|
||||
goalViewModel: NewGoalViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
BackHandler {
|
||||
goalViewModel.cancel()
|
||||
nav.popBack()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
PostingTopBar(
|
||||
titleRes = R.string.new_goal,
|
||||
isActive = goalViewModel::canPost,
|
||||
onCancel = {
|
||||
goalViewModel.cancel()
|
||||
nav.popBack()
|
||||
},
|
||||
onPost = {
|
||||
accountViewModel.launchSigner {
|
||||
goalViewModel.sendPostSync()
|
||||
nav.popBack()
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { pad ->
|
||||
Surface(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(pad)
|
||||
.consumeWindowInsets(pad)
|
||||
.imePadding(),
|
||||
) {
|
||||
NewGoalBody(goalViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NewGoalBody(goalViewModel: NewGoalViewModel) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
.padding(16.dp),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = goalViewModel.description,
|
||||
onValueChange = { goalViewModel.description = it },
|
||||
label = { Text(stringRes(R.string.goal_description_label)) },
|
||||
placeholder = { Text(stringRes(R.string.goal_description_placeholder)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 6,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = goalViewModel.amount,
|
||||
onValueChange = { goalViewModel.amount = it },
|
||||
label = { Text(stringRes(R.string.goal_amount_label)) },
|
||||
placeholder = { Text(stringRes(R.string.goal_amount_placeholder)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = goalViewModel.summary,
|
||||
onValueChange = { goalViewModel.summary = it },
|
||||
label = { Text(stringRes(R.string.goal_summary_label)) },
|
||||
placeholder = { Text(stringRes(R.string.goal_summary_placeholder)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = goalViewModel.imageUrl,
|
||||
onValueChange = { goalViewModel.imageUrl = it },
|
||||
label = { Text(stringRes(R.string.goal_image_label)) },
|
||||
placeholder = { Text(stringRes(R.string.goal_image_placeholder)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = goalViewModel.websiteUrl,
|
||||
onValueChange = { goalViewModel.websiteUrl = it },
|
||||
label = { Text(stringRes(R.string.goal_website_label)) },
|
||||
placeholder = { Text(stringRes(R.string.goal_website_placeholder)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
Checkbox(
|
||||
checked = goalViewModel.wantsDeadline,
|
||||
onCheckedChange = { goalViewModel.wantsDeadline = it },
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.goal_set_deadline),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.home.nip75Goals
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.EventTemplate
|
||||
import com.vitorpamplona.quartz.nip75ZapGoals.GoalEvent
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
@Stable
|
||||
class NewGoalViewModel : ViewModel() {
|
||||
lateinit var accountViewModel: AccountViewModel
|
||||
lateinit var account: Account
|
||||
|
||||
var description by mutableStateOf(TextFieldValue(""))
|
||||
var amount by mutableStateOf(TextFieldValue(""))
|
||||
var summary by mutableStateOf(TextFieldValue(""))
|
||||
var imageUrl by mutableStateOf(TextFieldValue(""))
|
||||
var websiteUrl by mutableStateOf(TextFieldValue(""))
|
||||
|
||||
var wantsDeadline by mutableStateOf(false)
|
||||
var deadlineTimestamp by mutableLongStateOf(TimeUtils.now() + TimeUtils.ONE_WEEK)
|
||||
|
||||
fun init(accountVM: AccountViewModel) {
|
||||
this.accountViewModel = accountVM
|
||||
this.account = accountVM.account
|
||||
}
|
||||
|
||||
fun canPost(): Boolean =
|
||||
description.text.isNotBlank() &&
|
||||
amount.text.isNotBlank() &&
|
||||
amount.text.toLongOrNull() != null &&
|
||||
(amount.text.toLongOrNull() ?: 0) > 0
|
||||
|
||||
fun cancel() {
|
||||
description = TextFieldValue("")
|
||||
amount = TextFieldValue("")
|
||||
summary = TextFieldValue("")
|
||||
imageUrl = TextFieldValue("")
|
||||
websiteUrl = TextFieldValue("")
|
||||
wantsDeadline = false
|
||||
deadlineTimestamp = TimeUtils.now() + TimeUtils.ONE_WEEK
|
||||
}
|
||||
|
||||
suspend fun sendPostSync() {
|
||||
val template = createTemplate() ?: return
|
||||
cancel()
|
||||
account.signAndComputeBroadcast(template)
|
||||
}
|
||||
|
||||
private fun createTemplate(): EventTemplate<out Event>? {
|
||||
val amountSats = amount.text.toLongOrNull() ?: return null
|
||||
val amountMillisats = amountSats * 1000L
|
||||
|
||||
val relays =
|
||||
account.outboxRelays.flow.value
|
||||
.map { it.url }
|
||||
|
||||
val closedAt = if (wantsDeadline) deadlineTimestamp else null
|
||||
val img = imageUrl.text.ifBlank { null }
|
||||
val sum = summary.text.ifBlank { null }
|
||||
val web = websiteUrl.text.ifBlank { null }
|
||||
|
||||
return GoalEvent.build(
|
||||
description = description.text,
|
||||
amount = amountMillisats,
|
||||
relays = relays,
|
||||
closedAt = closedAt,
|
||||
image = img,
|
||||
summary = sum,
|
||||
websiteUrl = web,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1955,4 +1955,22 @@
|
||||
<string name="web_bookmark_delete">Delete</string>
|
||||
<string name="web_bookmark_delete_confirm">Delete this web bookmark?</string>
|
||||
<string name="web_bookmark_open_url">Open URL</string>
|
||||
|
||||
<!-- NIP-75 Zap Goals -->
|
||||
<string name="goal_closed">This goal has closed</string>
|
||||
<string name="goal_progress">%1$s funded of %2$s sats goal</string>
|
||||
<string name="goal_amount_label">Goal amount (sats)</string>
|
||||
<string name="goal_amount_placeholder">100000</string>
|
||||
<string name="goal_description_label">Describe your goal</string>
|
||||
<string name="goal_description_placeholder">What are you fundraising for?</string>
|
||||
<string name="goal_summary_label">Short summary</string>
|
||||
<string name="goal_summary_placeholder">Brief description shown in previews</string>
|
||||
<string name="goal_image_label">Image URL (optional)</string>
|
||||
<string name="goal_image_placeholder">https://example.com/image.jpg</string>
|
||||
<string name="goal_website_label">Website URL (optional)</string>
|
||||
<string name="goal_website_placeholder">https://example.com</string>
|
||||
<string name="goal_deadline_label">Deadline (optional)</string>
|
||||
<string name="goal_set_deadline">Set a deadline</string>
|
||||
<string name="new_goal">New Goal</string>
|
||||
<string name="goal_create">Create Goal</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user