Merge branch 'main' into tr

This commit is contained in:
Vitor Pamplona
2023-12-08 11:21:46 -05:00
committed by GitHub
22 changed files with 715 additions and 88 deletions
@@ -20,11 +20,8 @@ class PublicChatChannel(idHex: String) : Channel(idHex) {
var info = ChannelCreateEvent.ChannelData(null, null, null) var info = ChannelCreateEvent.ChannelData(null, null, null)
fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) { fun updateChannelInfo(creator: User, channelInfo: ChannelCreateEvent.ChannelData, updatedAt: Long) {
this.creator = creator
this.info = channelInfo this.info = channelInfo
this.updatedMetadataAt = updatedAt super.updateChannelInfo(creator, updatedAt)
live.invalidateData()
} }
override fun toBestDisplayName(): String { override fun toBestDisplayName(): String {
@@ -781,7 +781,7 @@ object LocalCache {
fun consume(event: ChannelMetadataEvent) { fun consume(event: ChannelMetadataEvent) {
val channelId = event.channel() 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 if (channelId.isNullOrBlank()) return
// new event // new event
@@ -789,10 +789,8 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey) val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) { if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel.creator == null || oldChannel.creator == author) { if (oldChannel is PublicChatChannel) {
if (oldChannel is PublicChatChannel) { oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
}
} }
} else { } else {
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}") // Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()} ${formattedDateTime(event.createdAt)} > ${formattedDateTime(oldUser.updatedAt)}")
@@ -1,7 +1,12 @@
package com.vitorpamplona.amethyst.service package com.vitorpamplona.amethyst.service
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.BuildConfig
import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.time.Duration import java.time.Duration
@@ -48,11 +53,24 @@ object HttpClient {
.readTimeout(duration) .readTimeout(duration)
.connectTimeout(duration) .connectTimeout(duration)
.writeTimeout(duration) .writeTimeout(duration)
.addInterceptor(DefaultContentTypeInterceptor())
.followRedirects(true) .followRedirects(true)
.followSslRedirects(true) .followSslRedirects(true)
.build() .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 { fun getHttpClientForRelays(): OkHttpClient {
if (this.defaultHttpClient == null) { if (this.defaultHttpClient == null) {
this.defaultHttpClient = getHttpClient(defaultTimeout) this.defaultHttpClient = getHttpClient(defaultTimeout)
@@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.service.relays.TypedFilter
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChannelMessageEvent import com.vitorpamplona.quartz.events.ChannelMessageEvent
import com.vitorpamplona.quartz.events.ChannelMetadataEvent import com.vitorpamplona.quartz.events.ChannelMetadataEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
@@ -42,6 +43,54 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
job?.cancel() 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> { fun createLiveStreamFilter(): List<TypedFilter> {
val follows = account.liveDiscoveryFollowLists.value?.users?.toList() val follows = account.liveDiscoveryFollowLists.value?.users?.toList()
@@ -238,16 +287,19 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
} }
override fun updateChannelFilters() { override fun updateChannelFilters() {
discoveryFeedChannel.typedFilters = createLiveStreamFilter().plus(createPublicChatFilter()).plus( discoveryFeedChannel.typedFilters = createLiveStreamFilter()
listOfNotNull( .plus(createPublicChatFilter())
createLiveStreamTagsFilter(), .plus(createMarketplaceFilter())
createLiveStreamGeohashesFilter(), .plus(
createCommunitiesFilter(), listOfNotNull(
createPublicChatsTagsFilter(), createLiveStreamTagsFilter(),
createCommunitiesTagsFilter(), createLiveStreamGeohashesFilter(),
createCommunitiesGeohashesFilter(), createCommunitiesFilter(),
createPublicChatsGeohashesFilter() createCommunitiesTagsFilter(),
) createCommunitiesGeohashesFilter(),
).ifEmpty { null } createPublicChatsTagsFilter(),
createPublicChatsGeohashesFilter()
)
).ifEmpty { null }
} }
} }
@@ -1,10 +1,12 @@
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth 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.foundation.layout.size
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -22,7 +26,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDirection 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat.startActivity 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.R
import com.vitorpamplona.amethyst.model.ThemeType
import com.vitorpamplona.amethyst.service.CashuProcessor import com.vitorpamplona.amethyst.service.CashuProcessor
import com.vitorpamplona.amethyst.service.CashuToken import com.vitorpamplona.amethyst.service.CashuToken
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.note.CashuIcon import com.vitorpamplona.amethyst.ui.note.CashuIcon
import com.vitorpamplona.amethyst.ui.note.CopyIcon 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.note.ZapIcon
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder 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.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20dp 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.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -84,13 +96,45 @@ fun CashuPreview(cashutoken: String, accountViewModel: AccountViewModel) {
@Composable @Composable
fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) { fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
val lud16 = remember(accountViewModel) { CashuPreviewNew(token, accountViewModel::meltCashu, accountViewModel::toast)
accountViewModel.account.userProfile().info?.lud16 }
}
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 context = LocalContext.current
val scope = rememberCoroutineScope()
val clipboardManager = LocalClipboardManager.current val clipboardManager = LocalClipboardManager.current
Column( Column(
@@ -148,28 +192,10 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
Button( Button(
onClick = { onClick = {
if (lud16 != null) { isRedeeming = true
scope.launch(Dispatchers.IO) { melt(token, context) { title, message ->
isRedeeming = true toast(title, message)
CashuProcessor().melt( isRedeeming = false
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())
)
} }
}, },
shape = QuoteBorder, shape = QuoteBorder,
@@ -196,12 +222,12 @@ fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
Button( Button(
onClick = { onClick = {
try { 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 intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(context, intent, null) startActivity(context, intent, null)
} catch (e: Exception) { } 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, 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)
}
}
}
}
}
@@ -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()
}
}
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel 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.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
@@ -61,6 +62,7 @@ fun AppNavigation(
knownFeedViewModel: NostrChatroomListKnownFeedViewModel, knownFeedViewModel: NostrChatroomListKnownFeedViewModel,
newFeedViewModel: NostrChatroomListNewFeedViewModel, newFeedViewModel: NostrChatroomListNewFeedViewModel,
videoFeedViewModel: NostrVideoFeedViewModel, videoFeedViewModel: NostrVideoFeedViewModel,
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel, discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel, discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel, discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
@@ -137,6 +139,7 @@ fun AppNavigation(
Route.Discover.let { route -> Route.Discover.let { route ->
composable(route.route, route.arguments, content = { composable(route.route, route.arguments, content = {
DiscoverScreen( DiscoverScreen(
discoveryMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel, discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel, discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel, discoveryChatFeedViewModel = discoveryChatFeedViewModel,
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable 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.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow 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.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged 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.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag 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.DividerThickness
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer 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.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size35dp 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.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.StdPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_ENDED 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_LIVE
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
import com.vitorpamplona.quartz.events.Participant import com.vitorpamplona.quartz.events.Participant
import com.vitorpamplona.quartz.events.Price
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -95,7 +104,7 @@ fun ChannelCardCompose(
) { ) {
val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null) val hasEvent by baseNote.live().hasEvent.observeAsState(baseNote.event != null)
Crossfade(targetState = hasEvent) { Crossfade(targetState = hasEvent, label = "ChannelCardCompose") {
if (it) { if (it) {
if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) { if (forceEventKind == null || baseNote.event?.kind() == forceEventKind) {
CheckHiddenChannelCardCompose( CheckHiddenChannelCardCompose(
@@ -155,7 +164,7 @@ fun CheckHiddenChannelCardCompose(
note.isHiddenFor(it) note.isHiddenFor(it)
}.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note)) }.distinctUntilChanged().observeAsState(accountViewModel.isNoteHidden(note))
Crossfade(targetState = isHidden) { Crossfade(targetState = isHidden, label = "CheckHiddenChannelCardCompose") {
if (!it) { if (!it) {
LoadedChannelCardCompose( LoadedChannelCardCompose(
note, note,
@@ -195,7 +204,7 @@ fun LoadedChannelCardCompose(
} }
} }
Crossfade(targetState = state) { Crossfade(targetState = state, label = "CheckHiddenChannelCardCompose") {
RenderChannelCardReportState( RenderChannelCardReportState(
it, it,
note, note,
@@ -220,7 +229,7 @@ fun RenderChannelCardReportState(
) { ) {
var showReportedNote by remember { mutableStateOf(false) } var showReportedNote by remember { mutableStateOf(false) }
Crossfade(targetState = !state.isAcceptable && !showReportedNote) { showHiddenNote -> Crossfade(targetState = !state.isAcceptable && !showReportedNote, label = "CheckHiddenChannelCardCompose") { showHiddenNote ->
if (showHiddenNote) { if (showHiddenNote) {
HiddenNote( HiddenNote(
state.relevantReports, state.relevantReports,
@@ -334,6 +343,28 @@ fun InnerChannelCardWithReactions(
baseNote: Note, baseNote: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit 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) { Column(StdPadding) {
SensitivityWarning( 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 @Composable
private fun RenderNoteRow( private fun RenderNoteRow(
baseNote: Note, 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 @Immutable
data class LiveActivityCard( data class LiveActivityCard(
val name: String, val name: String,
@@ -13,6 +13,7 @@ import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Link import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.MoreVert 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.PushPin
import androidx.compose.material.icons.filled.Report import androidx.compose.material.icons.filled.Report
import androidx.compose.material.icons.filled.VolumeOff 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 @Composable
fun ExpandLessIcon(modifier: Modifier) { fun ExpandLessIcon(modifier: Modifier) {
Icon( Icon(
@@ -13,6 +13,8 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState 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.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button import androidx.compose.material3.Button
@@ -64,7 +66,9 @@ fun RefresheableView(
val modifier = remember { val modifier = remember {
if (enablePullRefresh) { if (enablePullRefresh) {
Modifier.fillMaxSize().pullRefresh(pullRefreshState) Modifier
.fillMaxSize()
.pullRefresh(pullRefreshState)
} else { } else {
Modifier.fillMaxSize() Modifier.fillMaxSize()
} }
@@ -115,6 +119,23 @@ fun SaveableFeedState(
content(listState) 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 @Composable
private fun RenderFeed( private fun RenderFeed(
viewModel: FeedViewModel, 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) @OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@Composable @Composable
private fun FeedLoaded( private fun FeedLoaded(
@@ -190,7 +226,9 @@ private fun FeedLoaded(
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item -> itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
val (value, elapsed) = measureTimedValue { val (value, elapsed) = measureTimedValue {
val defaultModifier = remember { val defaultModifier = remember {
Modifier.fillMaxWidth().animateItemPlacement() Modifier
.fillMaxWidth()
.animateItemPlacement()
} }
Row(defaultModifier) { Row(defaultModifier) {
@@ -26,6 +26,7 @@ import com.vitorpamplona.amethyst.ui.dal.CommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverChatFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter import com.vitorpamplona.amethyst.ui.dal.DiscoverCommunityFeedFilter
import com.vitorpamplona.amethyst.ui.dal.DiscoverLiveFeedFilter 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.FeedFilter
import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter import com.vitorpamplona.amethyst.ui.dal.GeoHashFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HashtagFeedFilter 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 NostrDiscoverLiveFeedViewModel(val account: Account) : FeedViewModel(DiscoverLiveFeedFilter(account)) {
class Factory(val account: Account) : ViewModelProvider.Factory { class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <NostrDiscoverLiveFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverLiveFeedViewModel>): NostrDiscoverLiveFeedViewModel { override fun <NostrDiscoverLiveFeedViewModel : ViewModel> create(modelClass: Class<NostrDiscoverLiveFeedViewModel>): NostrDiscoverLiveFeedViewModel {
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -22,6 +23,7 @@ object ScrollStateKeys {
val HOME_FOLLOWS = Route.Home.base + "Follows" val HOME_FOLLOWS = Route.Home.base + "Follows"
val HOME_REPLIES = Route.Home.base + "FollowsReplies" val HOME_REPLIES = Route.Home.base + "FollowsReplies"
val DISCOVER_MARKETPLACE = Route.Home.base + "Marketplace"
val DISCOVER_LIVE = Route.Home.base + "Live" val DISCOVER_LIVE = Route.Home.base + "Live"
val DISCOVER_COMMUNITY = Route.Home.base + "Communities" val DISCOVER_COMMUNITY = Route.Home.base + "Communities"
val DISCOVER_CHATS = Route.Home.base + "Chats" val DISCOVER_CHATS = Route.Home.base + "Chats"
@@ -32,6 +34,31 @@ object PagerStateKeys {
const val DISCOVER_SCREEN = "PagerDiscover" 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 @Composable
fun rememberForeverLazyListState( fun rememberForeverLazyListState(
key: String, key: String,
@@ -27,6 +27,8 @@ import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserState 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.HttpClient
import com.vitorpamplona.amethyst.service.Nip05Verifier import com.vitorpamplona.amethyst.service.Nip05Verifier
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
@@ -1047,6 +1049,34 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
account.dismissPaymentRequest(request) 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>) { class HasNotificationDot(bottomNavigationItems: ImmutableList<Route>) {
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight 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.HorizontalPager
import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.PagerState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.Tab import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect 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.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel 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.PagerStateKeys
import com.vitorpamplona.amethyst.ui.screen.RefresheableView import com.vitorpamplona.amethyst.ui.screen.RefresheableView
import com.vitorpamplona.amethyst.ui.screen.SaveableFeedState 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.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.FeedPadding import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@@ -64,6 +68,7 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun DiscoverScreen( fun DiscoverScreen(
discoveryMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel, discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel, discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel, discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
@@ -75,6 +80,7 @@ fun DiscoverScreen(
val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) { val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) {
mutableStateOf( mutableStateOf(
listOf( 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_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_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) 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 } val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size }
WatchAccountForDiscoveryScreen( WatchAccountForDiscoveryScreen(
discoverMarketplaceFeedViewModel = discoveryMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel, discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel, discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel, discoveryChatFeedViewModel = discoveryChatFeedViewModel,
@@ -122,11 +129,12 @@ private fun DiscoverPages(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
TabRow( ScrollableTabRow(
containerColor = MaterialTheme.colorScheme.background, containerColor = MaterialTheme.colorScheme.background,
contentColor = MaterialTheme.colorScheme.onBackground, contentColor = MaterialTheme.colorScheme.onBackground,
selectedTabIndex = pagerState.currentPage, selectedTabIndex = pagerState.currentPage,
modifier = TabRowHeight modifier = TabRowHeight,
edgePadding = 8.dp
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
@@ -145,15 +153,28 @@ private fun DiscoverPages(
HorizontalPager(state = pagerState) { page -> HorizontalPager(state = pagerState) { page ->
RefresheableView(tabs[page].viewModel, true) { RefresheableView(tabs[page].viewModel, true) {
SaveableFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState -> if (tabs[page].viewModel is NostrDiscoverMarketplaceFeedViewModel) {
RenderDiscoverFeed( SaveableGridFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
viewModel = tabs[page].viewModel, RenderDiscoverFeed(
routeForLastRead = tabs[page].routeForLastRead, viewModel = tabs[page].viewModel,
forceEventKind = tabs[page].forceEventKind, routeForLastRead = tabs[page].routeForLastRead,
listState = listState, forceEventKind = tabs[page].forceEventKind,
accountViewModel = accountViewModel, listState = listState,
nav = nav 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, viewModel: FeedViewModel,
routeForLastRead: String?, routeForLastRead: String?,
forceEventKind: Int?, forceEventKind: Int?,
listState: LazyListState, listState: ScrollableState,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit nav: (String) -> Unit
) { ) {
@@ -189,14 +210,25 @@ private fun RenderDiscoverFeed(
} }
is FeedState.Loaded -> { is FeedState.Loaded -> {
DiscoverFeedLoaded( if (listState is LazyGridState) {
state, DiscoverFeedColumnsLoaded(
routeForLastRead, state,
listState, routeForLastRead,
forceEventKind, listState,
accountViewModel, forceEventKind,
nav accountViewModel,
) nav
)
} else if (listState is LazyListState) {
DiscoverFeedLoaded(
state,
routeForLastRead,
listState,
forceEventKind,
accountViewModel,
nav
)
}
} }
is FeedState.Loading -> { is FeedState.Loading -> {
@@ -208,6 +240,7 @@ private fun RenderDiscoverFeed(
@Composable @Composable
fun WatchAccountForDiscoveryScreen( fun WatchAccountForDiscoveryScreen(
discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel, discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel,
discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel, discoveryCommunityFeedViewModel: NostrDiscoverCommunityFeedViewModel,
discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel, discoveryChatFeedViewModel: NostrDiscoverChatFeedViewModel,
@@ -217,6 +250,7 @@ fun WatchAccountForDiscoveryScreen(
LaunchedEffect(accountViewModel, listState) { LaunchedEffect(accountViewModel, listState) {
NostrDiscoveryDataSource.resetFilters() NostrDiscoveryDataSource.resetFilters()
discoverMarketplaceFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop() discoveryLiveFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop() discoveryCommunityFeedViewModel.checkKeysInvalidateDataAndSendToTop()
discoveryChatFeedViewModel.checkKeysInvalidateDataAndSendToTop() discoveryChatFeedViewModel.checkKeysInvalidateDataAndSendToTop()
@@ -260,7 +294,7 @@ private fun DiscoverFeedLoaded(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
private fun DiscoverFeedTwoColumnsLoaded( private fun DiscoverFeedColumnsLoaded(
state: FeedState.Loaded, state: FeedState.Loaded,
routeForLastRead: String?, routeForLastRead: String?,
listState: LazyGridState, listState: LazyGridState,
@@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverChatFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverCommunityFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrDiscoverLiveFeedViewModel 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.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
@@ -169,6 +170,11 @@ fun MainScreen(
factory = NostrVideoFeedViewModel.Factory(accountViewModel.account) factory = NostrVideoFeedViewModel.Factory(accountViewModel.account)
) )
val discoverMarketplaceFeedViewModel: NostrDiscoverMarketplaceFeedViewModel = viewModel(
key = "NostrDiscoveryMarketplaceFeedViewModel",
factory = NostrDiscoverMarketplaceFeedViewModel.Factory(accountViewModel.account)
)
val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel = viewModel( val discoveryLiveFeedViewModel: NostrDiscoverLiveFeedViewModel = viewModel(
key = "NostrDiscoveryLiveFeedViewModel", key = "NostrDiscoveryLiveFeedViewModel",
factory = NostrDiscoverLiveFeedViewModel.Factory(accountViewModel.account) factory = NostrDiscoverLiveFeedViewModel.Factory(accountViewModel.account)
@@ -364,6 +370,7 @@ fun MainScreen(
knownFeedViewModel = knownFeedViewModel, knownFeedViewModel = knownFeedViewModel,
newFeedViewModel = newFeedViewModel, newFeedViewModel = newFeedViewModel,
videoFeedViewModel = videoFeedViewModel, videoFeedViewModel = videoFeedViewModel,
discoverMarketplaceFeedViewModel = discoverMarketplaceFeedViewModel,
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel, discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel, discoveryCommunityFeedViewModel = discoveryCommunityFeedViewModel,
discoveryChatFeedViewModel = discoveryChatFeedViewModel, discoveryChatFeedViewModel = discoveryChatFeedViewModel,
@@ -27,6 +27,7 @@ val BottomTopHeight = Modifier.height(50.dp)
val TabRowHeight = Modifier val TabRowHeight = Modifier
val SmallBorder = RoundedCornerShape(7.dp) val SmallBorder = RoundedCornerShape(7.dp)
val SmallishBorder = RoundedCornerShape(9.dp)
val QuoteBorder = RoundedCornerShape(15.dp) val QuoteBorder = RoundedCornerShape(15.dp)
val ButtonBorder = RoundedCornerShape(20.dp) val ButtonBorder = RoundedCornerShape(20.dp)
val EditFieldBorder = RoundedCornerShape(25.dp) val EditFieldBorder = RoundedCornerShape(25.dp)
@@ -37,17 +37,17 @@ import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
private val DarkColorPalette = darkColorScheme( private val DarkColorPalette = darkColorScheme(
primary = Purple200, primary = Purple200,
secondary = Teal200, secondary = Teal200,
// secondary = Purple700,
tertiary = Teal200, tertiary = Teal200,
background = Color(0xFF000000), background = Color(0xFF000000),
surface = Color(0xFF000000) surface = Color(0xFF000000),
surfaceVariant = Color(red = 29, green = 26, blue = 34)
) )
private val LightColorPalette = lightColorScheme( private val LightColorPalette = lightColorScheme(
primary = Purple500, primary = Purple500,
secondary = Teal200, secondary = Teal200,
// secondary = Purple700, tertiary = Teal200,
tertiary = Teal200 surfaceVariant = Color(red = 250, green = 245, blue = 252)
) )
private val DarkNewItemBackground = DarkColorPalette.primary.copy(0.12f) private val DarkNewItemBackground = DarkColorPalette.primary.copy(0.12f)
File diff suppressed because one or more lines are too long
+38
View File
@@ -406,6 +406,9 @@
<string name="payment">Betaling</string> <string name="payment">Betaling</string>
<string name="cashu">Cashu token</string> <string name="cashu">Cashu token</string>
<string name="cashu_redeem">Inwisselen</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="no_lightning_address_set">Geen Lightning-adres ingesteld</string>
<string name="copied_token_to_clipboard">Token gekopieerd naar klembord</string> <string name="copied_token_to_clipboard">Token gekopieerd naar klembord</string>
<string name="live_stream_live_tag">LIVE</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_play_videos_description">Video\'s en GIF\'s automatisch afspelen</string>
<string name="automatically_show_url_preview_description">Toon URL-voorbeelden</string> <string name="automatically_show_url_preview_description">Toon URL-voorbeelden</string>
<string name="load_image_description">Wanneer afbeeldingen te laden</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_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="copy_the_note_id_to_the_clipboard">Note naar klembord kopiëren</string>
<string name="created_at">Gemaakt op</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_failed_redemption_explainer_already_spent">Cashu tokens zijn al uitgegeven.</string>
<string name="cashu_successful_redemption">Cashu ontvangen</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_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="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="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> <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="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_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="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> </resources>
+1
View File
@@ -489,6 +489,7 @@
<string name="relay_setup">Relays</string> <string name="relay_setup">Relays</string>
<string name="discover_marketplace">Marketplace</string>
<string name="discover_live">Live</string> <string name="discover_live">Live</string>
<string name="discover_community">Community</string> <string name="discover_community">Community</string>
<string name="discover_chat">Chats</string> <string name="discover_chat">Chats</string>
@@ -64,7 +64,9 @@ open class Event(
override fun toJson(): String = mapper.writeValueAsString(toJsonObject()) 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 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] } override fun taggedEvents() = tags.filter { it.size > 1 && it[0] == "e" }.map { it[1] }
@@ -68,6 +68,7 @@ interface EventInterface {
fun zapraiserAmount(): Long? fun zapraiserAmount(): Long?
fun hasAnyTaggedUser(): Boolean fun hasAnyTaggedUser(): Boolean
fun hasTagWithContent(tagName: String): Boolean
fun taggedAddresses(): List<ATag> fun taggedAddresses(): List<ATag>
fun taggedUsers(): List<HexKey> fun taggedUsers(): List<HexKey>