Reverts to the non-Google kotlin style.

This commit is contained in:
Vitor Pamplona
2024-01-06 11:32:41 -05:00
parent 0be1f89368
commit f8e7dd78d9
415 changed files with 64333 additions and 64218 deletions
@@ -28,7 +28,6 @@ import com.vitorpamplona.amethyst.service.Nip96Retriever
import com.vitorpamplona.amethyst.service.Nip96Uploader import com.vitorpamplona.amethyst.service.Nip96Uploader
import com.vitorpamplona.amethyst.ui.actions.ImageDownloader import com.vitorpamplona.amethyst.ui.actions.ImageDownloader
import com.vitorpamplona.quartz.crypto.KeyPair import com.vitorpamplona.quartz.crypto.KeyPair
import java.util.Base64
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail import junit.framework.TestCase.fail
@@ -36,6 +35,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.util.Base64
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ImageUploadTesting { class ImageUploadTesting {
@@ -111,32 +111,38 @@ class ImageUploadTesting {
} }
@Test() @Test()
fun testNostrCheck() = runBlocking { fun testNostrCheck() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostrcheck.me", "https://nostrcheck.me")) testBase(Nip96MediaServers.ServerName("nostrcheck.me", "https://nostrcheck.me"))
} }
@Test() @Test()
fun testNostrage() = runBlocking { fun testNostrage() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostrage", "https://nostrage.com")) testBase(Nip96MediaServers.ServerName("nostrage", "https://nostrage.com"))
} }
@Test() @Test()
fun testSove() = runBlocking { fun testSove() =
runBlocking {
testBase(Nip96MediaServers.ServerName("sove", "https://sove.rent")) testBase(Nip96MediaServers.ServerName("sove", "https://sove.rent"))
} }
@Test() @Test()
fun testNostrBuild() = runBlocking { fun testNostrBuild() =
runBlocking {
testBase(Nip96MediaServers.ServerName("nostr.build", "https://nostr.build")) testBase(Nip96MediaServers.ServerName("nostr.build", "https://nostr.build"))
} }
@Test() @Test()
fun testSovbit() = runBlocking { fun testSovbit() =
runBlocking {
testBase(Nip96MediaServers.ServerName("sovbit", "https://files.sovbit.host")) testBase(Nip96MediaServers.ServerName("sovbit", "https://files.sovbit.host"))
} }
@Test() @Test()
fun testVoidCat() = runBlocking { fun testVoidCat() =
runBlocking {
testBase(Nip96MediaServers.ServerName("void.cat", "https://void.cat")) testBase(Nip96MediaServers.ServerName("void.cat", "https://void.cat"))
} }
} }
@@ -115,7 +115,8 @@ class ThreadAssemblerTest {
.trimIndent() .trimIndent()
@Test @Test
fun threadOrderTest() = runBlocking { fun threadOrderTest() =
runBlocking {
val eventArray = val eventArray =
Event.mapper.readValue<ArrayList<Event>>(db) as List<Event> + Event.fromJson(header) Event.mapper.readValue<ArrayList<Event>>(db) as List<Event> + Event.fromJson(header)
@@ -151,12 +151,11 @@ fun SelectNotificationProvider(sharedPreferencesViewModel: SharedPreferencesView
} }
@Composable @Composable
fun LoadDistributors( fun LoadDistributors(onInner: @Composable (String, ImmutableList<String>, ImmutableList<TitleExplainer>) -> Unit) {
onInner: @Composable (String, ImmutableList<String>, ImmutableList<TitleExplainer>) -> Unit
) {
val currentDistributor = PushDistributorHandler.getSavedDistributor().ifBlank { null } ?: "None" val currentDistributor = PushDistributorHandler.getSavedDistributor().ifBlank { null } ?: "None"
val list = remember { val list =
remember {
PushDistributorHandler.getInstalledDistributors().plus("None").toImmutableList() PushDistributorHandler.getInstalledDistributors().plus("None").toImmutableList()
} }
@@ -36,8 +36,7 @@ fun TranslatableRichTextViewer(
backgroundColor: MutableState<Color>, backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) = ) = ExpandableRichTextViewer(
ExpandableRichTextViewer(
content, content,
canPreview, canPreview,
modifier, modifier,
@@ -45,4 +44,4 @@ fun TranslatableRichTextViewer(
backgroundColor, backgroundColor,
accountViewModel, accountViewModel,
nav, nav,
) )
@@ -44,17 +44,16 @@ import androidx.compose.ui.unit.Velocity
fun Modifier.pullRefresh( fun Modifier.pullRefresh(
state: PullRefreshState, state: PullRefreshState,
enabled: Boolean = true, enabled: Boolean = true,
) = ) = inspectable(
inspectable(
inspectorInfo = inspectorInfo =
debugInspectorInfo { debugInspectorInfo {
name = "pullRefresh" name = "pullRefresh"
properties["state"] = state properties["state"] = state
properties["enabled"] = enabled properties["enabled"] = enabled
}, },
) { ) {
Modifier.pullRefresh(state::onPull, state::onRelease, enabled) Modifier.pullRefresh(state::onPull, state::onRelease, enabled)
} }
/** /**
* A nested scroll modifier that provides [onPull] and [onRelease] callbacks to aid building custom * A nested scroll modifier that provides [onPull] and [onRelease] callbacks to aid building custom
@@ -81,8 +80,7 @@ fun Modifier.pullRefresh(
onPull: (pullDelta: Float) -> Float, onPull: (pullDelta: Float) -> Float,
onRelease: suspend (flingVelocity: Float) -> Float, onRelease: suspend (flingVelocity: Float) -> Float,
enabled: Boolean = true, enabled: Boolean = true,
) = ) = inspectable(
inspectable(
inspectorInfo = inspectorInfo =
debugInspectorInfo { debugInspectorInfo {
name = "pullRefresh" name = "pullRefresh"
@@ -90,9 +88,9 @@ fun Modifier.pullRefresh(
properties["onRelease"] = onRelease properties["onRelease"] = onRelease
properties["enabled"] = enabled properties["enabled"] = enabled
}, },
) { ) {
Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled)) Modifier.nestedScroll(PullRefreshNestedScrollConnection(onPull, onRelease, enabled))
} }
private class PullRefreshNestedScrollConnection( private class PullRefreshNestedScrollConnection(
private val onPull: (pullDelta: Float) -> Float, private val onPull: (pullDelta: Float) -> Float,
@@ -39,15 +39,14 @@ import androidx.compose.ui.platform.inspectable
fun Modifier.pullRefreshIndicatorTransform( fun Modifier.pullRefreshIndicatorTransform(
state: PullRefreshState, state: PullRefreshState,
scale: Boolean = false, scale: Boolean = false,
) = ) = inspectable(
inspectable(
inspectorInfo = inspectorInfo =
debugInspectorInfo { debugInspectorInfo {
name = "pullRefreshIndicatorTransform" name = "pullRefreshIndicatorTransform"
properties["state"] = state properties["state"] = state
properties["scale"] = scale properties["scale"] = scale
}, },
) { ) {
Modifier Modifier
// Essentially we only want to clip the at the top, so the indicator will not appear when // Essentially we only want to clip the at the top, so the indicator will not appear when
// the position is 0. It is preferable to clip the indicator as opposed to the layout that // the position is 0. It is preferable to clip the indicator as opposed to the layout that
@@ -75,4 +74,4 @@ fun Modifier.pullRefreshIndicatorTransform(
scaleY = scaleFraction scaleY = scaleFraction
} }
} }
} }
@@ -35,10 +35,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlin.math.abs
import kotlin.math.pow
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.abs
import kotlin.math.pow
/** /**
* Creates a [PullRefreshState] that is remembered across compositions. * Creates a [PullRefreshState] that is remembered across compositions.
@@ -98,12 +98,12 @@ fun rememberPullRefreshState(
* Should be created using [rememberPullRefreshState]. * Should be created using [rememberPullRefreshState].
*/ */
class PullRefreshState class PullRefreshState
internal constructor( internal constructor(
private val animationScope: CoroutineScope, private val animationScope: CoroutineScope,
private val onRefreshState: State<() -> Unit>, private val onRefreshState: State<() -> Unit>,
refreshingOffset: Float, refreshingOffset: Float,
threshold: Float, threshold: Float,
) { ) {
/** /**
* A float representing how far the user has pulled as a percentage of the refreshThreshold. * A float representing how far the user has pulled as a percentage of the refreshThreshold.
* *
@@ -211,7 +211,7 @@ internal constructor(
threshold + extraOffset threshold + extraOffset
} }
} }
} }
/** Default parameter values for [rememberPullRefreshState]. */ /** Default parameter values for [rememberPullRefreshState]. */
object PullRefreshDefaults { object PullRefreshDefaults {
@@ -29,14 +29,14 @@ import android.util.Log
import coil.ImageLoader import coil.ImageLoader
import coil.disk.DiskCache import coil.disk.DiskCache
import com.vitorpamplona.amethyst.service.playback.VideoCache import com.vitorpamplona.amethyst.service.playback.VideoCache
import java.io.File
import kotlin.time.measureTimedValue
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import kotlin.time.measureTimedValue
class Amethyst : Application() { class Amethyst : Application() {
val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) val applicationIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -53,13 +53,13 @@ import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.signers.NostrSignerInternal
import java.io.File
import java.util.Locale
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
// Release mode (!BuildConfig.DEBUG) always uses encrypted preferences // Release mode (!BuildConfig.DEBUG) always uses encrypted preferences
// To use plaintext SharedPreferences for debugging, set this to true // To use plaintext SharedPreferences for debugging, set this to true
@@ -201,8 +201,7 @@ object LocalPreferences {
addAccount(accInfo) addAccount(accInfo)
} }
suspend fun switchToAccount(accountInfo: AccountInfo) = suspend fun switchToAccount(accountInfo: AccountInfo) = withContext(Dispatchers.IO) { updateCurrentAccount(accountInfo.npub) }
withContext(Dispatchers.IO) { updateCurrentAccount(accountInfo.npub) }
/** Removes the account from the app level shared preferences */ /** Removes the account from the app level shared preferences */
private suspend fun removeAccount(accountInfo: AccountInfo) { private suspend fun removeAccount(accountInfo: AccountInfo) {
@@ -93,11 +93,6 @@ import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.signers.NostrSignerInternal
import com.vitorpamplona.quartz.utils.DualCase import com.vitorpamplona.quartz.utils.DualCase
import java.math.BigDecimal
import java.net.Proxy
import java.util.Locale
import java.util.UUID
import kotlin.coroutines.resume
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
@@ -117,6 +112,11 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.math.BigDecimal
import java.net.Proxy
import java.util.Locale
import java.util.UUID
import kotlin.coroutines.resume
val DefaultChannels = val DefaultChannels =
setOf( setOf(
@@ -2233,9 +2233,13 @@ class Account(
fun isAcceptable(note: Note): Boolean { fun isAcceptable(note: Note): Boolean {
return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author return note.author?.let { isAcceptable(it) } ?: true && // if user hasn't hided this author
isAcceptableDirect(note) && isAcceptableDirect(note) &&
((note.event !is RepostEvent && note.event !is GenericRepostEvent) || (
(note.replyTo?.firstOrNull { isAcceptableDirect(it) } != (note.event !is RepostEvent && note.event !is GenericRepostEvent) ||
null)) // is not a reaction about a blocked post (
note.replyTo?.firstOrNull { isAcceptableDirect(it) } !=
null
)
) // is not a reaction about a blocked post
} }
fun getRelevantReports(note: Note): Set<Note> { fun getRelevantReports(note: Note): Set<Note> {
@@ -2248,9 +2252,11 @@ class Account(
emptyList() emptyList()
} }
return (note.reportsBy(followsPlusMe) + return (
note.reportsBy(followsPlusMe) +
(note.author?.reportsBy(followsPlusMe) ?: emptyList()) + (note.author?.reportsBy(followsPlusMe) ?: emptyList()) +
innerReports) innerReports
)
.toSet() .toSet()
} }
@@ -32,8 +32,8 @@ import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.toNote import com.vitorpamplona.quartz.encoders.toNote
import com.vitorpamplona.quartz.events.ChannelCreateEvent import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import java.util.concurrent.ConcurrentHashMap
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import java.util.concurrent.ConcurrentHashMap
@Stable @Stable
class PublicChatChannel(idHex: String) : Channel(idHex) { class PublicChatChannel(idHex: String) : Channel(idHex) {
@@ -36,7 +36,8 @@ fun checkForHashtagWithIcon(
"btc", "btc",
"timechain", "timechain",
"bitcoiner", "bitcoiner",
"bitcoiners", -> "bitcoiners",
->
HashtagIcon( HashtagIcon(
R.drawable.ht_btc, R.drawable.ht_btc,
"Bitcoin", "Bitcoin",
@@ -46,7 +47,8 @@ fun checkForHashtagWithIcon(
"nostr", "nostr",
"nostrich", "nostrich",
"nostriches", "nostriches",
"thenostr", -> "thenostr",
->
HashtagIcon( HashtagIcon(
R.drawable.ht_nostr, R.drawable.ht_nostr,
"Nostr", "Nostr",
@@ -54,7 +56,8 @@ fun checkForHashtagWithIcon(
Modifier.padding(1.dp, 2.dp, 0.dp, 0.dp), Modifier.padding(1.dp, 2.dp, 0.dp, 0.dp),
) )
"lightning", "lightning",
"lightningnetwork", -> "lightningnetwork",
->
HashtagIcon( HashtagIcon(
R.drawable.ht_lightning, R.drawable.ht_lightning,
"Lightning", "Lightning",
@@ -70,7 +73,8 @@ fun checkForHashtagWithIcon(
"zapathon", "zapathon",
"zapraiser", "zapraiser",
"zaplife", "zaplife",
"zapchain", -> "zapchain",
->
HashtagIcon( HashtagIcon(
R.drawable.zap, R.drawable.zap,
"Zap", "Zap",
@@ -95,7 +99,8 @@ fun checkForHashtagWithIcon(
"ecash", "ecash",
"nut", "nut",
"nuts", "nuts",
"deeznuts", -> "deeznuts",
->
HashtagIcon( HashtagIcon(
R.drawable.cashu, R.drawable.cashu,
"Cashu", "Cashu",
@@ -104,7 +109,8 @@ fun checkForHashtagWithIcon(
) )
"plebs", "plebs",
"pleb", "pleb",
"plebchain", -> "plebchain",
->
HashtagIcon( HashtagIcon(
R.drawable.plebs, R.drawable.plebs,
"Pleb", "Pleb",
@@ -113,7 +119,8 @@ fun checkForHashtagWithIcon(
) )
"coffee", "coffee",
"coffeechain", "coffeechain",
"cafe", -> "cafe",
->
HashtagIcon( HashtagIcon(
R.drawable.coffee, R.drawable.coffee,
"Coffee", "Coffee",
@@ -129,7 +136,8 @@ fun checkForHashtagWithIcon(
) )
"grownostr", "grownostr",
"gardening", "gardening",
"garden", -> "garden",
->
HashtagIcon( HashtagIcon(
R.drawable.grownostr, R.drawable.grownostr,
"GrowNostr", "GrowNostr",
@@ -145,13 +153,15 @@ fun checkForHashtagWithIcon(
) )
"tunestr", "tunestr",
"music", "music",
"nowplaying", -> "nowplaying",
->
HashtagIcon(R.drawable.tunestr, "Tunestr", primary, Modifier.padding(0.dp, 3.dp, 0.dp, 1.dp)) HashtagIcon(R.drawable.tunestr, "Tunestr", primary, Modifier.padding(0.dp, 3.dp, 0.dp, 1.dp))
"weed", "weed",
"weedstr", "weedstr",
"420", "420",
"cannabis", "cannabis",
"marijuana", -> "marijuana",
->
HashtagIcon( HashtagIcon(
R.drawable.weed, R.drawable.weed,
"Weed", "Weed",
@@ -94,13 +94,6 @@ import com.vitorpamplona.quartz.events.StatusEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.ConcurrentHashMap
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -109,6 +102,13 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.concurrent.ConcurrentHashMap
object LocalCache { object LocalCache {
val antiSpam = AntiSpamFilter() val antiSpam = AntiSpamFilter()
@@ -1059,9 +1059,11 @@ object LocalCache {
refreshObservers(note) refreshObservers(note)
} }
@Suppress("UNUSED_PARAMETER") fun consume(event: ChannelHideMessageEvent) {} @Suppress("UNUSED_PARAMETER")
fun consume(event: ChannelHideMessageEvent) {}
@Suppress("UNUSED_PARAMETER") fun consume(event: ChannelMuteUserEvent) {} @Suppress("UNUSED_PARAMETER")
fun consume(event: ChannelMuteUserEvent) {}
fun consume(event: LnZapEvent) { fun consume(event: LnZapEvent) {
val note = getOrCreateNote(event.id) val note = getOrCreateNote(event.id)
@@ -1080,10 +1082,12 @@ object LocalCache {
val repliesTo = val repliesTo =
event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } + event.zappedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().map { getOrCreateAddressableNote(it) } + event.taggedAddresses().map { getOrCreateAddressableNote(it) } +
((zapRequest.event as? LnZapRequestEvent)?.taggedAddresses()?.map { (
(zapRequest.event as? LnZapRequestEvent)?.taggedAddresses()?.map {
getOrCreateAddressableNote(it) getOrCreateAddressableNote(it)
} }
?: emptySet<Note>()) ?: emptySet<Note>()
)
note.loadEvent(event, author, repliesTo) note.loadEvent(event, author, repliesTo)
@@ -1401,29 +1405,37 @@ object LocalCache {
} }
return notes.values.filter { return notes.values.filter {
(it.event !is GenericRepostEvent && (
it.event !is GenericRepostEvent &&
it.event !is RepostEvent && it.event !is RepostEvent &&
it.event !is CommunityPostApprovalEvent && it.event !is CommunityPostApprovalEvent &&
it.event !is ReactionEvent && it.event !is ReactionEvent &&
it.event !is GiftWrapEvent && it.event !is GiftWrapEvent &&
it.event !is LnZapEvent && it.event !is LnZapEvent &&
it.event !is LnZapRequestEvent) && it.event !is LnZapRequestEvent
(it.event?.content()?.contains(text, true) ) &&
(
it.event?.content()?.contains(text, true)
?: false || ?: false ||
it.event?.matchTag1With(text) ?: false || it.event?.matchTag1With(text) ?: false ||
it.idHex.startsWith(text, true) || it.idHex.startsWith(text, true) ||
it.idNote().startsWith(text, true)) it.idNote().startsWith(text, true)
)
} + } +
addressables.values.filter { addressables.values.filter {
(it.event !is GenericRepostEvent && (
it.event !is GenericRepostEvent &&
it.event !is RepostEvent && it.event !is RepostEvent &&
it.event !is CommunityPostApprovalEvent && it.event !is CommunityPostApprovalEvent &&
it.event !is ReactionEvent && it.event !is ReactionEvent &&
it.event !is GiftWrapEvent && it.event !is GiftWrapEvent &&
it.event !is LnZapEvent && it.event !is LnZapEvent &&
it.event !is LnZapRequestEvent) && it.event !is LnZapRequestEvent
(it.event?.content()?.contains(text, true) ) &&
?: false || it.event?.matchTag1With(text) ?: false || it.idHex.startsWith(text, true)) (
it.event?.content()?.contains(text, true)
?: false || it.event?.matchTag1With(text) ?: false || it.idHex.startsWith(text, true)
)
} }
} }
@@ -1454,10 +1466,12 @@ object LocalCache {
return addressables return addressables
.filter { .filter {
val noteEvent = it.value.event val noteEvent = it.value.event
(noteEvent is StatusEvent && (
noteEvent is StatusEvent &&
noteEvent.pubKey == user.pubkeyHex && noteEvent.pubKey == user.pubkeyHex &&
!noteEvent.isExpired() && !noteEvent.isExpired() &&
noteEvent.content.isNotBlank()) noteEvent.content.isNotBlank()
)
} }
.values .values
.sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex })) .sortedWith(compareBy({ it.event?.expiration() ?: it.event?.createdAt() }, { it.idHex }))
@@ -1558,12 +1572,14 @@ object LocalCache {
val toBeRemoved = val toBeRemoved =
notes notes
.filter { .filter {
((it.value.event is TextNoteEvent && !it.value.isNewThread()) || (
(it.value.event is TextNoteEvent && !it.value.isNewThread()) ||
it.value.event is ReactionEvent || it.value.event is ReactionEvent ||
it.value.event is LnZapEvent || it.value.event is LnZapEvent ||
it.value.event is LnZapRequestEvent || it.value.event is LnZapRequestEvent ||
it.value.event is ReportEvent || it.value.event is ReportEvent ||
it.value.event is GenericRepostEvent) && it.value.event is GenericRepostEvent
) &&
it.value.replyTo?.any { it.liveSet?.isInUse() == true } != true && it.value.replyTo?.any { it.liveSet?.isInUse() == true } != true &&
it.value.liveSet?.isInUse() != true && // don't delete if observing. it.value.liveSet?.isInUse() != true && // don't delete if observing.
it.value.author?.pubkeyHex !in it.value.author?.pubkeyHex !in
@@ -1662,8 +1678,10 @@ object LocalCache {
account.liveHiddenUsers.value account.liveHiddenUsers.value
?.hiddenUsers ?.hiddenUsers
?.map { userHex -> ?.map { userHex ->
(notes.values.filter { it.event?.pubKey() == userHex } + (
addressables.values.filter { it.event?.pubKey() == userHex }) notes.values.filter { it.event?.pubKey() == userHex } +
addressables.values.filter { it.event?.pubKey() == userHex }
)
.toSet() .toSet()
} }
?.flatten() ?.flatten()
@@ -63,12 +63,12 @@ import com.vitorpamplona.quartz.events.WrappedEvent
import com.vitorpamplona.quartz.signers.NostrSigner import com.vitorpamplona.quartz.signers.NostrSigner
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.utils.containsAny import com.vitorpamplona.quartz.utils.containsAny
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import java.math.BigDecimal import java.math.BigDecimal
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
@Stable @Stable
class AddressableNote(val address: ATag) : Note(address.toTag()) { class AddressableNote(val address: ATag) : Note(address.toTag()) {
@@ -223,7 +223,8 @@ open class Note(val idHex: String) {
} }
val parent = val parent =
(replyTo (
replyTo
.filter { .filter {
it.idHex in eventsToConsider it.idHex in eventsToConsider
} // This forces the signature to be based on a branch, avoiding two roots } // This forces the signature to be based on a branch, avoiding two roots
@@ -239,7 +240,8 @@ open class Note(val idHex: String) {
) )
.apply { cachedSignatures.put(it, this) } .apply { cachedSignatures.put(it, this) }
} }
.maxByOrNull { it.signature.length }) .maxByOrNull { it.signature.length }
)
val parentSignature = parent?.signature?.removeSuffix(";") ?: "" val parentSignature = parent?.signature?.removeSuffix(";") ?: ""
@@ -717,15 +719,19 @@ open class Note(val idHex: String) {
fun hasAnyReports(): Boolean { fun hasAnyReports(): Boolean {
val dayAgo = TimeUtils.oneDayAgo() val dayAgo = TimeUtils.oneDayAgo()
return reports.isNotEmpty() || return reports.isNotEmpty() ||
(author?.reports?.any { it.value.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null } (
?: false) author?.reports?.any { it.value.firstOrNull { (it.createdAt() ?: 0) > dayAgo } != null }
?: false
)
} }
fun isNewThread(): Boolean { fun isNewThread(): Boolean {
return (event is RepostEvent || return (
event is RepostEvent ||
event is GenericRepostEvent || event is GenericRepostEvent ||
replyTo == null || replyTo == null ||
replyTo?.size == 0) && replyTo?.size == 0
) &&
event !is ChannelMessageEvent && event !is ChannelMessageEvent &&
event !is LiveActivitiesChatMessageEvent event !is LiveActivitiesChatMessageEvent
} }
@@ -47,8 +47,7 @@ class RelayInformation(
val mapper = val mapper =
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
fun fromJson(json: String): RelayInformation = fun fromJson(json: String): RelayInformation = mapper.readValue(json, RelayInformation::class.java)
mapper.readValue(json, RelayInformation::class.java)
} }
} }
@@ -37,8 +37,7 @@ object UrlCachedPreviewer {
suspend fun previewInfo( suspend fun previewInfo(
url: String, url: String,
onReady: suspend (UrlPreviewState) -> Unit, onReady: suspend (UrlPreviewState) -> Unit,
) = ) = withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) {
cache[url]?.let { cache[url]?.let {
onReady(it) onReady(it)
return@withContext return@withContext
@@ -43,10 +43,10 @@ import com.vitorpamplona.quartz.events.MetadataEvent
import com.vitorpamplona.quartz.events.ReportEvent import com.vitorpamplona.quartz.events.ReportEvent
import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import java.math.BigDecimal
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import java.math.BigDecimal
@Stable @Stable
class User(val pubkeyHex: String) { class User(val pubkeyHex: String) {
@@ -187,8 +187,7 @@ object BlurHashDecoder {
calculate: Boolean, calculate: Boolean,
height: Int, height: Int,
numCompY: Int, numCompY: Int,
) = ) = when {
when {
calculate -> { calculate -> {
DoubleArray(height * numCompY).also { cacheCosinesY[height * numCompY] = it } DoubleArray(height * numCompY).also { cacheCosinesY[height * numCompY] = it }
} }
@@ -201,8 +200,7 @@ object BlurHashDecoder {
calculate: Boolean, calculate: Boolean,
width: Int, width: Int,
numCompX: Int, numCompX: Int,
) = ) = when {
when {
calculate -> { calculate -> {
DoubleArray(width * numCompX).also { cacheCosinesX[width * numCompX] = it } DoubleArray(width * numCompX).also { cacheCosinesX[width * numCompX] = it }
} }
@@ -35,7 +35,6 @@ import com.vitorpamplona.amethyst.ui.components.removeQueryParamsForExtensionCom
import com.vitorpamplona.amethyst.ui.components.tagIndex import com.vitorpamplona.amethyst.ui.components.tagIndex
import com.vitorpamplona.amethyst.ui.components.videoExtensions import com.vitorpamplona.amethyst.ui.components.videoExtensions
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import java.util.regex.Pattern
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
@@ -43,6 +42,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import java.util.regex.Pattern
@Immutable @Immutable
data class RichTextViewerState( data class RichTextViewerState(
@@ -28,10 +28,10 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
import com.vitorpamplona.amethyst.ui.components.GenericLoadable import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import java.util.Base64
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.util.Base64
@Immutable @Immutable
data class CashuToken( data class CashuToken(
@@ -29,9 +29,11 @@ fun String.isUTF16Char(pos: Int): Boolean {
fun String.firstFullCharOld(): String { fun String.firstFullCharOld(): String {
return when (this.length) { return when (this.length) {
0, 0,
1, -> return this 1,
-> return this
2, 2,
3, -> return if (isUTF16Char(0)) this.take(2) else this.take(1) 3,
-> return if (isUTF16Char(0)) this.take(2) else this.take(1)
else -> { else -> {
val first = isUTF16Char(0) val first = isUTF16Char(0)
val second = isUTF16Char(2) val second = isUTF16Char(2)
@@ -65,7 +67,8 @@ fun String.firstFullChar(): String {
start = next start = next
} else { } else {
// If join, searches for the next char // If join, searches for the next char
if (codePoint == 0xFE0F) {} else if (codePoint == 0x200D) { if (codePoint == 0xFE0F) {
} else if (codePoint == 0x200D) {
isInJoin = true isInJoin = true
} else { } else {
// stops when two chars are not joined together // stops when two chars are not joined together
@@ -260,5 +260,6 @@ class ByteArrayMediaDataSource(var imageData: ByteArray) : MediaDataSource() {
return newSize return newSize
} }
@Throws(IOException::class) override fun close() {} @Throws(IOException::class)
override fun close() {}
} }
@@ -22,15 +22,15 @@ package com.vitorpamplona.amethyst.service
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException 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
import kotlin.properties.Delegates import kotlin.properties.Delegates
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
object HttpClient { object HttpClient {
val DEFAULT_TIMEOUT_ON_WIFI = Duration.ofSeconds(10L) val DEFAULT_TIMEOUT_ON_WIFI = Duration.ofSeconds(10L)
@@ -47,8 +47,7 @@ class Nip05NostrAddressVerifier() {
nip05: String, nip05: String,
onSuccess: (String) -> Unit, onSuccess: (String) -> Unit,
onError: (String) -> Unit, onError: (String) -> Unit,
) = ) = withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) {
checkNotInMainThread() checkNotInMainThread()
val url = assembleUrl(nip05) val url = assembleUrl(nip05)
@@ -23,11 +23,11 @@ package com.vitorpamplona.amethyst.service
import android.util.Log import android.util.Log
import android.util.LruCache import android.util.LruCache
import com.vitorpamplona.amethyst.model.RelayInformation import com.vitorpamplona.amethyst.model.RelayInformation
import java.io.IOException
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import java.io.IOException
object Nip11CachedRetriever { object Nip11CachedRetriever {
val relayInformationDocumentCache = LruCache<String, RelayInformation>(100) val relayInformationDocumentCache = LruCache<String, RelayInformation>(100)
@@ -30,9 +30,6 @@ import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import java.io.InputStream
import java.util.Base64
import kotlin.coroutines.resume
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
@@ -42,6 +39,9 @@ import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import okio.BufferedSink import okio.BufferedSink
import okio.source import okio.source
import java.io.InputStream
import java.util.Base64
import kotlin.coroutines.resume
val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9') val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
@@ -214,7 +214,8 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
), ),
) )
val accountChannel = requestNewChannel { time, relayUrl -> val accountChannel =
requestNewChannel { time, relayUrl ->
if (hasLoadedTheBasics[account.userProfile()] != null) { if (hasLoadedTheBasics[account.userProfile()] != null) {
latestEOSEs.addOrUpdate( latestEOSEs.addOrUpdate(
account.userProfile(), account.userProfile(),
@@ -93,7 +93,8 @@ object NostrChatroomDataSource : NostrDataSource("ChatroomFeed") {
latestEOSEs.removeDataFor(account.userProfile()) latestEOSEs.removeDataFor(account.userProfile())
} }
val inandoutChannel = requestNewChannel { time, relayUrl -> val inandoutChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), withRoom.hashCode().toString(), relayUrl, time) latestEOSEs.addOrUpdate(account.userProfile(), withRoom.hashCode().toString(), relayUrl, time)
} }
@@ -131,7 +131,8 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
} }
} }
val chatroomListChannel = requestNewChannel { time, relayUrl -> val chatroomListChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate(account.userProfile(), chatRoomList, relayUrl, time) latestEOSEs.addOrUpdate(account.userProfile(), chatRoomList, relayUrl, time)
} }
@@ -28,14 +28,14 @@ import com.vitorpamplona.amethyst.service.relays.Subscription
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
abstract class NostrDataSource(val debugName: String) { abstract class NostrDataSource(val debugName: String) {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -392,7 +392,8 @@ object NostrDiscoveryDataSource : NostrDataSource("DiscoveryFeed") {
) )
} }
val discoveryFeedChannel = requestNewChannel { time, relayUrl -> val discoveryFeedChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate( latestEOSEs.addOrUpdate(
account.userProfile(), account.userProfile(),
account.defaultDiscoveryFollowList.value, account.defaultDiscoveryFollowList.value,
@@ -212,7 +212,8 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
) )
} }
val followAccountChannel = requestNewChannel { time, relayUrl -> val followAccountChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate( latestEOSEs.addOrUpdate(
account.userProfile(), account.userProfile(),
account.defaultHomeFollowList.value, account.defaultHomeFollowList.value,
@@ -43,8 +43,10 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
private fun createReactionsToWatchInAddressFilter(): List<TypedFilter>? { private fun createReactionsToWatchInAddressFilter(): List<TypedFilter>? {
val addressesToWatch = val addressesToWatch =
(eventsToWatch.filter { it.address() != null } + (
addressesToWatch.filter { it.address() != null }) eventsToWatch.filter { it.address() != null } +
addressesToWatch.filter { it.address() != null }
)
.toSet() .toSet()
if (addressesToWatch.isEmpty()) { if (addressesToWatch.isEmpty()) {
@@ -168,7 +170,8 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
) )
} }
val singleEventChannel = requestNewChannel { time, relayUrl -> val singleEventChannel =
requestNewChannel { time, relayUrl ->
// Ignores EOSE if it is in the middle of a filter change. // Ignores EOSE if it is in the middle of a filter change.
if (changingFilters.get()) return@requestNewChannel if (changingFilters.get()) return@requestNewChannel
@@ -86,7 +86,8 @@ object NostrSingleUserDataSource : NostrDataSource("SingleUserFeed") {
.flatten() .flatten()
} }
val userChannel = requestNewChannel { time, relayUrl -> val userChannel =
requestNewChannel { time, relayUrl ->
checkNotInMainThread() checkNotInMainThread()
usersToWatch.forEach { usersToWatch.forEach {
@@ -51,7 +51,8 @@ object NostrThreadDataSource : NostrDataSource("SingleThreadFeed") {
) )
} }
val loadEventsChannel = requestNewChannel { _, _ -> val loadEventsChannel =
requestNewChannel { _, _ ->
// Many relays operate with limits in the amount of filters. // Many relays operate with limits in the amount of filters.
// As information comes, the filters will be rotated to get more data. // As information comes, the filters will be rotated to get more data.
invalidateFilters() invalidateFilters()
@@ -131,7 +131,8 @@ object NostrVideoDataSource : NostrDataSource("VideoFeed") {
) )
} }
val videoFeedChannel = requestNewChannel { time, relayUrl -> val videoFeedChannel =
requestNewChannel { time, relayUrl ->
latestEOSEs.addOrUpdate( latestEOSEs.addOrUpdate(
account.userProfile(), account.userProfile(),
account.defaultStoriesFollowList.value, account.defaultStoriesFollowList.value,
@@ -32,12 +32,12 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse import com.vitorpamplona.quartz.events.PayInvoiceErrorResponse
import com.vitorpamplona.quartz.events.ZapSplitSetup import com.vitorpamplona.quartz.events.ZapSplitSetup
import kotlin.math.round
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.round
class ZapPaymentHandler(val account: Account) { class ZapPaymentHandler(val account: Account) {
@Immutable @Immutable
@@ -58,8 +58,7 @@ class ZapPaymentHandler(val account: Account) {
onProgress: (percent: Float) -> Unit, onProgress: (percent: Float) -> Unit,
onPayViaIntent: (ImmutableList<Payable>) -> Unit, onPayViaIntent: (ImmutableList<Payable>) -> Unit,
zapType: LnZapEvent.ZapType, zapType: LnZapEvent.ZapType,
) = ) = withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) {
val zapSplitSetup = note.event?.zapSplitSetup() val zapSplitSetup = note.event?.zapSplitSetup()
val noteEvent = note.event val noteEvent = note.event
@@ -29,10 +29,10 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.Lud06 import com.vitorpamplona.quartz.encoders.Lud06
import com.vitorpamplona.quartz.encoders.toLnUrl import com.vitorpamplona.quartz.encoders.toLnUrl
import okhttp3.Request
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
import java.net.URLEncoder import java.net.URLEncoder
import okhttp3.Request
class LightningAddressResolver() { class LightningAddressResolver() {
val client = HttpClient.getHttpClient() val client = HttpClient.getHttpClient()
@@ -28,9 +28,11 @@ object UserZaps {
fun forProfileFeed(zaps: Map<Note, Note?>?): List<ZapReqResponse> { fun forProfileFeed(zaps: Map<Note, Note?>?): List<ZapReqResponse> {
if (zaps == null) return emptyList() if (zaps == null) return emptyList()
return (zaps return (
zaps
.mapNotNull { entry -> entry.value?.let { ZapReqResponse(entry.key, it) } } .mapNotNull { entry -> entry.value?.let { ZapReqResponse(entry.key, it) } }
.sortedBy { (it.zapEvent.event as? LnZapEventInterface)?.amount() } .sortedBy { (it.zapEvent.event as? LnZapEventInterface)?.amount() }
.reversed()) .reversed()
)
} }
} }
@@ -40,8 +40,8 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent
import com.vitorpamplona.quartz.events.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent import com.vitorpamplona.quartz.events.SealedGossipEvent
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import java.math.BigDecimal
class EventNotificationConsumer(private val applicationContext: Context) { class EventNotificationConsumer(private val applicationContext: Context) {
suspend fun consume(event: GiftWrapEvent) { suspend fun consume(event: GiftWrapEvent) {
@@ -118,8 +118,10 @@ class EventNotificationConsumer(private val applicationContext: Context) {
val followingKeySet = acc.followingKeySet() val followingKeySet = acc.followingKeySet()
val isKnownRoom = val isKnownRoom =
(acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true || (
acc.userProfile().hasSentMessagesTo(chatRoom)) && !acc.isAllHidden(chatRoom.users) acc.userProfile().privateChatrooms[chatRoom]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(chatRoom)
) && !acc.isAllHidden(chatRoom.users)
if (isKnownRoom) { if (isKnownRoom) {
val content = chatNote.event?.content() ?: "" val content = chatNote.event?.content() ?: ""
@@ -157,8 +159,10 @@ class EventNotificationConsumer(private val applicationContext: Context) {
.privateChatrooms .privateChatrooms
.keys .keys
.filter { .filter {
(acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true || (
acc.userProfile().hasSentMessagesTo(it)) && !acc.isAllHidden(it.users) acc.userProfile().privateChatrooms[it]?.senderIntersects(followingKeySet) == true ||
acc.userProfile().hasSentMessagesTo(it)
) && !acc.isAllHidden(it.users)
} }
.toSet() .toSet()
@@ -32,11 +32,11 @@ import androidx.media3.common.Player.STATE_READY
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import com.vitorpamplona.amethyst.ui.MainActivity import com.vitorpamplona.amethyst.ui.MainActivity
import kotlin.math.abs
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.abs
class MultiPlayerPlaybackManager( class MultiPlayerPlaybackManager(
private val dataSourceFactory: androidx.media3.exoplayer.source.MediaSource.Factory? = null, private val dataSourceFactory: androidx.media3.exoplayer.source.MediaSource.Factory? = null,
@@ -27,8 +27,8 @@ import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor import androidx.media3.datasource.cache.LeastRecentlyUsedCacheEvictor
import androidx.media3.datasource.cache.SimpleCache import androidx.media3.datasource.cache.SimpleCache
import androidx.media3.datasource.okhttp.OkHttpDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource
import java.io.File
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.io.File
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
class VideoCache { class VideoCache {
@@ -21,8 +21,8 @@
package com.vitorpamplona.amethyst.service.previews package com.vitorpamplona.amethyst.service.previews
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import java.net.URL
import okhttp3.MediaType import okhttp3.MediaType
import java.net.URL
@Immutable @Immutable
class UrlInfoItem( class UrlInfoItem(
@@ -24,12 +24,12 @@ import android.util.Log
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.events.Event import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.events.EventInterface import com.vitorpamplona.quartz.events.EventInterface
import java.util.UUID
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID
/** /**
* The Nostr Client manages multiple personae the user may switch between. Events are received and * The Nostr Client manages multiple personae the user may switch between. Events are received and
@@ -31,12 +31,12 @@ import com.vitorpamplona.quartz.events.EventInterface
import com.vitorpamplona.quartz.events.RelayAuthEvent import com.vitorpamplona.quartz.events.RelayAuthEvent
import com.vitorpamplona.quartz.events.bytesUsedInMemory import com.vitorpamplona.quartz.events.bytesUsedInMemory
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.lang.StringBuilder
import java.util.concurrent.atomic.AtomicBoolean
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.WebSocket import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import java.lang.StringBuilder
import java.util.concurrent.atomic.AtomicBoolean
enum class FeedType { enum class FeedType {
FOLLOWS, FOLLOWS,
@@ -156,8 +156,7 @@ inline fun TextToSpeech.setListener(
crossinline onError: (String?) -> Unit = {}, crossinline onError: (String?) -> Unit = {},
crossinline onRange: (Int, Int) -> Unit = { _, _ -> }, crossinline onRange: (Int, Int) -> Unit = { _, _ -> },
crossinline onDone: (String?) -> Unit, crossinline onDone: (String?) -> Unit,
) = ) = this.apply {
this.apply {
setOnUtteranceProgressListener( setOnUtteranceProgressListener(
object : UtteranceProgressListener() { object : UtteranceProgressListener() {
override fun onStart(p0: String?) { override fun onStart(p0: String?) {
@@ -184,4 +183,4 @@ inline fun TextToSpeech.setListener(
} }
}, },
) )
} }
@@ -174,4 +174,4 @@ class TextToSpeechHelper private constructor(private val context: WeakReference<
destroy { customActionForDestroy?.invoke() } destroy { customActionForDestroy?.invoke() }
} }
} }
} }
@@ -64,14 +64,14 @@ import com.vitorpamplona.quartz.events.ChannelMetadataEvent
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.PrivateDmEvent import com.vitorpamplona.quartz.events.PrivateDmEvent
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.Timer
import kotlin.concurrent.schedule
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.util.Timer
import kotlin.concurrent.schedule
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val isOnMobileDataState = mutableStateOf(false) private val isOnMobileDataState = mutableStateOf(false)
@@ -20,9 +20,9 @@
*/ */
package com.vitorpamplona.amethyst.ui.actions package com.vitorpamplona.amethyst.ui.actions
import kotlinx.coroutines.delay
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import kotlinx.coroutines.delay
class ImageDownloader { class ImageDownloader {
suspend fun waitAndGetImage(imageUrl: String): ByteArray? { suspend fun waitAndGetImage(imageUrl: String): ByteArray? {
@@ -31,8 +31,6 @@ import android.webkit.MimeTypeMap
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.service.HttpClient import com.vitorpamplona.amethyst.service.HttpClient
import java.io.File
import java.util.UUID
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Request import okhttp3.Request
@@ -42,6 +40,8 @@ import okio.IOException
import okio.buffer import okio.buffer
import okio.sink import okio.sink
import okio.source import okio.source
import java.io.File
import java.util.UUID
object ImageSaver { object ImageSaver {
/** /**
@@ -223,7 +223,8 @@ private fun RenderSearch(
} }
DisposableEffect(lifeCycleOwner) { DisposableEffect(lifeCycleOwner) {
val observer = LifecycleEventObserver { _, event -> val observer =
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
println("Join Start") println("Join Start")
NostrSearchEventOrUserDataSource.start() NostrSearchEventOrUserDataSource.start()
@@ -323,7 +324,8 @@ private fun RenderSearchResults(
val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle() val users by searchBarViewModel.searchResultsUsers.collectAsStateWithLifecycle()
val channels by searchBarViewModel.searchResultsChannels.collectAsStateWithLifecycle() val channels by searchBarViewModel.searchResultsChannels.collectAsStateWithLifecycle()
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -199,7 +199,8 @@ fun ImageVideoPost(
), ),
) )
val fileServerOptions = remember { val fileServerOptions =
remember {
fileServers.map { TitleExplainer(it.server.name, it.server.baseUrl) }.toImmutableList() fileServers.map { TitleExplainer(it.server.name, it.server.baseUrl) }.toImmutableList()
} }
val resolver = LocalContext.current.contentResolver val resolver = LocalContext.current.contentResolver
@@ -47,7 +47,7 @@ fun NewPollRecipientsField(
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = pollViewModel.zapRecipients[0], value = pollViewModel.zapRecipients[0],
onValueChange = { /* TODO */}, onValueChange = { /* TODO */ },
enabled = false, enabled = false,
label = { label = {
Text( Text(
@@ -177,7 +177,6 @@ import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.events.ClassifiedsEvent import com.vitorpamplona.quartz.events.ClassifiedsEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import java.lang.Math.round
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -185,6 +184,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -911,7 +911,8 @@ fun SellProduct(postViewModel: NewPostViewModel) {
), ),
) )
val conditionOptions = remember { val conditionOptions =
remember {
conditionTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() conditionTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList()
} }
@@ -970,7 +971,8 @@ fun SellProduct(postViewModel: NewPostViewModel) {
val categoryTypes = categoryList.map { Triple(it, stringResource(id = it), null) } val categoryTypes = categoryList.map { Triple(it, stringResource(id = it), null) }
val categoryOptions = remember { val categoryOptions =
remember {
categoryTypes.map { TitleExplainer(it.second, null) }.toImmutableList() categoryTypes.map { TitleExplainer(it.second, null) }.toImmutableList()
} }
TextSpinner( TextSpinner(
@@ -1587,7 +1589,8 @@ fun ImageVideoDescription(
), ),
) )
val fileServerOptions = remember { val fileServerOptions =
remember {
fileServers.map { TitleExplainer(it.server.name, it.server.baseUrl) }.toImmutableList() fileServers.map { TitleExplainer(it.server.name, it.server.baseUrl) }.toImmutableList()
} }
@@ -63,7 +63,6 @@ import com.vitorpamplona.quartz.events.PrivateDmEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import com.vitorpamplona.quartz.events.ZapSplitSetup import com.vitorpamplona.quartz.events.ZapSplitSetup
import com.vitorpamplona.quartz.events.findURLs import com.vitorpamplona.quartz.events.findURLs
import java.net.URLEncoder
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@@ -71,6 +70,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.net.URLEncoder
enum class UserSuggestionAnchor { enum class UserSuggestionAnchor {
MAIN_MESSAGE, MAIN_MESSAGE,
@@ -682,14 +682,22 @@ open class NewPostViewModel() : ViewModel() {
!wantsInvoice && !wantsInvoice &&
(!wantsZapraiser || zapRaiserAmount != null) && (!wantsZapraiser || zapRaiserAmount != null) &&
(!wantsDirectMessage || !toUsers.text.isNullOrBlank()) && (!wantsDirectMessage || !toUsers.text.isNullOrBlank()) &&
(!wantsPoll || (
(pollOptions.values.all { it.isNotEmpty() } && !wantsPoll ||
(
pollOptions.values.all { it.isNotEmpty() } &&
isValidvalueMinimum.value && isValidvalueMinimum.value &&
isValidvalueMaximum.value)) && isValidvalueMaximum.value
(!wantsProduct || )
(!title.text.isNullOrBlank() && ) &&
(
!wantsProduct ||
(
!title.text.isNullOrBlank() &&
!price.text.isNullOrBlank() && !price.text.isNullOrBlank() &&
!category.text.isNullOrBlank())) && !category.text.isNullOrBlank()
)
) &&
contentToAddUrl == null contentToAddUrl == null
} }
@@ -935,7 +943,8 @@ open class NewPostViewModel() : ViewModel() {
} else { } else {
valueMinimum = int valueMinimum = int
} }
} catch (e: Exception) {} } catch (e: Exception) {
}
} else { } else {
valueMinimum = null valueMinimum = null
} }
@@ -952,7 +961,8 @@ open class NewPostViewModel() : ViewModel() {
} else { } else {
valueMaximum = int valueMaximum = int
} }
} catch (e: Exception) {} } catch (e: Exception) {
}
} else { } else {
valueMaximum = null valueMaximum = null
} }
@@ -101,8 +101,8 @@ import com.vitorpamplona.amethyst.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.warningColor import com.vitorpamplona.amethyst.ui.theme.warningColor
import java.lang.Math.round
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.lang.Math.round
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -331,7 +331,8 @@ fun ServerConfig(
) )
} }
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -930,10 +931,9 @@ fun countToHumanReadableBytes(counter: Int) =
fun countToHumanReadable( fun countToHumanReadable(
counter: Int, counter: Int,
str: String, str: String,
) = ) = when {
when {
counter >= 1000000000 -> "${round(counter / 1000000000f)}G $str" counter >= 1000000000 -> "${round(counter / 1000000000f)}G $str"
counter >= 1000000 -> "${round(counter / 1000000f)}M $str" counter >= 1000000 -> "${round(counter / 1000000f)}M $str"
counter >= 1000 -> "${round(counter / 1000f)}K $str" counter >= 1000 -> "${round(counter / 1000f)}K $str"
else -> "$counter $str" else -> "$counter $str"
} }
@@ -35,12 +35,12 @@ import com.vitorpamplona.amethyst.ui.components.MediaCompressor
import com.vitorpamplona.quartz.events.GitHubIdentity import com.vitorpamplona.quartz.events.GitHubIdentity
import com.vitorpamplona.quartz.events.MastodonIdentity import com.vitorpamplona.quartz.events.MastodonIdentity
import com.vitorpamplona.quartz.events.TwitterIdentity import com.vitorpamplona.quartz.events.TwitterIdentity
import java.io.ByteArrayInputStream
import java.io.StringWriter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
import java.io.StringWriter
class NewUserMetadataViewModel : ViewModel() { class NewUserMetadataViewModel : ViewModel() {
private lateinit var account: Account private lateinit var account: Account
@@ -69,7 +69,8 @@ fun RelayInformationDialog(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -34,8 +34,8 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import java.io.File
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
/** /**
* A button to save the remote image to the gallery. May require a storage permission. * A button to save the remote image to the gallery. May require a storage permission.
@@ -58,9 +58,9 @@ import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.GetMediaActivityResultContract import com.vitorpamplona.amethyst.ui.GetMediaActivityResultContract
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import java.util.concurrent.atomic.AtomicBoolean
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
@@ -49,7 +49,8 @@ fun buildAnnotatedStringWithUrlHighlighting(
): TransformedText { ): TransformedText {
val substitutions = mutableListOf<RangesChanges>() val substitutions = mutableListOf<RangesChanges>()
val newText = buildAnnotatedString { val newText =
buildAnnotatedString {
val builderBefore = StringBuilder() // important to correctly measure Tag start and end val builderBefore = StringBuilder() // important to correctly measure Tag start and end
val builderAfter = StringBuilder() // important to correctly measure Tag start and end val builderAfter = StringBuilder() // important to correctly measure Tag start and end
append( append(
@@ -22,8 +22,6 @@ package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -33,6 +31,8 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
/** This class is designed to have a waiting time between two calls of invalidate */ /** This class is designed to have a waiting time between two calls of invalidate */
@Stable @Stable
@@ -625,7 +625,8 @@ fun ClickableInLineIconRenderer(
} }
.associate { it.first to it.second } .associate { it.first to it.second }
val annotatedText = buildAnnotatedString { val annotatedText =
buildAnnotatedString {
wordsInOrder.forEachIndexed { idx, value -> wordsInOrder.forEachIndexed { idx, value ->
withStyle( withStyle(
style, style,
@@ -700,7 +701,8 @@ fun InLineIconRenderer(
} }
.associate { it.first to it.second } .associate { it.first to it.second }
val annotatedText = remember { val annotatedText =
remember {
buildAnnotatedString { buildAnnotatedString {
wordsInOrder.forEachIndexed { idx, value -> wordsInOrder.forEachIndexed { idx, value ->
withStyle( withStyle(
@@ -58,9 +58,9 @@ import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.subtleBorder import com.vitorpamplona.amethyst.ui.theme.subtleBorder
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import java.text.NumberFormat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.text.NumberFormat
@Stable data class InvoiceAmount(val invoice: String, val amount: String?) @Stable data class InvoiceAmount(val invoice: String, val amount: String?)
@@ -104,12 +104,12 @@ import com.vitorpamplona.amethyst.ui.theme.replyModifier
import com.vitorpamplona.amethyst.ui.uriToRoute import com.vitorpamplona.amethyst.ui.uriToRoute
import com.vitorpamplona.quartz.encoders.Nip19 import com.vitorpamplona.quartz.encoders.Nip19
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.net.MalformedURLException import java.net.MalformedURLException
import java.net.URISyntaxException import java.net.URISyntaxException
import java.net.URL import java.net.URL
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg") val imageExtensions = listOf("png", "jpg", "gif", "bmp", "jpeg", "webp", "svg")
val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3", "m3u8") val videoExtensions = listOf("mp4", "avi", "wmv", "mpg", "amv", "webm", "mov", "mp3", "m3u8")
@@ -393,7 +393,8 @@ private fun RenderContentAsMarkdown(
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val uri = LocalUriHandler.current val uri = LocalUriHandler.current
val onClick = remember { val onClick =
remember {
{ link: String -> { link: String ->
val route = uriToRoute(link) val route = uriToRoute(link)
if (route != null) { if (route != null) {
@@ -641,7 +642,8 @@ private fun RenderHashtag(
val regularText = remember { SpanStyle(color = background) } val regularText = remember { SpanStyle(color = background) }
val clickableTextStyle = remember { SpanStyle(color = primary) } val clickableTextStyle = remember { SpanStyle(color = primary) }
val annotatedTermsString = remember { val annotatedTermsString =
remember {
buildAnnotatedString { buildAnnotatedString {
withStyle(clickableTextStyle) { withStyle(clickableTextStyle) {
pushStringAnnotation("routeToHashtag", "") pushStringAnnotation("routeToHashtag", "")
@@ -32,8 +32,8 @@ import coil.request.ImageRequest
import coil.request.Options import coil.request.Options
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.quartz.utils.Robohash import com.vitorpamplona.quartz.utils.Robohash
import java.nio.charset.Charset
import okio.Buffer import okio.Buffer
import java.nio.charset.Charset
@Stable @Stable
class HashImageFetcher( class HashImageFetcher(
@@ -51,7 +51,8 @@ class HashImageFetcher(
Charset.defaultCharset(), Charset.defaultCharset(),
) )
buffer buffer
} finally {} } finally {
}
return SourceResult( return SourceResult(
source = ImageSource(source, context), source = ImageSource(source, context),
@@ -105,8 +105,6 @@ import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.ui.theme.Size75dp import com.vitorpamplona.amethyst.ui.theme.Size75dp
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import java.util.UUID
import kotlin.math.abs
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -114,6 +112,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.UUID
import kotlin.math.abs
public val DEFAULT_MUTED_SETTING = mutableStateOf(true) public val DEFAULT_MUTED_SETTING = mutableStateOf(true)
@@ -195,7 +195,8 @@ fun VideoView(
) { ) {
val defaultToStart by remember(videoUri) { mutableStateOf(DEFAULT_MUTED_SETTING.value) } val defaultToStart by remember(videoUri) { mutableStateOf(DEFAULT_MUTED_SETTING.value) }
val automaticallyStartPlayback = remember { val automaticallyStartPlayback =
remember {
mutableStateOf<Boolean>( mutableStateOf<Boolean>(
if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback.value, if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback.value,
) )
@@ -488,7 +489,8 @@ fun GetVideoController(
// User pauses and resumes the app. What to do with videos? // User pauses and resumes the app. What to do with videos?
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(key1 = lifeCycleOwner) { DisposableEffect(key1 = lifeCycleOwner) {
val observer = LifecycleEventObserver { _, event -> val observer =
LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) { if (event == Lifecycle.Event.ON_RESUME) {
// if the controller is null, restarts the controller with a new one // if the controller is null, restarts the controller with a new one
// if the controller is not null, just continue playing what the controller was playing // if the controller is not null, just continue playing what the controller was playing
@@ -654,7 +656,8 @@ private fun RenderVideoPlayer(
) { ) {
val borders = MaterialTheme.colorScheme.imageModifier val borders = MaterialTheme.colorScheme.imageModifier
val myModifier = remember { val myModifier =
remember {
if (roundedCorner) { if (roundedCorner) {
modifier.then( modifier.then(
borders.defaultMinSize(minHeight = 75.dp).align(Alignment.Center), borders.defaultMinSize(minHeight = 75.dp).align(Alignment.Center),
@@ -721,7 +724,8 @@ private fun RenderVideoPlayer(
val startingMuteState = remember(controller) { controller.volume < 0.001 } val startingMuteState = remember(controller) { controller.volume < 0.001 }
val topPadding = remember { val topPadding =
remember {
derivedStateOf { derivedStateOf {
if (topPaddingForControllers.isSpecified && videoPlaybackHeight.value.value > 0) { if (topPaddingForControllers.isSpecified && videoPlaybackHeight.value.value > 0) {
val space = (abs(this.maxHeight.value - videoPlaybackHeight.value.value) / 2).dp val space = (abs(this.maxHeight.value - videoPlaybackHeight.value.value) / 2).dp
@@ -898,13 +902,14 @@ fun ControlWhenPlayerIsActive(
} }
} }
fun Modifier.onVisiblePositionChanges(onVisiblePosition: (Float?) -> Unit): Modifier = composed { fun Modifier.onVisiblePositionChanges(onVisiblePosition: (Float?) -> Unit): Modifier =
composed {
val view = LocalView.current val view = LocalView.current
onGloballyPositioned { coordinates -> onGloballyPositioned { coordinates ->
onVisiblePosition(coordinates.getDistanceToVertCenterIfVisible(view)) onVisiblePosition(coordinates.getDistanceToVertCenterIfVisible(view))
} }
} }
fun LayoutCoordinates.getDistanceToVertCenterIfVisible(view: View): Float? { fun LayoutCoordinates.getDistanceToVertCenterIfVisible(view: View): Float? {
if (!isAttached) return null if (!isAttached) return null
@@ -941,7 +946,8 @@ private fun MuteButton(
topPadding: State<Dp>, topPadding: State<Dp>,
toggle: (Boolean) -> Unit, toggle: (Boolean) -> Unit,
) { ) {
val holdOn = remember { val holdOn =
remember {
mutableStateOf<Boolean>( mutableStateOf<Boolean>(
true, true,
) )
@@ -129,7 +129,6 @@ import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.crypto.CryptoUtils import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.toHexKey import com.vitorpamplona.quartz.encoders.toHexKey
import java.io.File
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -137,6 +136,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.engawapg.lib.zoomable.rememberZoomState import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable import net.engawapg.lib.zoomable.zoomable
import java.io.File
@Immutable @Immutable
abstract class ZoomableContent( abstract class ZoomableContent(
@@ -311,13 +311,15 @@ private fun LocalImageView(
) { ) {
if (content.localFile != null && content.localFile.exists()) { if (content.localFile != null && content.localFile.exists()) {
BoxWithConstraints(contentAlignment = Alignment.Center) { BoxWithConstraints(contentAlignment = Alignment.Center) {
val showImage = remember { val showImage =
remember {
mutableStateOf( mutableStateOf(
if (alwayShowImage) true else accountViewModel.settings.showImages.value, if (alwayShowImage) true else accountViewModel.settings.showImages.value,
) )
} }
val myModifier = remember { val myModifier =
remember {
mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight) mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight)
/* /*
.run { .run {
@@ -328,7 +330,8 @@ private fun LocalImageView(
*/ */
} }
val contentScale = remember { val contentScale =
remember {
if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
} }
@@ -374,13 +377,15 @@ private fun UrlImageView(
alwayShowImage: Boolean = false, alwayShowImage: Boolean = false,
) { ) {
BoxWithConstraints(contentAlignment = Alignment.Center) { BoxWithConstraints(contentAlignment = Alignment.Center) {
val showImage = remember { val showImage =
remember {
mutableStateOf<Boolean>( mutableStateOf<Boolean>(
if (alwayShowImage) true else accountViewModel.settings.showImages.value, if (alwayShowImage) true else accountViewModel.settings.showImages.value,
) )
} }
val myModifier = remember { val myModifier =
remember {
mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight) mainImageModifier.widthIn(max = maxWidth).heightIn(max = maxHeight)
/* Is this necessary? It makes images bleed into other pages /* Is this necessary? It makes images bleed into other pages
.run { .run {
@@ -391,7 +396,8 @@ private fun UrlImageView(
*/ */
} }
val contentScale = remember { val contentScale =
remember {
if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth if (maxHeight.isFinite) ContentScale.Fit else ContentScale.FillWidth
} }
@@ -438,7 +444,8 @@ fun ImageUrlWithDownloadButton(
val regularText = remember { SpanStyle(color = background) } val regularText = remember { SpanStyle(color = background) }
val clickableTextStyle = remember { SpanStyle(color = primary) } val clickableTextStyle = remember { SpanStyle(color = primary) }
val annotatedTermsString = remember { val annotatedTermsString =
remember {
buildAnnotatedString { buildAnnotatedString {
withStyle(clickableTextStyle) { withStyle(clickableTextStyle) {
pushStringAnnotation("routeToImage", "") pushStringAnnotation("routeToImage", "")
@@ -514,7 +521,8 @@ private fun AddedImageFeatures(
} else { } else {
when (painter.value) { when (painter.value) {
null, null,
is AsyncImagePainter.State.Loading, -> { is AsyncImagePainter.State.Loading,
-> {
if (content.blurhash != null) { if (content.blurhash != null) {
if (ratio != null) { if (ratio != null) {
DisplayBlurHash( DisplayBlurHash(
@@ -577,7 +585,8 @@ private fun AddedImageFeatures(
when (painter.value) { when (painter.value) {
null, null,
is AsyncImagePainter.State.Loading, -> { is AsyncImagePainter.State.Loading,
-> {
if (content.blurhash != null) { if (content.blurhash != null) {
if (ratio != null) { if (ratio != null) {
DisplayBlurHash( DisplayBlurHash(
@@ -663,7 +672,8 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) {
val regularText = remember { SpanStyle(color = background) } val regularText = remember { SpanStyle(color = background) }
val clickableTextStyle = remember { SpanStyle(color = primary) } val clickableTextStyle = remember { SpanStyle(color = primary) }
val annotatedTermsString = remember { val annotatedTermsString =
remember {
buildAnnotatedString { buildAnnotatedString {
if (content is ZoomableUrlContent) { if (content is ZoomableUrlContent) {
withStyle(clickableTextStyle) { withStyle(clickableTextStyle) {
@@ -685,7 +695,8 @@ private fun DisplayUrlWithLoadingSymbolWait(content: ZoomableContent) {
val inlineContent = mapOf("inlineContent" to InlineLoadingIcon()) val inlineContent = mapOf("inlineContent" to InlineLoadingIcon())
val pressIndicator = remember { val pressIndicator =
remember {
if (content is ZoomableUrlContent) { if (content is ZoomableUrlContent) {
Modifier.clickable { runCatching { uri.openUri(content.url) } } Modifier.clickable { runCatching { uri.openUri(content.url) } }
} else { } else {
@@ -176,9 +176,11 @@ class ChatroomListKnownFeedFilter(val account: Account) : AdditiveFeedFilter<Not
if (roomKey != null && room != null) { if (roomKey != null && room != null) {
if ( if (
(newNote.author?.pubkeyHex == me.pubkeyHex || (
newNote.author?.pubkeyHex == me.pubkeyHex ||
room.senderIntersects(followingKeySet) || room.senderIntersects(followingKeySet) ||
me.hasSentMessagesTo(roomKey)) && !account.isAllHidden(roomKey.users) me.hasSentMessagesTo(roomKey)
) && !account.isAllHidden(roomKey.users)
) { ) {
val lastNote = newRelevantPrivateMessages.get(roomKey) val lastNote = newRelevantPrivateMessages.get(roomKey)
if (lastNote != null) { if (lastNote != null) {
@@ -117,9 +117,11 @@ class ChatroomListNewFeedFilter(val account: Account) : AdditiveFeedFilter<Note>
if ( if (
roomKey != null && roomKey != null &&
room != null && room != null &&
(newNote.author?.pubkeyHex != me.pubkeyHex && (
newNote.author?.pubkeyHex != me.pubkeyHex &&
room.senderIntersects(followingKeySet) && room.senderIntersects(followingKeySet) &&
!me.hasSentMessagesTo(roomKey)) && !me.hasSentMessagesTo(roomKey)
) &&
!account.isAllHidden(roomKey.users) !account.isAllHidden(roomKey.users)
) { ) {
val lastNote = newRelevantPrivateMessages.get(roomKey) val lastNote = newRelevantPrivateMessages.get(roomKey)
@@ -49,12 +49,14 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
return collection return collection
.asSequence() .asSequence()
.filter { .filter {
(it.event is TextNoteEvent || (
it.event is TextNoteEvent ||
it.event is LongTextNoteEvent || it.event is LongTextNoteEvent ||
it.event is ChannelMessageEvent || it.event is ChannelMessageEvent ||
it.event is PrivateDmEvent || it.event is PrivateDmEvent ||
it.event is PollNoteEvent || it.event is PollNoteEvent ||
it.event is AudioHeaderEvent) && it.event?.isTaggedGeoHash(myTag) == true it.event is AudioHeaderEvent
) && it.event?.isTaggedGeoHash(myTag) == true
} }
.filter { account.isAcceptable(it) } .filter { account.isAcceptable(it) }
.toSet() .toSet()
@@ -49,12 +49,14 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
return collection return collection
.asSequence() .asSequence()
.filter { .filter {
(it.event is TextNoteEvent || (
it.event is TextNoteEvent ||
it.event is LongTextNoteEvent || it.event is LongTextNoteEvent ||
it.event is ChannelMessageEvent || it.event is ChannelMessageEvent ||
it.event is PrivateDmEvent || it.event is PrivateDmEvent ||
it.event is PollNoteEvent || it.event is PollNoteEvent ||
it.event is AudioHeaderEvent) && it.event?.isTaggedHash(myTag) == true it.event is AudioHeaderEvent
) && it.event?.isTaggedHash(myTag) == true
} }
.filter { account.isAcceptable(it) } .filter { account.isAcceptable(it) }
.toSet() .toSet()
@@ -65,14 +65,18 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
return collection return collection
.asSequence() .asSequence()
.filter { .filter {
(it.event is TextNoteEvent || (
it.event is TextNoteEvent ||
it.event is PollNoteEvent || it.event is PollNoteEvent ||
it.event is ChannelMessageEvent || it.event is ChannelMessageEvent ||
it.event is LiveActivitiesChatMessageEvent) && it.event is LiveActivitiesChatMessageEvent
(isGlobal || ) &&
(
isGlobal ||
it.author?.pubkeyHex in followingKeySet || it.author?.pubkeyHex in followingKeySet ||
it.event?.isTaggedHashes(followingTagSet) ?: false || it.event?.isTaggedHashes(followingTagSet) ?: false ||
it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false) && it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false
) &&
// && account.isAcceptable(it) // This filter follows only. No need to check if // && account.isAcceptable(it) // This filter follows only. No need to check if
// acceptable // acceptable
(isHiddenList || it.author?.let { !account.isHidden(it) } ?: true) && (isHiddenList || it.author?.let { !account.isHidden(it) } ?: true) &&
@@ -81,7 +81,8 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
.filter { it -> .filter { it ->
val noteEvent = it.event val noteEvent = it.event
val isGlobalRelay = it.relays.any { gRelays.contains(it.url) } val isGlobalRelay = it.relays.any { gRelays.contains(it.url) }
(noteEvent is TextNoteEvent || (
noteEvent is TextNoteEvent ||
noteEvent is ClassifiedsEvent || noteEvent is ClassifiedsEvent ||
noteEvent is RepostEvent || noteEvent is RepostEvent ||
noteEvent is GenericRepostEvent || noteEvent is GenericRepostEvent ||
@@ -89,23 +90,33 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
noteEvent is PollNoteEvent || noteEvent is PollNoteEvent ||
noteEvent is HighlightEvent || noteEvent is HighlightEvent ||
noteEvent is AudioTrackEvent || noteEvent is AudioTrackEvent ||
noteEvent is AudioHeaderEvent) && noteEvent is AudioHeaderEvent
) &&
(!ignoreAddressables || noteEvent.kind() < 10000) && (!ignoreAddressables || noteEvent.kind() < 10000) &&
((isGlobal && isGlobalRelay) || (
(isGlobal && isGlobalRelay) ||
it.author?.pubkeyHex in followingKeySet || it.author?.pubkeyHex in followingKeySet ||
noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedHashes(followingTagSet) ||
noteEvent.isTaggedGeoHashes(followingGeohashSet) || noteEvent.isTaggedGeoHashes(followingGeohashSet) ||
noteEvent.isTaggedAddressableNotes(followingCommunities)) && noteEvent.isTaggedAddressableNotes(followingCommunities)
) &&
// && account.isAcceptable(it) // This filter follows only. No need to check if // && account.isAcceptable(it) // This filter follows only. No need to check if
// acceptable // acceptable
(isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) && (isHiddenList || it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true) &&
((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) && ((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) &&
it.isNewThread() && it.isNewThread() &&
((noteEvent !is RepostEvent && noteEvent !is GenericRepostEvent) || // not a repost (
(it.replyTo?.lastOrNull()?.author?.pubkeyHex !in followingKeySet || (noteEvent !is RepostEvent && noteEvent !is GenericRepostEvent) || // not a repost
(noteEvent.createdAt() > (
(it.replyTo?.lastOrNull()?.createdAt() it.replyTo?.lastOrNull()?.author?.pubkeyHex !in followingKeySet ||
?: 0) + oneHr)) // or a repost of by a non-follower's post (likely not seen yet) (
noteEvent.createdAt() >
(
it.replyTo?.lastOrNull()?.createdAt()
?: 0
) + oneHr
)
) // or a repost of by a non-follower's post (likely not seen yet)
) )
} }
.toSet() .toSet()
@@ -103,8 +103,10 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
} }
return isAuthoredPostCited || return isAuthoredPostCited ||
(event.citedUsers().contains(authorHex) || (
note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true) event.citedUsers().contains(authorHex) ||
note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true
)
} }
if (event is ReactionEvent) { if (event is ReactionEvent) {
@@ -47,10 +47,12 @@ class UserProfileConversationsFeedFilter(val user: User, val account: Account) :
return collection return collection
.filter { .filter {
it.author == user && it.author == user &&
(it.event is TextNoteEvent || (
it.event is TextNoteEvent ||
it.event is PollNoteEvent || it.event is PollNoteEvent ||
it.event is ChannelMessageEvent || it.event is ChannelMessageEvent ||
it.event is LiveActivitiesChatMessageEvent) && it.event is LiveActivitiesChatMessageEvent
) &&
!it.isNewThread() && !it.isNewThread() &&
account.isAcceptable(it) == true account.isAcceptable(it) == true
} }
@@ -55,7 +55,8 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) :
return collection return collection
.filter { .filter {
it.author == user && it.author == user &&
(it.event is TextNoteEvent || (
it.event is TextNoteEvent ||
it.event is ClassifiedsEvent || it.event is ClassifiedsEvent ||
it.event is RepostEvent || it.event is RepostEvent ||
it.event is GenericRepostEvent || it.event is GenericRepostEvent ||
@@ -63,7 +64,8 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) :
it.event is PollNoteEvent || it.event is PollNoteEvent ||
it.event is HighlightEvent || it.event is HighlightEvent ||
it.event is AudioTrackEvent || it.event is AudioTrackEvent ||
it.event is AudioHeaderEvent) && it.event is AudioHeaderEvent
) &&
it.isNewThread() && it.isNewThread() &&
account.isAcceptable(it) == true account.isAcceptable(it) == true
} }
@@ -69,9 +69,9 @@ import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import java.math.BigDecimal
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal
@Stable data class Reward(val amount: BigDecimal) @Stable data class Reward(val amount: BigDecimal)
@@ -191,7 +191,8 @@ fun DisplayAccount(
Box( Box(
modifier = Modifier.width(55.dp).padding(0.dp), modifier = Modifier.width(55.dp).padding(0.dp),
) { ) {
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -93,7 +93,8 @@ fun AppNavigation(
sharedPreferencesViewModel: SharedPreferencesViewModel, sharedPreferencesViewModel: SharedPreferencesViewModel,
) { ) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val nav = remember { val nav =
remember {
{ route: String -> { route: String ->
scope.launch { scope.launch {
if (getRouteWithArguments(navController) != route) { if (getRouteWithArguments(navController) != route) {
@@ -545,7 +545,8 @@ private fun LoggedInUserPictureDrawer(
accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState() accountViewModel.account.userProfile().live().profilePictureChanges.observeAsState()
val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex } val pubkeyHex = remember { accountViewModel.userProfile().pubkeyHex }
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -728,9 +729,11 @@ class FollowListViewModel(val account: Account) : ViewModel() {
if ( if (
newNotes.any { newNotes.any {
it.event?.pubKey() == account.userProfile().pubkeyHex && it.event?.pubKey() == account.userProfile().pubkeyHex &&
(it.event is PeopleListEvent || (
it.event is PeopleListEvent ||
it.event is MuteListEvent || it.event is MuteListEvent ||
it.event is ContactListEvent) it.event is ContactListEvent
)
} }
) { ) {
refresh() refresh()
@@ -746,9 +749,7 @@ class FollowListViewModel(val account: Account) : ViewModel() {
} }
class Factory(val account: Account) : ViewModelProvider.Factory { class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <FollowListViewModel : ViewModel> create( override fun <FollowListViewModel : ViewModel> create(modelClass: Class<FollowListViewModel>): FollowListViewModel {
modelClass: Class<FollowListViewModel>
): FollowListViewModel {
return FollowListViewModel(account) as FollowListViewModel return FollowListViewModel(account) as FollowListViewModel
} }
} }
@@ -126,7 +126,8 @@ fun DrawerContent(
Unit Unit
} }
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -33,8 +33,8 @@ import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.CommunityDefinitionEvent import com.vitorpamplona.quartz.events.CommunityDefinitionEvent
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
import com.vitorpamplona.quartz.events.LiveActivitiesEvent import com.vitorpamplona.quartz.events.LiveActivitiesEvent
import java.net.URLEncoder
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import java.net.URLEncoder
fun routeFor( fun routeFor(
note: Note, note: Note,
@@ -309,7 +309,8 @@ private fun CheckNewAndRenderChannelCard(
) { ) {
val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor val newItemColor = MaterialTheme.colorScheme.newItemBackgroundColor
val defaultBackgroundColor = MaterialTheme.colorScheme.background val defaultBackgroundColor = MaterialTheme.colorScheme.background
val backgroundColor = remember { val backgroundColor =
remember {
mutableStateOf<Color>( mutableStateOf<Color>(
parentBackgroundColor?.value ?: defaultBackgroundColor, parentBackgroundColor?.value ?: defaultBackgroundColor,
) )
@@ -177,7 +177,8 @@ private fun ChannelRoomCompose(
val hasNewMessages = remember { mutableStateOf<Boolean>(false) } val hasNewMessages = remember { mutableStateOf<Boolean>(false) }
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -261,7 +261,8 @@ fun NormalChatNote(
val otherColors = MaterialTheme.colorScheme.subtleBorder val otherColors = MaterialTheme.colorScheme.subtleBorder
val defaultBackground = MaterialTheme.colorScheme.background val defaultBackground = MaterialTheme.colorScheme.background
val backgroundBubbleColor = remember { val backgroundBubbleColor =
remember {
if (accountViewModel.isLoggedUser(note.author)) { if (accountViewModel.isLoggedUser(note.author)) {
mutableStateOf( mutableStateOf(
loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground), loggedInColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground),
@@ -270,14 +271,16 @@ fun NormalChatNote(
mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground)) mutableStateOf(otherColors.compositeOver(parentBackgroundColor?.value ?: defaultBackground))
} }
} }
val alignment: Arrangement.Horizontal = remember { val alignment: Arrangement.Horizontal =
remember {
if (accountViewModel.isLoggedUser(note.author)) { if (accountViewModel.isLoggedUser(note.author)) {
Arrangement.End Arrangement.End
} else { } else {
Arrangement.Start Arrangement.Start
} }
} }
val shape: Shape = remember { val shape: Shape =
remember {
if (accountViewModel.isLoggedUser(note.author)) { if (accountViewModel.isLoggedUser(note.author)) {
ChatBubbleShapeMe ChatBubbleShapeMe
} else { } else {
@@ -301,7 +304,8 @@ fun NormalChatNote(
val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier
val clickableModifier = remember { val clickableModifier =
remember {
Modifier.combinedClickable( Modifier.combinedClickable(
onClick = { onClick = {
if (note.event is ChannelCreateEvent) { if (note.event is ChannelCreateEvent) {
@@ -366,7 +370,8 @@ private fun RenderBubble(
) { ) {
val bubbleSize = remember { mutableIntStateOf(0) } val bubbleSize = remember { mutableIntStateOf(0) }
val bubbleModifier = remember { val bubbleModifier =
remember {
Modifier.padding(start = 10.dp, end = 5.dp, bottom = 5.dp).onSizeChanged { Modifier.padding(start = 10.dp, end = 5.dp, bottom = 5.dp).onSizeChanged {
if (bubbleSize.intValue != it.width) { if (bubbleSize.intValue != it.width) {
bubbleSize.intValue = it.width bubbleSize.intValue = it.width
@@ -91,12 +91,12 @@ import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier import com.vitorpamplona.amethyst.ui.theme.profile35dpModifier
import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.EmptyTagList
import kotlin.time.ExperimentalTime
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
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.time.ExperimentalTime
@OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalTime::class)
@Composable @Composable
@@ -364,7 +364,8 @@ private fun ParseAuthorCommentAndAmount(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onReady: @Composable (MutableState<ZapAmountCommentNotification>) -> Unit, onReady: @Composable (MutableState<ZapAmountCommentNotification>) -> Unit,
) { ) {
val content = remember { val content =
remember {
mutableStateOf( mutableStateOf(
ZapAmountCommentNotification( ZapAmountCommentNotification(
user = zapRequest.author, user = zapRequest.author,
@@ -541,7 +542,8 @@ fun WatchUserMetadataAndFollowsAndRenderUserProfilePicture(
author: User, author: User,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -73,9 +73,9 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.events.AddressableEvent import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.UserMetadata import com.vitorpamplona.quartz.events.UserMetadata
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.time.Duration.Companion.seconds
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlin.time.Duration.Companion.seconds
@Composable @Composable
fun nip05VerificationAsAState( fun nip05VerificationAsAState(
@@ -228,14 +228,14 @@ import com.vitorpamplona.quartz.events.VideoEvent
import com.vitorpamplona.quartz.events.VideoHorizontalEvent import com.vitorpamplona.quartz.events.VideoHorizontalEvent
import com.vitorpamplona.quartz.events.VideoVerticalEvent import com.vitorpamplona.quartz.events.VideoVerticalEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import java.io.File
import java.net.URL
import java.util.Locale
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
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
import java.net.URL
import java.util.Locale
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
@@ -483,7 +483,8 @@ fun NormalNote(
if (isQuotedNote || isBoostedNote) { if (isQuotedNote || isBoostedNote) {
when (baseNote.event) { when (baseNote.event) {
is ChannelCreateEvent, is ChannelCreateEvent,
is ChannelMetadataEvent, -> is ChannelMetadataEvent,
->
ChannelHeader( ChannelHeader(
channelNote = baseNote, channelNote = baseNote,
showVideo = !makeItShort, showVideo = !makeItShort,
@@ -527,7 +528,8 @@ fun NormalNote(
} else { } else {
when (baseNote.event) { when (baseNote.event) {
is ChannelCreateEvent, is ChannelCreateEvent,
is ChannelMetadataEvent, -> is ChannelMetadataEvent,
->
ChannelHeader( ChannelHeader(
channelNote = baseNote, channelNote = baseNote,
showVideo = !makeItShort, showVideo = !makeItShort,
@@ -775,7 +777,8 @@ fun ShortCommunityHeader(
val noteEvent = val noteEvent =
remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -1038,7 +1041,8 @@ fun InnerNoteWithReactions(
if (isNotRepost) { if (isNotRepost) {
if (makeItShort) { if (makeItShort) {
if (isBoostedNote) {} else { if (isBoostedNote) {
} else {
Spacer(modifier = DoubleVertSpacer) Spacer(modifier = DoubleVertSpacer)
} }
} else { } else {
@@ -2296,7 +2300,8 @@ private fun RenderReport(
.toSet() .toSet()
.joinToString(", ") .joinToString(", ")
val content = remember { val content =
remember {
reportType + (note.event?.content()?.ifBlank { null }?.let { ": $it" } ?: "") reportType + (note.event?.content()?.ifBlank { null }?.let { ": $it" } ?: "")
} }
@@ -2394,7 +2399,8 @@ private fun ReplyRow(
Spacer(modifier = StdVertSpacer) Spacer(modifier = StdVertSpacer)
} else if (showChannelInfo != null) { } else if (showChannelInfo != null) {
val replies = remember { note.replyTo?.toImmutableList() } val replies = remember { note.replyTo?.toImmutableList() }
val mentions = remember { val mentions =
remember {
(note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf() (note.event as? BaseTextNoteEvent)?.mentions()?.toImmutableList() ?: persistentListOf()
} }
@@ -2771,7 +2777,8 @@ fun DisplayHighlight(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: (String) -> Unit, nav: (String) -> Unit,
) { ) {
val quote = remember { val quote =
remember {
highlight.split("\n").joinToString("\n") { "> *${it.removeSuffix(" ")}*" } highlight.split("\n").joinToString("\n") { "> *${it.removeSuffix(" ")}*" }
} }
@@ -2846,7 +2853,8 @@ private fun LoadAndDisplayPost(
@Composable @Composable
private fun LoadAndDisplayUrl(url: String) { private fun LoadAndDisplayUrl(url: String) {
val validatedUrl = remember { val validatedUrl =
remember {
try { try {
URL(url) URL(url)
} catch (e: Exception) { } catch (e: Exception) {
@@ -442,8 +442,7 @@ private fun BlockAlertDialog(
note: Note, note: Note,
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) = ) = QuickActionAlertDialog(
QuickActionAlertDialog(
title = stringResource(R.string.report_dialog_block_hide_user_btn), title = stringResource(R.string.report_dialog_block_hide_user_btn),
textContent = stringResource(R.string.report_dialog_blocking_a_user), textContent = stringResource(R.string.report_dialog_blocking_a_user),
buttonIcon = Icons.Default.Block, buttonIcon = Icons.Default.Block,
@@ -463,7 +462,7 @@ private fun BlockAlertDialog(
onDismiss() onDismiss()
}, },
onDismiss = onDismiss, onDismiss = onDismiss,
) )
@Composable @Composable
fun QuickActionAlertDialog( fun QuickActionAlertDialog(
@@ -87,11 +87,11 @@ import com.vitorpamplona.quartz.events.EmptyTagList
import com.vitorpamplona.quartz.events.ImmutableListOfLists import com.vitorpamplona.quartz.events.ImmutableListOfLists
import com.vitorpamplona.quartz.events.LnZapEvent import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.toImmutableListOfLists import com.vitorpamplona.quartz.events.toImmutableListOfLists
import kotlin.math.roundToInt
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@Composable @Composable
fun PollNote( fun PollNote(
@@ -35,12 +35,12 @@ import com.vitorpamplona.quartz.events.PollNoteEvent
import com.vitorpamplona.quartz.events.VALUE_MAXIMUM import com.vitorpamplona.quartz.events.VALUE_MAXIMUM
import com.vitorpamplona.quartz.events.VALUE_MINIMUM import com.vitorpamplona.quartz.events.VALUE_MINIMUM
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.math.BigDecimal
import java.math.RoundingMode
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
@Immutable @Immutable
data class PollOption( data class PollOption(
@@ -134,8 +134,7 @@ class PollNoteViewModel : ViewModel() {
return account.userProfile() != note.author && !wasZappedByLoggedInAccount return account.userProfile() != note.author && !wasZappedByLoggedInAccount
} }
fun isVoteAmountAtomic() = fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
fun isPollClosed(): Boolean = fun isPollClosed(): Boolean =
closedAt?.let { // allow 2 minute leeway for zap to propagate closedAt?.let { // allow 2 minute leeway for zap to propagate
@@ -128,10 +128,6 @@ import com.vitorpamplona.amethyst.ui.theme.TinyBorders
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter import com.vitorpamplona.amethyst.ui.theme.placeholderTextColorFilter
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.math.roundToInt
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -140,6 +136,10 @@ import kotlinx.collections.immutable.toImmutableMap
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.math.roundToInt
@Composable @Composable
fun ReactionsRow( fun ReactionsRow(
@@ -597,10 +597,12 @@ private fun <S> AnimatedContentTransitionScope<S>.transitionSpec(): ContentTrans
@ExperimentalAnimationApi @ExperimentalAnimationApi
val slideAnimation: ContentTransform = val slideAnimation: ContentTransform =
(slideInVertically(animationSpec = tween(durationMillis = 100)) { height -> height } + (
slideInVertically(animationSpec = tween(durationMillis = 100)) { height -> height } +
fadeIn( fadeIn(
animationSpec = tween(durationMillis = 100), animationSpec = tween(durationMillis = 100),
)) )
)
.togetherWith( .togetherWith(
slideOutVertically(animationSpec = tween(durationMillis = 100)) { height -> -height } + slideOutVertically(animationSpec = tween(durationMillis = 100)) { height -> -height } +
fadeOut( fadeOut(
@@ -149,7 +149,8 @@ fun RenderRelay(
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = Size15dp) val ripple = rememberRipple(bounded = false, radius = Size15dp)
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -135,9 +135,7 @@ class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
} }
class Factory(val account: Account) : ViewModelProvider.Factory { class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <UpdateReactionTypeViewModel : ViewModel> create( override fun <UpdateReactionTypeViewModel : ViewModel> create(modelClass: Class<UpdateReactionTypeViewModel>): UpdateReactionTypeViewModel {
modelClass: Class<UpdateReactionTypeViewModel>
): UpdateReactionTypeViewModel {
return UpdateReactionTypeViewModel(account) as UpdateReactionTypeViewModel return UpdateReactionTypeViewModel(account) as UpdateReactionTypeViewModel
} }
} }
@@ -194,11 +194,13 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() {
} }
fun hasChanged(): Boolean { fun hasChanged(): Boolean {
return (selectedZapType != account?.defaultZapType || return (
selectedZapType != account?.defaultZapType ||
amountSet != account?.zapAmountChoices || amountSet != account?.zapAmountChoices ||
walletConnectPubkey.text != (account?.zapPaymentRequest?.pubKeyHex ?: "") || walletConnectPubkey.text != (account?.zapPaymentRequest?.pubKeyHex ?: "") ||
walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "") || walletConnectRelay.text != (account?.zapPaymentRequest?.relayUri ?: "") ||
walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "")) walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "")
)
} }
fun updateNIP47(uri: String) { fun updateNIP47(uri: String) {
@@ -211,9 +213,7 @@ class UpdateZapAmountViewModel(val account: Account) : ViewModel() {
} }
class Factory(val account: Account) : ViewModelProvider.Factory { class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <UpdateZapAmountViewModel : ViewModel> create( override fun <UpdateZapAmountViewModel : ViewModel> create(modelClass: Class<UpdateZapAmountViewModel>): UpdateZapAmountViewModel {
modelClass: Class<UpdateZapAmountViewModel>
): UpdateZapAmountViewModel {
return UpdateZapAmountViewModel(account) as UpdateZapAmountViewModel return UpdateZapAmountViewModel(account) as UpdateZapAmountViewModel
} }
} }
@@ -261,7 +261,8 @@ fun UpdateZapAmountDialog(
), ),
) )
val zapOptions = remember { val zapOptions =
remember {
zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList()
} }
@@ -114,7 +114,8 @@ fun DisplayBlankAuthor(
) { ) {
val backgroundColor = MaterialTheme.colorScheme.background val backgroundColor = MaterialTheme.colorScheme.background
val nullModifier = remember { val nullModifier =
remember {
modifier.size(size).clip(shape = CircleShape).background(backgroundColor) modifier.size(size).clip(shape = CircleShape).background(backgroundColor)
} }
@@ -182,7 +183,8 @@ fun ClickableUserPicture(
val ripple = rememberRipple(bounded = false, radius = size) val ripple = rememberRipple(bounded = false, radius = size)
// BaseUser is the same reference as accountState.user // BaseUser is the same reference as accountState.user
val myModifier = remember { val myModifier =
remember {
if (onClick != null && onLongClick != null) { if (onClick != null && onLongClick != null) {
Modifier.size(size) Modifier.size(size)
.combinedClickable( .combinedClickable(
@@ -371,11 +373,13 @@ fun InnerUserPicture(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
) { ) {
val backgroundColor = MaterialTheme.colorScheme.background val backgroundColor = MaterialTheme.colorScheme.background
val myImageModifier = remember { val myImageModifier =
remember {
modifier.size(size).clip(shape = CircleShape).background(backgroundColor) modifier.size(size).clip(shape = CircleShape).background(backgroundColor)
} }
val automaticallyShowProfilePicture = remember { val automaticallyShowProfilePicture =
remember {
accountViewModel.settings.showProfilePictures.value accountViewModel.settings.showProfilePictures.value
} }
@@ -73,11 +73,6 @@ import com.vitorpamplona.quartz.events.LnZapEvent
import com.vitorpamplona.quartz.events.ReactionEvent import com.vitorpamplona.quartz.events.ReactionEvent
import com.vitorpamplona.quartz.events.RepostEvent import com.vitorpamplona.quartz.events.RepostEvent
import com.vitorpamplona.quartz.events.TextNoteEvent import com.vitorpamplona.quartz.events.TextNoteEvent
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -85,6 +80,11 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Composable @Composable
fun UserReactionsRow( fun UserReactionsRow(
@@ -433,9 +433,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
} }
class Factory(val account: Account) : ViewModelProvider.Factory { class Factory(val account: Account) : ViewModelProvider.Factory {
override fun <UserReactionsViewModel : ViewModel> create( override fun <UserReactionsViewModel : ViewModel> create(modelClass: Class<UserReactionsViewModel>): UserReactionsViewModel {
modelClass: Class<UserReactionsViewModel>
): UserReactionsViewModel {
return UserReactionsViewModel(account) as UserReactionsViewModel return UserReactionsViewModel(account) as UserReactionsViewModel
} }
} }
@@ -152,7 +152,8 @@ fun ZapCustomDialog(
), ),
) )
val zapOptions = remember { val zapOptions =
remember {
zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList() zapTypes.map { TitleExplainer(it.second, it.third) }.toImmutableList()
} }
var selectedZapType by var selectedZapType by
@@ -39,7 +39,6 @@ import com.vitorpamplona.quartz.encoders.toNpub
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
import com.vitorpamplona.quartz.signers.NostrSignerExternal import com.vitorpamplona.quartz.signers.NostrSignerExternal
import com.vitorpamplona.quartz.signers.NostrSignerInternal import com.vitorpamplona.quartz.signers.NostrSignerInternal
import java.util.regex.Pattern
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@@ -48,6 +47,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.regex.Pattern
val EMAIL_PATTERN = Pattern.compile(".+@.+\\.[a-z]+") val EMAIL_PATTERN = Pattern.compile(".+@.+\\.[a-z]+")
@@ -81,8 +81,7 @@ class AccountStateViewModel() : ViewModel() {
proxyPort: Int, proxyPort: Int,
loginWithExternalSigner: Boolean = false, loginWithExternalSigner: Boolean = false,
packageName: String = "", packageName: String = "",
) = ) = withContext(Dispatchers.IO) {
withContext(Dispatchers.IO) {
val parsed = Nip19.uriToRoute(key) val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.hexToByteArray() val pubKeyParsed = parsed?.hex?.hexToByteArray()
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort) val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)

Some files were not shown because too many files have changed in this diff Show More