Merge branch 'main' into tr

pull/715/head
Vitor Pamplona 2023-12-08 11:21:46 -05:00 zatwierdzone przez GitHub
commit 26273270ba
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
22 zmienionych plików z 715 dodań i 88 usunięć

Wyświetl plik

@ -20,11 +20,8 @@ class PublicChatChannel(idHex: String) : Channel(idHex) {
var info = ChannelCreateEvent.ChannelData(null, null, null)
fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) {
this.creator = creator
this.info = channelInfo
this.updatedMetadataAt = updatedAt
live.invalidateData()
super.updateChannelInfo(creator, updatedAt)
}
override fun toBestDisplayName(): String {

Wyświetl plik

@ -781,7 +781,7 @@ object LocalCache {
fun consume(event: ChannelMetadataEvent) {
val channelId = event.channel()
// Log.d("MT", "New User ${users.size} ${event.contactMetaData.name}")
// Log.d("MT", "New PublicChatMetadata ${event.channelInfo()}")
if (channelId.isNullOrBlank()) return
// new event
@ -789,10 +789,8 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel.creator == null || oldChannel.creator == author) {
if (oldChannel is PublicChatChannel) {
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
}
if (oldChannel is PublicChatChannel) {
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
}
} else {
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")

Wyświetl plik

@ -1,7 +1,12 @@
package com.vitorpamplona.amethyst.service
import android.util.Log
import com.vitorpamplona.amethyst.BuildConfig
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Proxy
import java.time.Duration
@ -48,11 +53,24 @@ object HttpClient {
.readTimeout(duration)
.connectTimeout(duration)
.writeTimeout(duration)
.addInterceptor(DefaultContentTypeInterceptor())
.followRedirects(true)
.followSslRedirects(true)
.build()
}
class DefaultContentTypeInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest: Request = chain.request()
val requestWithUserAgent: Request = originalRequest
.newBuilder()
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
.build()
return chain.proceed(requestWithUserAgent)
}
}
fun getHttpClientForRelays(): OkHttpClient {
if (this.defaultHttpClient == null) {
this.defaultHttpClient = getHttpClient(defaultTimeout)

Wyświetl plik

@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
@ -42,6 +43,54 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
job?.cancel()
}
fun createMarketplaceFilter(): List<TypedFilter> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
val hashToLoad = account.liveDiscoveryFollowLists.value?.hashtags?.toList()
val geohashToLoad = account.liveDiscoveryFollowLists.value?.geotags?.toList()
return listOfNotNull(
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
authors = follows,
kinds = listOf(ClassifiedsEvent.kind),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
)
),
hashToLoad?.let {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(ClassifiedsEvent.kind),
tags = mapOf(
"t" to it.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
)
)
},
geohashToLoad?.let {
TypedFilter(
types = setOf(FeedType.GLOBAL),
filter = JsonFilter(
kinds = listOf(ClassifiedsEvent.kind),
tags = mapOf(
"g" to it.map {
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
}.flatten()
),
limit = 300,
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultDiscoveryFollowList.value)?.relayList
)
)
}
)
}
fun createLiveStreamFilter(): List<TypedFilter> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
@ -238,16 +287,19 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
}
override fun updateChannelFilters() {
discoveryFeedChannel.typedFilters = createLiveStreamFilter().plus(createPublicChatFilter()).plus(
listOfNotNull(
createLiveStreamTagsFilter(),
createLiveStreamGeohashesFilter(),
createCommunitiesFilter(),
createPublicChatsTagsFilter(),
createCommunitiesTagsFilter(),
createCommunitiesGeohashesFilter(),
createPublicChatsGeohashesFilter()
)
).ifEmpty { null }
discoveryFeedChannel.typedFilters = createLiveStreamFilter()
.plus(createPublicChatFilter())
.plus(createMarketplaceFilter())
.plus(
listOfNotNull(
createLiveStreamTagsFilter(),
createLiveStreamGeohashesFilter(),
createCommunitiesFilter(),
createCommunitiesTagsFilter(),
createCommunitiesGeohashesFilter(),
createPublicChatsTagsFilter(),
createPublicChatsGeohashesFilter()
)
).ifEmpty { null }
}
}

Wyświetl plik

@ -1,10 +1,12 @@
package com.vitorpamplona.amethyst.ui.components
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@ -12,7 +14,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.Divider
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@ -22,7 +26,6 @@ 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.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -35,21 +38,30 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity
import androidx.lifecycle.viewmodel.compose.viewModel
import com.fasterxml.jackson.databind.node.TextNode
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.ThemeType
import com.vitorpamplona.amethyst.service.CashuProcessor
import com.vitorpamplona.amethyst.service.CashuToken
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.note.CashuIcon
import com.vitorpamplona.amethyst.ui.note.CopyIcon
import com.vitorpamplona.amethyst.ui.note.OpenInNewIcon
import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.coroutines.Dispatchers
@ -84,13 +96,45 @@ fun CashuPreview(cashutoken: String, accountViewModel: AccountViewModel) {
@Composable
fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
val lud16 = remember(accountViewModel) {
accountViewModel.account.userProfile().info?.lud16
}
CashuPreviewNew(token, accountViewModel::meltCashu, accountViewModel::toast)
}
val useWebService = false
@Composable
@Preview()
fun CashuPreviewPreview() {
val sharedPreferencesViewModel: SharedPreferencesViewModel = viewModel()
sharedPreferencesViewModel.init()
sharedPreferencesViewModel.updateTheme(ThemeType.DARK)
AmethystTheme(sharedPrefsViewModel = sharedPreferencesViewModel) {
Column() {
CashuPreview(
token = CashuToken("token", "mint", 32400, TextNode("")),
melt = { token, context, onDone ->
},
toast = { title, message ->
}
)
CashuPreviewNew(
token = CashuToken("token", "mint", 32400, TextNode("")),
melt = { token, context, onDone ->
},
toast = { title, message ->
}
)
}
}
}
@Composable
fun CashuPreview(
token: CashuToken,
melt: (CashuToken, Context, (String, String) -> Unit) -> Unit,
toast: (String, String) -> Unit
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current
Column(
@ -148,28 +192,10 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
Button(
onClick = {
if (lud16 != null) {
scope.launch(Dispatchers.IO) {
isRedeeming = true
CashuProcessor().melt(
token,
lud16,
onSuccess = { title, message ->
isRedeeming = false
accountViewModel.toast(title, message)
},
onError = { title, message ->
isRedeeming = false
accountViewModel.toast(title, message)
},
context
)
}
} else {
accountViewModel.toast(
context.getString(R.string.no_lightning_address_set),
context.getString(R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, accountViewModel.account.userProfile().toBestDisplayName())
)
isRedeeming = true
melt(token, context) { title, message ->
toast(title, message)
isRedeeming = false
}
},
shape = QuoteBorder,
@ -196,12 +222,12 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
Button(
onClick = {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("cashu://$token"))
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("cashu://${token.token}"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(context, intent, null)
} catch (e: Exception) {
accountViewModel.toast("Cashu", context.getString(R.string.cashu_no_wallet_found))
toast("Cashu", context.getString(R.string.cashu_no_wallet_found))
}
},
shape = QuoteBorder,
@ -232,3 +258,111 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
}
}
}
@Composable
fun CashuPreviewNew(
token: CashuToken,
melt: (CashuToken, Context, (String, String) -> Unit) -> Unit,
toast: (String, String) -> Unit
) {
val context = LocalContext.current
val clipboardManager = LocalClipboardManager.current
Card(
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp)
.clip(shape = QuoteBorder)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(10.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painterResource(R.drawable.cashu),
null,
modifier = Modifier.size(13.dp),
tint = Color.Unspecified
)
Text(
text = stringResource(R.string.cashu),
fontSize = 12.sp,
modifier = Modifier.padding(start = 5.dp, bottom = 1.dp)
)
}
Text(
text = "${token.totalAmount} ${stringResource(id = R.string.sats)}",
fontSize = 20.sp
)
Row(modifier = Modifier.padding(top = 5.dp)) {
var isRedeeming by remember {
mutableStateOf(false)
}
FilledTonalButton(
onClick = {
isRedeeming = true
melt(token, context) { title, message ->
toast(title, message)
isRedeeming = false
}
},
shape = SmallishBorder
) {
if (isRedeeming) {
LoadingAnimation()
} else {
ZapIcon(Size20dp, tint = MaterialTheme.colorScheme.onBackground)
}
Spacer(StdHorzSpacer)
Text(
"Redeem",
color = MaterialTheme.colorScheme.onBackground,
fontSize = 16.sp
)
}
Spacer(modifier = StdHorzSpacer)
FilledTonalButton(
onClick = {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("cashu://${token.token}"))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(context, intent, null)
} catch (e: Exception) {
toast("Cashu", context.getString(R.string.cashu_no_wallet_found))
}
},
shape = SmallishBorder,
contentPadding = PaddingValues(0.dp)
) {
OpenInNewIcon(Size18Modifier, tint = MaterialTheme.colorScheme.onBackground)
}
Spacer(modifier = StdHorzSpacer)
FilledTonalButton(
onClick = {
// Copying the token to clipboard
clipboardManager.setText(AnnotatedString(token.token))
},
shape = SmallishBorder,
contentPadding = PaddingValues(0.dp)
) {
CopyIcon(Size18Modifier, tint = MaterialTheme.colorScheme.onBackground)
}
}
}
}
}

Wyświetl plik

@ -0,0 +1,72 @@
package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.MuteListEvent
import com.vitorpamplona.quartz.events.PeopleListEvent
import com.vitorpamplona.quartz.utils.TimeUtils
open class DiscoverMarketplaceFeedFilter(
val account: Account
) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
return account.userProfile().pubkeyHex + "-" + followList()
}
open fun followList(): String {
return account.defaultDiscoveryFollowList.value
}
override fun showHiddenKey(): Boolean {
return followList() == PeopleListEvent.blockListFor(account.userProfile().pubkeyHex) ||
followList() == MuteListEvent.blockListFor(account.userProfile().pubkeyHex)
}
override fun feed(): List<Note> {
val classifieds = LocalCache.addressables
.filter { it.value.event is ClassifiedsEvent }
.map { it.value }
val notes = innerApplyFilter(classifieds)
return sort(notes)
}
override fun applyFilter(collection: Set<Note>): Set<Note> {
return innerApplyFilter(collection)
}
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = TimeUtils.now()
val isGlobal = account.defaultDiscoveryFollowList.value == GLOBAL_FOLLOWS
val isHiddenList = showHiddenKey()
val followingKeySet = account.liveDiscoveryFollowLists.value?.users ?: emptySet()
val followingTagSet = account.liveDiscoveryFollowLists.value?.hashtags ?: emptySet()
val followingGeohashSet = account.liveDiscoveryFollowLists.value?.geotags ?: emptySet()
val activities = collection
.asSequence()
.filter {
it.event is ClassifiedsEvent &&
it.event?.hasTagWithContent("image") == true &&
it.event?.hasTagWithContent("price") == true &&
it.event?.hasTagWithContent("title") == true
}
.filter {
isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(followingTagSet) == true || it.event?.isTaggedGeoHashes(followingGeohashSet) == true
}
.filter { isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true }
.filter { (it.createdAt() ?: 0) <= now }
.toSet()
return activities
}
override fun sort(collection: Set<Note>): List<Note> {
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
}
}

Wyświetl plik

@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
@ -61,6 +62,7 @@ fun AppNavigation(
knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
newFeedViewModel: NostrChatroomListNewFeedViewModel,
videoFeedViewModel: NostrVideoFeedViewModel,
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
@ -137,6 +139,7 @@ fun AppNavigation(
Route.Discover.let { route ->
composable(route.route, route.arguments, content = {
DiscoverScreen(
discoveryMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel,

Wyświetl plik

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -15,8 +16,10 @@ 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.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@ -39,6 +42,7 @@ import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
@ -58,23 +62,28 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.EndedFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.Size5dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_ENDED
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
import com.vitorpamplona.quartz.events.Participant
import com.vitorpamplona.quartz.events.Price
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -95,7 +104,7 @@ fun ChannelCardCompose(
) {
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent) {
Crossfade(targetState = hasEvent, label = "ChannelCardCompose") {
if (it) {
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
CheckHiddenChannelCardCompose(
@ -155,7 +164,7 @@ fun CheckHiddenChannelCardCompose(
note.isHiddenFor(it)
}.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note))
Crossfade(targetState = isHidden) {
Crossfade(targetState = isHidden, label = "CheckHiddenChannelCardCompose") {
if (!it) {
LoadedChannelCardCompose(
note,
@ -195,7 +204,7 @@ fun LoadedChannelCardCompose(
}
}
Crossfade(targetState = state) {
Crossfade(targetState = state, label = "CheckHiddenChannelCardCompose") {
RenderChannelCardReportState(
it,
note,
@ -220,7 +229,7 @@ fun RenderChannelCardReportState(
) {
var showReportedNote by remember { mutableStateOf(false) }
Crossfade(targetState = !state.isAcceptable && !showReportedNote) { showHiddenNote ->
Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "CheckHiddenChannelCardCompose") { showHiddenNote ->
if (showHiddenNote) {
HiddenNote(
state.relevantReports,
@ -334,6 +343,28 @@ fun InnerChannelCardWithReactions(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
when (remember { baseNote.event }) {
is LiveActivitiesEvent -> {
InnerCardRow(baseNote, accountViewModel, nav)
}
is CommunityDefinitionEvent -> {
InnerCardRow(baseNote, accountViewModel, nav)
}
is ChannelCreateEvent -> {
InnerCardRow(baseNote, accountViewModel, nav)
}
is ClassifiedsEvent -> {
InnerCardBox(baseNote, accountViewModel, nav)
}
}
}
@Composable
fun InnerCardRow(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Column(StdPadding) {
SensitivityWarning(
@ -353,6 +384,22 @@ fun InnerChannelCardWithReactions(
)
}
@Composable
fun InnerCardBox(
baseNote: Note,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
Column(HalfPadding) {
SensitivityWarning(
note = baseNote,
accountViewModel = accountViewModel
) {
RenderClassifiedsThumb(baseNote, accountViewModel, nav)
}
}
}
@Composable
private fun RenderNoteRow(
baseNote: Note,
@ -372,6 +419,116 @@ private fun RenderNoteRow(
}
}
@Immutable
data class ClassifiedsThumb(
val image: String?,
val title: String?,
val price: Price?
)
@Composable
fun RenderClassifiedsThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val noteEvent = baseNote.event as? ClassifiedsEvent ?: return
val card by baseNote.live().metadata.map {
val noteEvent = it.note.event as? ClassifiedsEvent
ClassifiedsThumb(
image = noteEvent?.image(),
title = noteEvent?.title(),
price = noteEvent?.price()
)
}.distinctUntilChanged().observeAsState(
ClassifiedsThumb(
image = noteEvent.image(),
title = noteEvent.title(),
price = noteEvent.price()
)
)
RenderClassifiedsThumb(card, baseNote.author)
}
@Preview
@Composable
fun RenderClassifiedsThumbPreview() {
Surface(Modifier.size(200.dp)) {
RenderClassifiedsThumb(
card = ClassifiedsThumb(
image = null,
title = "Like New",
price = Price("800000", "SATS", null)
),
author = null
)
}
}
@Composable
fun RenderClassifiedsThumb(card: ClassifiedsThumb, author: User?) {
Box(
Modifier
.fillMaxWidth()
.aspectRatio(1f),
contentAlignment = BottomStart
) {
card.image?.let {
AsyncImage(
model = it,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize()
)
} ?: run {
author?.let {
DisplayAuthorBanner(it)
}
}
Row(
Modifier
.fillMaxWidth()
.background(Color.Black.copy(0.6f))
.padding(Size5dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
card.title?.let {
Text(
text = it,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = Color.White,
modifier = Modifier.weight(1f)
)
}
card.price?.let {
val priceTag = remember(card) {
val newAmount = it.amount.toBigDecimalOrNull()?.let {
showAmountAxis(it)
} ?: it.amount
if (it.frequency != null && it.currency != null) {
"$newAmount ${it.currency}/${it.frequency}"
} else if (it.currency != null) {
"$newAmount ${it.currency}"
} else {
newAmount
}
}
Text(
text = priceTag,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = Color.White
)
}
}
}
}
@Immutable
data class LiveActivityCard(
val name: String,

Wyświetl plik

@ -13,6 +13,7 @@ import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.icons.filled.Report
import androidx.compose.material.icons.filled.VolumeOff
@ -196,6 +197,16 @@ fun CopyIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
)
}
@Composable
fun OpenInNewIcon(modifier: Modifier, tint: Color = Color.Unspecified) {
Icon(
imageVector = Icons.Default.OpenInNew,
stringResource(id = R.string.copy_to_clipboard),
tint = tint,
modifier = modifier
)
}
@Composable
fun ExpandLessIcon(modifier: Modifier) {
Icon(

Wyświetl plik

@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button
@ -64,7 +66,9 @@ fun RefresheableView(
val modifier = remember {
if (enablePullRefresh) {
Modifier.fillMaxSize().pullRefresh(pullRefreshState)
Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
} else {
Modifier.fillMaxSize()
}
@ -115,6 +119,23 @@ fun SaveableFeedState(
content(listState)
}
@Composable
fun SaveableGridFeedState(
viewModel: FeedViewModel,
scrollStateKey: String? = null,
content: @Composable (LazyGridState) -> Unit
) {
val gridState = if (scrollStateKey != null) {
rememberForeverLazyGridState(scrollStateKey)
} else {
rememberLazyGridState()
}
WatchScrollToTop(viewModel, gridState)
content(gridState)
}
@Composable
private fun RenderFeed(
viewModel: FeedViewModel,
@ -174,6 +195,21 @@ private fun WatchScrollToTop(
}
}
@Composable
private fun WatchScrollToTop(
viewModel: FeedViewModel,
listState: LazyGridState
) {
val scrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
LaunchedEffect(scrollToTop) {
if (scrollToTop > 0 && viewModel.scrolltoTopPending) {
listState.scrollToItem(index = 0)
viewModel.sentToTop()
}
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@Composable
private fun FeedLoaded(
@ -190,7 +226,9 @@ private fun FeedLoaded(
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val (value, elapsed) = measureTimedValue {
val defaultModifier = remember {
Modifier.fillMaxWidth().animateItemPlacement()
Modifier
.fillMaxWidth()
.animateItemPlacement()
}
Row(defaultModifier) {

Wyświetl plik

@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.ui.dal.CommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverMarketplaceFeedFilter
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter
@ -72,6 +73,16 @@ class NostrVideoFeedViewModel(val account: Account) : FeedViewModel(VideoFeedFil
}
}
class NostrDiscoverMarketplaceFeedViewModel(val account: Account) : FeedViewModel(
DiscoverMarketplaceFeedFilter(account)
) {
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <NostrDiscoverMarketplaceFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverMarketplaceFeedViewModel>): NostrDiscoverMarketplaceFeedViewModel {
return NostrDiscoverMarketplaceFeedViewModel(account) as NostrDiscoverMarketplaceFeedViewModel
}
}
}
class NostrDiscoverLiveFeedViewModel(val account: Account) : FeedViewModel(DiscoverLiveFeedFilter(account)) {
class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <NostrDiscoverLiveFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverLiveFeedViewModel>): NostrDiscoverLiveFeedViewModel {

Wyświetl plik

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
@ -22,6 +23,7 @@ object ScrollStateKeys {
val HOME_FOLLOWS = Route.Home.base + "Follows"
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
val DISCOVER_MARKETPLACE = Route.Home.base + "Marketplace"
val DISCOVER_LIVE = Route.Home.base + "Live"
val DISCOVER_COMMUNITY = Route.Home.base + "Communities"
val DISCOVER_CHATS = Route.Home.base + "Chats"
@ -32,6 +34,31 @@ object PagerStateKeys {
const val DISCOVER_SCREEN = "PagerDiscover"
}
@Composable
fun rememberForeverLazyGridState(
key: String,
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Int = 0
): LazyGridState {
val scrollState = rememberSaveable(saver = LazyGridState.Saver) {
val savedValue = savedScrollStates[key]
val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex
val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset.toFloat()
LazyGridState(
savedIndex,
savedOffset.roundToInt()
)
}
DisposableEffect(scrollState) {
onDispose {
val lastIndex = scrollState.firstVisibleItemIndex
val lastOffset = scrollState.firstVisibleItemScrollOffset
savedScrollStates[key] = ScrollState(lastIndex, lastOffset.toFloat())
}
}
return scrollState
}
@Composable
fun rememberForeverLazyListState(
key: String,

Wyświetl plik

@ -27,6 +27,8 @@ import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState
import com.vitorpamplona.amethyst.service.CashuProcessor
import com.vitorpamplona.amethyst.service.CashuToken
import com.vitorpamplona.amethyst.service.HttpClient
import com.vitorpamplona.amethyst.service.Nip05Verifier
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
@ -1047,6 +1049,34 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
account.dismissPaymentRequest(request)
}
}
fun meltCashu(
token: CashuToken,
context: Context,
onDone: (String, String) -> Unit
) {
val lud16 = account.userProfile().info?.lud16
if (lud16 != null) {
viewModelScope.launch(Dispatchers.IO) {
CashuProcessor().melt(
token,
lud16,
onSuccess = { title, message ->
onDone(title, message)
},
onError = { title, message ->
onDone(title, message)
},
context
)
}
} else {
onDone(
context.getString(R.string.no_lightning_address_set),
context.getString(R.string.user_x_does_not_have_a_lightning_address_setup_to_receive_sats, account.userProfile().toBestDisplayName())
)
}
}
}
class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) {

Wyświetl plik

@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
@ -18,8 +19,8 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@ -47,14 +48,17 @@ import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.PagerStateKeys
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState
import com.vitorpamplona.amethyst.ui.screen.SaveableGridFeedState
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import kotlinx.collections.immutable.ImmutableList
@ -64,6 +68,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun DiscoverScreen(
discoveryMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
@ -75,6 +80,7 @@ fun DiscoverScreen(
val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) {
mutableStateOf(
listOf(
TabItem(R.string.discover_marketplace, discoveryMarketplaceFeedViewModel, Route.Discover.base + "Marketplace", ScrollStateKeys.DISCOVER_MARKETPLACE, ClassifiedsEvent.kind),
TabItem(R.string.discover_live, discoveryLiveFeedViewModel, Route.Discover.base + "Live", ScrollStateKeys.DISCOVER_LIVE, LiveActivitiesEvent.kind),
TabItem(R.string.discover_community, discoveryCommunityFeedViewModel, Route.Discover.base + "Community", ScrollStateKeys.DISCOVER_COMMUNITY, CommunityDefinitionEvent.kind),
TabItem(R.string.discover_chat, discoveryChatFeedViewModel, Route.Discover.base + "Chats", ScrollStateKeys.DISCOVER_CHATS, ChannelCreateEvent.kind)
@ -85,6 +91,7 @@ fun DiscoverScreen(
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size }
WatchAccountForDiscoveryScreen(
discoverMarketplaceFeedViewModel = discoveryMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel,
@ -122,11 +129,12 @@ private fun DiscoverPages(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
TabRow(
ScrollableTabRow(
containerColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground,
selectedTabIndex = pagerState.currentPage,
modifier = TabRowHeight
modifier = TabRowHeight,
edgePadding = 8.dp
) {
val coroutineScope = rememberCoroutineScope()
@ -145,15 +153,28 @@ private fun DiscoverPages(
HorizontalPager(state = pagerState) { page ->
RefresheableView(tabs[page].viewModel, true) {
SaveableFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
RenderDiscoverFeed(
viewModel = tabs[page].viewModel,
routeForLastRead = tabs[page].routeForLastRead,
forceEventKind = tabs[page].forceEventKind,
listState = listState,
accountViewModel = accountViewModel,
nav = nav
)
if (tabs[page].viewModel is NostrDiscoverMarketplaceFeedViewModel) {
SaveableGridFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
RenderDiscoverFeed(
viewModel = tabs[page].viewModel,
routeForLastRead = tabs[page].routeForLastRead,
forceEventKind = tabs[page].forceEventKind,
listState = listState,
accountViewModel = accountViewModel,
nav = nav
)
}
} else {
SaveableFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
RenderDiscoverFeed(
viewModel = tabs[page].viewModel,
routeForLastRead = tabs[page].routeForLastRead,
forceEventKind = tabs[page].forceEventKind,
listState = listState,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
}
@ -164,7 +185,7 @@ private fun RenderDiscoverFeed(
viewModel: FeedViewModel,
routeForLastRead: String?,
forceEventKind: Int?,
listState: LazyListState,
listState: ScrollableState,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
@ -189,14 +210,25 @@ private fun RenderDiscoverFeed(
}
is FeedState.Loaded -> {
DiscoverFeedLoaded(
state,
routeForLastRead,
listState,
forceEventKind,
accountViewModel,
nav
)
if (listState is LazyGridState) {
DiscoverFeedColumnsLoaded(
state,
routeForLastRead,
listState,
forceEventKind,
accountViewModel,
nav
)
} else if (listState is LazyListState) {
DiscoverFeedLoaded(
state,
routeForLastRead,
listState,
forceEventKind,
accountViewModel,
nav
)
}
}
is FeedState.Loading -> {
@ -208,6 +240,7 @@ private fun RenderDiscoverFeed(
@Composable
fun WatchAccountForDiscoveryScreen(
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
@ -217,6 +250,7 @@ fun WatchAccountForDiscoveryScreen(
LaunchedEffect(accountViewModel, listState) {
NostrDiscoveryDataSource.resetFilters()
discoverMarketplaceFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryChatFeedViewModel.checkKeysInvalidateDataAndSendToTop()
@ -260,7 +294,7 @@ private fun DiscoverFeedLoaded(
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun DiscoverFeedTwoColumnsLoaded(
private fun DiscoverFeedColumnsLoaded(
state: FeedState.Loaded,
routeForLastRead: String?,
listState: LazyGridState,

Wyświetl plik

@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverMarketplaceFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
@ -169,6 +170,11 @@ fun MainScreen(
factory = NostrVideoFeedViewModel.Factory(accountViewModel.account)
)
val discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel = viewModel(
key = "NostrDiscoveryMarketplaceFeedViewModel",
factory = NostrDiscoverMarketplaceFeedViewModel.Factory(accountViewModel.account)
)
val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel = viewModel(
key = "NostrDiscoveryLiveFeedViewModel",
factory = NostrDiscoverLiveFeedViewModel.Factory(accountViewModel.account)
@ -364,6 +370,7 @@ fun MainScreen(
knownFeedViewModel = knownFeedViewModel,
newFeedViewModel = newFeedViewModel,
videoFeedViewModel = videoFeedViewModel,
discoverMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel,

Wyświetl plik

@ -27,6 +27,7 @@ val BottomTopHeight = Modifier.height(50.dp)
val TabRowHeight = Modifier
val SmallBorder = RoundedCornerShape(7.dp)
val SmallishBorder = RoundedCornerShape(9.dp)
val QuoteBorder = RoundedCornerShape(15.dp)
val ButtonBorder = RoundedCornerShape(20.dp)
val EditFieldBorder = RoundedCornerShape(25.dp)

Wyświetl plik

@ -37,17 +37,17 @@ import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
private val DarkColorPalette = darkColorScheme(
primary = Purple200,
secondary = Teal200,
// secondary = Purple700,
tertiary = Teal200,
background = Color(0xFF000000),
surface = Color(0xFF000000)
surface = Color(0xFF000000),
surfaceVariant = Color(red = 29, green = 26, blue = 34)
)
private val LightColorPalette = lightColorScheme(
primary = Purple500,
secondary = Teal200,
// secondary = Purple700,
tertiary = Teal200
tertiary = Teal200,
surfaceVariant = Color(red = 250, green = 245, blue = 252)
)
private val DarkNewItemBackground = DarkColorPalette.primary.copy(0.12f)

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -406,6 +406,9 @@
<string name="payment">Betaling</string>
<string name="cashu">Cashu token</string>
<string name="cashu_redeem">Inwisselen</string>
<string name="cashu_redeem_to_zap">Stuur naar Zap wallet</string>
<string name="cashu_redeem_to_cashu">Openen in Cashu wallet</string>
<string name="cashu_copy_token">Token kopiëren</string>
<string name="no_lightning_address_set">Geen Lightning-adres ingesteld</string>
<string name="copied_token_to_clipboard">Token gekopieerd naar klembord</string>
<string name="live_stream_live_tag">LIVE</string>
@ -475,6 +478,7 @@
<string name="automatically_play_videos_description">Video\'s en GIF\'s automatisch afspelen</string>
<string name="automatically_show_url_preview_description">Toon URL-voorbeelden</string>
<string name="load_image_description">Wanneer afbeeldingen te laden</string>
<string name="copy_to_clipboard">Kopiëren naar klembord</string>
<string name="copy_url_to_clipboard">Kopieer URL naar klembord</string>
<string name="copy_the_note_id_to_the_clipboard">Note naar klembord kopiëren</string>
<string name="created_at">Gemaakt op</string>
@ -530,6 +534,7 @@
<string name="cashu_failed_redemption_explainer_already_spent">Cashu tokens zijn al uitgegeven.</string>
<string name="cashu_successful_redemption">Cashu ontvangen</string>
<string name="cashu_successful_redemption_explainer">%1$s sats zijn naar uw wallet gestuurd. (Fees: %2$s sats)</string>
<string name="cashu_no_wallet_found">Geen compatibele Cashu wallet gevonden in het systeem</string>
<string name="error_unable_to_fetch_invoice">Kan invoice niet ophalen van de servers van de ontvanger</string>
<string name="wallet_connect_pay_invoice_error_error">De volgende fout is geretourneerd door uw wallet connect provider: %1$s</string>
<string name="could_not_connect_to_tor">Kan geen verbinding maken met Tor</string>
@ -561,4 +566,37 @@
<string name="send_the_seller_a_message">Stuur de verkoper een bericht</string>
<string name="hi_seller_is_this_still_available">Hallo %1$s, is dit nog steeds beschikbaar?</string>
<string name="hi_there_is_this_still_available">Hallo daar, is dit nog steeds beschikbaar?</string>
<string name="classifieds">Verkoop een artikel</string>
<string name="classifieds_title">Titel</string>
<string name="classifieds_title_placeholder">iPhone 13</string>
<string name="classifieds_condition">Staat</string>
<string name="classifieds_category">Categorie</string>
<string name="classifieds_price">Prijs (in sats)</string>
<string name="classifieds_price_placeholder">1000</string>
<string name="classifieds_location">Locatie</string>
<string name="classifieds_location_placeholder">Stad, provincie, land</string>
<string name="classifieds_condition_new">Nieuw</string>
<string name="classifieds_condition_new_explainer">Het is een gloednieuw artikel, in de originele doos</string>
<string name="classifieds_condition_like_new">Als nieuw</string>
<string name="classifieds_condition_like_new_explainer">Gebruikt, maar er zijn geen gebruikerssporen</string>
<string name="classifieds_condition_good">Goed</string>
<string name="classifieds_condition_good_explainer">Het heeft een aantal oppervlakkige gebruikerssporen</string>
<string name="classifieds_condition_fair">Redelijk</string>
<string name="classifieds_condition_fair_explainer">Het is nog steeds in acceptabele en functionele vorm</string>
<string name="classifieds_category_clothing">Kleding</string>
<string name="classifieds_category_accessories">Accessoires</string>
<string name="classifieds_category_electronics">Electronica</string>
<string name="classifieds_category_furniture">Meubels</string>
<string name="classifieds_category_collectibles">Verzamelobjecten</string>
<string name="classifieds_category_books">Boeken</string>
<string name="classifieds_category_pets">Huisdieren</string>
<string name="classifieds_category_sports">Sport</string>
<string name="classifieds_category_fitness">Fitness</string>
<string name="classifieds_category_art">Kunst</string>
<string name="classifieds_category_crafts">Ambachten</string>
<string name="classifieds_category_home">Startpagina</string>
<string name="classifieds_category_office">Kantoor</string>
<string name="classifieds_category_food">Eten</string>
<string name="classifieds_category_misc">Overige</string>
<string name="classifieds_category_other">Anders</string>
</resources>

Wyświetl plik

@ -489,6 +489,7 @@
<string name="relay_setup">Relays</string>
<string name="discover_marketplace">Marketplace</string>
<string name="discover_live">Live</string>
<string name="discover_community">Community</string>
<string name="discover_chat">Chats</string>

Wyświetl plik

@ -64,7 +64,9 @@ open class Event(
override fun toJson(): String = mapper.writeValueAsString(toJsonObject())
override fun hasAnyTaggedUser() = tags.any { it.size > 1 && it[0] == "p" }
override fun hasAnyTaggedUser() = hasTagWithContent("p")
override fun hasTagWithContent(tagName: String) = tags.any { it.size > 1 && it[0] == tagName }
override fun taggedUsers() = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }
override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }

Wyświetl plik

@ -68,6 +68,7 @@ interface EventInterface {
fun zapraiserAmount(): Long?
fun hasAnyTaggedUser(): Boolean
fun hasTagWithContent(tagName: String): Boolean
fun taggedAddresses(): List<ATag>
fun taggedUsers(): List<HexKey>