Porównaj commity

...

23 Commity

Autor SHA1 Wiadomość Data
Crowdin Bot da2b59af49 New Crowdin translations by GitHub Action 2024-05-21 18:52:37 +00:00
Vitor Pamplona 7acdf56e68 Slightly improving the threading for badges. 2024-05-21 14:50:36 -04:00
Vitor Pamplona 24be6cd90d No need to launch, just use IO 2024-05-21 12:26:39 -04:00
Vitor Pamplona afee0ddc53
Merge pull request #869 from believethehype/NIP90-ContentDiscovery
update paid dvms with processing status after payment
2024-05-21 08:41:38 -04:00
Believethehype c85b8a1d83 update paid dvms with processing status after payment 2024-05-21 12:11:44 +02:00
Vitor Pamplona cfeedfa4e2 Fixes the use of cache for video files. 2024-05-20 17:42:21 -04:00
Vitor Pamplona dd112d28ae updating some libraries (others are not ready for the app) 2024-05-20 17:11:03 -04:00
Vitor Pamplona fc27526113 Removes reposts from the Dot Notification in the home's bottom bar icon 2024-05-20 16:45:08 -04:00
Vitor Pamplona 666635811b Migrating to AGP 8.4.1 2024-05-20 16:42:11 -04:00
Vitor Pamplona 4e43938f96 Can zap DVMs directly on the regular zap button. 2024-05-20 16:22:27 -04:00
Vitor Pamplona 2a744205f0
Merge pull request #868 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-20 15:20:33 -04:00
Crowdin Bot 7e6ca34d2a New Crowdin translations by GitHub Action 2024-05-20 19:16:35 +00:00
Vitor Pamplona 24f7991116 Fixes a white space after a new line 2024-05-20 15:15:06 -04:00
Vitor Pamplona beb901120e Fixes call from main thread 2024-05-20 13:25:53 -04:00
Vitor Pamplona 0936df9851 Fixes sat division and the state changing text. 2024-05-20 13:19:04 -04:00
Vitor Pamplona 9ceb8866ed Refactoring the DVM payment code. 2024-05-20 12:37:55 -04:00
Vitor Pamplona c88b21b547
Merge pull request #867 from believethehype/NIP90-ContentDiscovery
Nip90 content discovery - Consider status and amount tags
2024-05-20 11:38:13 -04:00
Vitor Pamplona 5c366d5cfc
Merge branch 'main' into NIP90-ContentDiscovery 2024-05-20 11:37:07 -04:00
Vitor Pamplona ea70d44ac7 liniting 2024-05-20 11:24:03 -04:00
Believethehype 67f10920f6 Update NIP90ContentDiscoveryScreen.kt 2024-05-20 00:35:39 +02:00
Believethehype 654632a585 Update NIP90ContentDiscoveryScreen.kt 2024-05-20 00:32:26 +02:00
Believethehype 794b05106b add possibility to zap/pay invoices for dvms 2024-05-20 00:15:30 +02:00
Vitor Pamplona 5812e290c9
Merge pull request #866 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-05-17 18:32:35 -04:00
17 zmienionych plików z 482 dodań i 63 usunięć

Wyświetl plik

@ -143,17 +143,17 @@ object LocalCache {
val deletionIndex = DeletionIndex()
val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag>>(10)
val observablesByKindAndETag = ConcurrentHashMap<Int, ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>>(10)
fun observeETag(
fun <T : Event> observeETag(
kind: Int,
eventId: HexKey,
onCreate: () -> LatestByKindWithETag,
): LatestByKindWithETag {
onCreate: () -> LatestByKindWithETag<T>,
): LatestByKindWithETag<T> {
var eTagList = observablesByKindAndETag.get(kind)
if (eTagList == null) {
eTagList = ConcurrentHashMap<HexKey, LatestByKindWithETag>(1)
eTagList = ConcurrentHashMap<HexKey, LatestByKindWithETag<T>>(1) as ConcurrentHashMap<HexKey, LatestByKindWithETag<Event>>
observablesByKindAndETag.put(kind, eTagList)
}
@ -162,10 +162,10 @@ object LocalCache {
return if (value != null) {
value
} else {
val newObject = onCreate()
val newObject = onCreate() as LatestByKindWithETag<Event>
val obj = eTagList.putIfAbsent(eventId, newObject) ?: newObject
obj
}
} as LatestByKindWithETag<T>
}
fun updateObservables(event: Event) {

Wyświetl plik

@ -26,11 +26,11 @@ import com.vitorpamplona.quartz.events.Event
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class LatestByKindWithETag(private val kind: Int, private val eTag: String) {
private val _latest = MutableStateFlow<Event?>(null)
class LatestByKindWithETag<T : Event>(private val kind: Int, private val eTag: String) {
private val _latest = MutableStateFlow<T?>(null)
val latest = _latest.asStateFlow()
fun updateIfMatches(event: Event) {
fun updateIfMatches(event: T) {
if (event.kind == kind && event.isTaggedEvent(eTag)) {
if (event.createdAt > (_latest.value?.createdAt ?: 0)) {
_latest.tryEmit(event)
@ -65,7 +65,7 @@ class LatestByKindWithETag(private val kind: Int, private val eTag: String) {
firstEvent.createdAt().compareTo(secondEvent.createdAt())
}
},
)?.event as? Event
)?.event as? T
_latest.tryEmit(latestNote)
}

Wyświetl plik

@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.screen.loggedIn.collectSuccessfulSigningOperations
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
@ -67,6 +68,25 @@ class ZapPaymentHandler(val account: Account) {
zapSplitSetup
} else if (noteEvent is LiveActivitiesEvent && noteEvent.hasHost()) {
noteEvent.hosts().map { ZapSplitSetup(it, null, weight = 1.0, false) }
} else if (noteEvent is AppDefinitionEvent) {
val appLud16 = noteEvent.appMetaData()?.lnAddress()
if (appLud16 != null) {
listOf(ZapSplitSetup(appLud16, null, weight = 1.0, true))
} else {
val lud16 = note.author?.info?.lnAddress()
if (lud16.isNullOrBlank()) {
onError(
context.getString(R.string.missing_lud16),
context.getString(
R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats,
),
)
return@withContext
}
listOf(ZapSplitSetup(lud16, null, weight = 1.0, true))
}
} else {
val lud16 = note.author?.info?.lnAddress()

Wyświetl plik

@ -32,6 +32,7 @@ import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.service.HttpClientManager
import okhttp3.OkHttpClient
@ -80,7 +81,7 @@ class PlaybackService : MediaSessionService() {
fun newAllInOneDataSource(): MediaSource.Factory {
// This might be needed for live kit.
// return WssOrHttpFactory(HttpClientManager.getHttpClient())
return DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient()))
return DefaultMediaSourceFactory(Amethyst.instance.videoCache.get(HttpClientManager.getHttpClient()))
}
fun lazyDS(): MultiPlayerPlaybackManager {

Wyświetl plik

@ -45,6 +45,8 @@ import com.vitorpamplona.amethyst.ui.theme.Size23dp
import com.vitorpamplona.amethyst.ui.theme.Size24dp
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.GenericRepostEvent
import com.vitorpamplona.quartz.events.RepostEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -320,6 +322,14 @@ object HomeLatestItem : LatestItem() {
return (newestItem?.createdAt() ?: 0) > lastTime
}
override fun filterMore(
newItems: Set<Note>,
account: Account,
): Set<Note> {
// removes reposts from the dot notifications.
return newItems.filter { it.event !is GenericRepostEvent && it.event !is RepostEvent }.toSet()
}
}
object NotificationLatestItem : LatestItem() {

Wyświetl plik

@ -43,6 +43,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Composable
fun LoadDecryptedContent(
@ -116,10 +117,12 @@ fun LoadAddressableNote(
if (note == null) {
LaunchedEffect(key1 = aTag) {
accountViewModel.getOrCreateAddressableNote(aTag) { newNote ->
if (newNote != note) {
note = newNote
val newNote =
withContext(Dispatchers.IO) {
accountViewModel.getOrCreateAddressableNote(aTag)
}
if (note != newNote) {
note = newNote
}
}
}

Wyświetl plik

@ -83,6 +83,7 @@ import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.Participant
import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.Response
import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.utils.TimeUtils
@ -176,16 +177,16 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
account.reactTo(note, reaction)
}
fun observeByETag(
fun <T : Event> observeByETag(
kind: Int,
eTag: HexKey,
): StateFlow<Event?> {
): StateFlow<T?> {
val observable =
LocalCache.observeETag(
LocalCache.observeETag<T>(
kind = kind,
eventId = eTag,
) {
LatestByKindWithETag(kind, eTag)
LatestByKindWithETag<T>(kind, eTag)
}
viewModelScope.launch(Dispatchers.IO) {
@ -945,7 +946,7 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
viewModelScope.launch(Dispatchers.IO) { onResult(checkGetOrCreateAddressableNote(key)) }
}
private suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? {
suspend fun getOrCreateAddressableNote(key: ATag): AddressableNote? {
return LocalCache.getOrCreateAddressableNote(key)
}
@ -1353,6 +1354,17 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
}
}
fun sendZapPaymentRequestFor(
bolt11: String,
zappedNote: Note?,
onSent: () -> Unit,
onResponse: (Response?) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
account.sendZapPaymentRequestFor(bolt11, zappedNote, onSent, onResponse)
}
}
val draftNoteCache = CachedDraftNotes(this)
class CachedDraftNotes(val accountViewModel: AccountViewModel) : GenericBaseCacheAsync<DraftEvent, Note>(20) {

Wyświetl plik

@ -20,28 +20,42 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.animateFloatAsState
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.distinctUntilChanged
@ -51,21 +65,41 @@ import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.ZapPaymentHandler
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.navigation.routeToMessage
import com.vitorpamplona.amethyst.ui.note.DVMCard
import com.vitorpamplona.amethyst.ui.note.ErrorMessageDialog
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.ObserveZapIcon
import com.vitorpamplona.amethyst.ui.note.PayViaIntentDialog
import com.vitorpamplona.amethyst.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.ui.note.ZapAmountChoicePopup
import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.note.ZappedIcon
import com.vitorpamplona.amethyst.ui.note.elements.customZapClick
import com.vitorpamplona.amethyst.ui.note.payViaIntent
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
import com.vitorpamplona.amethyst.ui.screen.NostrNIP90ContentDiscoveryFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableBox
import com.vitorpamplona.amethyst.ui.screen.RenderFeedState
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.NIP90ContentDiscoveryResponseEvent
import com.vitorpamplona.quartz.events.NIP90StatusEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@Composable
fun NIP90ContentDiscoveryScreen(
@ -81,7 +115,7 @@ fun NIP90ContentDiscoveryScreen(
NIP90ContentDiscoveryScreen(baseNote, accountViewModel, nav)
},
onBlank = {
FeedEmptywithStatus(baseNote, stringResource(R.string.dvm_looking_for_app), accountViewModel, nav)
FeedEmptyWithStatus(baseNote, stringResource(R.string.dvm_looking_for_app), accountViewModel, nav)
},
)
}
@ -125,7 +159,8 @@ fun NIP90ContentDiscoveryScreen(
)
} else {
// TODO: Make a good splash screen with loading animation for this DVM.
FeedEmptywithStatus(appDefinition, stringResource(R.string.dvm_requesting_job), accountViewModel, nav)
// FeedDVM(appDefinition, null, accountViewModel, nav)
FeedEmptyWithStatus(appDefinition, stringResource(R.string.dvm_requesting_job), accountViewModel, nav)
}
}
}
@ -143,7 +178,7 @@ fun ObserverContentDiscoveryResponse(
val resultFlow =
remember(dvmRequestId) {
accountViewModel.observeByETag(NIP90ContentDiscoveryResponseEvent.KIND, dvmRequestId.idHex)
accountViewModel.observeByETag<NIP90ContentDiscoveryResponseEvent>(NIP90ContentDiscoveryResponseEvent.KIND, dvmRequestId.idHex)
}
val latestResponse by resultFlow.collectAsStateWithLifecycle()
@ -175,19 +210,19 @@ fun ObserverDvmStatusResponse(
) {
val statusFlow =
remember(dvmRequestId) {
accountViewModel.observeByETag(NIP90StatusEvent.KIND, dvmRequestId)
accountViewModel.observeByETag<NIP90StatusEvent>(NIP90StatusEvent.KIND, dvmRequestId)
}
val latestStatus by statusFlow.collectAsStateWithLifecycle()
// TODO: Make a good splash screen with loading animation for this DVM.
if (latestStatus != null) {
// TODO: Make a good splash screen with loading animation for this DVM.
latestStatus?.let {
FeedEmptywithStatus(appDefinition, it.content(), accountViewModel, nav)
FeedDVM(appDefinition, it, accountViewModel, nav)
}
} else {
// TODO: Make a good splash screen with loading animation for this DVM.
FeedEmptywithStatus(appDefinition, stringResource(R.string.dvm_waiting_status), accountViewModel, nav)
FeedEmptyWithStatus(appDefinition, stringResource(R.string.dvm_waiting_status), accountViewModel, nav)
}
}
@ -220,9 +255,6 @@ fun RenderNostrNIP90ContentDiscoveryScreen(
nav: (String) -> Unit,
) {
Column(Modifier.fillMaxHeight()) {
// TODO (Optional) Maybe render a nice header with image and DVM name from the dvmID
// TODO (Optional) How do we get the event information here?, LocalCache.checkGetOrCreateNote() returns note but event is empty
// TODO (Optional) otherwise we have the NIP89 info in (note.event as AppDefinitionEvent).appMetaData()
SaveableFeedState(resultFeedViewModel, null) { listState ->
// TODO (Optional) Instead of a like reaction, do a Kind 31989 NIP89 App recommendation
RenderFeedState(
@ -232,7 +264,6 @@ fun RenderNostrNIP90ContentDiscoveryScreen(
nav,
null,
onEmpty = {
// TODO (Optional) Maybe also show some dvm image/text while waiting for the notes in this custom component
FeedEmpty {
onRefresh()
}
@ -243,7 +274,295 @@ fun RenderNostrNIP90ContentDiscoveryScreen(
}
@Composable
fun FeedEmptywithStatus(
fun FeedDVM(
appDefinitionNote: Note,
latestStatus: NIP90StatusEvent,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
) {
val status = latestStatus.status() ?: return
var currentStatus by remember {
mutableStateOf(status.description)
}
Column(
Modifier
.fillMaxSize()
.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
val card = observeAppDefinition(appDefinitionNote)
card.cover?.let {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier =
Modifier
.size(Size75dp)
.clip(QuoteBorder),
)
} ?: run { NoteAuthorPicture(appDefinitionNote, nav, accountViewModel, Size75dp) }
Spacer(modifier = DoubleVertSpacer)
Text(
text = card.name,
fontWeight = FontWeight.Bold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Spacer(modifier = DoubleVertSpacer)
Text(currentStatus, textAlign = TextAlign.Center)
if (status.code == "payment-required") {
val amountTag = latestStatus.firstAmount()
val amount = amountTag?.amount
val invoice = amountTag?.lnInvoice
val thankYou = stringResource(id = R.string.dvm_waiting_to_confim_payment)
val nwcPaymentRequest = stringResource(id = R.string.nwc_payment_request)
if (invoice != null) {
val context = LocalContext.current
Button(onClick = {
if (accountViewModel.account.hasWalletConnectSetup()) {
accountViewModel.sendZapPaymentRequestFor(
bolt11 = invoice,
zappedNote = null,
onSent = {
currentStatus = nwcPaymentRequest
},
onResponse = { response ->
currentStatus =
if (response is PayInvoiceErrorResponse) {
context.getString(
R.string.wallet_connect_pay_invoice_error_error,
response.error?.message
?: response.error?.code?.toString() ?: "Error parsing error message",
)
} else {
thankYou
}
},
)
} else {
payViaIntent(
invoice,
context,
onPaid = {
currentStatus = thankYou
},
onError = {
currentStatus = it
},
)
}
}) {
val amountInInvoice =
try {
LnInvoiceUtil.getAmountInSats(invoice).toLong()
} catch (e: Exception) {
null
}
if (amountInInvoice != null) {
Text(text = "Pay $amountInInvoice sats to the DVM")
} else {
Text(text = "Pay Invoice from the DVM")
}
}
} else if (amount != null) {
LoadNote(baseNoteHex = latestStatus.id, accountViewModel = accountViewModel) { stateNote ->
stateNote?.let {
ZapDVMButton(
baseNote = it,
amount = amount,
grayTint = MaterialTheme.colorScheme.onPrimary,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
}
} else if (status.code == "processing") {
currentStatus = status.description
}
}
}
@Composable
fun ZapDVMButton(
baseNote: Note,
amount: Long,
grayTint: Color,
accountViewModel: AccountViewModel,
iconSize: Dp = Size35dp,
iconSizeModifier: Modifier = Size20Modifier,
animationSize: Dp = 14.dp,
nav: (String) -> Unit,
) {
val noteAuthor = baseNote.author ?: return
var wantsToZap by remember { mutableStateOf<List<Long>?>(null) }
var showErrorMessageDialog by remember { mutableStateOf<String?>(null) }
var wantsToPay by
remember(baseNote) {
mutableStateOf<ImmutableList<ZapPaymentHandler.Payable>>(
persistentListOf(),
)
}
// Makes sure the user is loaded to get his ln address
val userState = noteAuthor.live().metadata.observeAsState()
val context = LocalContext.current
val scope = rememberCoroutineScope()
var zappingProgress by remember { mutableFloatStateOf(0f) }
var hasZapped by remember { mutableStateOf(false) }
Button(
onClick = {
customZapClick(
baseNote,
accountViewModel,
context,
onZappingProgress = { progress: Float ->
scope.launch { zappingProgress = progress }
},
onMultipleChoices = { options -> wantsToZap = options },
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
}
},
onPayViaIntent = { wantsToPay = it },
)
},
modifier = Modifier.fillMaxWidth(),
) {
if (wantsToZap != null) {
ZapAmountChoicePopup(
baseNote = baseNote,
zapAmountChoices = listOf(amount / 1000),
popupYOffset = iconSize,
accountViewModel = accountViewModel,
onDismiss = {
wantsToZap = null
zappingProgress = 0f
},
onChangeAmount = {
wantsToZap = null
},
onError = { _, message ->
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = message
}
},
onProgress = {
scope.launch(Dispatchers.Main) { zappingProgress = it }
},
onPayViaIntent = { wantsToPay = it },
)
}
if (showErrorMessageDialog != null) {
ErrorMessageDialog(
title = stringResource(id = R.string.error_dialog_zap_error),
textContent = showErrorMessageDialog ?: "",
onClickStartMessage = {
baseNote.author?.let {
scope.launch(Dispatchers.IO) {
val route = routeToMessage(it, showErrorMessageDialog, accountViewModel)
nav(route)
}
}
},
onDismiss = { showErrorMessageDialog = null },
)
}
if (wantsToPay.isNotEmpty()) {
PayViaIntentDialog(
payingInvoices = wantsToPay,
accountViewModel = accountViewModel,
onClose = { wantsToPay = persistentListOf() },
onError = {
wantsToPay = persistentListOf()
scope.launch {
zappingProgress = 0f
showErrorMessageDialog = it
}
},
justShowError = {
scope.launch {
showErrorMessageDialog = it
}
},
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = iconSizeModifier,
) {
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
Spacer(ModifierWidth3dp)
CircularProgressIndicator(
progress =
animateFloatAsState(
targetValue = zappingProgress,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
label = "ZapIconIndicator",
)
.value,
modifier = remember { Modifier.size(animationSize) },
strokeWidth = 2.dp,
color = grayTint,
)
} else {
ObserveZapIcon(
baseNote,
accountViewModel,
) { wasZappedByLoggedInUser ->
LaunchedEffect(wasZappedByLoggedInUser.value) {
hasZapped = wasZappedByLoggedInUser.value
if (wasZappedByLoggedInUser.value && !accountViewModel.account.hasDonatedInThisVersion()) {
delay(1000)
accountViewModel.markDonatedInThisVersion()
}
}
Crossfade(targetState = wasZappedByLoggedInUser.value, label = "ZapIcon") {
if (it) {
ZappedIcon(iconSizeModifier)
} else {
ZapIcon(iconSizeModifier, grayTint)
}
}
}
}
}
if (hasZapped) {
Text(text = stringResource(id = R.string.thank_you))
} else {
Text(text = "Zap " + (amount / 1000).toString() + " sats to the DVM") // stringResource(id = R.string.donate_now))
}
}
}
@Composable
fun FeedEmptyWithStatus(
appDefinitionNote: Note,
status: String,
accountViewModel: AccountViewModel,
@ -263,7 +582,10 @@ fun FeedEmptywithStatus(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(Size75dp).clip(QuoteBorder),
modifier =
Modifier
.size(Size75dp)
.clip(QuoteBorder),
)
} ?: run { NoteAuthorPicture(appDefinitionNote, nav, accountViewModel, Size75dp) }

Wyświetl plik

@ -48,6 +48,7 @@ import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@ -163,7 +164,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.events.AppDefinitionEvent
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
import com.vitorpamplona.quartz.events.BadgeProfilesEvent
@ -1289,14 +1289,8 @@ private fun DisplayBadges(
}
LoadAddressableNote(
aTag =
ATag(
BadgeProfilesEvent.KIND,
baseUser.pubkeyHex,
BadgeProfilesEvent.STANDARD_D_TAG,
null,
),
accountViewModel,
aTag = BadgeProfilesEvent.createAddressTag(baseUser.pubkeyHex),
accountViewModel = accountViewModel,
) { note ->
if (note != null) {
WatchAndRenderBadgeList(note, automaticallyShowProfilePicture, nav)
@ -1342,11 +1336,13 @@ private fun LoadAndRenderBadge(
loadProfilePicture: Boolean,
nav: (String) -> Unit,
) {
var baseNote by remember { mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEventHex)) }
var baseNote by remember(badgeAwardEventHex) { mutableStateOf(LocalCache.getNoteIfExists(badgeAwardEventHex)) }
LaunchedEffect(key1 = badgeAwardEventHex) {
if (baseNote == null) {
launch(Dispatchers.IO) { baseNote = LocalCache.checkGetOrCreateNote(badgeAwardEventHex) }
withContext(Dispatchers.IO) {
baseNote = LocalCache.checkGetOrCreateNote(badgeAwardEventHex)
}
}
}
@ -1438,7 +1434,7 @@ private fun WatchAndRenderBadgeImage(
pictureModifier
.width(size)
.height(size)
.clip(shape = CircleShape)
.clip(shape = CutCornerShape(20))
.background(bgColor)
.run {
if (onClick != null) {

Wyświetl plik

@ -711,4 +711,9 @@
<string name="it_s_not_possible_to_zap_to_a_draft_note">Il n\'est pas possible de zapper un brouillon</string>
<string name="draft_note">Brouillon</string>
<string name="load_from_text">Depuis message</string>
<string name="dvm_looking_for_app">Recherche d\'application</string>
<string name="dvm_waiting_status">Tâche demandée, en attente d\'une réponse</string>
<string name="dvm_requesting_job">Tâche demandée par DVM</string>
<string name="nwc_payment_request">Demande de paiement envoyée, en attente de confirmation depuis votre portefeuille</string>
<string name="dvm_waiting_to_confim_payment">En attente que DVM confirme le paiement ou envoie les résultats</string>
</resources>

Wyświetl plik

@ -429,6 +429,7 @@
<string name="are_you_sure_you_want_to_log_out">Uitloggen verwijdert al je lokale informatie. Zorg ervoor dat je een back-up hebt van je geheime sleutels om te voorkomen dat je je account kwijtraakt. Wilt u doorgaan?</string>
<string name="followed_tags">Gevolgde tags</string>
<string name="relay_setup">Relays</string>
<string name="discover_content">Note ontdekking</string>
<string name="discover_marketplace">Marktplaats</string>
<string name="discover_live">Live</string>
<string name="discover_community">Community</string>
@ -669,6 +670,10 @@
<string name="show_npub_as_a_qr_code">Toon npub als een QR-code</string>
<string name="invalid_nip19_uri">Ongeldig adres</string>
<string name="invalid_nip19_uri_description">Amethist ontving een URI om te openen, maar die uri was ongeldig: %1$s</string>
<string name="dm_relays_not_found">Stel uw Privé Postvak relays in</string>
<string name="dm_relays_not_found_description">Deze instelling geeft iedereen informatie over de relays die gebruikt worden bij het verzenden van berichten. Zonder hen mis je mogelijk een bericht.</string>
<string name="dm_relays_not_found_explanation">DM postvak relays accepteren berichten van iedereen, maar kunnen ze alleen downloaden. Bijvoorbeeld, inbox.nostr.wine werkt op deze manier. </string>
<string name="dm_relays_not_found_create_now">Nu instellen</string>
<string name="zap_the_devs_title">Zap de devs!</string>
<string name="zap_the_devs_description">Jouw donatie helpt ons om een verschil te maken. Elke sat telt!</string>
<string name="donate_now">Nu doneren</string>
@ -706,4 +711,9 @@
<string name="it_s_not_possible_to_zap_to_a_draft_note">Het is niet mogelijk om een concept op te zappen</string>
<string name="draft_note">Concept notitie</string>
<string name="load_from_text">Van Msg</string>
<string name="dvm_looking_for_app">Zoeken naar applicatie</string>
<string name="dvm_waiting_status">Taak aangevraagd, wachten op een antwoord</string>
<string name="dvm_requesting_job">Taak aanvragen bij DVM</string>
<string name="nwc_payment_request">Betalingsverzoek verzonden, wachten op bevestiging van uw wallet</string>
<string name="dvm_waiting_to_confim_payment">Wachten op DVM-betaling te bevestigen of resultaten te verzenden</string>
</resources>

Wyświetl plik

@ -850,4 +850,6 @@
<string name="dvm_looking_for_app">Looking for Application</string>
<string name="dvm_waiting_status">Job Requested, waiting for a reply</string>
<string name="dvm_requesting_job">Requesting Job from DVM</string>
<string name="nwc_payment_request">Payment request sent, waiting for confirmation from your wallet</string>
<string name="dvm_waiting_to_confim_payment">Waiting for DVM to confirm payment or send results</string>
</resources>

Wyświetl plik

@ -118,4 +118,13 @@ class TextFieldValueExtensionTest {
assertEquals("a http://a.b b", next.text)
assertEquals(TextRange(13, 13), next.selection)
}
@Test
fun testInsertAfterNewLine() {
val current = TextFieldValue("ab\n\n", selection = TextRange(4, 4))
val next = current.insertUrlAtCursor("https://i.nostr.build/zdMW4.jpg")
assertEquals("ab\n\nhttps://i.nostr.build/zdMW4.jpg", next.text)
assertEquals(TextRange(35, 35), next.selection)
}
}

Wyświetl plik

@ -24,15 +24,15 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
fun TextFieldValue.insertUrlAtCursor(url: String): TextFieldValue {
var toInsert = url
if (selection.start > 0 && text[selection.start - 1] != ' ') {
var toInsert = url.trim()
if (selection.start > 0 && text[selection.start - 1] != ' ' && text[selection.start - 1] != '\n') {
toInsert = " $toInsert"
}
// takes the position before adding an empty char after the url
val endOfUrlIndex = selection.start + toInsert.length
if (selection.end < text.length && text[selection.end] != ' ') {
if (selection.end < text.length && text[selection.end] != ' ' && text[selection.end] != '\n') {
toInsert = "$toInsert "
}

Wyświetl plik

@ -1,7 +1,7 @@
[versions]
accompanistAdaptive = "0.34.0"
activityCompose = "1.9.0"
agp = "8.4.0"
agp = "8.4.1"
androidKotlinGeohash = "1.0"
androidLifecycle = "2.7.0"
androidxJunit = "1.2.0-alpha04"
@ -16,7 +16,7 @@ composeBom = "2024.05.00"
coreKtx = "1.13.1"
espressoCore = "3.5.1"
firebaseBom = "33.0.0"
fragmentKtx = "1.7.0"
fragmentKtx = "1.7.1"
gms = "4.4.1"
jacksonModuleKotlin = "2.17.1"
jna = "5.14.0"
@ -28,7 +28,7 @@ lazysodiumAndroid = "5.1.0"
lightcompressor = "1.3.2"
markdown = "077a2cde64"
media3 = "1.3.1"
mockk = "1.13.10"
mockk = "1.13.11"
navigationCompose = "2.7.7"
okhttp = "5.0.0-alpha.14"
runner = "1.5.2"

Wyświetl plik

@ -33,21 +33,17 @@ class BadgeProfilesEvent(
content: String,
sig: HexKey,
) : BaseAddressableEvent(id, pubKey, createdAt, KIND, tags, content, sig) {
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun badgeAwardEvents() = taggedEvents()
fun badgeAwardDefinitions() =
tags
.filter { it.firstOrNull() == "a" }
.mapNotNull {
val aTagValue = it.getOrNull(1)
val relay = it.getOrNull(2)
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
fun badgeAwardDefinitions() = taggedAddresses()
companion object {
const val KIND = 30008
const val STANDARD_D_TAG = "profile_badges"
const val ALT = "List of accepted badges by the author"
private const val STANDARD_D_TAG = "profile_badges"
private const val ALT = "List of accepted badges by the author"
fun createAddressTag(pubKey: HexKey): ATag {
return ATag(KIND, pubKey, STANDARD_D_TAG, null)
}
}
}

Wyświetl plik

@ -34,6 +34,39 @@ class NIP90StatusEvent(
content: String,
sig: HexKey,
) : Event(id, pubKey, createdAt, KIND, tags, content, sig) {
class StatusCode(val code: String, val description: String)
class AmountInvoice(val amount: Long?, val lnInvoice: String?)
fun status(): StatusCode? {
return tags.firstOrNull { it.size > 1 && it[0] == "status" }?.let {
if (it.size > 2 && content == "") {
StatusCode(it[1], it[2])
} else {
StatusCode(it[1], content)
}
}
}
fun firstAmount(): AmountInvoice? {
return tags.firstOrNull { it.size > 1 && it[0] == "amount" }?.let {
val amount = it[1].toLongOrNull()
if (it.size > 2) {
if (it[2].isNotBlank()) {
AmountInvoice(amount, it[2])
} else {
null
}
} else {
if (amount != null) {
AmountInvoice(amount, null)
} else {
null
}
}
}
}
companion object {
const val KIND = 7000
const val ALT = "NIP90 Status update"