mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-30 12:36:00 +02:00
Merge branch 'main' into tr
This commit is contained in:
commit
26273270ba
@ -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 {
|
||||
|
@ -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)}")
|
||||
|
@ -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)
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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.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,
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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>) {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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] }
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user