Refactoring of Badge Box codes and Time classes

This commit is contained in:
Vitor Pamplona 2023-07-10 13:50:49 -04:00
parent 9a7e678bbe
commit 7ea5be0152
53 changed files with 921 additions and 799 deletions

View File

@ -19,7 +19,6 @@ import java.math.BigDecimal
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.regex.Pattern
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
@ -434,7 +433,7 @@ open class Note(val idHex: String) {
}
fun hasAnyReports(): Boolean {
val dayAgo = Date().time / 1000 - 24 * 60 * 60
val dayAgo = TimeUtils.oneDayAgo()
return reports.isNotEmpty() ||
(
author?.reports?.values?.any {
@ -471,8 +470,7 @@ open class Note(val idHex: String) {
}
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val currentTime = Date().time / 1000
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5) } != null // 5 minute protection
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > TimeUtils.fiveMinutesAgo() } != null // 5 minute protection
}
fun boostedBy(loggedIn: User): List<Note> {

View File

@ -0,0 +1,13 @@
package com.vitorpamplona.amethyst.model
object TimeUtils {
const val fiveMinutes = 60 * 5
const val oneHour = 60 * 60
const val oneDay = 24 * 60 * 60
fun now() = System.currentTimeMillis() / 1000
fun fiveMinutesAgo() = now() - fiveMinutes
fun oneHourAgo() = now() - oneHour
fun oneDayAgo() = now() - oneDay
fun eightHoursAgo() = now() - (oneHour * 8)
}

View File

@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
import android.util.Log
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.relays.Client
@ -12,7 +13,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.util.Date
import java.util.UUID
import kotlin.Error
@ -57,7 +57,7 @@ abstract class NostrDataSource(val debugName: String) {
if (type == Relay.Type.EOSE && channel != null) {
// updates a per subscripton since date
subscriptions[channel]?.updateEOSE(Date().time / 1000, relay.url)
subscriptions[channel]?.updateEOSE(TimeUtils.now(), relay.url)
}
}

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class AudioTrackEvent(
@ -42,7 +42,7 @@ class AudioTrackEvent(
cover: String? = null,
subject: String? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): AudioTrackEvent {
val tags = listOfNotNull(
listOf(MEDIA, media),

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class BookmarkListEvent(
@ -30,7 +30,7 @@ class BookmarkListEvent(
privAddresses: List<ATag>? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): BookmarkListEvent {
val pubKey = Utils.pubkeyCreate(privateKey)
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)

View File

@ -3,9 +3,9 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelCreateEvent(
@ -26,7 +26,7 @@ class ChannelCreateEvent(
companion object {
const val kind = 40
fun create(channelInfo: ChannelData?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelCreateEvent {
fun create(channelInfo: ChannelData?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelCreateEvent {
val content = try {
if (channelInfo != null) {
gson.toJson(channelInfo)

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelHideMessageEvent(
@ -26,7 +26,7 @@ class ChannelHideMessageEvent(
companion object {
const val kind = 43
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent {
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelHideMessageEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags =
messagesToHide?.map {

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelMessageEvent(
@ -34,7 +34,7 @@ class ChannelMessageEvent(
mentions: List<String>? = null,
zapReceiver: String?,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?
): ChannelMessageEvent {

View File

@ -3,9 +3,9 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelMetadataEvent(
@ -29,7 +29,7 @@ class ChannelMetadataEvent(
companion object {
const val kind = 41
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMetadataEvent {
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMetadataEvent {
val content =
if (newChannelInfo != null) {
gson.toJson(newChannelInfo)

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ChannelMuteUserEvent(
@ -26,7 +26,7 @@ class ChannelMuteUserEvent(
companion object {
const val kind = 44
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent {
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMuteUserEvent {
val content = reason
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags =

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class CommunityDefinitionEvent(
@ -30,7 +30,7 @@ class CommunityDefinitionEvent(
fun create(
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): CommunityDefinitionEvent {
val tags = mutableListOf<List<String>>()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()

View File

@ -3,10 +3,10 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
import java.util.Date
@Immutable
class CommunityPostApprovalEvent(
@ -45,7 +45,7 @@ class CommunityPostApprovalEvent(
companion object {
const val kind = 4550
fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = Date().time / 1000): GenericRepostEvent {
fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
val content = approvedPost.toJson()
val communities = listOf("a", community.address().toTag())

View File

@ -5,10 +5,10 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
data class Contact(val pubKeyHex: String, val relayUri: String?)
@ -73,7 +73,7 @@ class ContactListEvent(
companion object {
const val kind = 3
fun create(follows: List<Contact>, followTags: List<String>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ContactListEvent {
fun create(follows: List<Contact>, followTags: List<String>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
val content = if (relayUse != null) {
gson.toJson(relayUse)
} else {

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class DeletionEvent(
@ -20,7 +20,7 @@ class DeletionEvent(
companion object {
const val kind = 5
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): DeletionEvent {
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): DeletionEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = deleteEvents.map { listOf("e", it) }

View File

@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable
import com.google.gson.*
import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import fr.acinq.secp256k1.Hex
import fr.acinq.secp256k1.Secp256k1
@ -314,7 +315,7 @@ open class Event(
return MessageDigest.getInstance("SHA-256").digest(rawEventJson.toByteArray())
}
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = Date().time / 1000): Event {
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now()): Event {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = Companion.generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey).toHexKey()

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class FileHeaderEvent(
@ -52,7 +52,7 @@ class FileHeaderEvent(
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): FileHeaderEvent {
val tags = listOfNotNull(
listOf(URL, url),

View File

@ -3,10 +3,10 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Base64
import java.util.Date
@Immutable
class FileStorageEvent(
@ -44,7 +44,7 @@ class FileStorageEvent(
mimeType: String,
data: ByteArray,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): FileStorageEvent {
val tags = listOfNotNull(
listOf(TYPE, mimeType)

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class FileStorageHeaderEvent(
@ -52,7 +52,7 @@ class FileStorageHeaderEvent(
encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): FileStorageHeaderEvent {
val tags = listOfNotNull(
listOf("e", storageEvent.id),

View File

@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
import java.util.Date
@Immutable
class GenericRepostEvent(
@ -29,7 +29,7 @@ class GenericRepostEvent(
companion object {
const val kind = 16
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): GenericRepostEvent {
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
val content = boostedPost.toJson()
val replyToPost = listOf("e", boostedPost.id())

View File

@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.security.MessageDigest
import java.util.Date
@Immutable
class HTTPAuthorizationEvent(
@ -25,7 +25,7 @@ class HTTPAuthorizationEvent(
method: String,
body: String? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): HTTPAuthorizationEvent {
val sha256 = MessageDigest.getInstance("SHA-256")

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class HighlightEvent(
@ -26,7 +26,7 @@ class HighlightEvent(
fun create(
msg: String,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): PollNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class LiveActivitiesChatMessageEvent(
@ -49,7 +49,7 @@ class LiveActivitiesChatMessageEvent(
mentions: List<String>? = null,
zapReceiver: String?,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean,
zapRaiserAmount: Long?
): LiveActivitiesChatMessageEvent {

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class LiveActivitiesEvent(
@ -32,7 +32,7 @@ class LiveActivitiesEvent(
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
fun checkStatus(eventStatus: String?): String? {
return if (eventStatus == STATUS_LIVE && createdAt < Date().time / 1000 - (60 * 60 * 8)) { // 2 hours {
return if (eventStatus == STATUS_LIVE && createdAt < TimeUtils.eightHoursAgo()) {
STATUS_ENDED
} else {
eventStatus
@ -48,7 +48,7 @@ class LiveActivitiesEvent(
fun create(
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): LiveActivitiesEvent {
val tags = mutableListOf<List<String>>()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()

View File

@ -7,11 +7,11 @@ import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.lang.reflect.Type
import java.util.Date
@Immutable
class LnZapPaymentRequestEvent(
@ -57,7 +57,7 @@ class LnZapPaymentRequestEvent(
lnInvoice: String,
walletServicePubkey: String,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): LnZapPaymentRequestEvent {
val pubKey = Utils.pubkeyCreate(privateKey)
val serializedRequest = gson.toJson(PayInvoiceMethod.create(lnInvoice))

View File

@ -64,7 +64,7 @@ class LnZapRequestEvent(
pollOption: Int?,
message: String,
zapType: LnZapEvent.ZapType,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): LnZapRequestEvent {
var content = message
var privkey = privateKey
@ -104,7 +104,7 @@ class LnZapRequestEvent(
privateKey: ByteArray,
message: String,
zapType: LnZapEvent.ZapType,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): LnZapRequestEvent {
var content = message
var privkey = privateKey

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class LongTextNoteEvent(
@ -32,7 +32,7 @@ class LongTextNoteEvent(
companion object {
const val kind = 30023
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent {
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): LongTextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()
replyTos?.forEach {

View File

@ -7,11 +7,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.gson.Gson
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.io.ByteArrayInputStream
import java.util.Date
@Stable
abstract class IdentityClaim(
@ -171,7 +171,7 @@ class MetadataEvent(
.readerFor(UserMetadata::class.java)
}
fun create(contactMetaData: String, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
fun create(contactMetaData: String, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): MetadataEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()

View File

@ -4,10 +4,10 @@ import android.util.Log
import androidx.compose.runtime.Immutable
import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class MuteListEvent(
@ -71,7 +71,7 @@ class MuteListEvent(
privAddresses: List<ATag>? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): MuteListEvent {
val pubKey = Utils.pubkeyCreate(privateKey)

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class PeopleListEvent(
@ -30,7 +30,7 @@ class PeopleListEvent(
privAddresses: List<ATag>? = null,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): PeopleListEvent {
val pubKey = Utils.pubkeyCreate(privateKey)
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class PinListEvent(
@ -27,7 +27,7 @@ class PinListEvent(
fun create(
pins: List<String>,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): PinListEvent {
val tags = mutableListOf<List<String>>()
pins.forEach {

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
const val POLL_OPTION = "poll_option"
const val VALUE_MAXIMUM = "value_maximum"
@ -45,7 +45,7 @@ class PollNoteEvent(
mentions: List<String>?,
addresses: List<ATag>?,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
createdAt: Long = TimeUtils.now(),
pollOptions: Map<Int, String>,
valueMaximum: Int?,
valueMinimum: Int?,

View File

@ -3,12 +3,12 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
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
import java.util.Date
@Immutable
class PrivateDmEvent(
@ -83,7 +83,7 @@ class PrivateDmEvent(
mentions: List<String>? = null,
zapReceiver: String?,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000,
createdAt: Long = TimeUtils.now(),
publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true,
markAsSensitive: Boolean,

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class ReactionEvent(
@ -22,15 +22,15 @@ class ReactionEvent(
companion object {
const val kind = 7
fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
return create("\u26A0\uFE0F", originalNote, privateKey, createdAt)
}
fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
return create("+", originalNote, privateKey, createdAt)
}
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))

View File

@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.net.URI
import java.util.Date
@Immutable
class RecommendRelayEvent(
@ -27,7 +27,7 @@ class RecommendRelayEvent(
companion object {
const val kind = 2
fun create(relay: URI, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RecommendRelayEvent {
fun create(relay: URI, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RecommendRelayEvent {
val content = relay.toString()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf<List<String>>()

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class RelayAuthEvent(
@ -21,7 +21,7 @@ class RelayAuthEvent(
companion object {
const val kind = 22242
fun create(relay: String, challenge: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RelayAuthEvent {
fun create(relay: String, challenge: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RelayAuthEvent {
val content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf(

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
class RelaySetEvent(
@ -28,7 +28,7 @@ class RelaySetEvent(
fun create(
relays: List<String>,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): RelaySetEvent {
val tags = mutableListOf<List<String>>()
relays.forEach {

View File

@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils
import java.util.Date
@Immutable
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
@ -58,7 +58,7 @@ class ReportEvent(
type: ReportType,
privateKey: ByteArray,
content: String = "",
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): ReportEvent {
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
@ -75,7 +75,7 @@ class ReportEvent(
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReportEvent {
val content = ""
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())

View File

@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils
import java.util.Date
@Immutable
class RepostEvent(
@ -29,7 +29,7 @@ class RepostEvent(
companion object {
const val kind = 6
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent {
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RepostEvent {
val content = boostedPost.toJson()
val replyToPost = listOf("e", boostedPost.id())

View File

@ -4,10 +4,10 @@ import androidx.compose.runtime.Immutable
import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags
import nostr.postr.Utils
import java.util.Date
@Immutable
class TextNoteEvent(
@ -40,7 +40,7 @@ class TextNoteEvent(
directMentions: Set<HexKey>,
privateKey: ByteArray,
createdAt: Long = Date().time / 1000
createdAt: Long = TimeUtils.now()
): TextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>()

View File

@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.relays
import android.util.Log
import com.google.gson.JsonElement
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.EventInterface
@ -14,7 +15,6 @@ import okhttp3.WebSocket
import okhttp3.WebSocketListener
import java.net.Proxy
import java.time.Duration
import java.util.Date
enum class FeedType {
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH, WALLET_CONNECT
@ -179,7 +179,7 @@ class Relay(
socket = null
isReady = false
afterEOSE = false
closingTime = Date().time / 1000
closingTime = TimeUtils.now()
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) }
}
@ -193,7 +193,7 @@ class Relay(
socket = null
isReady = false
afterEOSE = false
closingTime = Date().time / 1000
closingTime = TimeUtils.now()
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response")
t.printStackTrace()
@ -208,7 +208,7 @@ class Relay(
errorCounter++
isReady = false
afterEOSE = false
closingTime = Date().time / 1000
closingTime = TimeUtils.now()
Log.e("Relay", "Relay Invalid $url")
e.printStackTrace()
}
@ -216,7 +216,7 @@ class Relay(
fun disconnect() {
// httpClient.dispatcher.executorService.shutdown()
closingTime = Date().time / 1000
closingTime = TimeUtils.now()
socket?.close(1000, "Normal close")
socket = null
isReady = false
@ -241,7 +241,7 @@ class Relay(
}
} else {
// waits 60 seconds to reconnect after disconnected.
if (Date().time / 1000 > closingTime + 60) {
if (TimeUtils.now() > closingTime + 60) {
// sends all filters after connection is successful.
requestAndWatch()
}
@ -254,7 +254,7 @@ class Relay(
if (socket == null) {
// waits 60 seconds to reconnect after disconnected.
if (Date().time / 1000 > closingTime + 60) {
if (TimeUtils.now() > closingTime + 60) {
// println("sendfilter Only if Disconnected ${url} ")
requestAndWatch()
}

View File

@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.*
open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
@ -25,7 +26,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
}
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = System.currentTimeMillis() / 1000
val now = TimeUtils.now()
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()

View File

@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.*
open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
@ -25,7 +26,7 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
}
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = System.currentTimeMillis() / 1000
val now = TimeUtils.now()
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()

View File

@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_ENDED
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
@ -30,7 +31,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
}
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = System.currentTimeMillis() / 1000
val now = TimeUtils.now()
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val followingKeySet =

View File

@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
@ -29,7 +30,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
val now = System.currentTimeMillis() / 1000
val now = TimeUtils.now()
return collection
.asSequence()

View File

@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
import com.vitorpamplona.amethyst.service.model.HighlightEvent
@ -11,7 +12,6 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import java.util.Date
class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String {
@ -35,7 +35,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
val oneMinuteInTheFuture = Date().time / 1000 + (1 * 60) // one minute in the future.
val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future.
val oneHr = 60 * 60
return collection

View File

@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.*
class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
@ -22,7 +23,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
}
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = System.currentTimeMillis() / 1000
val now = TimeUtils.now()
val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()

View File

@ -5,29 +5,19 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
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.LaunchedEffect
import androidx.compose.runtime.MutableState
@ -36,7 +26,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -48,26 +37,21 @@ import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
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.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
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.actions.loadRelayInfo
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
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
@ -76,13 +60,9 @@ 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.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
@ -567,7 +547,7 @@ private fun StatusRow(
Column(modifier = ReactionRowHeightChat) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
ChatTimeAgo(baseNote)
RelayBadges(baseNote, accountViewModel, nav = nav)
RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav)
Spacer(modifier = DoubleHorzSpacer)
}
}
@ -806,131 +786,3 @@ private fun DisplayMessageUsername(
Spacer(modifier = StdHorzSpacer)
DrawPlayName(userDisplayName)
}
@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
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)
FlowRow(StdStartPadding) {
val relaysToDisplay = remember(noteRelays, expanded.value) {
if (expanded.value) noteRelays else noteRelays.take(3)
}
relaysToDisplay.forEach {
RenderRelay(it, accountViewModel, nav)
}
}
}
@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,
tint = MaterialTheme.colors.placeholderText
)
}
}
@Composable
fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val iconUrl by remember(dirtyUrl) {
derivedStateOf {
val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/")
"https://$cleanUrl/favicon.ico"
}
}
var relayInfo: RelayInformation? by remember { mutableStateOf(null) }
if (relayInfo != null) {
RelayInformationDialog(
onClose = {
relayInfo = null
},
relayInfo = relayInfo!!,
accountViewModel,
nav
)
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = Size15dp)
val clickableModifier = remember(dirtyUrl) {
Modifier
.padding(1.dp)
.size(Size15dp)
.clickable(
role = Role.Button,
interactionSource = interactionSource,
indication = ripple,
onClick = {
loadRelayInfo(dirtyUrl, context, scope) {
relayInfo = it
}
}
)
}
Box(
modifier = clickableModifier
) {
RenderRelayIcon(iconUrl)
}
}
@Composable
private fun RenderRelayIcon(iconUrl: String) {
val backgroundColor = MaterialTheme.colors.background
val iconModifier = remember {
Modifier
.size(Size13dp)
.clip(shape = CircleShape)
.background(backgroundColor)
}
RobohashFallbackAsyncImage(
robot = iconUrl,
model = iconUrl,
contentDescription = stringResource(id = R.string.relay_icon),
colorFilter = RelayIconFilter,
modifier = iconModifier
)
}

View File

@ -32,6 +32,7 @@ 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.TimeUtils
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.service.Nip05Verifier
@ -39,14 +40,13 @@ import com.vitorpamplona.amethyst.ui.theme.Nip05
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.Date
@Composable
fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
val nip05Verified = remember(user.nip05) {
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
val nip05Verified = remember(userMetadata.nip05) {
// starts with null if must verify or already filled in if verified in the last hour
val default = if ((user.nip05LastVerificationTime ?: 0) > (Date().time / 1000 - 60 * 60)) { // 1hour
user.nip05Verified
val default = if ((userMetadata.nip05LastVerificationTime ?: 0) > TimeUtils.oneHourAgo()) {
userMetadata.nip05Verified
} else {
null
}
@ -55,23 +55,23 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableSta
}
if (nip05Verified.value == null) {
LaunchedEffect(key1 = user.nip05) {
LaunchedEffect(key1 = userMetadata.nip05) {
launch(Dispatchers.IO) {
user.nip05?.ifBlank { null }?.let { nip05 ->
userMetadata.nip05?.ifBlank { null }?.let { nip05 ->
Nip05Verifier().verifyNip05(
nip05,
onSuccess = {
// Marks user as verified
if (it == pubkeyHex) {
user.nip05Verified = true
user.nip05LastVerificationTime = Date().time / 1000
userMetadata.nip05Verified = true
userMetadata.nip05LastVerificationTime = TimeUtils.now()
if (nip05Verified.value != true) {
nip05Verified.value = true
}
} else {
user.nip05Verified = false
user.nip05LastVerificationTime = 0
userMetadata.nip05Verified = false
userMetadata.nip05LastVerificationTime = 0
if (nip05Verified.value != false) {
nip05Verified.value = false
@ -79,8 +79,8 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableSta
}
},
onError = {
user.nip05LastVerificationTime = 0
user.nip05Verified = false
userMetadata.nip05LastVerificationTime = 0
userMetadata.nip05Verified = false
if (nip05Verified.value != false) {
nip05Verified.value = false

View File

@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.note
import android.content.Intent
import android.graphics.Bitmap
import android.util.Log
import androidx.compose.animation.Crossfade
@ -10,7 +9,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -28,8 +26,6 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle
@ -38,12 +34,10 @@ import androidx.compose.material.Text
import androidx.compose.material.darkColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Link
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.lightColors
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
@ -58,7 +52,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
@ -71,15 +64,12 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.get
import androidx.lifecycle.distinctUntilChanged
@ -133,7 +123,6 @@ import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
@ -154,7 +143,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@ -165,9 +153,6 @@ import com.vitorpamplona.amethyst.ui.theme.HalfPadding
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconButtonModifier
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconModifier
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
import com.vitorpamplona.amethyst.ui.theme.Size25dp
@ -3317,529 +3302,3 @@ fun CreateImageHeader(
}
}
}
@Composable
private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var expanded by remember { mutableStateOf(false) }
var showShowMore by remember { mutableStateOf(false) }
var lazyRelayList by remember {
val baseNumber = baseNote.relays.map {
it.removePrefix("wss://").removePrefix("ws://")
}.toImmutableList()
mutableStateOf(baseNumber)
}
var shortRelayList by remember {
mutableStateOf(lazyRelayList.take(3).toImmutableList())
}
val scope = rememberCoroutineScope()
WatchRelayLists(baseNote) { relayList ->
if (!equalImmutableLists(relayList, lazyRelayList)) {
scope.launch(Dispatchers.Main) {
lazyRelayList = relayList
shortRelayList = relayList.take(3).toImmutableList()
}
}
val nextShowMore = relayList.size > 3
if (nextShowMore != showShowMore) {
scope.launch(Dispatchers.Main) {
// only triggers recomposition when actually different
showShowMore = nextShowMore
}
}
}
Spacer(DoubleVertSpacer)
if (expanded) {
VerticalRelayPanelWithFlow(lazyRelayList, accountViewModel, nav)
} else {
VerticalRelayPanelWithFlow(shortRelayList, accountViewModel, nav)
}
if (showShowMore && !expanded) {
ShowMoreRelaysButton {
expanded = true
}
}
}
@Composable
private fun WatchRelayLists(baseNote: Note, onListChanges: (ImmutableList<String>) -> Unit) {
val noteRelaysState by baseNote.live().relays.observeAsState()
LaunchedEffect(key1 = noteRelaysState) {
launch(Dispatchers.IO) {
val relayList = noteRelaysState?.note?.relays?.map {
it.removePrefix("wss://").removePrefix("ws://")
} ?: emptyList()
onListChanges(relayList.toImmutableList())
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
@Stable
private fun VerticalRelayPanelWithFlow(
relays: ImmutableList<String>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
// FlowRow Seems to be a lot faster than LazyVerticalGrid
FlowRow() {
relays.forEach { url ->
RenderRelay(url, accountViewModel, nav)
}
}
}
@Composable
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
Row(
modifier = ShowMoreRelaysButtonBoxModifer,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
) {
IconButton(
modifier = ShowMoreRelaysButtonIconButtonModifier,
onClick = onClick
) {
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = ShowMoreRelaysButtonIconModifier,
tint = MaterialTheme.colors.placeholderText
)
}
}
}
@Composable
fun NoteAuthorPicture(
baseNote: Note,
nav: (String) -> Unit,
accountViewModel: AccountViewModel,
size: Dp,
pictureModifier: Modifier = Modifier
) {
NoteAuthorPicture(baseNote, size, accountViewModel, pictureModifier) {
nav("User/${it.pubkeyHex}")
}
}
@Composable
fun NoteAuthorPicture(
baseNote: Note,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null
) {
val author by baseNote.live().metadata.map {
it.note.author
}.distinctUntilChanged().observeAsState(baseNote.author)
Crossfade(targetState = author) {
if (it == null) {
DisplayBlankAuthor(size, modifier)
} else {
ClickableUserPicture(it, size, accountViewModel, modifier, onClick)
}
}
}
@Composable
fun DisplayBlankAuthor(size: Dp, modifier: Modifier = Modifier) {
val backgroundColor = MaterialTheme.colors.background
val nullModifier = remember {
modifier
.size(size)
.clip(shape = CircleShape)
.background(backgroundColor)
}
RobohashAsyncImage(
robot = "authornotfound",
contentDescription = stringResource(R.string.unknown_author),
modifier = nullModifier
)
}
@Composable
fun UserPicture(
user: User,
size: Dp,
pictureModifier: Modifier = remember { Modifier },
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val route by remember {
derivedStateOf {
"User/${user.pubkeyHex}"
}
}
val scope = rememberCoroutineScope()
ClickableUserPicture(
baseUser = user,
size = size,
accountViewModel = accountViewModel,
modifier = pictureModifier,
onClick = {
scope.launch {
nav(route)
}
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ClickableUserPicture(
baseUser: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = remember { Modifier },
onClick: ((User) -> Unit)? = null,
onLongClick: ((User) -> Unit)? = null
) {
val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = size)
// BaseUser is the same reference as accountState.user
val myModifier = remember {
if (onClick != null && onLongClick != null) {
Modifier
.size(size)
.combinedClickable(
onClick = { onClick(baseUser) },
onLongClick = { onLongClick(baseUser) },
role = Role.Button,
interactionSource = interactionSource,
indication = ripple
)
} else if (onClick != null) {
Modifier
.size(size)
.clickable(
onClick = { onClick(baseUser) },
role = Role.Button,
interactionSource = interactionSource,
indication = ripple
)
} else {
Modifier.size(size)
}
}
Box(modifier = myModifier, contentAlignment = TopEnd) {
BaseUserPicture(baseUser, size, accountViewModel, modifier)
}
}
@Composable
fun NonClickableUserPicture(
baseUser: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = remember { Modifier }
) {
val myBoxModifier = remember {
Modifier.size(size)
}
Box(myBoxModifier, contentAlignment = TopEnd) {
BaseUserPicture(baseUser, size, accountViewModel, modifier)
}
}
@Composable
fun BaseUserPicture(
baseUser: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = remember { Modifier }
) {
val userPubkey = remember {
baseUser.pubkeyHex
}
val userProfile by baseUser.live().metadata.map {
it.user.profilePicture()
}.distinctUntilChanged().observeAsState(baseUser.profilePicture())
val myBoxModifier = remember {
Modifier.size(size)
}
Box(myBoxModifier, contentAlignment = TopEnd) {
PictureAndFollowingMark(
userHex = userPubkey,
userPicture = userProfile,
size = size,
modifier = modifier,
accountViewModel = accountViewModel
)
}
}
@Composable
fun PictureAndFollowingMark(
userHex: String,
userPicture: String?,
size: Dp,
modifier: Modifier,
accountViewModel: AccountViewModel
) {
val backgroundColor = MaterialTheme.colors.background
val myImageModifier = remember {
modifier
.size(size)
.clip(shape = CircleShape)
.background(backgroundColor)
}
RobohashAsyncImageProxy(
robot = userHex,
model = userPicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = myImageModifier,
contentScale = ContentScale.Crop
)
val myIconSize by remember(size) {
derivedStateOf {
size.div(3.5f)
}
}
ObserveAndDisplayFollowingMark(userHex, myIconSize, accountViewModel)
}
@Composable
fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
WatchFollows(userHex, accountViewModel) {
Crossfade(targetState = it) {
if (it) {
Box(contentAlignment = TopEnd) {
FollowingIcon(iconSize)
}
}
}
}
}
@Composable
fun WatchFollows(userHex: String, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) {
val showFollowingMark by accountViewModel.userFollows.map {
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
}.distinctUntilChanged().observeAsState(
accountViewModel.account.userProfile().isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
)
onFollowChanges(showFollowingMark)
}
@Composable
fun FollowingIcon(iconSize: Dp) {
val modifier = remember {
Modifier.size(iconSize)
}
Icon(
painter = painterResource(R.drawable.verified_follow_shield),
contentDescription = stringResource(id = R.string.following),
modifier = modifier,
tint = Color.Unspecified
)
}
@Immutable
data class DropDownParams(
val isFollowingAuthor: Boolean,
val isPrivateBookmarkNote: Boolean,
val isPublicBookmarkNote: Boolean,
val isLoggedUser: Boolean,
val isSensitive: Boolean,
val showSensitiveContent: Boolean?
)
@Composable
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
var reportDialogShowing by remember { mutableStateOf(false) }
var state by remember {
mutableStateOf<DropDownParams>(
DropDownParams(
isFollowingAuthor = false,
isPrivateBookmarkNote = false,
isPublicBookmarkNote = false,
isLoggedUser = false,
isSensitive = false,
showSensitiveContent = null
)
)
}
DropdownMenu(
expanded = popupExpanded,
onDismissRequest = onDismiss
) {
val clipboardManager = LocalClipboardManager.current
val appContext = LocalContext.current.applicationContext
val actContext = LocalContext.current
WatchBookmarksFollowsAndAccount(note, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
val scope = rememberCoroutineScope()
if (!state.isFollowingAuthor) {
DropdownMenuItem(onClick = {
accountViewModel.follow(
note.author ?: return@DropdownMenuItem
); onDismiss()
}) {
Text(stringResource(R.string.follow))
}
Divider()
}
DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: ""))
onDismiss()
}
}
) {
Text(stringResource(R.string.copy_text))
}
DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
onDismiss()
}
}
) {
Text(stringResource(R.string.copy_user_pubkey))
}
DropdownMenuItem(onClick = {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent()))
onDismiss()
}
}) {
Text(stringResource(R.string.copy_note_id))
}
DropdownMenuItem(onClick = {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
externalLinkForNote(note)
)
putExtra(Intent.EXTRA_TITLE, actContext.getString(R.string.quick_action_share_browser_link))
}
val shareIntent = Intent.createChooser(sendIntent, appContext.getString(R.string.quick_action_share))
ContextCompat.startActivity(actContext, shareIntent, null)
onDismiss()
}) {
Text(stringResource(R.string.quick_action_share))
}
Divider()
if (state.isPrivateBookmarkNote) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePrivateBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.remove_from_private_bookmarks))
}
} else {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPrivateBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.add_to_private_bookmarks))
}
}
if (state.isPublicBookmarkNote) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePublicBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.remove_from_public_bookmarks))
}
} else {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPublicBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.add_to_public_bookmarks))
}
}
Divider()
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.broadcast(note); onDismiss() } }) {
Text(stringResource(R.string.broadcast))
}
Divider()
if (state.isLoggedUser) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.delete(note); onDismiss() } }) {
Text(stringResource(R.string.request_deletion))
}
} else {
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
Text("Block / Report")
}
}
Divider()
if (state.showSensitiveContent == null || state.showSensitiveContent == true) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.hideSensitiveContent(); onDismiss() } }) {
Text(stringResource(R.string.content_warning_hide_all_sensitive_content))
}
}
if (state.showSensitiveContent == null || state.showSensitiveContent == false) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.disableContentWarnings(); onDismiss() } }) {
Text(stringResource(R.string.content_warning_show_all_sensitive_content))
}
}
if (state.showSensitiveContent != null) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.seeContentWarnings(); onDismiss() } }) {
Text(stringResource(R.string.content_warning_see_warnings))
}
}
}
if (reportDialogShowing) {
ReportNoteDialog(note = note, accountViewModel = accountViewModel) {
reportDialogShowing = false
onDismiss()
}
}
}
@Composable
fun WatchBookmarksFollowsAndAccount(note: Note, accountViewModel: AccountViewModel, onNew: (DropDownParams) -> Unit) {
val followState by accountViewModel.userProfile().live().follows.observeAsState()
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
val accountState by accountViewModel.accountLiveData.observeAsState()
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = accountState) {
launch(Dispatchers.IO) {
val newState = DropDownParams(
isFollowingAuthor = accountViewModel.isFollowing(note.author),
isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
isLoggedUser = accountViewModel.isLoggedUser(note.author),
isSensitive = note.event?.isSensitive() ?: false,
showSensitiveContent = accountState?.account?.showSensitiveContent
)
launch(Dispatchers.Main) {
onNew(
newState
)
}
}
}
}

View File

@ -7,6 +7,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.*
import kotlinx.coroutines.flow.MutableStateFlow
@ -86,7 +87,7 @@ class PollNoteViewModel : ViewModel() {
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
fun isPollClosed(): Boolean = closedAt?.let { // allow 2 minute leeway for zap to propagate
pollNote?.createdAt()?.plus(it * (86400 + 120))!! < Date().time / 1000
pollNote?.createdAt()?.plus(it * (86400 + 120))!! < TimeUtils.now()
} == true
fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) {

View File

@ -0,0 +1,136 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconButtonModifier
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
public fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var expanded by remember { mutableStateOf(false) }
var showShowMore by remember { mutableStateOf(false) }
var lazyRelayList by remember {
val baseNumber = baseNote.relays.map {
it.removePrefix("wss://").removePrefix("ws://")
}.toImmutableList()
mutableStateOf(baseNumber)
}
var shortRelayList by remember {
mutableStateOf(lazyRelayList.take(3).toImmutableList())
}
val scope = rememberCoroutineScope()
WatchRelayLists(baseNote) { relayList ->
if (!equalImmutableLists(relayList, lazyRelayList)) {
scope.launch(Dispatchers.Main) {
lazyRelayList = relayList
shortRelayList = relayList.take(3).toImmutableList()
}
}
val nextShowMore = relayList.size > 3
if (nextShowMore != showShowMore) {
scope.launch(Dispatchers.Main) {
// only triggers recomposition when actually different
showShowMore = nextShowMore
}
}
}
Spacer(DoubleVertSpacer)
if (expanded) {
VerticalRelayPanelWithFlow(lazyRelayList, accountViewModel, nav)
} else {
VerticalRelayPanelWithFlow(shortRelayList, accountViewModel, nav)
}
if (showShowMore && !expanded) {
ShowMoreRelaysButton {
expanded = true
}
}
}
@Composable
private fun WatchRelayLists(baseNote: Note, onListChanges: (ImmutableList<String>) -> Unit) {
val noteRelaysState by baseNote.live().relays.observeAsState()
LaunchedEffect(key1 = noteRelaysState) {
launch(Dispatchers.IO) {
val relayList = noteRelaysState?.note?.relays?.map {
it.removePrefix("wss://").removePrefix("ws://")
} ?: emptyList()
onListChanges(relayList.toImmutableList())
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
@Stable
private fun VerticalRelayPanelWithFlow(
relays: ImmutableList<String>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
// FlowRow Seems to be a lot faster than LazyVerticalGrid
FlowRow() {
relays.forEach { url ->
RenderRelay(url, accountViewModel, nav)
}
}
}
@Composable
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
Row(
modifier = ShowMoreRelaysButtonBoxModifer,
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.Top
) {
IconButton(
modifier = ShowMoreRelaysButtonIconButtonModifier,
onClick = onClick
) {
Icon(
imageVector = Icons.Default.ExpandMore,
null,
modifier = ShowMoreRelaysButtonIconModifier,
tint = MaterialTheme.colors.placeholderText
)
}
}
}

View File

@ -0,0 +1,174 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
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.MutableState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
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.RelayInformation
import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.actions.loadRelayInfo
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
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.StdStartPadding
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
public fun RelayBadgesHorizontal(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
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)
FlowRow(StdStartPadding) {
val relaysToDisplay = remember(noteRelays, expanded.value) {
if (expanded.value) noteRelays else noteRelays.take(3)
}
relaysToDisplay.forEach {
RenderRelay(it, accountViewModel, nav)
}
}
}
@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,
tint = MaterialTheme.colors.placeholderText
)
}
}
@Composable
fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val iconUrl by remember(dirtyUrl) {
derivedStateOf {
val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/")
"https://$cleanUrl/favicon.ico"
}
}
var relayInfo: RelayInformation? by remember { mutableStateOf(null) }
if (relayInfo != null) {
RelayInformationDialog(
onClose = {
relayInfo = null
},
relayInfo = relayInfo!!,
accountViewModel,
nav
)
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = Size15dp)
val clickableModifier = remember(dirtyUrl) {
Modifier
.padding(1.dp)
.size(Size15dp)
.clickable(
role = Role.Button,
interactionSource = interactionSource,
indication = ripple,
onClick = {
loadRelayInfo(dirtyUrl, context, scope) {
relayInfo = it
}
}
)
}
Box(
modifier = clickableModifier
) {
RenderRelayIcon(iconUrl)
}
}
@Composable
private fun RenderRelayIcon(iconUrl: String) {
val backgroundColor = MaterialTheme.colors.background
val iconModifier = remember {
Modifier
.size(Size13dp)
.clip(shape = CircleShape)
.background(backgroundColor)
}
RobohashFallbackAsyncImage(
robot = iconUrl,
model = iconUrl,
contentDescription = stringResource(id = R.string.relay_icon),
colorFilter = RelayIconFilter,
modifier = iconModifier
)
}

View File

@ -0,0 +1,483 @@
package com.vitorpamplona.amethyst.ui.note
import android.content.Intent
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
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.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.Dp
import androidx.core.content.ContextCompat
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun NoteAuthorPicture(
baseNote: Note,
nav: (String) -> Unit,
accountViewModel: AccountViewModel,
size: Dp,
pictureModifier: Modifier = Modifier
) {
NoteAuthorPicture(baseNote, size, accountViewModel, pictureModifier) {
nav("User/${it.pubkeyHex}")
}
}
@Composable
fun NoteAuthorPicture(
baseNote: Note,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = Modifier,
onClick: ((User) -> Unit)? = null
) {
val author by baseNote.live().metadata.map {
it.note.author
}.distinctUntilChanged().observeAsState(baseNote.author)
Crossfade(targetState = author) {
if (it == null) {
DisplayBlankAuthor(size, modifier)
} else {
ClickableUserPicture(it, size, accountViewModel, modifier, onClick)
}
}
}
@Composable
fun DisplayBlankAuthor(size: Dp, modifier: Modifier = Modifier) {
val backgroundColor = MaterialTheme.colors.background
val nullModifier = remember {
modifier
.size(size)
.clip(shape = CircleShape)
.background(backgroundColor)
}
RobohashAsyncImage(
robot = "authornotfound",
contentDescription = stringResource(R.string.unknown_author),
modifier = nullModifier
)
}
@Composable
fun UserPicture(
user: User,
size: Dp,
pictureModifier: Modifier = remember { Modifier },
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val route by remember {
derivedStateOf {
"User/${user.pubkeyHex}"
}
}
val scope = rememberCoroutineScope()
ClickableUserPicture(
baseUser = user,
size = size,
accountViewModel = accountViewModel,
modifier = pictureModifier,
onClick = {
scope.launch {
nav(route)
}
}
)
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ClickableUserPicture(
baseUser: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = remember { Modifier },
onClick: ((User) -> Unit)? = null,
onLongClick: ((User) -> Unit)? = null
) {
val interactionSource = remember { MutableInteractionSource() }
val ripple = rememberRipple(bounded = false, radius = size)
// BaseUser is the same reference as accountState.user
val myModifier = remember {
if (onClick != null && onLongClick != null) {
Modifier
.size(size)
.combinedClickable(
onClick = { onClick(baseUser) },
onLongClick = { onLongClick(baseUser) },
role = Role.Button,
interactionSource = interactionSource,
indication = ripple
)
} else if (onClick != null) {
Modifier
.size(size)
.clickable(
onClick = { onClick(baseUser) },
role = Role.Button,
interactionSource = interactionSource,
indication = ripple
)
} else {
Modifier.size(size)
}
}
Box(modifier = myModifier, contentAlignment = Alignment.TopEnd) {
BaseUserPicture(baseUser, size, accountViewModel, modifier)
}
}
@Composable
fun NonClickableUserPicture(
baseUser: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = remember { Modifier }
) {
val myBoxModifier = remember {
Modifier.size(size)
}
Box(myBoxModifier, contentAlignment = Alignment.TopEnd) {
BaseUserPicture(baseUser, size, accountViewModel, modifier)
}
}
@Composable
fun BaseUserPicture(
baseUser: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier = remember { Modifier }
) {
val myBoxModifier = remember {
Modifier.size(size)
}
Box(myBoxModifier, contentAlignment = Alignment.TopEnd) {
InnerBaseUserPicture(baseUser, size, accountViewModel, modifier)
}
}
@Composable
fun InnerBaseUserPicture(
baseUser: User,
size: Dp,
accountViewModel: AccountViewModel,
modifier: Modifier
) {
val userProfile by baseUser.live().metadata.map {
it.user.profilePicture()
}.distinctUntilChanged().observeAsState(baseUser.profilePicture())
PictureAndFollowingMark(
userHex = baseUser.pubkeyHex,
userPicture = userProfile,
size = size,
modifier = modifier,
accountViewModel = accountViewModel
)
}
@Composable
fun PictureAndFollowingMark(
userHex: String,
userPicture: String?,
size: Dp,
modifier: Modifier,
accountViewModel: AccountViewModel
) {
val backgroundColor = MaterialTheme.colors.background
val myImageModifier = remember {
modifier
.size(size)
.clip(shape = CircleShape)
.background(backgroundColor)
}
RobohashAsyncImageProxy(
robot = userHex,
model = userPicture,
contentDescription = stringResource(id = R.string.profile_image),
modifier = myImageModifier,
contentScale = ContentScale.Crop
)
val myIconSize by remember(size) {
derivedStateOf {
size.div(3.5f)
}
}
ObserveAndDisplayFollowingMark(userHex, myIconSize, accountViewModel)
}
@Composable
fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
WatchFollows(userHex, accountViewModel) { newFollowingState ->
Crossfade(targetState = newFollowingState) { following ->
if (following) {
Box(contentAlignment = Alignment.TopEnd) {
FollowingIcon(iconSize)
}
}
}
}
}
@Composable
fun WatchFollows(userHex: String, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) {
val showFollowingMark by accountViewModel.userFollows.map {
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
}.distinctUntilChanged().observeAsState(
accountViewModel.account.userProfile().isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
)
onFollowChanges(showFollowingMark)
}
@Composable
fun FollowingIcon(iconSize: Dp) {
val modifier = remember {
Modifier.size(iconSize)
}
Icon(
painter = painterResource(R.drawable.verified_follow_shield),
contentDescription = stringResource(id = R.string.following),
modifier = modifier,
tint = Color.Unspecified
)
}
@Immutable
data class DropDownParams(
val isFollowingAuthor: Boolean,
val isPrivateBookmarkNote: Boolean,
val isPublicBookmarkNote: Boolean,
val isLoggedUser: Boolean,
val isSensitive: Boolean,
val showSensitiveContent: Boolean?
)
@Composable
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
var reportDialogShowing by remember { mutableStateOf(false) }
var state by remember {
mutableStateOf<DropDownParams>(
DropDownParams(
isFollowingAuthor = false,
isPrivateBookmarkNote = false,
isPublicBookmarkNote = false,
isLoggedUser = false,
isSensitive = false,
showSensitiveContent = null
)
)
}
DropdownMenu(
expanded = popupExpanded,
onDismissRequest = onDismiss
) {
val clipboardManager = LocalClipboardManager.current
val appContext = LocalContext.current.applicationContext
val actContext = LocalContext.current
WatchBookmarksFollowsAndAccount(note, accountViewModel) { newState ->
if (state != newState) {
state = newState
}
}
val scope = rememberCoroutineScope()
if (!state.isFollowingAuthor) {
DropdownMenuItem(onClick = {
accountViewModel.follow(
note.author ?: return@DropdownMenuItem
); onDismiss()
}) {
Text(stringResource(R.string.follow))
}
Divider()
}
DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: ""))
onDismiss()
}
}
) {
Text(stringResource(R.string.copy_text))
}
DropdownMenuItem(
onClick = {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
onDismiss()
}
}
) {
Text(stringResource(R.string.copy_user_pubkey))
}
DropdownMenuItem(onClick = {
scope.launch(Dispatchers.IO) {
clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent()))
onDismiss()
}
}) {
Text(stringResource(R.string.copy_note_id))
}
DropdownMenuItem(onClick = {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
externalLinkForNote(note)
)
putExtra(Intent.EXTRA_TITLE, actContext.getString(R.string.quick_action_share_browser_link))
}
val shareIntent = Intent.createChooser(sendIntent, appContext.getString(R.string.quick_action_share))
ContextCompat.startActivity(actContext, shareIntent, null)
onDismiss()
}) {
Text(stringResource(R.string.quick_action_share))
}
Divider()
if (state.isPrivateBookmarkNote) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePrivateBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.remove_from_private_bookmarks))
}
} else {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPrivateBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.add_to_private_bookmarks))
}
}
if (state.isPublicBookmarkNote) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePublicBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.remove_from_public_bookmarks))
}
} else {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPublicBookmark(note); onDismiss() } }) {
Text(stringResource(R.string.add_to_public_bookmarks))
}
}
Divider()
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.broadcast(note); onDismiss() } }) {
Text(stringResource(R.string.broadcast))
}
Divider()
if (state.isLoggedUser) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.delete(note); onDismiss() } }) {
Text(stringResource(R.string.request_deletion))
}
} else {
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
Text("Block / Report")
}
}
Divider()
if (state.showSensitiveContent == null || state.showSensitiveContent == true) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.hideSensitiveContent(); onDismiss() } }) {
Text(stringResource(R.string.content_warning_hide_all_sensitive_content))
}
}
if (state.showSensitiveContent == null || state.showSensitiveContent == false) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.disableContentWarnings(); onDismiss() } }) {
Text(stringResource(R.string.content_warning_show_all_sensitive_content))
}
}
if (state.showSensitiveContent != null) {
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.seeContentWarnings(); onDismiss() } }) {
Text(stringResource(R.string.content_warning_see_warnings))
}
}
}
if (reportDialogShowing) {
ReportNoteDialog(note = note, accountViewModel = accountViewModel) {
reportDialogShowing = false
onDismiss()
}
}
}
@Composable
fun WatchBookmarksFollowsAndAccount(note: Note, accountViewModel: AccountViewModel, onNew: (DropDownParams) -> Unit) {
val followState by accountViewModel.userProfile().live().follows.observeAsState()
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
val accountState by accountViewModel.accountLiveData.observeAsState()
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = accountState) {
launch(Dispatchers.IO) {
val newState = DropDownParams(
isFollowingAuthor = accountViewModel.isFollowing(note.author),
isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
isLoggedUser = accountViewModel.isLoggedUser(note.author),
isSensitive = note.event?.isSensitive() ?: false,
showSensitiveContent = accountState?.account?.showSensitiveContent
)
launch(Dispatchers.Main) {
onNew(
newState
)
}
}
}
}