mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9af2d8982 | ||
|
|
655a563f61 | ||
|
|
9777c709a1 | ||
|
|
251f4854fc | ||
|
|
43c32f0b0e | ||
|
|
ec3b07147c | ||
|
|
0e6a2c339e | ||
|
|
844f7cd478 | ||
|
|
a2edc9f3b2 | ||
|
|
35d09aee77 | ||
|
|
a79d1d5bd6 | ||
|
|
ba9315fcd1 | ||
|
|
f5c71b4f90 |
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 276
|
||||
versionName "0.74.3"
|
||||
versionCode 277
|
||||
versionName "0.74.4"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -790,6 +790,23 @@ class Account(
|
||||
|
||||
Client.send(signedEvent, relayList = relayList)
|
||||
LocalCache.consume(signedEvent)
|
||||
|
||||
// broadcast replied notes
|
||||
replyingTo?.let {
|
||||
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
replyTo?.forEach {
|
||||
it.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendPoll(
|
||||
@@ -832,6 +849,17 @@ class Account(
|
||||
// println("Sending new PollNoteEvent: %s".format(signedEvent.toJson()))
|
||||
Client.send(signedEvent, relayList = relayList)
|
||||
LocalCache.consume(signedEvent)
|
||||
|
||||
replyTo?.forEach {
|
||||
it.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
addresses?.forEach {
|
||||
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||
Client.send(it, relayList = relayList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun sendChannelMessage(message: String, toChannel: String, replyTo: List<Note>?, mentions: List<User>?, zapReceiver: String? = null, wantsToMarkAsSensitive: Boolean, zapRaiserAmount: Long? = null, geohash: String? = null) {
|
||||
|
||||
@@ -72,7 +72,22 @@ open class Note(val idHex: String) {
|
||||
open fun idNote() = id().toNote()
|
||||
|
||||
open fun toNEvent(): String {
|
||||
return Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
|
||||
val myEvent = event
|
||||
return if (myEvent is WrappedEvent) {
|
||||
val host = myEvent.host
|
||||
if (host != null) {
|
||||
Nip19.createNEvent(
|
||||
host.id,
|
||||
host.pubKey,
|
||||
host.kind(),
|
||||
relays.firstOrNull()
|
||||
)
|
||||
} else {
|
||||
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
|
||||
}
|
||||
} else {
|
||||
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
|
||||
}
|
||||
}
|
||||
|
||||
fun toNostrUri(): String {
|
||||
@@ -615,6 +630,7 @@ open class Note(val idHex: String) {
|
||||
|
||||
fun clearLive() {
|
||||
if (liveSet != null && liveSet?.isInUse() == false) {
|
||||
liveSet?.destroy()
|
||||
liveSet = null
|
||||
}
|
||||
}
|
||||
@@ -656,12 +672,26 @@ class NoteLiveSet(u: Note) {
|
||||
relays.hasObservers() ||
|
||||
zaps.hasObservers()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
metadata.destroy()
|
||||
reactions.destroy()
|
||||
boosts.destroy()
|
||||
replies.destroy()
|
||||
reports.destroy()
|
||||
relays.destroy()
|
||||
zaps.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
fun destroy() {
|
||||
bundler.cancel()
|
||||
}
|
||||
|
||||
fun invalidateData() {
|
||||
if (!hasObservers()) return
|
||||
|
||||
|
||||
@@ -372,6 +372,7 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
fun clearLive() {
|
||||
if (liveSet != null && liveSet?.isInUse() == false) {
|
||||
liveSet?.destroy()
|
||||
liveSet = null
|
||||
}
|
||||
}
|
||||
@@ -400,6 +401,18 @@ class UserLiveSet(u: User) {
|
||||
zaps.hasObservers() ||
|
||||
bookmarks.hasObservers()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
follows.destroy()
|
||||
followers.destroy()
|
||||
reports.destroy()
|
||||
messages.destroy()
|
||||
relays.destroy()
|
||||
relayInfo.destroy()
|
||||
metadata.destroy()
|
||||
zaps.destroy()
|
||||
bookmarks.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
@Immutable
|
||||
@@ -470,6 +483,10 @@ class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||
|
||||
fun destroy() {
|
||||
bundler.cancel()
|
||||
}
|
||||
|
||||
fun invalidateData() {
|
||||
if (!hasObservers()) return
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -45,7 +45,7 @@ class CashuProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
suspend fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
checkNotInMainThread()
|
||||
|
||||
runCatching {
|
||||
|
||||
@@ -2,10 +2,7 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
@@ -26,55 +23,46 @@ class Nip05Verifier() {
|
||||
return null
|
||||
}
|
||||
|
||||
fun fetchNip05Json(nip05address: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
fetchNip05JsonSuspend(nip05address, onSuccess, onError)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchNip05JsonSuspend(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
suspend fun fetchNip05Json(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val url = assembleUrl(nip05)
|
||||
|
||||
if (url == null) {
|
||||
onError("Could not assemble url from Nip05: \"${nip05}\". Check the user's setup")
|
||||
return
|
||||
return@withContext
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.build()
|
||||
try {
|
||||
val request = Request.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
HttpClient.getHttpClient().newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
checkNotInMainThread()
|
||||
HttpClient.getHttpClient().newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
checkNotInMainThread()
|
||||
|
||||
response.use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("Could not resolve $nip05. Error: ${it.code}. Check if the server up and if the address $nip05 is correct")
|
||||
}
|
||||
response.use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("Could not resolve $nip05. Error: ${it.code}. Check if the server up and if the address $nip05 is correct")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: java.io.IOException) {
|
||||
onError("Could not resolve $url. Check if the server up and if the address $nip05 is correct")
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
} catch (e: java.lang.Exception) {
|
||||
onError("Could not resolve '$url': ${e.message}")
|
||||
}
|
||||
override fun onFailure(call: Call, e: java.io.IOException) {
|
||||
onError("Could not resolve $url. Check if the server up and if the address $nip05 is correct")
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
} catch (e: java.lang.Exception) {
|
||||
onError("Could not resolve '$url': ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun verifyNip05(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
suspend fun verifyNip05(nip05: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
// check fails on tests
|
||||
checkNotInMainThread()
|
||||
|
||||
|
||||
@@ -10,12 +10,15 @@ import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import kotlin.Error
|
||||
|
||||
abstract class NostrDataSource(val debugName: String) {
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
private var subscriptions = mapOf<String, Subscription>()
|
||||
data class Counter(var counter: Int)
|
||||
|
||||
@@ -76,6 +79,8 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
fun destroy() {
|
||||
stop()
|
||||
Client.unsubscribe(clientListener)
|
||||
scope.cancel()
|
||||
bundler.cancel()
|
||||
}
|
||||
|
||||
open fun start() {
|
||||
@@ -116,8 +121,7 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
}
|
||||
|
||||
fun resetFilters() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
resetFiltersSuspend()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.encoders.Bech32
|
||||
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
|
||||
import com.vitorpamplona.quartz.encoders.toLnUrl
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
@@ -40,96 +37,70 @@ class LightningAddressResolver() {
|
||||
return null
|
||||
}
|
||||
|
||||
fun fetchLightningAddressJson(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
fetchLightningAddressJsonSuspend(lnaddress, onSuccess, onError)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchLightningAddressJsonSuspend(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
private suspend fun fetchLightningAddressJson(lnaddress: String, onSuccess: suspend (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val url = assembleUrl(lnaddress)
|
||||
|
||||
if (url == null) {
|
||||
onError("Could not assemble LNUrl from Lightning Address \"${lnaddress}\". Check the user's setup")
|
||||
return
|
||||
return@withContext
|
||||
}
|
||||
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val request: Request = Request.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("Could not resolve $lnaddress. Error: ${it.code}. Check if the server up and if the lightning address $lnaddress is correct")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: java.io.IOException) {
|
||||
onError("Could not resolve $url. Check if the server up and if the lightning address $lnaddress is correct")
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onError("Could not resolve $url. Check if the server up and if the lightning address $lnaddress is correct")
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
fetchLightningInvoiceSuspend(lnCallback, milliSats, message, nostrRequest, onSuccess, onError)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchLightningInvoiceSuspend(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val encodedMessage = URLEncoder.encode(message, "utf-8")
|
||||
|
||||
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
|
||||
var url = "$lnCallback${urlBinder}amount=$milliSats&comment=$encodedMessage"
|
||||
|
||||
if (nostrRequest != null) {
|
||||
val encodedNostrRequest = URLEncoder.encode(nostrRequest, "utf-8")
|
||||
url += "&nostr=$encodedNostrRequest"
|
||||
}
|
||||
|
||||
val request: Request = Request.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(response.body.string())
|
||||
} else {
|
||||
onError("Could not fetch invoice from $lnCallback")
|
||||
}
|
||||
}
|
||||
client.newCall(request).execute().use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(it.body.string())
|
||||
} else {
|
||||
onError("Could not resolve $lnaddress. Error: ${it.code}. Check if the server up and if the lightning address $lnaddress is correct")
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: java.io.IOException) {
|
||||
onError("Could not fetch an invoice from $lnCallback. Message ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onError("Could not resolve $url. Check if the server up and if the lightning address $lnaddress is correct")
|
||||
}
|
||||
}
|
||||
|
||||
fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
suspend fun fetchLightningInvoice(lnCallback: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit) = withContext(Dispatchers.IO) {
|
||||
val encodedMessage = URLEncoder.encode(message, "utf-8")
|
||||
|
||||
val urlBinder = if (lnCallback.contains("?")) "&" else "?"
|
||||
var url = "$lnCallback${urlBinder}amount=$milliSats&comment=$encodedMessage"
|
||||
|
||||
if (nostrRequest != null) {
|
||||
val encodedNostrRequest = URLEncoder.encode(nostrRequest, "utf-8")
|
||||
url += "&nostr=$encodedNostrRequest"
|
||||
}
|
||||
|
||||
val request: Request = Request.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.build()
|
||||
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use {
|
||||
if (it.isSuccessful) {
|
||||
onSuccess(response.body.string())
|
||||
} else {
|
||||
onError("Could not fetch invoice from $lnCallback")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call, e: java.io.IOException) {
|
||||
onError("Could not fetch an invoice from $lnCallback. Message ${e.message}")
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
suspend fun lnAddressToLnUrl(lnaddress: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
fetchLightningAddressJson(
|
||||
lnaddress,
|
||||
onSuccess = {
|
||||
@@ -139,7 +110,15 @@ class LightningAddressResolver() {
|
||||
)
|
||||
}
|
||||
|
||||
fun lnAddressInvoice(lnaddress: String, milliSats: Long, message: String, nostrRequest: String? = null, onSuccess: (String) -> Unit, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit) {
|
||||
suspend fun lnAddressInvoice(
|
||||
lnaddress: String,
|
||||
milliSats: Long,
|
||||
message: String,
|
||||
nostrRequest: String? = null,
|
||||
onSuccess: (String) -> Unit,
|
||||
onError: (String) -> Unit,
|
||||
onProgress: (percent: Float) -> Unit
|
||||
) {
|
||||
val mapper = jacksonObjectMapper()
|
||||
|
||||
fetchLightningAddressJson(
|
||||
|
||||
@@ -20,26 +20,20 @@ import com.vitorpamplona.quartz.events.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EventNotificationConsumer(private val applicationContext: Context) {
|
||||
fun consume(event: Event) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
if (LocalCache.notes[event.id] == null) {
|
||||
if (LocalCache.justVerify(event)) {
|
||||
LocalCache.justConsume(event, null)
|
||||
|
||||
val manager = notificationManager()
|
||||
if (manager.areNotificationsEnabled()) {
|
||||
when (event) {
|
||||
is PrivateDmEvent -> notify(event)
|
||||
is LnZapEvent -> notify(event)
|
||||
is GiftWrapEvent -> unwrapAndNotify(event)
|
||||
}
|
||||
fun consume(event: Event) {
|
||||
if (LocalCache.notes[event.id] == null) {
|
||||
if (LocalCache.justVerify(event)) {
|
||||
LocalCache.justConsume(event, null)
|
||||
|
||||
val manager = notificationManager()
|
||||
if (manager.areNotificationsEnabled()) {
|
||||
when (event) {
|
||||
is PrivateDmEvent -> notify(event)
|
||||
is LnZapEvent -> notify(event)
|
||||
is GiftWrapEvent -> unwrapAndNotify(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,8 @@ import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.quartz.events.RelayAuthEvent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
@@ -64,12 +62,9 @@ class RegisterAccounts(
|
||||
}
|
||||
}
|
||||
|
||||
fun go(notificationToken: String) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
postRegistrationEvent(
|
||||
signEventsToProveControlOfAccounts(accounts, notificationToken)
|
||||
)
|
||||
}
|
||||
suspend fun go(notificationToken: String) = withContext(Dispatchers.IO) {
|
||||
postRegistrationEvent(
|
||||
signEventsToProveControlOfAccounts(accounts, notificationToken)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,11 @@ import androidx.lifecycle.LiveData
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import com.vitorpamplona.quartz.events.EventInterface
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* RelayPool manages the connection to multiple Relays and lets consumers deal with simple events.
|
||||
*/
|
||||
object RelayPool : Relay.Listener {
|
||||
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
|
||||
private var relays = listOf<Relay>()
|
||||
private var listeners = setOf<Listener>()
|
||||
|
||||
@@ -136,9 +129,7 @@ object RelayPool : Relay.Listener {
|
||||
val live: RelayPoolLiveData = RelayPoolLiveData(this)
|
||||
|
||||
private fun refreshObservers() {
|
||||
scope.launch {
|
||||
live.refresh()
|
||||
}
|
||||
live.refresh()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,9 @@ class MainActivity : AppCompatActivity() {
|
||||
ServiceManager.start(this@MainActivity)
|
||||
}
|
||||
|
||||
PushNotificationUtils().init(LocalPreferences.allSavedAccounts())
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
PushNotificationUtils().init(LocalPreferences.allSavedAccounts())
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
|
||||
@@ -110,7 +110,6 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun NewPostView(
|
||||
onClose: () -> Unit,
|
||||
|
||||
@@ -48,7 +48,7 @@ import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit, navScrollToTop: (Route, Boolean) -> Unit) {
|
||||
var wantsToPost by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
@@ -62,11 +62,9 @@ fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val postViewModel: NewMediaModel = viewModel()
|
||||
postViewModel.onceUploaded {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
// awaits an refresh on the list
|
||||
delay(250)
|
||||
delay(500)
|
||||
withContext(Dispatchers.Main) {
|
||||
val route = Route.Video.route.replace("{scrollToTop}", "true")
|
||||
nav(route)
|
||||
navScrollToTop(Route.Video, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -21,6 +22,8 @@ class BundledUpdate(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
) {
|
||||
val scope = CoroutineScope(dispatcher + SupervisorJob())
|
||||
|
||||
private var onlyOneInBlock = AtomicBoolean()
|
||||
private var invalidatesAgain = false
|
||||
|
||||
@@ -32,7 +35,6 @@ class BundledUpdate(
|
||||
return
|
||||
}
|
||||
|
||||
val scope = CoroutineScope(Job() + dispatcher)
|
||||
scope.launch {
|
||||
try {
|
||||
onUpdate()
|
||||
@@ -48,6 +50,10 @@ class BundledUpdate(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,6 +64,8 @@ class BundledInsert<T>(
|
||||
val delay: Long,
|
||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
) {
|
||||
val scope = CoroutineScope(dispatcher + SupervisorJob())
|
||||
|
||||
private var onlyOneInBlock = AtomicBoolean()
|
||||
private var queue = LinkedBlockingQueue<T>()
|
||||
|
||||
@@ -69,7 +77,6 @@ class BundledInsert<T>(
|
||||
return
|
||||
}
|
||||
|
||||
val scope = CoroutineScope(Job() + dispatcher)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val mySet = mutableSetOf<T>()
|
||||
@@ -88,4 +95,8 @@ class BundledInsert<T>(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -152,23 +153,25 @@ fun InvoiceRequest(
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
|
||||
onClick = {
|
||||
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message, account.defaultZapType)
|
||||
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount * 1000,
|
||||
message,
|
||||
zapRequest?.toJson(),
|
||||
onSuccess = onSuccess,
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
onClose()
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lud16,
|
||||
amount * 1000,
|
||||
message,
|
||||
zapRequest?.toJson(),
|
||||
onSuccess = onSuccess,
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
onClose()
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
}
|
||||
},
|
||||
onProgress = {
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
|
||||
@@ -11,13 +11,19 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
|
||||
open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
open class DiscoverLiveFeedFilter(
|
||||
val account: Account
|
||||
) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
||||
return account.userProfile().pubkeyHex + "-" + followList()
|
||||
}
|
||||
|
||||
open fun followList(): String {
|
||||
return account.defaultDiscoveryFollowList
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
|
||||
return followList() == PeopleListEvent.blockList
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
@@ -39,15 +45,15 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
val isHiddenList = showHiddenKey()
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
val followingKeySet = account.selectedUsersFollowList(followList()) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(followList()) ?: emptySet()
|
||||
val followingGeohashSet = account.selectedGeohashesFollowList(followList()) ?: emptySet()
|
||||
|
||||
val activities = collection
|
||||
.asSequence()
|
||||
.filter { it.event is LiveActivitiesEvent }
|
||||
.filter {
|
||||
isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(
|
||||
isGlobal || (it.event as LiveActivitiesEvent).participantsIntersect(followingKeySet) || it.event?.isTaggedHashes(
|
||||
followingTagSet
|
||||
) == true || it.event?.isTaggedGeoHashes(
|
||||
followingGeohashSet
|
||||
@@ -61,7 +67,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
}
|
||||
|
||||
override fun sort(collection: Set<Note>): List<Note> {
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
||||
val followingKeySet = account.selectedUsersFollowList(followList())
|
||||
|
||||
val counter = ParticipantListBuilder()
|
||||
val participantCounts = collection.associate {
|
||||
|
||||
@@ -1,12 +1,26 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.quartz.events.*
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
|
||||
class DiscoverLiveNowFeedFilter(account: Account) : DiscoverLiveFeedFilter(account) {
|
||||
class DiscoverLiveNowFeedFilter(
|
||||
account: Account
|
||||
) : DiscoverLiveFeedFilter(account) {
|
||||
override fun followList(): String {
|
||||
// uses follows by default, but other lists if they were selected in the top bar
|
||||
val currentList = super.followList()
|
||||
return if (currentList == GLOBAL_FOLLOWS) {
|
||||
KIND3_FOLLOWS
|
||||
} else {
|
||||
currentList
|
||||
}
|
||||
}
|
||||
|
||||
override fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val allItems = super.innerApplyFilter(collection)
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AppBarDefaults
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.Divider
|
||||
@@ -91,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
|
||||
import com.vitorpamplona.amethyst.ui.note.SearchIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.ShortCommunityHeader
|
||||
import com.vitorpamplona.amethyst.ui.note.UserCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -107,8 +110,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowVideoStreaming
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size34dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||
@@ -118,7 +121,6 @@ import com.vitorpamplona.quartz.events.PeopleListEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -232,7 +234,9 @@ private fun CommunityTopBar(
|
||||
ShortCommunityHeader(baseNote, fontWeight = FontWeight.Medium, accountViewModel, nav)
|
||||
},
|
||||
extendableRow = {
|
||||
LongCommunityHeader(baseNote, accountViewModel, nav)
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
LongCommunityHeader(baseNote = baseNote, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
},
|
||||
popBack = navPopBack
|
||||
)
|
||||
@@ -298,6 +302,17 @@ private fun RenderRoomTopBar(
|
||||
}
|
||||
}
|
||||
},
|
||||
extendableRow = {
|
||||
LoadUser(baseUserHex = room.users.first()) {
|
||||
if (it != null) {
|
||||
UserCompose(
|
||||
baseUser = it,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
popBack = navPopBack
|
||||
)
|
||||
} else {
|
||||
@@ -309,10 +324,17 @@ private fun RenderRoomTopBar(
|
||||
size = Size34dp
|
||||
)
|
||||
|
||||
RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp).weight(1f), fontWeight = FontWeight.Medium, accountViewModel.userProfile())
|
||||
RoomNameOnlyDisplay(
|
||||
room,
|
||||
Modifier
|
||||
.padding(start = 10.dp)
|
||||
.weight(1f),
|
||||
fontWeight = FontWeight.Medium,
|
||||
accountViewModel.userProfile()
|
||||
)
|
||||
},
|
||||
extendableRow = {
|
||||
LongRoomHeader(room, accountViewModel, nav)
|
||||
LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav)
|
||||
},
|
||||
popBack = navPopBack
|
||||
)
|
||||
@@ -343,7 +365,7 @@ private fun ChannelTopBar(
|
||||
)
|
||||
},
|
||||
extendableRow = {
|
||||
LongChannelHeader(baseChannel, accountViewModel, nav)
|
||||
LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav)
|
||||
},
|
||||
popBack = navPopBack
|
||||
)
|
||||
@@ -536,8 +558,7 @@ class FollowListViewModel(val account: Account) : ViewModel() {
|
||||
val followLists = _followLists.asStateFlow()
|
||||
|
||||
fun refresh() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
refreshFollows()
|
||||
}
|
||||
}
|
||||
@@ -682,6 +703,7 @@ fun FlexibleTopBarWithBackButton(
|
||||
},
|
||||
actions = {}
|
||||
)
|
||||
Spacer(modifier = HalfVertSpacer)
|
||||
Divider(thickness = 0.25.dp)
|
||||
}
|
||||
}
|
||||
@@ -732,9 +754,9 @@ fun debugState(context: Context) {
|
||||
Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
||||
Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
||||
|
||||
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
|
||||
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
|
||||
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
|
||||
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
|
||||
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
|
||||
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
|
||||
|
||||
Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB")
|
||||
|
||||
@@ -863,9 +885,7 @@ fun MyExtensibleTopAppBar(
|
||||
|
||||
if (expanded.value && extendableRow != null) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = Size10dp),
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -58,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.EndedFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
@@ -361,6 +363,10 @@ fun InnerChannelCardWithReactions(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
thickness = DividerThickness
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -25,6 +26,7 @@ import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -118,5 +120,9 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
||||
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
thickness = DividerThickness
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -54,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
@@ -159,6 +161,10 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
|
||||
|
||||
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
||||
}
|
||||
|
||||
Divider(
|
||||
thickness = DividerThickness
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,11 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Divider
|
||||
@@ -113,6 +115,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NormalTimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
@@ -124,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||
@@ -202,7 +206,6 @@ import java.io.File
|
||||
import java.math.BigDecimal
|
||||
import java.net.URL
|
||||
import java.util.Locale
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -518,7 +521,7 @@ fun CommunityHeader(
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = modifier.clickable {
|
||||
modifier = Modifier.clickable {
|
||||
if (sendToCommunity) {
|
||||
routeFor(baseNote, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
@@ -535,7 +538,14 @@ fun CommunityHeader(
|
||||
)
|
||||
|
||||
if (expanded.value) {
|
||||
LongCommunityHeader(baseNote, accountViewModel, nav)
|
||||
Column(Modifier.verticalScroll(rememberScrollState())) {
|
||||
LongCommunityHeader(
|
||||
baseNote = baseNote,
|
||||
lineModifier = modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,23 +558,41 @@ fun CommunityHeader(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun LongCommunityHeader(
|
||||
baseNote: AddressableNote,
|
||||
lineModifier: Modifier = Modifier.padding(horizontal = Size10dp, vertical = Size5dp),
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
lineModifier
|
||||
) {
|
||||
val rulesLabel = stringResource(id = R.string.rules)
|
||||
val summary = remember(noteState) {
|
||||
noteEvent.description()?.ifBlank { null }
|
||||
val subject = noteEvent.subject()?.ifEmpty { null }
|
||||
val body = noteEvent.description()?.ifBlank { null }
|
||||
val rules = noteEvent.rules()?.ifBlank { null }
|
||||
|
||||
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
|
||||
if (rules == null) {
|
||||
"## $subject\n$body"
|
||||
} else {
|
||||
"## $subject\n$body\n\n## $rulesLabel\n\n$rules"
|
||||
}
|
||||
} else {
|
||||
if (rules == null) {
|
||||
body
|
||||
} else {
|
||||
"$body\n\n$rulesLabel\n$rules"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 10.dp)
|
||||
Modifier.weight(1f)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val defaultBackground = MaterialTheme.colors.background
|
||||
@@ -594,40 +622,8 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
|
||||
}
|
||||
}
|
||||
|
||||
val rules = remember(noteState) {
|
||||
noteEvent.rules()?.ifBlank { null }
|
||||
}
|
||||
|
||||
rules?.let {
|
||||
Spacer(DoubleVertSpacer)
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp)
|
||||
) {
|
||||
val defaultBackground = MaterialTheme.colors.background
|
||||
val background = remember {
|
||||
mutableStateOf(defaultBackground)
|
||||
}
|
||||
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = false,
|
||||
tags = tags,
|
||||
backgroundColor = background,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(DoubleVertSpacer)
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp),
|
||||
lineModifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
@@ -640,8 +636,6 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
|
||||
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) })
|
||||
TimeAgo(baseNote)
|
||||
MoreOptionsButton(baseNote, accountViewModel)
|
||||
}
|
||||
|
||||
var participantUsers by remember(baseNote) {
|
||||
@@ -665,12 +659,9 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
|
||||
|
||||
participantUsers.forEach {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 10.dp)
|
||||
.clickable {
|
||||
nav("User/${it.second.pubkeyHex}")
|
||||
},
|
||||
lineModifier.clickable {
|
||||
nav("User/${it.second.pubkeyHex}")
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
it.first.role?.let { it1 ->
|
||||
@@ -687,6 +678,21 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
|
||||
UsernameDisplay(it.second, remember { Modifier.weight(1f) })
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
lineModifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.created_at),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.width(75.dp)
|
||||
)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
NormalTimeAgo(baseNote = baseNote, Modifier.weight(1f))
|
||||
MoreOptionsButton(baseNote, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -897,7 +903,6 @@ fun ClickableNote(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
fun InnerNoteWithReactions(
|
||||
baseNote: Note,
|
||||
@@ -3426,7 +3431,12 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou
|
||||
}
|
||||
|
||||
content?.let {
|
||||
Row(verticalAlignment = CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) {
|
||||
Row(
|
||||
verticalAlignment = CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 5.dp)
|
||||
) {
|
||||
TranslatableRichTextViewer(
|
||||
content = it,
|
||||
canPreview = true,
|
||||
|
||||
@@ -114,7 +114,6 @@ import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.DecimalFormat
|
||||
import kotlin.math.roundToInt
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.time.measureTimedValue
|
||||
|
||||
@Composable
|
||||
@@ -141,7 +140,6 @@ fun ReactionsRow(
|
||||
Spacer(modifier = HalfDoubleVertSpacer)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
@Composable
|
||||
private fun InnerReactionRow(
|
||||
baseNote: Note,
|
||||
|
||||
@@ -13,18 +13,12 @@ import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
|
||||
@Composable
|
||||
fun UserCompose(
|
||||
baseUser: User,
|
||||
overallModifier: Modifier = remember {
|
||||
Modifier
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp
|
||||
)
|
||||
},
|
||||
overallModifier: Modifier = StdPadding,
|
||||
showDiviser: Boolean = true,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -56,7 +50,6 @@ fun UserCompose(
|
||||
|
||||
if (showDiviser) {
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -326,6 +326,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
||||
|
||||
override fun onCleared() {
|
||||
collectorJob?.cancel()
|
||||
bundlerInsert.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@@ -22,6 +23,7 @@ import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||
@@ -89,5 +91,9 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
|
||||
UserCompose(baseUser = zapSetCard.user, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
thickness = DividerThickness
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,9 @@ import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -89,8 +87,7 @@ class AccountStateViewModel(val context: Context) : ViewModel() {
|
||||
} else {
|
||||
_accountContent.update { AccountState.LoggedInViewOnly(account) }
|
||||
}
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.start(account, context)
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
|
||||
@@ -28,7 +28,6 @@ import com.vitorpamplona.quartz.events.ReactionEvent
|
||||
import com.vitorpamplona.quartz.events.RepostEvent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -76,8 +75,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
private var lastNotes: Set<Note>? = null
|
||||
|
||||
fun refresh() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
@@ -217,8 +215,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: ImmutableList<Card>) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val currentState = _feedContent.value
|
||||
|
||||
if (notes.isEmpty()) {
|
||||
@@ -353,6 +350,8 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
||||
|
||||
override fun onCleared() {
|
||||
clear()
|
||||
bundlerInsert.cancel()
|
||||
bundler.cancel()
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
|
||||
import com.vitorpamplona.quartz.events.ChatroomKey
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -228,8 +227,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
@@ -251,8 +249,7 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: ImmutableList<Note>) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val currentState = _feedContent.value
|
||||
if (notes.isEmpty()) {
|
||||
_feedContent.update { FeedState.Empty }
|
||||
@@ -340,6 +337,8 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
bundlerInsert.cancel()
|
||||
bundler.cancel()
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -36,8 +35,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<ZapReqResponse>) : View
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
private fun refresh() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
@@ -58,8 +56,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<ZapReqResponse>) : View
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: ImmutableList<ZapReqResponse>) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val currentState = _feedContent.value
|
||||
if (notes.isEmpty()) {
|
||||
_feedContent.update { LnZapFeedState.Empty }
|
||||
@@ -96,6 +93,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<ZapReqResponse>) : View
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
bundler.cancel()
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@@ -91,6 +91,11 @@ class RelayFeedViewModel : ViewModel() {
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
bundler.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
|
||||
@@ -18,7 +18,6 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowersFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -64,8 +63,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
private fun refresh() {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
@@ -87,8 +85,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: ImmutableList<User>) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val currentState = _feedContent.value
|
||||
if (notes.isEmpty()) {
|
||||
_feedContent.update { UserFeedState.Empty }
|
||||
@@ -125,6 +122,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
bundler.cancel()
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
@@ -105,12 +105,12 @@ import com.vitorpamplona.amethyst.ui.note.LoadChannel
|
||||
import com.vitorpamplona.amethyst.ui.note.MoreOptionsButton
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.TimeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.routeFor
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
|
||||
@@ -580,7 +580,7 @@ fun ChannelHeader(
|
||||
)
|
||||
|
||||
if (expanded.value) {
|
||||
LongChannelHeader(baseChannel, accountViewModel, nav)
|
||||
LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -722,6 +722,7 @@ fun ShortChannelHeader(
|
||||
@Composable
|
||||
fun LongChannelHeader(
|
||||
baseChannel: Channel,
|
||||
lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
@@ -731,18 +732,14 @@ fun LongChannelHeader(
|
||||
} ?: return
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
lineModifier
|
||||
) {
|
||||
val summary = remember(channelState) {
|
||||
channel.summary()?.ifBlank { null }
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 10.dp)
|
||||
Modifier.weight(1f)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val defaultBackground = MaterialTheme.colors.background
|
||||
@@ -769,7 +766,9 @@ fun LongChannelHeader(
|
||||
}
|
||||
|
||||
if (baseChannel is LiveActivitiesChannel) {
|
||||
val hashtags = remember(baseChannel.info) { baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf() }
|
||||
val hashtags = remember(baseChannel.info) {
|
||||
baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf()
|
||||
}
|
||||
DisplayUncitedHashtags(hashtags, summary ?: "", nav)
|
||||
}
|
||||
}
|
||||
@@ -784,28 +783,37 @@ fun LongChannelHeader(
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(DoubleVertSpacer)
|
||||
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
LoadNote(baseNoteHex = channel.idHex) {
|
||||
it?.let {
|
||||
LoadNote(baseNoteHex = channel.idHex) { loadingNote ->
|
||||
loadingNote?.let { note ->
|
||||
Row(
|
||||
lineModifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.owner),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.width(55.dp)
|
||||
modifier = Modifier.width(75.dp)
|
||||
)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
NoteAuthorPicture(it, nav, accountViewModel, Size25dp)
|
||||
NoteAuthorPicture(note, nav, accountViewModel, Size25dp)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
NoteUsernameDisplay(it, remember { Modifier.weight(1f) })
|
||||
TimeAgo(it)
|
||||
MoreOptionsButton(it, accountViewModel)
|
||||
NoteUsernameDisplay(note, remember { Modifier.weight(1f) })
|
||||
}
|
||||
|
||||
Row(
|
||||
lineModifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.created_at),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.width(75.dp)
|
||||
)
|
||||
Spacer(DoubleHorzSpacer)
|
||||
NormalTimeAgo(note, remember { Modifier.weight(1f) })
|
||||
MoreOptionsButton(note, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -831,9 +839,7 @@ fun LongChannelHeader(
|
||||
|
||||
participantUsers.forEach {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 10.dp)
|
||||
lineModifier
|
||||
.clickable {
|
||||
nav("User/${it.second.pubkeyHex}")
|
||||
},
|
||||
@@ -856,6 +862,24 @@ fun LongChannelHeader(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NormalTimeAgo(baseNote: Note, modifier: Modifier) {
|
||||
val nowStr = stringResource(id = R.string.now)
|
||||
|
||||
val time by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
timeAgoShort(baseNote.createdAt() ?: 0, nowStr)
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = time,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShortChannelActionOptions(
|
||||
channel: PublicChatChannel,
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
@@ -550,7 +549,7 @@ fun GroupChatroomHeader(
|
||||
}
|
||||
|
||||
if (expanded.value) {
|
||||
LongRoomHeader(room, accountViewModel, nav)
|
||||
LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -687,15 +686,18 @@ fun NewSubjectView(onClose: () -> Unit, accountViewModel: AccountViewModel, room
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun LongRoomHeader(
|
||||
room: ChatroomKey,
|
||||
lineModifier: Modifier = StdPadding,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val list = remember(room) {
|
||||
room.users.toPersistentList()
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(top = 10.dp)
|
||||
.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
@@ -712,16 +714,18 @@ fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
contentPadding = PaddingValues(
|
||||
bottom = 10.dp
|
||||
),
|
||||
modifier = Modifier,
|
||||
state = rememberLazyListState()
|
||||
) {
|
||||
itemsIndexed(list, key = { _, item -> item }) { _, item ->
|
||||
LoadUser(baseUserHex = item) {
|
||||
if (it != null) {
|
||||
UserCompose(baseUser = it, accountViewModel = accountViewModel, nav = nav)
|
||||
UserCompose(
|
||||
baseUser = it,
|
||||
overallModifier = lineModifier,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,7 +217,7 @@ fun MainScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingButtons(navState, accountViewModel, accountStateViewModel, nav)
|
||||
FloatingButtons(navState, accountViewModel, accountStateViewModel, nav, navBottomRow)
|
||||
},
|
||||
scaffoldState = scaffoldState
|
||||
) {
|
||||
@@ -247,7 +247,8 @@ fun FloatingButtons(
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
accountViewModel: AccountViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
nav: (String) -> Unit
|
||||
nav: (String) -> Unit,
|
||||
navScrollToTop: (Route, Boolean) -> Unit
|
||||
) {
|
||||
val accountState by accountStateViewModel.accountContent.collectAsState()
|
||||
|
||||
@@ -260,7 +261,7 @@ fun FloatingButtons(
|
||||
// Does nothing.
|
||||
}
|
||||
is AccountState.LoggedIn -> {
|
||||
WritePermissionButtons(navEntryState, accountViewModel, nav)
|
||||
WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,7 +271,8 @@ fun FloatingButtons(
|
||||
private fun WritePermissionButtons(
|
||||
navEntryState: State<NavBackStackEntry?>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
nav: (String) -> Unit,
|
||||
navScrollToTop: (Route, Boolean) -> Unit
|
||||
) {
|
||||
val currentRoute by remember(navEntryState.value) {
|
||||
derivedStateOf {
|
||||
@@ -281,7 +283,7 @@ private fun WritePermissionButtons(
|
||||
when (currentRoute) {
|
||||
Route.Home.base -> NewNoteButton(accountViewModel, nav)
|
||||
Route.Message.base -> ChannelFabColumn(accountViewModel, nav)
|
||||
Route.Video.base -> NewImageButton(accountViewModel, nav)
|
||||
Route.Video.base -> NewImageButton(accountViewModel, nav, navScrollToTop)
|
||||
Route.Community.base -> {
|
||||
val communityId by remember(navEntryState.value) {
|
||||
derivedStateOf {
|
||||
|
||||
@@ -195,6 +195,11 @@ class SearchBarViewModel(val account: Account) : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
bundler.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun isSearchingFun() = searchValue.isNotBlank()
|
||||
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
|
||||
@@ -546,4 +546,7 @@
|
||||
|
||||
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
|
||||
<string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
|
||||
|
||||
<string name="created_at">Created at</string>
|
||||
<string name="rules">Rules</string>
|
||||
</resources>
|
||||
|
||||
@@ -8,22 +8,37 @@ import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateDMChannel
|
||||
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.getOrCreateZapChannel
|
||||
import com.vitorpamplona.quartz.events.Event
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PushNotificationReceiverService : FirebaseMessagingService() {
|
||||
val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
// this is called when a message is received
|
||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||
remoteMessage.data.let {
|
||||
val eventStr = remoteMessage.data["event"] ?: return
|
||||
val event = Event.fromJson(eventStr)
|
||||
EventNotificationConsumer(applicationContext).consume(event)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
remoteMessage.data.let {
|
||||
val eventStr = remoteMessage.data["event"] ?: return@let
|
||||
val event = Event.fromJson(eventStr)
|
||||
EventNotificationConsumer(applicationContext).consume(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onNewToken(token: String) {
|
||||
RegisterAccounts(LocalPreferences.allSavedAccounts()).go(token)
|
||||
notificationManager().getOrCreateZapChannel(applicationContext)
|
||||
notificationManager().getOrCreateDMChannel(applicationContext)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
RegisterAccounts(LocalPreferences.allSavedAccounts()).go(token)
|
||||
notificationManager().getOrCreateZapChannel(applicationContext)
|
||||
notificationManager().getOrCreateDMChannel(applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
fun notificationManager(): NotificationManager {
|
||||
|
||||
@@ -1,31 +1,13 @@
|
||||
package com.vitorpamplona.amethyst.service.notifications
|
||||
|
||||
import android.util.Log
|
||||
import com.google.android.gms.tasks.OnCompleteListener
|
||||
import com.google.firebase.messaging.FirebaseMessaging
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.tasks.await
|
||||
|
||||
class PushNotificationUtils {
|
||||
fun init(accounts: List<AccountInfo>) {
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
// get user notification token provided by firebase
|
||||
FirebaseMessaging.getInstance().token.addOnCompleteListener(
|
||||
OnCompleteListener { task ->
|
||||
if (!task.isSuccessful) {
|
||||
Log.w("FirebaseMsgService", "Fetching FCM registration token failed", task.exception)
|
||||
return@OnCompleteListener
|
||||
}
|
||||
// Get new FCM registration token
|
||||
val notificationToken = task.result
|
||||
|
||||
RegisterAccounts(accounts).go(notificationToken)
|
||||
}
|
||||
)
|
||||
}
|
||||
suspend fun init(accounts: List<AccountInfo>) = with(Dispatchers.IO) {
|
||||
// get user notification token provided by firebase
|
||||
RegisterAccounts(accounts).go(FirebaseMessaging.getInstance().token.await())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.os.Looper
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.SpyK
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkAll
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
@@ -30,7 +32,7 @@ class Nip05VerifierTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test with matching case on user name`() {
|
||||
fun `test with matching case on user name`() = runBlocking {
|
||||
// Set-up
|
||||
val userNameToTest = ALL_UPPER_CASE_USER_NAME
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
@@ -41,7 +43,7 @@ class Nip05VerifierTest {
|
||||
" }\n" +
|
||||
"}"
|
||||
|
||||
every { nip05Verifier.fetchNip05Json(any(), any(), any()) } answers {
|
||||
coEvery { nip05Verifier.fetchNip05Json(any(), any(), any()) } answers {
|
||||
secondArg<(String) -> Unit>().invoke(nostrJson)
|
||||
}
|
||||
|
||||
@@ -64,7 +66,7 @@ class Nip05VerifierTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test with NOT matching case on user name`() {
|
||||
fun `test with NOT matching case on user name`() = runBlocking {
|
||||
// Set-up
|
||||
val expectedPubKey = "ca29c211f1c72d5b6622268ff43d2288ea2b2cb5b9aa196ff9f1704fc914b71b"
|
||||
|
||||
@@ -73,7 +75,7 @@ class Nip05VerifierTest {
|
||||
" \"$ALL_UPPER_CASE_USER_NAME\": \"$expectedPubKey\" \n" +
|
||||
" }\n" +
|
||||
"}"
|
||||
every { nip05Verifier.fetchNip05Json(any(), any(), any()) } answers {
|
||||
coEvery { nip05Verifier.fetchNip05Json(any(), any(), any()) } answers {
|
||||
secondArg<(String) -> Unit>().invoke(nostrJson)
|
||||
}
|
||||
|
||||
|
||||
@@ -173,7 +173,7 @@ class ContactListEvent(
|
||||
|
||||
return create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && !it[1].equals(hashtag, true) },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
|
||||
@@ -40,6 +40,10 @@ class LiveActivitiesEvent(
|
||||
}
|
||||
}
|
||||
|
||||
fun participantsIntersect(keySet: Set<String>): Boolean {
|
||||
return tags.any { it.size > 1 && it[0] == "p" && it[1] in keySet }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val kind = 30311
|
||||
|
||||
|
||||
Reference in New Issue
Block a user