mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-06 22:54:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2e9fbebea | ||
|
|
52af109b4e | ||
|
|
833e6bc3e1 | ||
|
|
9bed6fdb9a | ||
|
|
a4a867ebcd | ||
|
|
4da2f18957 | ||
|
|
4141a52d51 | ||
|
|
4dbc71d577 | ||
|
|
3b5d792562 | ||
|
|
dd7f3225c6 | ||
|
|
3477e7b8ba | ||
|
|
a3782bb459 | ||
|
|
d4003eee04 | ||
|
|
c9bbb66284 | ||
|
|
1498fa9868 | ||
|
|
26b493aa1d | ||
|
|
f9d652f849 | ||
|
|
30b0ee444f | ||
|
|
bf8341ef58 | ||
|
|
f9f1b0d98e | ||
|
|
743a4c9d87 | ||
|
|
275baa7497 | ||
|
|
ac1fb879c3 | ||
|
|
270ad56450 | ||
|
|
d155241efc | ||
|
|
d61927cbee | ||
|
|
996b68f9c6 | ||
|
|
f6913663d1 | ||
|
|
58600e13a8 | ||
|
|
54e15ad3cd | ||
|
|
777c652bf5 | ||
|
|
7fb1473e21 | ||
|
|
95db1ec84f | ||
|
|
7438973a20 | ||
|
|
fe31855860 | ||
|
|
d7bce4414d | ||
|
|
b4a81c137c | ||
|
|
a6390f58d9 | ||
|
|
ca3f4c3ec6 | ||
|
|
53bfd40a31 | ||
|
|
798be25c43 | ||
|
|
ebe1bcedd0 | ||
|
|
4731575b87 | ||
|
|
383e761372 | ||
|
|
54a95e6a4e | ||
|
|
3105ade755 | ||
|
|
f651034303 | ||
|
|
7554dc010c | ||
|
|
70a962062b | ||
|
|
5cc7f3bf2b | ||
|
|
b040e0ca13 | ||
|
|
775094356c | ||
|
|
0771b179b5 | ||
|
|
8af307e8bb | ||
|
|
e6ec3f1ce4 |
+2
-2
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 223
|
||||
versionName "0.62.2"
|
||||
versionCode 226
|
||||
versionName "0.62.5"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -1134,7 +1134,7 @@ class Account(
|
||||
user.countReportAuthorsBy(followingKeySet()) < 5
|
||||
}
|
||||
|
||||
fun isAcceptableDirect(note: Note): Boolean {
|
||||
private fun isAcceptableDirect(note: Note): Boolean {
|
||||
if (!warnAboutPostsWithReports) {
|
||||
return note.reportsBy(userProfile()).isEmpty()
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ class AntiSpamFilter {
|
||||
val recentMessages = LruCache<Int, String>(1000)
|
||||
val spamMessages = LruCache<Int, Spammer>(1000)
|
||||
|
||||
@Synchronized
|
||||
fun isSpam(event: Event, relay: Relay?): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
@@ -45,15 +44,9 @@ class AntiSpamFilter {
|
||||
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}")
|
||||
|
||||
// Log down offenders
|
||||
if (spamMessages.get(hash) == null) {
|
||||
spamMessages.put(hash, Spammer(event.pubKey, setOf(recentMessages[hash], event.id)))
|
||||
liveSpam.invalidateData()
|
||||
} else {
|
||||
val spammer = spamMessages.get(hash)
|
||||
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
|
||||
logOffender(hash, event)
|
||||
|
||||
liveSpam.invalidateData()
|
||||
}
|
||||
liveSpam.invalidateData()
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -63,6 +56,16 @@ class AntiSpamFilter {
|
||||
return false
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun logOffender(hashCode: Int, event: Event) {
|
||||
if (spamMessages.get(hashCode) == null) {
|
||||
spamMessages.put(hashCode, Spammer(event.pubKey, setOf(recentMessages[hashCode], event.id)))
|
||||
} else {
|
||||
val spammer = spamMessages.get(hashCode)
|
||||
spammer.duplicatedMessages = spammer.duplicatedMessages + event.id
|
||||
}
|
||||
}
|
||||
|
||||
val liveSpam: AntiSpamLiveData = AntiSpamLiveData(this)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ class PublicChatChannel(idHex: String) : Channel(idHex) {
|
||||
}
|
||||
|
||||
override fun profilePicture(): String? {
|
||||
if (info.picture.isNullOrBlank()) return null
|
||||
if (info.picture.isNullOrBlank()) return super.profilePicture()
|
||||
return info.picture ?: super.profilePicture()
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class LiveActivitiesChannel(val address: ATag) : Channel(address.toTag()) {
|
||||
}
|
||||
|
||||
override fun profilePicture(): String? {
|
||||
return info?.image()?.ifBlank { null } ?: super.profilePicture()
|
||||
return info?.image()?.ifBlank { null }
|
||||
}
|
||||
|
||||
override fun anyNameStartsWith(prefix: String): Boolean {
|
||||
|
||||
@@ -3,9 +3,9 @@ package com.vitorpamplona.amethyst.model
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.service.HexValidator
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
import com.vitorpamplona.amethyst.service.model.ATag.Companion.isATag
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledInsert
|
||||
@@ -13,7 +13,6 @@ import fr.acinq.secp256k1.Hex
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import nostr.postr.toNpub
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
@@ -42,14 +41,12 @@ object LocalCache {
|
||||
return null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateUser(key: HexKey): User {
|
||||
// checkNotInMainThread()
|
||||
|
||||
return users[key] ?: run {
|
||||
val answer = User(key)
|
||||
users.put(key, answer)
|
||||
answer
|
||||
val newObject = User(key)
|
||||
users.putIfAbsent(key, newObject) ?: newObject
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,14 +81,12 @@ object LocalCache {
|
||||
return null
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateNote(idHex: String): Note {
|
||||
checkNotInMainThread()
|
||||
|
||||
return notes[idHex] ?: run {
|
||||
val answer = Note(idHex)
|
||||
notes.put(idHex, answer)
|
||||
answer
|
||||
return notes.get(idHex) ?: run {
|
||||
val newObject = Note(idHex)
|
||||
notes.putIfAbsent(idHex, newObject) ?: newObject
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,23 +110,15 @@ object LocalCache {
|
||||
private fun isValidHexNpub(key: String): Boolean {
|
||||
if (key.contains(":")) return false
|
||||
|
||||
return try {
|
||||
Hex.decode(key).toNpub()
|
||||
true
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Log.e("LocalCache", "Invalid Key to create user: $key", e)
|
||||
false
|
||||
}
|
||||
return HexValidator.isHex(key)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateChannel(key: String, channelFactory: (String) -> Channel): Channel {
|
||||
checkNotInMainThread()
|
||||
|
||||
return channels[key] ?: run {
|
||||
val answer = channelFactory(key)
|
||||
channels.put(key, answer)
|
||||
answer
|
||||
val newObject = channelFactory(key)
|
||||
channels.putIfAbsent(key, newObject) ?: newObject
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,16 +136,14 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateAddressableNoteInternal(key: ATag): AddressableNote {
|
||||
checkNotInMainThread()
|
||||
|
||||
// we can't use naddr here because naddr might include relay info and
|
||||
// the preferred relay should not be part of the index.
|
||||
return addressables[key.toTag()] ?: run {
|
||||
val answer = AddressableNote(key)
|
||||
addressables.put(key.toTag(), answer)
|
||||
answer
|
||||
val newObject = AddressableNote(key)
|
||||
addressables.putIfAbsent(key.toTag(), newObject) ?: newObject
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,8 +532,7 @@ object LocalCache {
|
||||
|
||||
// Log.d("PM", "${author.toBestDisplayName()} to ${recipient?.toBestDisplayName()}")
|
||||
|
||||
val repliesTo = event.tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val repliesTo = event.taggedEvents().mapNotNull { checkGetOrCreateNote(it) }
|
||||
|
||||
note.loadEvent(event, author, repliesTo)
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ import com.vitorpamplona.amethyst.service.model.MetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.EOSETime
|
||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import fr.acinq.secp256k1.Hex
|
||||
@@ -187,34 +189,34 @@ class User(val pubkeyHex: String) {
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
||||
private fun getOrCreatePrivateChatroomSync(user: User): Chatroom {
|
||||
checkNotInMainThread()
|
||||
|
||||
return privateChatrooms[user] ?: run {
|
||||
val privateChatroom = Chatroom(setOf<Note>())
|
||||
val privateChatroom = Chatroom()
|
||||
privateChatrooms = privateChatrooms + Pair(user, privateChatroom)
|
||||
privateChatroom
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addMessage(user: User, msg: Note) {
|
||||
checkNotInMainThread()
|
||||
private fun getOrCreatePrivateChatroom(user: User): Chatroom {
|
||||
return privateChatrooms[user] ?: getOrCreatePrivateChatroomSync(user)
|
||||
}
|
||||
|
||||
fun addMessage(user: User, msg: Note) {
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg !in privateChatroom.roomMessages) {
|
||||
privateChatroom.roomMessages = privateChatroom.roomMessages + msg
|
||||
privateChatroom.addMessageSync(msg)
|
||||
liveSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeMessage(user: User, msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
val privateChatroom = getOrCreatePrivateChatroom(user)
|
||||
if (msg in privateChatroom.roomMessages) {
|
||||
privateChatroom.roomMessages = privateChatroom.roomMessages - msg
|
||||
privateChatroom.removeMessageSync(msg)
|
||||
liveSet?.messages?.invalidateData()
|
||||
}
|
||||
}
|
||||
@@ -237,6 +239,7 @@ class User(val pubkeyHex: String) {
|
||||
info = newUserInfo
|
||||
info?.latestMetadata = latestMetadata
|
||||
info?.updatedMetadataAt = latestMetadata.createdAt
|
||||
info?.tags = latestMetadata.tags.toImmutableListOfLists()
|
||||
|
||||
if (newUserInfo.lud16.isNullOrBlank() && newUserInfo.lud06?.lowercase()?.startsWith("lnurl") == true) {
|
||||
try {
|
||||
@@ -373,7 +376,27 @@ data class RelayInfo(
|
||||
var counter: Long
|
||||
)
|
||||
|
||||
data class Chatroom(var roomMessages: Set<Note>)
|
||||
class Chatroom() {
|
||||
var roomMessages: Set<Note> = setOf()
|
||||
|
||||
@Synchronized
|
||||
fun addMessageSync(msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (msg !in roomMessages) {
|
||||
roomMessages = roomMessages + msg
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeMessageSync(msg: Note) {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (msg !in roomMessages) {
|
||||
roomMessages = roomMessages + msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
class UserMetadata {
|
||||
@@ -401,6 +424,7 @@ class UserMetadata {
|
||||
|
||||
var updatedMetadataAt: Long = 0
|
||||
var latestMetadata: MetadataEvent? = null
|
||||
var tags: ImmutableListOfLists<String>? = null
|
||||
|
||||
fun anyName(): String? {
|
||||
return display_name ?: displayName ?: name ?: username
|
||||
@@ -414,6 +438,23 @@ class UserMetadata {
|
||||
fun lnAddress(): String? {
|
||||
return (lud16?.trim() ?: lud06?.trim())?.ifBlank { null }
|
||||
}
|
||||
|
||||
fun bestUsername(): String? {
|
||||
return name?.ifBlank { null } ?: username?.ifBlank { null }
|
||||
}
|
||||
|
||||
fun bestDisplayName(): String? {
|
||||
return displayName?.ifBlank { null } ?: display_name?.ifBlank { null }
|
||||
}
|
||||
|
||||
fun nip05(): String? {
|
||||
return nip05?.ifBlank { null }
|
||||
}
|
||||
|
||||
fun profilePicture(): String? {
|
||||
if (picture.isNullOrBlank()) picture = null
|
||||
return picture
|
||||
}
|
||||
}
|
||||
|
||||
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import java.util.Base64
|
||||
|
||||
@Immutable
|
||||
data class CashuToken(
|
||||
val mint: String,
|
||||
val totalAmount: Long,
|
||||
val fees: Int,
|
||||
val redeemInvoiceAmount: Long,
|
||||
val proofs: JsonArray
|
||||
)
|
||||
|
||||
class CashuProcessor {
|
||||
fun parse(cashuToken: String): GenericLoadable<CashuToken> {
|
||||
try {
|
||||
val base64token = cashuToken.replace("cashuA", "")
|
||||
val cashu = JsonParser.parseString(String(Base64.getDecoder().decode(base64token)))
|
||||
val token = cashu.asJsonObject.get("token").asJsonArray[0].asJsonObject
|
||||
val proofs = token["proofs"].asJsonArray
|
||||
val mint = token["mint"].asString
|
||||
|
||||
var totalAmount = 0L
|
||||
for (proof in proofs) {
|
||||
totalAmount += proof.asJsonObject["amount"].asLong
|
||||
}
|
||||
val fees = Math.max(((totalAmount * 0.02).toInt()), 2)
|
||||
val redeemInvoiceAmount = totalAmount - fees
|
||||
|
||||
return GenericLoadable.Loaded(CashuToken(mint, totalAmount, fees, redeemInvoiceAmount, proofs))
|
||||
} catch (e: Exception) {
|
||||
return GenericLoadable.Error<CashuToken>("Could not parse this cashu token")
|
||||
}
|
||||
}
|
||||
|
||||
fun melt(token: CashuToken, lud16: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
runCatching {
|
||||
LightningAddressResolver().lnAddressInvoice(
|
||||
lnaddress = lud16,
|
||||
milliSats = token.redeemInvoiceAmount * 1000, // Make invoice and leave room for fees
|
||||
message = "Reedem Cashu",
|
||||
onSuccess = { invoice ->
|
||||
meltInvoice(token, invoice, onSuccess, onError)
|
||||
},
|
||||
onProgress = {
|
||||
},
|
||||
onError = onError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun meltInvoice(token: CashuToken, invoice: String, onSuccess: (String) -> Unit, onError: (String) -> Unit) {
|
||||
try {
|
||||
val client = HttpClient.getHttpClient()
|
||||
val url = token.mint + "/melt" // Melt cashu tokens at Mint
|
||||
|
||||
val jsonObject = JsonObject()
|
||||
jsonObject.add("proofs", token.proofs)
|
||||
jsonObject.addProperty("pr", invoice)
|
||||
|
||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||
val requestBody = jsonObject.toString().toRequestBody(mediaType)
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody)
|
||||
.build()
|
||||
|
||||
val response = client.newCall(request).execute()
|
||||
val body = response.body.string()
|
||||
val tree = jacksonObjectMapper().readTree(body)
|
||||
|
||||
val successful = tree?.get("paid")?.asText() == "true"
|
||||
|
||||
if (successful) {
|
||||
onSuccess("Redeemed ${token.totalAmount} Sats" + " (Fees: ${token.fees} Sats)")
|
||||
} else {
|
||||
onError(tree?.get("detail")?.asText()?.split('.')?.getOrNull(0) ?: "Error")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
onError("Token melt failure: " + e.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
object HexValidator {
|
||||
|
||||
private fun isHex2(c: Char): Boolean {
|
||||
return when (c) {
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F', ' ' -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun isHex(hex: String?): Boolean {
|
||||
if (hex == null) return false
|
||||
var isHex = true
|
||||
for (c in hex.toCharArray()) {
|
||||
if (!isHex2(c)) {
|
||||
isHex = false
|
||||
break
|
||||
}
|
||||
}
|
||||
return isHex
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import okhttp3.Request
|
||||
|
||||
@Immutable
|
||||
data class OnlineCheckResult(val timeInMs: Long, val online: Boolean)
|
||||
|
||||
object OnlineChecker {
|
||||
val checkOnlineCache = LruCache<String, OnlineCheckResult>(10)
|
||||
val fiveMinutes = 1000 * 60 * 5
|
||||
|
||||
fun isOnline(url: String?): Boolean {
|
||||
checkNotInMainThread()
|
||||
|
||||
if (url.isNullOrBlank()) return false
|
||||
if ((checkOnlineCache.get(url)?.timeInMs ?: 0) > System.currentTimeMillis() - fiveMinutes) {
|
||||
return checkOnlineCache.get(url).online
|
||||
}
|
||||
|
||||
val request = Request.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return try {
|
||||
val result = HttpClient.getHttpClient().newCall(request).execute().code == 200
|
||||
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result))
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), false))
|
||||
Log.e("LiveActivities", "Failed to check streaming url $url", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@ open class Event(
|
||||
}
|
||||
|
||||
override fun zapraiserAmount() = tags.firstOrNull() {
|
||||
(it.size > 1 && it[0].equals("zapraiser", true))
|
||||
(it.size > 1 && it[0] == "zapraiser")
|
||||
}?.get(1)?.toLongOrNull()
|
||||
|
||||
override fun zapAddress() = tags.firstOrNull { it.size > 1 && it[0] == "zap" }?.get(1)
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.HexValidator
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import nostr.postr.Utils
|
||||
import nostr.postr.toHex
|
||||
@@ -23,11 +24,18 @@ class PrivateDmEvent(
|
||||
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
|
||||
* for initial messages.
|
||||
*/
|
||||
private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }
|
||||
private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }?.get(1)
|
||||
|
||||
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this[1]) }?.getOrNull()
|
||||
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this) }?.getOrNull()
|
||||
|
||||
fun verifiedRecipientPubKey() = recipientPubKey()?.runCatching { Hex.decode(this[1]).toHexKey() }?.getOrNull() // makes sure its a valid one
|
||||
fun verifiedRecipientPubKey(): HexKey? {
|
||||
val recipient = recipientPubKey()
|
||||
return if (HexValidator.isHex(recipient)) {
|
||||
recipient
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun talkingWith(oneSideHex: String): HexKey {
|
||||
return if (pubKey == oneSideHex) verifiedRecipientPubKey() ?: pubKey else pubKey
|
||||
|
||||
@@ -39,6 +39,7 @@ import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class MainActivity : FragmentActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchBarViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
@@ -389,7 +390,7 @@ fun UserComposeForChat(
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
ClickableUserPicture(baseUser, 55.dp, accountViewModel)
|
||||
ClickableUserPicture(baseUser, Size55dp, accountViewModel)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat.startActivity
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.CashuProcessor
|
||||
import com.vitorpamplona.amethyst.service.CashuToken
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun CashuPreview(cashutoken: String, accountViewModel: AccountViewModel) {
|
||||
var cachuData by remember { mutableStateOf<GenericLoadable<CashuToken>>(GenericLoadable.Loading<CashuToken>()) }
|
||||
|
||||
LaunchedEffect(key1 = cashutoken) {
|
||||
launch(Dispatchers.IO) {
|
||||
cachuData = CashuProcessor().parse(cashutoken)
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = cachuData) {
|
||||
when (it) {
|
||||
is GenericLoadable.Loaded<CashuToken> -> CashuPreview(it.loaded, accountViewModel)
|
||||
is GenericLoadable.Error<CashuToken> -> Text(
|
||||
text = "$cashutoken ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CashuPreview(token: CashuToken, accountViewModel: AccountViewModel) {
|
||||
val lud16 = remember(accountViewModel) {
|
||||
accountViewModel.account.userProfile().info?.lud16
|
||||
}
|
||||
|
||||
val useWebService = false
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 30.dp, end = 30.dp)
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(1.dp, MaterialTheme.colors.subtleBorder, QuoteBorder)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(30.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 10.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.cashu),
|
||||
null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.cashu),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
token.totalAmount.let {
|
||||
Text(
|
||||
text = "$it ${stringResource(id = R.string.sats)}",
|
||||
fontSize = 25.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp),
|
||||
onClick = {
|
||||
// Just in case we want to use a webservice instead of directly contacting the mint
|
||||
if (useWebService) {
|
||||
val url = "https://redeem.cashu.me?token=$token&lightning=$lud16&autopay=true"
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
|
||||
startActivity(context, intent, null)
|
||||
} else {
|
||||
if (lud16 != null) {
|
||||
CashuProcessor().melt(
|
||||
token,
|
||||
lud16,
|
||||
onSuccess = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
scope.launch {
|
||||
Toast.makeText(context, it, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast.makeText(context, "No Lightning Address set", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
shape = QuoteBorder,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.cashu_redeem), color = Color.White, fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
sealed class GenericLoadable<T> {
|
||||
|
||||
@Immutable
|
||||
class Loading<T> : GenericLoadable<T>()
|
||||
|
||||
@Immutable
|
||||
class Loaded<T>(val loaded: T) : GenericLoadable<T>()
|
||||
|
||||
@Immutable
|
||||
class Empty<T> : GenericLoadable<T>()
|
||||
|
||||
@Immutable
|
||||
class Error<T>(val errorMessage: String) : GenericLoadable<T>()
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
import android.util.Log
|
||||
import android.util.LruCache
|
||||
import android.util.Patterns
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
@@ -26,6 +27,8 @@ import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
||||
import com.halilibo.richtext.ui.HeadingStyle
|
||||
@@ -40,7 +43,6 @@ import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.checkForHashtagWithIcon
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.MarkdownTextStyle
|
||||
@@ -260,7 +262,7 @@ private fun parseUrls(
|
||||
}
|
||||
|
||||
enum class WordType {
|
||||
IMAGE, LINK, EMOJI, INVOICE, WITHDRAW, EMAIL, PHONE, BECH, HASH_INDEX, HASHTAG, SCHEMELESS_URL, OTHER
|
||||
IMAGE, LINK, EMOJI, INVOICE, WITHDRAW, CASHU, EMAIL, PHONE, BECH, HASH_INDEX, HASHTAG, SCHEMELESS_URL, OTHER
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -284,6 +286,8 @@ private fun RenderWord(
|
||||
WordType.INVOICE
|
||||
} else if (word.startsWith("lnurl", true)) {
|
||||
WordType.WITHDRAW
|
||||
} else if (word.startsWith("cashuA", true)) {
|
||||
WordType.CASHU
|
||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||
WordType.EMAIL
|
||||
} else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
|
||||
@@ -339,6 +343,7 @@ private fun RenderWordWithoutPreview(
|
||||
WordType.INVOICE -> NormalWord(wordSpace)
|
||||
// Don't offer to withdraw
|
||||
WordType.WITHDRAW -> NormalWord(wordSpace)
|
||||
WordType.CASHU -> NormalWord(wordSpace)
|
||||
WordType.EMAIL -> ClickableEmail(word)
|
||||
WordType.PHONE -> ClickablePhone(word)
|
||||
WordType.BECH -> BechLink(word, false, backgroundColor, accountViewModel, nav)
|
||||
@@ -369,6 +374,7 @@ private fun RenderWordWithPreview(
|
||||
WordType.EMOJI -> RenderCustomEmoji(word, state)
|
||||
WordType.INVOICE -> MayBeInvoicePreview(word)
|
||||
WordType.WITHDRAW -> MayBeWithdrawal(word)
|
||||
WordType.CASHU -> CashuPreview(word, accountViewModel)
|
||||
WordType.EMAIL -> ClickableEmail(word)
|
||||
WordType.PHONE -> ClickablePhone(word)
|
||||
WordType.BECH -> BechLink(word, true, backgroundColor, accountViewModel, nav)
|
||||
@@ -747,7 +753,11 @@ fun BechLink(word: String, canPreview: Boolean, backgroundColor: MutableState<Co
|
||||
}
|
||||
}
|
||||
|
||||
loadedLink = LoadedBechLink(returningNote, it)
|
||||
val newLink = LoadedBechLink(returningNote, it)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
loadedLink = newLink
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -834,7 +844,9 @@ fun HashTag(word: String, nav: (String) -> Unit) {
|
||||
}
|
||||
|
||||
if (myTag != null) {
|
||||
tagSuffixPair = Pair(myTag, mySuffix)
|
||||
launch(Dispatchers.Main) {
|
||||
tagSuffixPair = Pair(myTag, mySuffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -918,11 +930,15 @@ fun TagLink(word: String, tags: ImmutableListOfLists<String>, canPreview: Boolea
|
||||
if (tag.size > 1) {
|
||||
if (tag[0] == "p") {
|
||||
LocalCache.checkGetOrCreateUser(tag[1])?.let {
|
||||
loadedTag = LoadedTag(it, null, suffix)
|
||||
launch(Dispatchers.Main) {
|
||||
loadedTag = LoadedTag(it, null, suffix)
|
||||
}
|
||||
}
|
||||
} else if (tag[0] == "e" || tag[0] == "a") {
|
||||
LocalCache.checkGetOrCreateNote(tag[1])?.let {
|
||||
loadedTag = LoadedTag(null, it, suffix)
|
||||
launch(Dispatchers.Main) {
|
||||
loadedTag = LoadedTag(null, it, suffix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -991,25 +1007,23 @@ private fun DisplayUserFromTag(
|
||||
) {
|
||||
val route = remember { "User/${baseUser.pubkeyHex}" }
|
||||
val suffix = remember { "$addedChars " }
|
||||
val hex = remember { baseUser.pubkeyDisplayHex() }
|
||||
|
||||
val innerUserState by baseUser.live().metadata.observeAsState()
|
||||
val displayName by remember(innerUserState) {
|
||||
derivedStateOf {
|
||||
innerUserState?.user?.toBestDisplayName() ?: ""
|
||||
}
|
||||
}
|
||||
val userTags by remember(innerUserState) {
|
||||
derivedStateOf {
|
||||
innerUserState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
|
||||
}
|
||||
}
|
||||
val meta by baseUser.live().metadata.map {
|
||||
it.user.info
|
||||
}.distinctUntilChanged().observeAsState(baseUser.info)
|
||||
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = displayName,
|
||||
suffix = suffix,
|
||||
maxLines = 1,
|
||||
route = route,
|
||||
nav = nav,
|
||||
tags = userTags
|
||||
)
|
||||
Crossfade(targetState = meta) {
|
||||
val displayName = remember(it) {
|
||||
it?.bestDisplayName() ?: hex
|
||||
}
|
||||
CreateClickableTextWithEmoji(
|
||||
clickablePart = displayName,
|
||||
suffix = suffix,
|
||||
maxLines = 1,
|
||||
route = route,
|
||||
nav = nav,
|
||||
tags = it?.tags ?: ImmutableListOfLists()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ fun RobohashAsyncImage(
|
||||
@Composable
|
||||
fun RobohashFallbackAsyncImage(
|
||||
robot: String,
|
||||
model: String,
|
||||
model: String?,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
alignment: Alignment = Alignment.Center,
|
||||
@@ -91,28 +91,15 @@ fun RobohashAsyncImageProxy(
|
||||
colorFilter: ColorFilter? = null,
|
||||
filterQuality: FilterQuality = DrawScope.DefaultFilterQuality
|
||||
) {
|
||||
if (model == null) {
|
||||
RobohashAsyncImage(
|
||||
robot = robot,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality
|
||||
)
|
||||
} else {
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = robot,
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality
|
||||
)
|
||||
}
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = robot,
|
||||
model = model,
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier,
|
||||
alignment = alignment,
|
||||
contentScale = contentScale,
|
||||
alpha = alpha,
|
||||
colorFilter = colorFilter,
|
||||
filterQuality = filterQuality
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
@@ -10,10 +11,10 @@ import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.pager.PagerState
|
||||
@@ -36,6 +38,7 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -50,6 +53,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.Placeholder
|
||||
@@ -439,12 +443,33 @@ private fun DisplayBlurHash(
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<ZoomableContent> = listOf(imageUrl).toImmutableList(), onDismiss: () -> Unit) {
|
||||
val view = LocalView.current
|
||||
|
||||
DisposableEffect(key1 = Unit) {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
view.windowInsetsController?.hide(
|
||||
android.view.WindowInsets.Type.systemBars()
|
||||
)
|
||||
}
|
||||
|
||||
onDispose {
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
view.windowInsetsController?.show(
|
||||
android.view.WindowInsets.Type.systemBars()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss,
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
properties = DialogProperties(
|
||||
usePlatformDefaultWidth = false,
|
||||
decorFitsSystemWindows = false
|
||||
)
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
Column() {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
|
||||
val pagerState: PagerState = remember { PagerState() }
|
||||
|
||||
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
|
||||
@@ -454,6 +479,18 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<Zoom
|
||||
}
|
||||
}
|
||||
|
||||
if (allImages.size > 1) {
|
||||
SlidingCarousel(
|
||||
pagerState = pagerState,
|
||||
itemsCount = allImages.size,
|
||||
itemContent = { index ->
|
||||
RenderImageOrVideo(allImages[index])
|
||||
}
|
||||
)
|
||||
} else {
|
||||
RenderImageOrVideo(imageUrl)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(10.dp)
|
||||
@@ -470,18 +507,6 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<Zoom
|
||||
SaveToGallery(localFile = myContent.localFile, mimeType = myContent.mimeType)
|
||||
}
|
||||
}
|
||||
|
||||
if (allImages.size > 1) {
|
||||
SlidingCarousel(
|
||||
pagerState = pagerState,
|
||||
itemsCount = allImages.size,
|
||||
itemContent = { index ->
|
||||
RenderImageOrVideo(allImages[index])
|
||||
}
|
||||
)
|
||||
} else {
|
||||
RenderImageOrVideo(imageUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-23
@@ -1,14 +1,11 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||
import okhttp3.Request
|
||||
import java.util.Date
|
||||
|
||||
class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
@@ -35,13 +32,13 @@ class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter<No
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
|
||||
val fortyEightHrs = (Date().time / 1000) - 60 * 60 * 48 // hrs
|
||||
val twoHrs = (Date().time / 1000) - 60 * 60 * 2 // hrs
|
||||
|
||||
return collection
|
||||
.asSequence()
|
||||
.filter { it ->
|
||||
val noteEvent = it.event
|
||||
(noteEvent is LiveActivitiesEvent && noteEvent.createdAt > fortyEightHrs && noteEvent.status() == "live" && checkIfOnline(noteEvent.streaming())) &&
|
||||
(noteEvent is LiveActivitiesEvent && noteEvent.createdAt > twoHrs && noteEvent.status() == "live" && OnlineChecker.isOnline(noteEvent.streaming())) &&
|
||||
(it.author?.pubkeyHex in followingKeySet || (noteEvent.isTaggedHashes(followingTagSet))) &&
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true
|
||||
@@ -55,20 +52,3 @@ class HomeLiveActivitiesFeedFilter(val account: Account) : AdditiveFeedFilter<No
|
||||
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
|
||||
}
|
||||
}
|
||||
|
||||
fun checkIfOnline(url: String?): Boolean {
|
||||
if (url.isNullOrBlank()) return false
|
||||
|
||||
val request = Request.Builder()
|
||||
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
return try {
|
||||
HttpClient.getHttpClient().newCall(request).execute().code == 200
|
||||
} catch (e: Exception) {
|
||||
Log.e("LiveActivities", "Failed to check streaming url $url", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
|
||||
val now = Date().time / 1000
|
||||
val oneMinuteInTheFuture = Date().time / 1000 + (1 * 60) // one minute in the future.
|
||||
val oneHr = 60 * 60
|
||||
|
||||
return collection
|
||||
@@ -43,7 +43,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
(it.author?.pubkeyHex in followingKeySet || (noteEvent.isTaggedHashes(followingTagSet))) &&
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true &&
|
||||
((it.event?.createdAt() ?: 0) < now) &&
|
||||
((it.event?.createdAt() ?: 0) < oneMinuteInTheFuture) &&
|
||||
it.isNewThread() &&
|
||||
(
|
||||
(noteEvent !is RepostEvent && noteEvent !is GenericRepostEvent) || // not a repost
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.vitorpamplona.amethyst.ui.dal
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
|
||||
object PeopleListFeedFilter : FeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val privKey = account.loggedIn.privKey ?: return emptyList()
|
||||
|
||||
val lists = LocalCache.addressables.values
|
||||
.asSequence()
|
||||
.filter {
|
||||
(it.event is PeopleListEvent)
|
||||
}
|
||||
.toSet()
|
||||
|
||||
return lists
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed()
|
||||
}
|
||||
}
|
||||
+4
-12
@@ -5,25 +5,17 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
|
||||
object UserProfileBookmarksFeedFilter : FeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
var user: User? = null
|
||||
|
||||
class UserProfileBookmarksFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex + "-" + user?.pubkeyHex
|
||||
}
|
||||
|
||||
fun loadUserProfile(accountLoggedIn: Account, user: User?) {
|
||||
account = accountLoggedIn
|
||||
this.user = user
|
||||
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val notes = user?.latestBookmarkList?.taggedEvents()?.mapNotNull {
|
||||
val notes = user.latestBookmarkList?.taggedEvents()?.mapNotNull {
|
||||
LocalCache.checkGetOrCreateNote(it)
|
||||
}?.toSet() ?: emptySet()
|
||||
|
||||
val addresses = user?.latestBookmarkList?.taggedAddresses()?.map {
|
||||
val addresses = user.latestBookmarkList?.taggedAddresses()?.map {
|
||||
LocalCache.getOrCreateAddressableNote(it)
|
||||
}?.toSet() ?: emptySet()
|
||||
|
||||
|
||||
+6
-14
@@ -4,23 +4,15 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
|
||||
object UserProfileConversationsFeedFilter : FeedFilter<Note>() {
|
||||
var account: Account? = null
|
||||
var user: User? = null
|
||||
|
||||
class UserProfileConversationsFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account?.userProfile()?.pubkeyHex + "-" + user?.pubkeyHex
|
||||
}
|
||||
|
||||
fun loadUserProfile(accountLoggedIn: Account, user: User?) {
|
||||
account = accountLoggedIn
|
||||
this.user = user
|
||||
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
return user?.notes
|
||||
?.filter { account?.isAcceptable(it) == true && !it.isNewThread() }
|
||||
?.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
?.reversed() ?: emptyList()
|
||||
return user.notes
|
||||
.filter { account.isAcceptable(it) == true && !it.isNewThread() }
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed() ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
+7
-15
@@ -8,27 +8,19 @@ import com.vitorpamplona.amethyst.service.model.AppRecommendationEvent
|
||||
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
|
||||
object UserProfileNewThreadFeedFilter : FeedFilter<Note>() {
|
||||
var account: Account? = null
|
||||
var user: User? = null
|
||||
|
||||
class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return account?.userProfile()?.pubkeyHex + "-" + user?.pubkeyHex
|
||||
}
|
||||
|
||||
fun loadUserProfile(accountLoggedIn: Account, user: User) {
|
||||
account = accountLoggedIn
|
||||
this.user = user
|
||||
return account.userProfile().pubkeyHex + "-" + user.pubkeyHex
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val longFormNotes = LocalCache.addressables.values
|
||||
.filter { it.author == user && (it.event !is PeopleListEvent && it.event !is BookmarkListEvent && it.event !is AppRecommendationEvent) }
|
||||
|
||||
return user?.notes
|
||||
?.plus(longFormNotes)
|
||||
?.filter { account?.isAcceptable(it) == true && it.isNewThread() }
|
||||
?.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
?.reversed() ?: emptyList()
|
||||
return user.notes
|
||||
.plus(longFormNotes)
|
||||
.filter { account.isAcceptable(it) == true && it.isNewThread() }
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
.reversed() ?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
+3
-11
@@ -5,21 +5,13 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
|
||||
object UserProfileReportsFeedFilter : FeedFilter<Note>() {
|
||||
var user: User? = null
|
||||
|
||||
class UserProfileReportsFeedFilter(val user: User) : FeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
return user?.pubkeyHex ?: ""
|
||||
}
|
||||
|
||||
fun loadUserProfile(user: User?) {
|
||||
this.user = user
|
||||
return user.pubkeyHex ?: ""
|
||||
}
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val myUser = user ?: return emptyList()
|
||||
|
||||
val reportNotes = LocalCache.notes.values.filter { (it.event as? ReportEvent)?.isTaggedUser(myUser.pubkeyHex) == true }
|
||||
val reportNotes = LocalCache.notes.values.filter { (it.event as? ReportEvent)?.isTaggedUser(user.pubkeyHex) == true }
|
||||
|
||||
return reportNotes
|
||||
.sortedWith(compareBy({ it.createdAt() }, { it.idHex }))
|
||||
|
||||
+36
-1
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
@@ -246,8 +247,42 @@ private fun LogoutButton(
|
||||
acc: AccountInfo,
|
||||
accountStateViewModel: AccountStateViewModel
|
||||
) {
|
||||
var logoutDialog by remember { mutableStateOf(false) }
|
||||
if (logoutDialog) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.log_out))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.are_you_sure_you_want_to_log_out))
|
||||
},
|
||||
onDismissRequest = {
|
||||
logoutDialog = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
logoutDialog = false
|
||||
accountStateViewModel.logOff(acc.npub)
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.log_out))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
logoutDialog = false
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = { accountStateViewModel.logOff(acc.npub) }
|
||||
onClick = { logoutDialog = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Logout,
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -40,6 +41,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
@@ -51,6 +53,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.grayText
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -63,11 +67,9 @@ fun ChatroomCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
|
||||
val isBlank = remember(noteState) {
|
||||
noteState?.note?.event == null
|
||||
}
|
||||
val isBlank by baseNote.live().metadata.map {
|
||||
it.note.event == null
|
||||
}.observeAsState(baseNote.event == null)
|
||||
|
||||
if (isBlank) {
|
||||
BlankNote(Modifier)
|
||||
@@ -82,7 +84,7 @@ fun ChatroomComposeChannelOrUser(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val channelHex by remember {
|
||||
val channelHex by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
baseNote.channelHex()
|
||||
}
|
||||
@@ -101,7 +103,7 @@ private fun ChatroomDirectMessage(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val userRoomHex by remember {
|
||||
val userRoomHex by remember(baseNote) {
|
||||
derivedStateOf {
|
||||
(baseNote.event as? PrivateDmEvent)?.talkingWith(accountViewModel.userProfile().pubkeyHex)
|
||||
}
|
||||
@@ -227,12 +229,23 @@ private fun UserRoomCompose(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var hasNewMessages = remember { mutableStateOf<Boolean>(false) }
|
||||
val hasNewMessages = remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
val route = remember(user) {
|
||||
"Room/${user.pubkeyHex}"
|
||||
}
|
||||
|
||||
val createAt by remember(note) {
|
||||
derivedStateOf {
|
||||
note.createdAt()
|
||||
}
|
||||
}
|
||||
|
||||
val content by remember(note) {
|
||||
derivedStateOf {
|
||||
accountViewModel.decrypt(note)
|
||||
}
|
||||
}
|
||||
WatchNotificationChanges(note, route, accountViewModel) { newHasNewMessages ->
|
||||
if (hasNewMessages.value != newHasNewMessages) {
|
||||
hasNewMessages.value = newHasNewMessages
|
||||
@@ -248,8 +261,8 @@ private fun UserRoomCompose(
|
||||
)
|
||||
},
|
||||
channelTitle = { UsernameDisplay(user, it) },
|
||||
channelLastTime = remember(note) { note.createdAt() },
|
||||
channelLastContent = remember(note) { accountViewModel.decrypt(note) },
|
||||
channelLastTime = createAt,
|
||||
channelLastContent = content,
|
||||
hasNewMessages = hasNewMessages,
|
||||
onClick = { nav(route) }
|
||||
)
|
||||
@@ -338,16 +351,28 @@ fun ChannelName(
|
||||
Row(
|
||||
modifier = remember { Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp) }
|
||||
) {
|
||||
Column(remember { Modifier.width(Size55dp) }) {
|
||||
Column(Size55Modifier) {
|
||||
channelPicture()
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
Column(
|
||||
modifier = remember { Modifier.padding(start = 10.dp).fillMaxWidth() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
FirstRow(channelTitle, channelLastTime)
|
||||
SecondRow(channelLastContent, hasNewMessages)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = remember { Modifier.padding(bottom = 4.dp) }
|
||||
) {
|
||||
FirstRow(channelTitle, channelLastTime, remember { Modifier.weight(1f) })
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
SecondRow(channelLastContent, hasNewMessages, remember { Modifier.weight(1f) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,60 +384,56 @@ fun ChannelName(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondRow(channelLastContent: String?, hasNewMessages: MutableState<Boolean>) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (channelLastContent != null) {
|
||||
Text(
|
||||
channelLastContent,
|
||||
color = MaterialTheme.colors.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
modifier = remember { Modifier.weight(1f) }
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.referenced_event_not_found),
|
||||
color = MaterialTheme.colors.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = remember { Modifier.weight(1f) }
|
||||
)
|
||||
}
|
||||
private fun SecondRow(channelLastContent: String?, hasNewMessages: MutableState<Boolean>, modifier: Modifier) {
|
||||
if (channelLastContent != null) {
|
||||
Text(
|
||||
channelLastContent,
|
||||
color = MaterialTheme.colors.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
modifier = modifier
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.referenced_event_not_found),
|
||||
color = MaterialTheme.colors.grayText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
if (hasNewMessages.value) {
|
||||
NewItemsBubble()
|
||||
}
|
||||
if (hasNewMessages.value) {
|
||||
NewItemsBubble()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FirstRow(
|
||||
channelTitle: @Composable (Modifier) -> Unit,
|
||||
channelLastTime: Long?
|
||||
channelLastTime: Long?,
|
||||
modifier: Modifier
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = remember { Modifier.padding(bottom = 4.dp) }
|
||||
) {
|
||||
channelTitle(
|
||||
remember {
|
||||
Modifier.weight(1f)
|
||||
}
|
||||
)
|
||||
channelTitle(modifier)
|
||||
TimeAgo(channelLastTime)
|
||||
}
|
||||
|
||||
channelLastTime?.let {
|
||||
val context = LocalContext.current
|
||||
val timeAgo = remember(channelLastTime) { timeAgo(channelLastTime, context) }
|
||||
Text(
|
||||
timeAgo,
|
||||
color = MaterialTheme.colors.grayText
|
||||
)
|
||||
@Composable
|
||||
private fun TimeAgo(channelLastTime: Long?) {
|
||||
if (channelLastTime == null) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val timeAgo by remember(channelLastTime) {
|
||||
derivedStateOf {
|
||||
timeAgo(channelLastTime, context)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = timeAgo,
|
||||
color = MaterialTheme.colors.grayText,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -431,6 +452,7 @@ fun NewItemsBubble() {
|
||||
color = Color.White,
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 12.sp,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.align(Alignment.Center)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
@@ -27,7 +28,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@@ -50,9 +50,9 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
@@ -71,24 +71,23 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleMaxSizeModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
|
||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size13dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -102,30 +101,33 @@ fun ChatroomMessageCompose(
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val noteEvent = remember(noteState) { noteState?.note?.event }
|
||||
val isBlank by baseNote.live().metadata.map {
|
||||
it.note.event == null
|
||||
}.observeAsState(baseNote.event == null)
|
||||
|
||||
if (noteEvent == null) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
Modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = showPopup
|
||||
)
|
||||
}
|
||||
Crossfade(targetState = isBlank) {
|
||||
if (it) {
|
||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||
BlankNote(
|
||||
remember {
|
||||
Modifier.combinedClickable(
|
||||
onClick = { },
|
||||
onLongClick = showPopup
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CheckHiddenChatMessage(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
innerQuote,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
onWantsToReply
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CheckHiddenChatMessage(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
innerQuote,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
onWantsToReply
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,32 +198,34 @@ fun LoadedChatMessageCompose(
|
||||
}
|
||||
}
|
||||
|
||||
if (showHiddenNote) {
|
||||
HiddenNote(
|
||||
state.relevantReports,
|
||||
accountViewModel,
|
||||
Modifier,
|
||||
innerQuote,
|
||||
nav,
|
||||
onClick = { showReportedNote = true }
|
||||
)
|
||||
} else {
|
||||
val canPreview by remember(state, showReportedNote) {
|
||||
derivedStateOf {
|
||||
(!state.isAcceptable && showReportedNote) || state.canPreview
|
||||
Crossfade(targetState = showHiddenNote) {
|
||||
if (it) {
|
||||
HiddenNote(
|
||||
state.relevantReports,
|
||||
accountViewModel,
|
||||
Modifier,
|
||||
innerQuote,
|
||||
nav,
|
||||
onClick = { showReportedNote = true }
|
||||
)
|
||||
} else {
|
||||
val canPreview by remember(state, showReportedNote) {
|
||||
derivedStateOf {
|
||||
(!state.isAcceptable && showReportedNote) || state.canPreview
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NormalChatNote(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
innerQuote,
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
onWantsToReply
|
||||
)
|
||||
NormalChatNote(
|
||||
baseNote,
|
||||
routeForLastRead,
|
||||
innerQuote,
|
||||
canPreview,
|
||||
parentBackgroundColor,
|
||||
accountViewModel,
|
||||
nav,
|
||||
onWantsToReply
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,7 +243,7 @@ fun NormalChatNote(
|
||||
) {
|
||||
val drawAuthorInfo by remember {
|
||||
derivedStateOf {
|
||||
(innerQuote || !accountViewModel.isLoggedUser(note.author)) && note.event !is PrivateDmEvent
|
||||
note.event !is PrivateDmEvent && (innerQuote || !accountViewModel.isLoggedUser(note.author))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,11 +302,11 @@ fun NormalChatNote(
|
||||
modifier = modif,
|
||||
horizontalArrangement = alignment
|
||||
) {
|
||||
val availableBubbleSize = remember { mutableStateOf(IntSize.Zero) }
|
||||
val availableBubbleSize = remember { mutableStateOf(0) }
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val modif2 = remember {
|
||||
if (innerQuote) Modifier else Modifier.fillMaxWidth(0.85f)
|
||||
if (innerQuote) Modifier else ChatBubbleMaxSizeModifier
|
||||
}
|
||||
|
||||
val clickableModifier = remember {
|
||||
@@ -320,7 +324,9 @@ fun NormalChatNote(
|
||||
Row(
|
||||
horizontalArrangement = alignment,
|
||||
modifier = modif2.onSizeChanged {
|
||||
availableBubbleSize.value = it
|
||||
if (availableBubbleSize.value != it.width) {
|
||||
availableBubbleSize.value = it.width
|
||||
}
|
||||
}
|
||||
) {
|
||||
Surface(
|
||||
@@ -362,17 +368,19 @@ private fun RenderBubble(
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
canPreview: Boolean,
|
||||
availableBubbleSize: MutableState<IntSize>,
|
||||
availableBubbleSize: MutableState<Int>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val bubbleSize = remember { mutableStateOf(IntSize.Zero) }
|
||||
val bubbleSize = remember { mutableStateOf(0) }
|
||||
|
||||
val bubbleModifier = remember {
|
||||
Modifier
|
||||
.padding(start = 10.dp, end = 5.dp, bottom = 5.dp)
|
||||
.onSizeChanged {
|
||||
bubbleSize.value = it
|
||||
if (bubbleSize.value != it.width) {
|
||||
bubbleSize.value = it.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,8 +412,8 @@ private fun MessageBubbleLines(
|
||||
accountViewModel: AccountViewModel,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
canPreview: Boolean,
|
||||
bubbleSize: MutableState<IntSize>,
|
||||
availableBubbleSize: MutableState<IntSize>
|
||||
bubbleSize: MutableState<Int>,
|
||||
availableBubbleSize: MutableState<Int>
|
||||
) {
|
||||
if (drawAuthorInfo) {
|
||||
DrawAuthorInfo(
|
||||
@@ -458,7 +466,7 @@ private fun RenderReplyRow(
|
||||
) {
|
||||
val hasReply by remember {
|
||||
derivedStateOf {
|
||||
innerQuote && note.replyTo?.lastOrNull() != null
|
||||
!innerQuote && note.replyTo?.lastOrNull() != null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,23 +536,22 @@ private fun NoteRow(
|
||||
|
||||
@Composable
|
||||
private fun ConstrainedStatusRow(
|
||||
bubbleSize: MutableState<IntSize>,
|
||||
availableBubbleSize: MutableState<IntSize>,
|
||||
bubbleSize: MutableState<Int>,
|
||||
availableBubbleSize: MutableState<Int>,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.then(
|
||||
with(LocalDensity.current) {
|
||||
Modifier.widthIn(
|
||||
bubbleSize.value.width.toDp(),
|
||||
availableBubbleSize.value.width.toDp()
|
||||
)
|
||||
}
|
||||
)
|
||||
modifier = with(LocalDensity.current) {
|
||||
Modifier
|
||||
.height(26.dp)
|
||||
.padding(top = 5.dp)
|
||||
.widthIn(
|
||||
bubbleSize.value.toDp(),
|
||||
availableBubbleSize.value.toDp()
|
||||
)
|
||||
}
|
||||
) {
|
||||
content()
|
||||
}
|
||||
@@ -557,29 +564,30 @@ private fun StatusRow(
|
||||
nav: (String) -> Unit,
|
||||
onWantsToReply: (Note) -> Unit
|
||||
) {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = ReactionRowHeightChat) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
|
||||
ChatTimeAgo(baseNote)
|
||||
RelayBadges(baseNote, accountViewModel, nav = nav)
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = ReactionRowHeightChat) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
|
||||
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
ReplyReaction(
|
||||
baseNote = baseNote,
|
||||
grayTint = MaterialTheme.colors.placeholderText,
|
||||
accountViewModel = accountViewModel,
|
||||
showCounter = false,
|
||||
iconSize = Size16dp
|
||||
iconSize = Size15dp
|
||||
) {
|
||||
onWantsToReply(baseNote)
|
||||
}
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -799,52 +807,61 @@ private fun DisplayMessageUsername(
|
||||
DrawPlayName(userDisplayName)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class RelayBadgesState(
|
||||
val shouldDisplayExpandButton: Boolean,
|
||||
val noteRelays: ImmutableList<String>,
|
||||
val noteRelaysSimple: ImmutableList<String>
|
||||
)
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
RenderRelayList(baseNote, expanded, accountViewModel, nav)
|
||||
|
||||
RenderExpandButton(baseNote, expanded) {
|
||||
ChatRelayExpandButton { expanded.value = true }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteRelaysState by baseNote.live().relays.observeAsState()
|
||||
fun RenderRelayList(baseNote: Note, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteRelays by baseNote.live().relays.map {
|
||||
it.note.relays
|
||||
}.observeAsState(baseNote.relays)
|
||||
|
||||
val state: RelayBadgesState by remember(noteRelaysState) {
|
||||
val newShouldDisplayExpandButton = (noteRelaysState?.note?.relays?.size ?: 0) > 3
|
||||
val noteRelays = noteRelaysState?.note?.relays?.toImmutableList() ?: persistentListOf()
|
||||
val noteRelaysSimple = noteRelaysState?.note?.relays?.take(3)?.toImmutableList() ?: persistentListOf()
|
||||
|
||||
mutableStateOf(RelayBadgesState(newShouldDisplayExpandButton, noteRelays, noteRelaysSimple))
|
||||
}
|
||||
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
val relaysToDisplay by remember(noteRelaysState) {
|
||||
derivedStateOf {
|
||||
if (expanded) state.noteRelays else state.noteRelaysSimple
|
||||
FlowRow(StdStartPadding) {
|
||||
val relaysToDisplay = remember(noteRelays, expanded.value) {
|
||||
if (expanded.value) noteRelays else noteRelays.take(3)
|
||||
}
|
||||
}
|
||||
|
||||
FlowRow(Modifier.padding(start = 10.dp)) {
|
||||
relaysToDisplay.forEach {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.shouldDisplayExpandButton && !expanded) {
|
||||
IconButton(
|
||||
@Composable
|
||||
fun RenderExpandButton(
|
||||
baseNote: Note,
|
||||
expanded: MutableState<Boolean>,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val showExpandButton by baseNote.live().relays.map {
|
||||
it.note.relays.size > 3
|
||||
}.observeAsState(baseNote.relays.size > 3)
|
||||
|
||||
if (showExpandButton && !expanded.value) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatRelayExpandButton(onClick: () -> Unit) {
|
||||
IconButton(
|
||||
modifier = Size15Modifier,
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
onClick = { expanded = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -28,6 +29,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -109,21 +111,16 @@ fun ObserveDisplayNip05Status(baseNote: Note, columnModifier: Modifier = Modifie
|
||||
|
||||
@Composable
|
||||
fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifier) {
|
||||
val userState by baseUser.live().metadata.observeAsState()
|
||||
val isValidNIP05 by remember(userState) {
|
||||
derivedStateOf {
|
||||
userState?.user?.nip05()?.split("@")?.size == 2
|
||||
}
|
||||
}
|
||||
val nip05 by remember(userState) {
|
||||
derivedStateOf {
|
||||
userState?.user?.nip05()
|
||||
}
|
||||
}
|
||||
val nip05 by baseUser.live().metadata.map {
|
||||
it.user.nip05()
|
||||
}.observeAsState()
|
||||
|
||||
if (isValidNIP05) {
|
||||
nip05?.let {
|
||||
DisplayNIP05Line(it, baseUser, columnModifier)
|
||||
Crossfade(targetState = nip05, modifier = columnModifier) {
|
||||
if (it != null) {
|
||||
val isValid = it.split("@").size == 2
|
||||
if (isValid) {
|
||||
DisplayNIP05Line(it, baseUser, columnModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,9 +128,11 @@ fun ObserveDisplayNip05Status(baseUser: User, columnModifier: Modifier = Modifie
|
||||
@Composable
|
||||
private fun DisplayNIP05Line(nip05: String, baseUser: User, columnModifier: Modifier = Modifier) {
|
||||
Column(modifier = columnModifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
|
||||
DisplayNIP05(nip05, nip05Verified)
|
||||
val nip05Verified = nip05VerificationAsAState(baseUser.info!!, baseUser.pubkeyHex)
|
||||
Crossfade(targetState = nip05Verified) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
DisplayNIP05(nip05, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +149,7 @@ private fun DisplayNIP05(
|
||||
|
||||
if (user != "_") {
|
||||
Text(
|
||||
text = AnnotatedString(user),
|
||||
text = remember(nip05) { AnnotatedString(user) },
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -160,7 +159,7 @@ private fun DisplayNIP05(
|
||||
NIP05VerifiedSymbol(nip05Verified)
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(domain),
|
||||
text = remember(nip05) { AnnotatedString(domain) },
|
||||
onClick = { runCatching { uri.openUri("https://$domain") } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary.copy(0.52f)),
|
||||
maxLines = 1,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.note
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.slideInVertically
|
||||
@@ -21,7 +22,6 @@ import androidx.compose.foundation.layout.Spacer
|
||||
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.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
@@ -82,6 +82,7 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Height4dpModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeight
|
||||
@@ -502,16 +503,16 @@ fun ReplyReaction(
|
||||
fun ReplyCounter(baseNote: Note, textColor: Color) {
|
||||
val repliesState by baseNote.live().replies.map {
|
||||
it.note.replies.size
|
||||
}.distinctUntilChanged().observeAsState()
|
||||
}.observeAsState(0)
|
||||
|
||||
SlidingAnimation(repliesState ?: 0, textColor)
|
||||
SlidingAnimation(repliesState, textColor)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
private fun SlidingAnimation(count: Int, textColor: Color) {
|
||||
private fun SlidingAnimation(baseCount: Int, textColor: Color) {
|
||||
AnimatedContent<Int>(
|
||||
targetState = count,
|
||||
targetState = baseCount,
|
||||
transitionSpec = {
|
||||
if (targetState > initialState) {
|
||||
slideInVertically { -it } with slideOutVertically { it }
|
||||
@@ -521,10 +522,11 @@ private fun SlidingAnimation(count: Int, textColor: Color) {
|
||||
}
|
||||
) { count ->
|
||||
Text(
|
||||
text = showCount(count),
|
||||
text = remember(count) { showCount(count) },
|
||||
fontSize = Font14SP,
|
||||
color = textColor,
|
||||
modifier = HalfStartPadding
|
||||
modifier = HalfStartPadding,
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -543,10 +545,10 @@ private fun SlidingAnimation(amount: String, textColor: Color) {
|
||||
}
|
||||
) { count ->
|
||||
Text(
|
||||
text = amount,
|
||||
text = count,
|
||||
fontSize = Font14SP,
|
||||
color = textColor,
|
||||
modifier = HalfStartPadding
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -721,15 +723,19 @@ fun LikeIcon(
|
||||
launch(Dispatchers.Default) {
|
||||
val newReactionType = reactionsState?.note?.isReactedBy(accountViewModel.userProfile())?.firstFullChar()
|
||||
if (reactionType != newReactionType) {
|
||||
reactionType = newReactionType
|
||||
launch(Dispatchers.Main) {
|
||||
reactionType = newReactionType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reactionType != null) {
|
||||
RenderReactionType(reactionType!!, iconSize, iconFontSize)
|
||||
} else {
|
||||
RenderLikeIcon(iconSize, grayTint)
|
||||
Crossfade(targetState = reactionType) {
|
||||
if (it != null) {
|
||||
RenderReactionType(it, iconSize, iconFontSize)
|
||||
} else {
|
||||
RenderLikeIcon(iconSize, grayTint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -787,7 +793,9 @@ fun LikeText(baseNote: Note, grayTint: Color) {
|
||||
launch(Dispatchers.Default) {
|
||||
val newReactionsCount = reactionsState?.note?.countReactions() ?: 0
|
||||
if (reactionsCount != newReactionsCount) {
|
||||
reactionsCount = newReactionsCount
|
||||
launch(Dispatchers.Main) {
|
||||
reactionsCount = newReactionsCount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -919,7 +927,7 @@ fun ZapReaction(
|
||||
}
|
||||
|
||||
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
|
||||
Spacer(Modifier.width(3.dp))
|
||||
Spacer(ModifierWidth3dp)
|
||||
|
||||
CircularProgressIndicator(
|
||||
progress = animateFloatAsState(
|
||||
@@ -1016,27 +1024,31 @@ private fun ZapIcon(
|
||||
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(it)
|
||||
|
||||
if (wasZappedByLoggedInUser != newWasZapped) {
|
||||
wasZappedByLoggedInUser = newWasZapped
|
||||
launch(Dispatchers.Main) {
|
||||
wasZappedByLoggedInUser = newWasZapped
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wasZappedByLoggedInUser) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = grayTint
|
||||
)
|
||||
Crossfade(targetState = wasZappedByLoggedInUser) {
|
||||
if (it) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = remember { Modifier.size(iconSize) },
|
||||
tint = grayTint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1055,7 +1067,9 @@ private fun ZapAmountText(
|
||||
zapsState?.note?.let {
|
||||
val newZapAmount = showAmount(accountViewModel.calculateZapAmount(it))
|
||||
if (newZapAmount != zapAmountTxt) {
|
||||
zapAmountTxt = newZapAmount
|
||||
launch(Dispatchers.Main) {
|
||||
zapAmountTxt = newZapAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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
|
||||
|
||||
@Composable
|
||||
fun UserCompose(
|
||||
@@ -39,9 +39,9 @@ fun UserCompose(
|
||||
modifier = overallModifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
UserPicture(baseUser, nav, accountViewModel, 55.dp)
|
||||
UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp).weight(1f)) {
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp).weight(1f) }) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
UsernameDisplay(baseUser)
|
||||
}
|
||||
@@ -49,7 +49,7 @@ fun UserCompose(
|
||||
AboutDisplay(baseUser)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Column(modifier = remember { Modifier.padding(start = 10.dp) }) {
|
||||
UserActionOptions(baseUser, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
@@ -20,13 +22,11 @@ import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.tts.TextToSpeechHelper
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdButtonSizeModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
@@ -36,38 +36,34 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
fun NoteUsernameDisplay(baseNote: Note, weight: Modifier = Modifier) {
|
||||
val authorState by baseNote.live().metadata.map {
|
||||
it.note.author
|
||||
}.distinctUntilChanged().observeAsState()
|
||||
}.observeAsState(baseNote.author)
|
||||
|
||||
authorState?.let {
|
||||
UsernameDisplay(it, weight)
|
||||
Crossfade(targetState = authorState, modifier = weight) {
|
||||
it?.let {
|
||||
UsernameDisplay(it, weight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UsernameDisplay(baseUser: User, weight: Modifier = Modifier) {
|
||||
val userState by baseUser.live().metadata.observeAsState()
|
||||
val bestUserName by remember(userState) {
|
||||
derivedStateOf {
|
||||
userState?.user?.bestUsername()
|
||||
}
|
||||
}
|
||||
val bestDisplayName by remember(userState) {
|
||||
derivedStateOf {
|
||||
userState?.user?.bestDisplayName()
|
||||
}
|
||||
}
|
||||
val npubDisplay by remember {
|
||||
derivedStateOf {
|
||||
baseUser.pubkeyDisplayHex()
|
||||
}
|
||||
}
|
||||
val tags by remember(userState) {
|
||||
derivedStateOf {
|
||||
userState?.user?.info?.latestMetadata?.tags?.toImmutableListOfLists()
|
||||
|
||||
val userMetadata by baseUser.live().metadata.map {
|
||||
it.user.info
|
||||
}.observeAsState(baseUser.info)
|
||||
|
||||
Crossfade(targetState = userMetadata, modifier = weight) {
|
||||
if (it != null) {
|
||||
UserNameDisplay(it.bestUsername(), it.bestDisplayName(), npubDisplay, it.tags, weight)
|
||||
} else {
|
||||
NPubDisplay(npubDisplay, weight)
|
||||
}
|
||||
}
|
||||
|
||||
UserNameDisplay(bestUserName, bestDisplayName, npubDisplay, tags, weight)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -106,16 +102,18 @@ private fun UserDisplay(
|
||||
tags: ImmutableListOfLists<String>?,
|
||||
modifier: Modifier
|
||||
) {
|
||||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
tags = tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
DrawPlayName(bestDisplayName)
|
||||
Row(modifier = modifier) {
|
||||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
tags = tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
DrawPlayName(bestDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -125,22 +123,24 @@ private fun UserAndUsernameDisplay(
|
||||
bestUserName: String,
|
||||
modifier: Modifier
|
||||
) {
|
||||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
tags = tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
CreateTextWithEmoji(
|
||||
text = remember { "@$bestUserName" },
|
||||
tags = tags,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
DrawPlayName(bestDisplayName)
|
||||
Row(modifier = modifier) {
|
||||
CreateTextWithEmoji(
|
||||
text = bestDisplayName,
|
||||
tags = tags,
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1
|
||||
)
|
||||
CreateTextWithEmoji(
|
||||
text = remember { "@$bestUserName" },
|
||||
tags = tags,
|
||||
color = MaterialTheme.colors.placeholderText,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = modifier
|
||||
)
|
||||
Spacer(StdHorzSpacer)
|
||||
DrawPlayName(bestDisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowUserButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -106,7 +107,7 @@ private fun RenderZapNote(
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
UserPicture(baseAuthor, nav, accountViewModel, 55.dp)
|
||||
UserPicture(baseAuthor, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
||||
|
||||
Column(
|
||||
modifier = remember {
|
||||
|
||||
@@ -11,11 +11,10 @@ import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -28,18 +27,19 @@ fun ChatroomListFeedView(
|
||||
markAsRead: MutableState<Boolean>
|
||||
) {
|
||||
RefresheableView(viewModel, true) {
|
||||
CorssFadeState(viewModel, accountViewModel, nav, markAsRead)
|
||||
CrossFadeState(viewModel, accountViewModel, nav, markAsRead)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CorssFadeState(
|
||||
private fun CrossFadeState(
|
||||
viewModel: FeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
markAsRead: MutableState<Boolean>
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
Crossfade(
|
||||
targetState = feedState,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
|
||||
@@ -197,8 +197,7 @@ private fun FeedLoaded(
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
val defaultModifier = remember {
|
||||
Modifier
|
||||
.fillMaxWidth().animateItemPlacement()
|
||||
Modifier.fillMaxWidth().animateItemPlacement()
|
||||
}
|
||||
|
||||
Row(defaultModifier) {
|
||||
|
||||
@@ -81,11 +81,38 @@ class NostrThreadFeedViewModel(val noteId: String) : FeedViewModel(ThreadFeedFil
|
||||
}
|
||||
}
|
||||
|
||||
class NostrUserProfileNewThreadsFeedViewModel(val user: User, val account: Account) : FeedViewModel(UserProfileNewThreadFeedFilter(user, account)) {
|
||||
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrUserProfileNewThreadsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileNewThreadsFeedViewModel>): NostrUserProfileNewThreadsFeedViewModel {
|
||||
return NostrUserProfileNewThreadsFeedViewModel(user, account) as NostrUserProfileNewThreadsFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
class NostrUserProfileConversationsFeedViewModel(val user: User, val account: Account) : FeedViewModel(UserProfileConversationsFeedFilter(user, account)) {
|
||||
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrUserProfileConversationsFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileConversationsFeedViewModel>): NostrUserProfileConversationsFeedViewModel {
|
||||
return NostrUserProfileConversationsFeedViewModel(user, account) as NostrUserProfileConversationsFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NostrHashtagFeedViewModel : FeedViewModel(HashtagFeedFilter)
|
||||
class NostrUserProfileNewThreadsFeedViewModel : FeedViewModel(UserProfileNewThreadFeedFilter)
|
||||
class NostrUserProfileConversationsFeedViewModel : FeedViewModel(UserProfileConversationsFeedFilter)
|
||||
class NostrUserProfileReportFeedViewModel : FeedViewModel(UserProfileReportsFeedFilter)
|
||||
class NostrUserProfileBookmarksFeedViewModel : FeedViewModel(UserProfileBookmarksFeedFilter)
|
||||
|
||||
class NostrUserProfileReportFeedViewModel(val user: User) : FeedViewModel(UserProfileReportsFeedFilter(user)) {
|
||||
class Factory(val user: User) : ViewModelProvider.Factory {
|
||||
override fun <NostrUserProfileReportFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileReportFeedViewModel>): NostrUserProfileReportFeedViewModel {
|
||||
return NostrUserProfileReportFeedViewModel(user) as NostrUserProfileReportFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
class NostrUserProfileBookmarksFeedViewModel(val user: User, val account: Account) : FeedViewModel(UserProfileBookmarksFeedFilter(user, account)) {
|
||||
class Factory(val user: User, val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrUserProfileBookmarksFeedViewModel : ViewModel> create(modelClass: Class<NostrUserProfileBookmarksFeedViewModel>): NostrUserProfileBookmarksFeedViewModel {
|
||||
return NostrUserProfileBookmarksFeedViewModel(user, account) as NostrUserProfileBookmarksFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NostrChatroomListKnownFeedViewModel(val account: Account) : FeedViewModel(ChatroomListKnownFeedFilter(account)) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrChatroomListKnownFeedViewModel : ViewModel> create(modelClass: Class<NostrChatroomListKnownFeedViewModel>): NostrChatroomListKnownFeedViewModel {
|
||||
|
||||
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.heightIn
|
||||
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.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
@@ -89,11 +88,12 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.PostButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableContentView
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableUrlVideo
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeReaction
|
||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapReaction
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
|
||||
@@ -116,7 +116,7 @@ fun ChannelScreen(
|
||||
) {
|
||||
if (channelId == null) return
|
||||
|
||||
var channelBase by remember { mutableStateOf<Channel?>(null) }
|
||||
var channelBase by remember { mutableStateOf<Channel?>(LocalCache.getChannelIfExists(channelId)) }
|
||||
|
||||
LaunchedEffect(channelId) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -518,7 +518,7 @@ fun ChannelHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
var baseChannel by remember { mutableStateOf<Channel?>(LocalCache.channels[channelHex]) }
|
||||
var baseChannel by remember { mutableStateOf(LocalCache.channels[channelHex]) }
|
||||
|
||||
if (baseChannel == null) {
|
||||
LaunchedEffect(key1 = channelHex) {
|
||||
@@ -549,40 +549,65 @@ fun ChannelHeader(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Column(
|
||||
Modifier.fillMaxWidth().clickable {
|
||||
nav("Channel/${baseChannel.idHex}")
|
||||
}
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
nav("Channel/${baseChannel.idHex}")
|
||||
}
|
||||
}
|
||||
) {
|
||||
val channelState by baseChannel.live.observeAsState()
|
||||
val channel = remember(channelState) { channelState?.channel } ?: return
|
||||
|
||||
val streamingUrl by remember(channelState) {
|
||||
derivedStateOf {
|
||||
(channel as? LiveActivitiesChannel)?.info?.streaming()
|
||||
val activity = channel as? LiveActivitiesChannel
|
||||
val description = activity?.info?.title()
|
||||
val url = activity?.info?.streaming()
|
||||
if (url != null) {
|
||||
ZoomableUrlVideo(url, description = description)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (streamingUrl != null && showVideo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = remember { Modifier.heightIn(max = 300.dp) }) {
|
||||
VideoView(
|
||||
videoUri = streamingUrl!!,
|
||||
description = null
|
||||
ZoomableContentView(
|
||||
content = streamingUrl!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(modifier = modifier) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
RobohashAsyncImageProxy(
|
||||
robot = channel.idHex,
|
||||
model = channel.profilePicture(),
|
||||
contentDescription = stringResource(R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(Size35dp)
|
||||
.height(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
)
|
||||
channel.creator?.let {
|
||||
UserPicture(
|
||||
user = it,
|
||||
size = Size35dp,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
channel.profilePicture()?.let {
|
||||
RobohashAsyncImageProxy(
|
||||
robot = channel.idHex,
|
||||
model = it,
|
||||
contentDescription = stringResource(R.string.profile_image),
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
.width(Size35dp)
|
||||
.height(Size35dp)
|
||||
.clip(shape = CircleShape)
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -592,7 +617,7 @@ fun ChannelHeader(
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
channel.toBestDisplayName(),
|
||||
text = remember(channelState) { channel.toBestDisplayName() },
|
||||
fontWeight = FontWeight.Bold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -619,7 +644,7 @@ fun ChannelHeader(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(Size35dp)
|
||||
.padding(bottom = 3.dp),
|
||||
.padding(start = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (channel is PublicChatChannel) {
|
||||
@@ -701,9 +726,9 @@ private fun LiveChannelActionOptions(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LiveFlag() {
|
||||
fun LiveFlag() {
|
||||
Text(
|
||||
text = "LIVE",
|
||||
text = stringResource(id = R.string.live_stream_live_tag),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = remember {
|
||||
@@ -715,6 +740,51 @@ private fun LiveFlag() {
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EndedFlag() {
|
||||
Text(
|
||||
text = stringResource(id = R.string.live_stream_ended_tag),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.drawBehind { drawRect(Color.Black) }
|
||||
.padding(horizontal = 5.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OfflineFlag() {
|
||||
Text(
|
||||
text = stringResource(id = R.string.live_stream_offline_tag),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.drawBehind { drawRect(Color.Black) }
|
||||
.padding(horizontal = 5.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScheduledFlag() {
|
||||
Text(
|
||||
text = stringResource(id = R.string.live_stream_planned_tag),
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.drawBehind { drawRect(Color.Black) }
|
||||
.padding(horizontal = 5.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NoteCopyButton(
|
||||
note: Channel
|
||||
|
||||
@@ -71,8 +71,6 @@ fun ChatroomListScreen(
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
NostrChatroomListDataSource.start()
|
||||
knownFeedViewModel.invalidateData(true)
|
||||
newFeedViewModel.invalidateData(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ import androidx.lifecycle.LifecycleEventObserver
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent
|
||||
import com.vitorpamplona.amethyst.ui.dal.checkIfOnline
|
||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.FeedState
|
||||
@@ -76,6 +76,8 @@ fun HomeScreen(
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(accountViewModel) {
|
||||
liveActivitiesViewModel.invalidateData(true)
|
||||
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
NostrHomeDataSource.invalidateFilters()
|
||||
@@ -187,13 +189,11 @@ private fun FeedLoaded(
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp
|
||||
),
|
||||
contentPadding = PaddingValues(),
|
||||
state = listState
|
||||
) {
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item.idHex }) { _, item ->
|
||||
CheckIfOnline(item) {
|
||||
CheckIfLiveActivityIsOnline(item) {
|
||||
ChannelHeader(
|
||||
channelHex = remember { item.idHex },
|
||||
showVideo = false,
|
||||
@@ -210,18 +210,20 @@ private fun FeedLoaded(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckIfOnline(note: Note, whenOnline: @Composable () -> Unit) {
|
||||
fun CheckIfLiveActivityIsOnline(note: Note, whenOnline: @Composable () -> Unit) {
|
||||
val noteState by note.live().metadata.observeAsState()
|
||||
var online by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(key1 = noteState) {
|
||||
launch(Dispatchers.IO) {
|
||||
online = checkIfOnline((note.event as? LiveActivitiesEvent)?.streaming())
|
||||
online = OnlineChecker.isOnline((note.event as? LiveActivitiesEvent)?.streaming())
|
||||
}
|
||||
}
|
||||
|
||||
if (online) {
|
||||
whenOnline()
|
||||
Crossfade(targetState = online) {
|
||||
if (it) {
|
||||
whenOnline()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,9 +76,6 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.ZoomableImageDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.figureOutMimeType
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileBookmarksFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileConversationsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
|
||||
import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
||||
@@ -104,7 +101,6 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Composable
|
||||
@@ -116,7 +112,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, nav: (Str
|
||||
if (userBase == null) {
|
||||
LaunchedEffect(userId) {
|
||||
// waits to resolve.
|
||||
withContext(Dispatchers.IO) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newUserBase = LocalCache.checkGetOrCreateUser(userId)
|
||||
if (newUserBase != userBase) {
|
||||
userBase = newUserBase
|
||||
@@ -166,33 +162,66 @@ fun PrepareViewModels(baseUser: User, accountViewModel: AccountViewModel, nav: (
|
||||
)
|
||||
)
|
||||
|
||||
val threadsViewModel: NostrUserProfileNewThreadsFeedViewModel = viewModel(
|
||||
key = baseUser.pubkeyHex + "UserProfileNewThreadsFeedViewModel",
|
||||
factory = NostrUserProfileNewThreadsFeedViewModel.Factory(
|
||||
baseUser,
|
||||
accountViewModel.account
|
||||
)
|
||||
)
|
||||
|
||||
val repliesViewModel: NostrUserProfileConversationsFeedViewModel = viewModel(
|
||||
key = baseUser.pubkeyHex + "UserProfileConversationsFeedViewModel",
|
||||
factory = NostrUserProfileConversationsFeedViewModel.Factory(
|
||||
baseUser,
|
||||
accountViewModel.account
|
||||
)
|
||||
)
|
||||
|
||||
val bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel = viewModel(
|
||||
key = baseUser.pubkeyHex + "UserProfileBookmarksFeedViewModel",
|
||||
factory = NostrUserProfileBookmarksFeedViewModel.Factory(
|
||||
baseUser,
|
||||
accountViewModel.account
|
||||
)
|
||||
)
|
||||
|
||||
val reportsFeedViewModel: NostrUserProfileReportFeedViewModel = viewModel(
|
||||
key = baseUser.pubkeyHex + "UserProfileReportFeedViewModel",
|
||||
factory = NostrUserProfileReportFeedViewModel.Factory(
|
||||
baseUser
|
||||
)
|
||||
)
|
||||
|
||||
ProfileScreen(
|
||||
baseUser = baseUser,
|
||||
threadsViewModel,
|
||||
repliesViewModel,
|
||||
followsFeedViewModel,
|
||||
followersFeedViewModel,
|
||||
appRecommendations,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
baseUser: User,
|
||||
threadsViewModel: NostrUserProfileNewThreadsFeedViewModel,
|
||||
repliesViewModel: NostrUserProfileConversationsFeedViewModel,
|
||||
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
UserProfileNewThreadFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||
UserProfileConversationsFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||
UserProfileReportsFeedFilter.loadUserProfile(baseUser)
|
||||
UserProfileBookmarksFeedFilter.loadUserProfile(accountViewModel.account, baseUser)
|
||||
|
||||
NostrUserProfileDataSource.loadUserProfile(baseUser)
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
@@ -224,16 +253,42 @@ fun ProfileScreen(
|
||||
}
|
||||
}
|
||||
|
||||
var columnSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
var tabsSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
RenderSurface(
|
||||
baseUser,
|
||||
threadsViewModel,
|
||||
repliesViewModel,
|
||||
appRecommendations,
|
||||
followsFeedViewModel,
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun RenderSurface(
|
||||
baseUser: User,
|
||||
threadsViewModel: NostrUserProfileNewThreadsFeedViewModel,
|
||||
repliesViewModel: NostrUserProfileConversationsFeedViewModel,
|
||||
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = MaterialTheme.colors.background
|
||||
) {
|
||||
val pagerState = rememberPagerState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
var columnSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
var tabsSize by remember { mutableStateOf(IntSize.Zero) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -242,58 +297,110 @@ fun ProfileScreen(
|
||||
columnSize = it
|
||||
}
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.nestedScroll(object : NestedScrollConnection {
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
// When scrolling vertically, scroll the container first.
|
||||
return if (available.y < 0 && scrollState.canScrollForward) {
|
||||
coroutineScope.launch {
|
||||
scrollState.scrollBy(-available.y)
|
||||
}
|
||||
Offset(0f, available.y)
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
})
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Column() {
|
||||
ProfileHeader(baseUser, appRecommendations, nav, accountViewModel)
|
||||
ScrollableTabRow(
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
edgePadding = 8.dp,
|
||||
modifier = Modifier.onSizeChanged {
|
||||
tabsSize = it
|
||||
}
|
||||
) {
|
||||
CreateAndRenderTabs(baseUser, pagerState)
|
||||
}
|
||||
HorizontalPager(
|
||||
pageCount = 8,
|
||||
state = pagerState,
|
||||
modifier = with(LocalDensity.current) {
|
||||
Modifier.height((columnSize.height - tabsSize.height).toDp())
|
||||
}
|
||||
) { page ->
|
||||
CreateAndRenderPages(
|
||||
page,
|
||||
baseUser,
|
||||
followsFeedViewModel,
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
val pagerState = rememberPagerState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
val tabRowModifier = remember {
|
||||
Modifier.onSizeChanged {
|
||||
tabsSize = it
|
||||
}
|
||||
}
|
||||
|
||||
val pagerModifier = with(LocalDensity.current) {
|
||||
Modifier.height((columnSize.height - tabsSize.height).toDp())
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.verticalScroll(scrollState)
|
||||
.nestedScroll(object : NestedScrollConnection {
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
// When scrolling vertically, scroll the container first.
|
||||
return if (available.y < 0 && scrollState.canScrollForward) {
|
||||
coroutineScope.launch {
|
||||
scrollState.scrollBy(-available.y)
|
||||
}
|
||||
Offset(0f, available.y)
|
||||
} else {
|
||||
Offset.Zero
|
||||
}
|
||||
}
|
||||
})
|
||||
.fillMaxHeight()
|
||||
}
|
||||
) {
|
||||
RenderScreen(
|
||||
baseUser,
|
||||
pagerState,
|
||||
tabRowModifier,
|
||||
pagerModifier,
|
||||
threadsViewModel,
|
||||
repliesViewModel,
|
||||
appRecommendations,
|
||||
followsFeedViewModel,
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
private fun RenderScreen(
|
||||
baseUser: User,
|
||||
pagerState: PagerState,
|
||||
tabRowModifier: Modifier,
|
||||
pagerModifier: Modifier,
|
||||
threadsViewModel: NostrUserProfileNewThreadsFeedViewModel,
|
||||
repliesViewModel: NostrUserProfileConversationsFeedViewModel,
|
||||
appRecommendations: NostrUserAppRecommendationsFeedViewModel,
|
||||
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
Column() {
|
||||
ProfileHeader(baseUser, appRecommendations, nav, accountViewModel)
|
||||
ScrollableTabRow(
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
edgePadding = 8.dp,
|
||||
modifier = tabRowModifier
|
||||
) {
|
||||
CreateAndRenderTabs(baseUser, pagerState)
|
||||
}
|
||||
HorizontalPager(
|
||||
pageCount = 8,
|
||||
state = pagerState,
|
||||
modifier = pagerModifier
|
||||
) { page ->
|
||||
CreateAndRenderPages(
|
||||
page,
|
||||
baseUser,
|
||||
threadsViewModel,
|
||||
repliesViewModel,
|
||||
followsFeedViewModel,
|
||||
followersFeedViewModel,
|
||||
zapFeedViewModel,
|
||||
bookmarksFeedViewModel,
|
||||
reportsFeedViewModel,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -302,20 +409,24 @@ fun ProfileScreen(
|
||||
private fun CreateAndRenderPages(
|
||||
page: Int,
|
||||
baseUser: User,
|
||||
threadsViewModel: NostrUserProfileNewThreadsFeedViewModel,
|
||||
repliesViewModel: NostrUserProfileConversationsFeedViewModel,
|
||||
followsFeedViewModel: NostrUserProfileFollowsUserFeedViewModel,
|
||||
followersFeedViewModel: NostrUserProfileFollowersUserFeedViewModel,
|
||||
zapFeedViewModel: NostrUserProfileZapsFeedViewModel,
|
||||
bookmarksFeedViewModel: NostrUserProfileBookmarksFeedViewModel,
|
||||
reportsFeedViewModel: NostrUserProfileReportFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (page) {
|
||||
0 -> TabNotesNewThreads(accountViewModel, nav)
|
||||
1 -> TabNotesConversations(accountViewModel, nav)
|
||||
0 -> TabNotesNewThreads(threadsViewModel, accountViewModel, nav)
|
||||
1 -> TabNotesConversations(repliesViewModel, accountViewModel, nav)
|
||||
2 -> TabFollows(baseUser, followsFeedViewModel, accountViewModel, nav)
|
||||
3 -> TabFollowers(baseUser, followersFeedViewModel, accountViewModel, nav)
|
||||
4 -> TabReceivedZaps(baseUser, zapFeedViewModel, accountViewModel, nav)
|
||||
5 -> TabBookmarks(baseUser, accountViewModel, nav)
|
||||
6 -> TabReports(baseUser, accountViewModel, nav)
|
||||
5 -> TabBookmarks(bookmarksFeedViewModel, accountViewModel, nav)
|
||||
6 -> TabReports(baseUser, reportsFeedViewModel, accountViewModel, nav)
|
||||
7 -> TabRelays(baseUser, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
@@ -368,8 +479,7 @@ private fun ReportsTabHeader(baseUser: User) {
|
||||
|
||||
LaunchedEffect(key1 = userState) {
|
||||
launch(Dispatchers.IO) {
|
||||
UserProfileReportsFeedFilter.user = baseUser
|
||||
val newSize = UserProfileReportsFeedFilter.feed().size
|
||||
val newSize = UserProfileReportsFeedFilter(baseUser).feed().size
|
||||
|
||||
if (newSize != userReports) {
|
||||
userReports = newSize
|
||||
@@ -1126,9 +1236,7 @@ public fun DrawBanner(baseUser: User) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabNotesNewThreads(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val feedViewModel: NostrUserProfileNewThreadsFeedViewModel = viewModel()
|
||||
|
||||
fun TabNotesNewThreads(feedViewModel: NostrUserProfileNewThreadsFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LaunchedEffect(Unit) {
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
@@ -1149,9 +1257,7 @@ fun TabNotesNewThreads(accountViewModel: AccountViewModel, nav: (String) -> Unit
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabNotesConversations(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val feedViewModel: NostrUserProfileConversationsFeedViewModel = viewModel()
|
||||
|
||||
fun TabNotesConversations(feedViewModel: NostrUserProfileConversationsFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LaunchedEffect(Unit) {
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
@@ -1172,9 +1278,7 @@ fun TabNotesConversations(accountViewModel: AccountViewModel, nav: (String) -> U
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabBookmarks(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val feedViewModel: NostrUserProfileBookmarksFeedViewModel = viewModel()
|
||||
|
||||
fun TabBookmarks(feedViewModel: NostrUserProfileBookmarksFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
LaunchedEffect(Unit) {
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
@@ -1264,9 +1368,7 @@ private fun WatchZapsAndUpdateFeed(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TabReports(baseUser: User, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val feedViewModel: NostrUserProfileReportFeedViewModel = viewModel()
|
||||
|
||||
fun TabReports(baseUser: User, feedViewModel: NostrUserProfileReportFeedViewModel, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
WatchReportsAndUpdateFeed(baseUser, feedViewModel)
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.theme
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
@@ -62,6 +63,7 @@ val StdPadding = Modifier.padding(10.dp)
|
||||
val Size15Modifier = Modifier.size(15.dp)
|
||||
val Size20Modifier = Modifier.size(20.dp)
|
||||
val Size22Modifier = Modifier.size(22.dp)
|
||||
val Size24Modifier = Modifier.size(24.dp)
|
||||
val Size30Modifier = Modifier.size(30.dp)
|
||||
val Size55Modifier = Modifier.size(55.dp)
|
||||
|
||||
@@ -75,7 +77,18 @@ val WidthAuthorPictureModifier = Modifier.width(55.dp)
|
||||
val DiviserThickness = 0.25.dp
|
||||
|
||||
val ReactionRowHeight = Modifier.height(24.dp).padding(start = 10.dp)
|
||||
val ReactionRowHeightChat = Modifier.height(25.dp)
|
||||
val UserNameRowHeight = Modifier.height(22.dp).fillMaxWidth()
|
||||
val UserNameMaxRowHeight = Modifier.heightIn(max = 22.dp).fillMaxWidth()
|
||||
|
||||
val Height4dpModifier = Modifier.height(4.dp)
|
||||
|
||||
val AccountPictureModifier = Modifier.width(55.dp).height(55.dp).clip(shape = CircleShape)
|
||||
|
||||
val ShowMoreRelaysButtonIconButtonModifier = Modifier.size(24.dp)
|
||||
val ShowMoreRelaysButtonIconModifier = Modifier.size(15.dp)
|
||||
val ShowMoreRelaysButtonBoxModifer = Modifier.fillMaxWidth().height(25.dp)
|
||||
|
||||
val ChatBubbleMaxSizeModifier = Modifier.fillMaxWidth(0.85f)
|
||||
|
||||
val ModifierWidth3dp = Modifier.width(3.dp)
|
||||
|
||||
@@ -121,6 +121,26 @@ val LightImageModifier = Modifier
|
||||
QuoteBorder
|
||||
)
|
||||
|
||||
val DarkReplyBorderModifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
DarkSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
|
||||
val LightReplyBorderModifier = Modifier
|
||||
.padding(top = 5.dp)
|
||||
.fillMaxWidth()
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
LightSubtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
|
||||
val MarkDownStyleOnDark = richTextDefaults.copy(
|
||||
paragraphSpacing = DefaultParagraphSpacing,
|
||||
headingStyle = DefaultHeadingStyle,
|
||||
@@ -243,6 +263,9 @@ val Colors.repostProfileBorder: Modifier
|
||||
val Colors.imageModifier: Modifier
|
||||
get() = if (isLight) LightImageModifier else DarkImageModifier
|
||||
|
||||
val Colors.replyModifier: Modifier
|
||||
get() = if (isLight) LightReplyBorderModifier else DarkReplyBorderModifier
|
||||
|
||||
@Composable
|
||||
fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||
val colors = if (darkTheme) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.Amethyst" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/purple_700</item>
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>\
|
||||
</style>
|
||||
</resources>
|
||||
@@ -344,9 +344,106 @@
|
||||
|
||||
<string name="upload_server_relays_nip95">Uw relays (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">Bestanden worden geüpload naar en gehost door relays. Ze zijn vrij van een vaste url (afhankelijkheid van derden). Zorg ervoor dat u een NIP-95 relay in uw lijst met relays hebt.</string>
|
||||
|
||||
|
||||
<string name="connect_via_tor_short">Orbot/Tor setup</string>
|
||||
<string name="connect_via_tor">Verbinden via Orbot setup</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">Verbinding Orbot/Tor verbreken?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">Je gegevens worden onmiddellijk overgezet via het reguliere netwerk</string>
|
||||
<string name="yes">Ja</string>
|
||||
<string name="no">Nee</string>
|
||||
|
||||
<string name="follow_list_selection">Volgerslijst</string>
|
||||
<string name="follow_list_kind3follows">Alle gevolgden</string>
|
||||
<string name="follow_list_kind3follows">Iedereen</string>
|
||||
<string name="follow_list_global">Globaal</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Verbind via Tor met Orbot
|
||||
\n\n1. Installeer [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)
|
||||
\n2. Start Orbot
|
||||
\n3. Controleer in Orbot de Socks-poort. Standaard wordt 9050 gebruikt
|
||||
\n4. Wijzig indien nodig de poort in Orbot
|
||||
\n5. Configureer de Socks-poort in dit scherm
|
||||
\n6. Druk op de knop Activeren om Orbot als proxy te gebruiken
|
||||
</string>
|
||||
<string name="orbot_socks_port">Orbot Socks Poort</string>
|
||||
<string name="invalid_port_number">Ongeldige poort</string>
|
||||
<string name="use_orbot">Gebruik Orbot</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Ontkoppel Orbot/Tor</string>
|
||||
|
||||
<string name="app_notification_channel_id" translatable="false">StandaardKanaalID</string>
|
||||
<string name="app_notification_private_message" translatable="false">Nieuwe notificatie ontvangen</string>
|
||||
|
||||
<string name="app_notification_dms_channel_id" translatable="false">PrivéberichtenID</string>
|
||||
<string name="app_notification_dms_channel_name">Privéberichten</string>
|
||||
<string name="app_notification_dms_channel_description">Geeft een melding wanneer er een privébericht binnenkomt</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_id" translatable="false">ZapsID</string>
|
||||
<string name="app_notification_zaps_channel_name">Zaps ontvangen</string>
|
||||
<string name="app_notification_zaps_channel_description">Geeft een melding als iemand je zapt</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">Van %1$s</string>
|
||||
<string name="app_notification_zaps_channel_message_for">voor %1$s</string>
|
||||
|
||||
<string name="reply_notify">Meld: </string>
|
||||
|
||||
<string name="channel_list_join_conversation">Doe mee aan het gesprek</string>
|
||||
<string name="channel_list_user_or_group_id">Gebruiker of groeps ID</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub, nevent of hex</string>
|
||||
<string name="channel_list_create_channel">Maken</string>
|
||||
<string name="channel_list_join_channel">Deelnemen</string>
|
||||
|
||||
<string name="today">Vandaag</string>
|
||||
|
||||
<string name="content_warning">Content waarschuwing</string>
|
||||
<string name="content_warning_explanation">Dit bericht bevat gevoelige inhoud die sommige mensen beledigend of verontrustend kunnen vinden</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">Verberg altijd gevoelige inhoud</string>
|
||||
<string name="content_warning_show_all_sensitive_content">Toon altijd gevoelige inhoud</string>
|
||||
<string name="content_warning_see_warnings">Toon altijd content waarshuwingen</string>
|
||||
|
||||
<string name="recommended_apps">Raadt aan: </string>
|
||||
<string name="filter_spam_from_strangers">Filter spam van onbekenden</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">Waarschuw wanneer berichten meldingen hebben van je volgers</string>
|
||||
|
||||
<string name="new_reaction_symbol">Nieuwe reactie symbool</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">Geen reactietypes geselecteerd. Lang indrukken om te wijzigen</string>
|
||||
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
<string name="zapraiser_explainer">Voegt een streefaantal sats toe dat moet worden opgehaald voor deze post. Ondersteunende clients kunnen dit als een voortgangsbalk weergeven om donaties te stimuleren.</string>
|
||||
<string name="zapraiser_target_amount_in_sats">Streefaantal in sats</string>
|
||||
|
||||
<string name="sats_to_complete">Zapraiser op %1$s. %2$s sats tot doel</string>
|
||||
<string name="read_from_relay">Lees van relay</string>
|
||||
<string name="write_to_relay">Schrijf naar relay</string>
|
||||
<string name="an_error_ocurred_trying_to_get_relay_information">Er is een fout opgetreden bij het ophalen van de relay-informatie van %1$s</string>
|
||||
<string name="owner">Eigenaar</string>
|
||||
<string name="version">Versie</string>
|
||||
<string name="software">Software</string>
|
||||
<string name="contact">Contact</string>
|
||||
<string name="supports">Ondersteunde NIPs</string>
|
||||
<string name="admission_fees">Toelatingskosten</string>
|
||||
<string name="payments_url">Betaallink</string>
|
||||
<string name="limitations">Beperkingen</string>
|
||||
<string name="countries">Landen</string>
|
||||
<string name="languages">Talen</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="posting_policy">Postbeleid</string>
|
||||
<string name="message_length">Lengte van bericht</string>
|
||||
<string name="subscriptions">Abonnementen</string>
|
||||
<string name="filters">Filters</string>
|
||||
<string name="subscription_id_length">Lengte abonnement-id</string>
|
||||
<string name="minimum_prefix">Minimum prefix</string>
|
||||
<string name="maximum_event_tags">Maximum event tags</string>
|
||||
<string name="content_length">Content lengte</string>
|
||||
<string name="minimum_pow">Minimum PoW</string>
|
||||
<string name="auth">Auth</string>
|
||||
<string name="payment">Betaling</string>
|
||||
|
||||
<string name="live_stream_live_tag">LIVE</string>
|
||||
<string name="live_stream_offline_tag">OFFLINE</string>
|
||||
<string name="live_stream_ended_tag">GEËINDIGD</string>
|
||||
<string name="live_stream_planned_tag">INGEPLAND</string>
|
||||
|
||||
<string name="live_stream_is_offline">Livestream is offline</string>
|
||||
<string name="live_stream_has_ended">Livestream is geëindigd</string>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -456,4 +456,15 @@
|
||||
<string name="minimum_pow">Minimum PoW</string>
|
||||
<string name="auth">Auth</string>
|
||||
<string name="payment">Payment</string>
|
||||
<string name="cashu">Cashu Token</string>
|
||||
<string name="cashu_redeem">Redeem</string>
|
||||
|
||||
<string name="live_stream_live_tag">LIVE</string>
|
||||
<string name="live_stream_offline_tag">OFFLINE</string>
|
||||
<string name="live_stream_ended_tag">ENDED</string>
|
||||
<string name="live_stream_planned_tag">SCHEDULED</string>
|
||||
|
||||
<string name="live_stream_is_offline">Livestream is Offline</string>
|
||||
<string name="live_stream_has_ended">Livestream Ended</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">Logging out deletes all your local information. Make sure to have your private keys backed up to avoid losing your account. Do you want to continue?</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.Amethyst" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/purple_700</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user