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
@@ -19,7 +19,6 @@ import java.math.BigDecimal
import java.time.Instant import java.time.Instant
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.Date
import java.util.regex.Pattern import java.util.regex.Pattern
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]") val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
@@ -434,7 +433,7 @@ open class Note(val idHex: String) {
} }
fun hasAnyReports(): Boolean { fun hasAnyReports(): Boolean {
val dayAgo = Date().time / 1000 - 24 * 60 * 60 val dayAgo = TimeUtils.oneDayAgo()
return reports.isNotEmpty() || return reports.isNotEmpty() ||
( (
author?.reports?.values?.any { author?.reports?.values?.any {
@@ -471,8 +470,7 @@ open class Note(val idHex: String) {
} }
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean { fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
val currentTime = Date().time / 1000 return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > TimeUtils.fiveMinutesAgo() } != null // 5 minute protection
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5) } != null // 5 minute protection
} }
fun boostedBy(loggedIn: User): List<Note> { fun boostedBy(loggedIn: User): List<Note> {
@@ -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)
}
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
import android.util.Log import android.util.Log
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
@@ -12,7 +13,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Date
import java.util.UUID import java.util.UUID
import kotlin.Error import kotlin.Error
@@ -57,7 +57,7 @@ abstract class NostrDataSource(val debugName: String) {
if (type == Relay.Type.EOSE && channel != null) { if (type == Relay.Type.EOSE && channel != null) {
// updates a per subscripton since date // updates a per subscripton since date
subscriptions[channel]?.updateEOSE(Date().time / 1000, relay.url) subscriptions[channel]?.updateEOSE(TimeUtils.now(), relay.url)
} }
} }
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class AudioTrackEvent( class AudioTrackEvent(
@@ -42,7 +42,7 @@ class AudioTrackEvent(
cover: String? = null, cover: String? = null,
subject: String? = null, subject: String? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): AudioTrackEvent { ): AudioTrackEvent {
val tags = listOfNotNull( val tags = listOfNotNull(
listOf(MEDIA, media), listOf(MEDIA, media),
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class BookmarkListEvent( class BookmarkListEvent(
@@ -30,7 +30,7 @@ class BookmarkListEvent(
privAddresses: List<ATag>? = null, privAddresses: List<ATag>? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): BookmarkListEvent { ): BookmarkListEvent {
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey)
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey) val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
@@ -3,9 +3,9 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class ChannelCreateEvent( class ChannelCreateEvent(
@@ -26,7 +26,7 @@ class ChannelCreateEvent(
companion object { companion object {
const val kind = 40 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 { val content = try {
if (channelInfo != null) { if (channelInfo != null) {
gson.toJson(channelInfo) gson.toJson(channelInfo)
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class ChannelHideMessageEvent( class ChannelHideMessageEvent(
@@ -26,7 +26,7 @@ class ChannelHideMessageEvent(
companion object { companion object {
const val kind = 43 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 pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = val tags =
messagesToHide?.map { messagesToHide?.map {
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class ChannelMessageEvent( class ChannelMessageEvent(
@@ -34,7 +34,7 @@ class ChannelMessageEvent(
mentions: List<String>? = null, mentions: List<String>? = null,
zapReceiver: String?, zapReceiver: String?,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000, createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean, markAsSensitive: Boolean,
zapRaiserAmount: Long? zapRaiserAmount: Long?
): ChannelMessageEvent { ): ChannelMessageEvent {
@@ -3,9 +3,9 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class ChannelMetadataEvent( class ChannelMetadataEvent(
@@ -29,7 +29,7 @@ class ChannelMetadataEvent(
companion object { companion object {
const val kind = 41 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 = val content =
if (newChannelInfo != null) { if (newChannelInfo != null) {
gson.toJson(newChannelInfo) gson.toJson(newChannelInfo)
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class ChannelMuteUserEvent( class ChannelMuteUserEvent(
@@ -26,7 +26,7 @@ class ChannelMuteUserEvent(
companion object { companion object {
const val kind = 44 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 content = reason
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = val tags =
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class CommunityDefinitionEvent( class CommunityDefinitionEvent(
@@ -30,7 +30,7 @@ class CommunityDefinitionEvent(
fun create( fun create(
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): CommunityDefinitionEvent { ): CommunityDefinitionEvent {
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
@@ -3,10 +3,10 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class CommunityPostApprovalEvent( class CommunityPostApprovalEvent(
@@ -45,7 +45,7 @@ class CommunityPostApprovalEvent(
companion object { companion object {
const val kind = 4550 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 content = approvedPost.toJson()
val communities = listOf("a", community.address().toTag()) val communities = listOf("a", community.address().toTag())
@@ -5,10 +5,10 @@ import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.decodePublicKey import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
data class Contact(val pubKeyHex: String, val relayUri: String?) data class Contact(val pubKeyHex: String, val relayUri: String?)
@@ -73,7 +73,7 @@ class ContactListEvent(
companion object { companion object {
const val kind = 3 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) { val content = if (relayUse != null) {
gson.toJson(relayUse) gson.toJson(relayUse)
} else { } else {
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class DeletionEvent( class DeletionEvent(
@@ -20,7 +20,7 @@ class DeletionEvent(
companion object { companion object {
const val kind = 5 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 content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = deleteEvents.map { listOf("e", it) } val tags = deleteEvents.map { listOf("e", it) }
@@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable
import com.google.gson.* import com.google.gson.*
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import fr.acinq.secp256k1.Secp256k1 import fr.acinq.secp256k1.Secp256k1
@@ -314,7 +315,7 @@ open class Event(
return MessageDigest.getInstance("SHA-256").digest(rawEventJson.toByteArray()) 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 pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val id = Companion.generateId(pubKey, createdAt, kind, tags, content) val id = Companion.generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey).toHexKey() val sig = Utils.sign(id, privateKey).toHexKey()
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class FileHeaderEvent( class FileHeaderEvent(
@@ -52,7 +52,7 @@ class FileHeaderEvent(
encryptionKey: AESGCM? = null, encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null, sensitiveContent: Boolean? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): FileHeaderEvent { ): FileHeaderEvent {
val tags = listOfNotNull( val tags = listOfNotNull(
listOf(URL, url), listOf(URL, url),
@@ -3,10 +3,10 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Base64 import java.util.Base64
import java.util.Date
@Immutable @Immutable
class FileStorageEvent( class FileStorageEvent(
@@ -44,7 +44,7 @@ class FileStorageEvent(
mimeType: String, mimeType: String,
data: ByteArray, data: ByteArray,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): FileStorageEvent { ): FileStorageEvent {
val tags = listOfNotNull( val tags = listOfNotNull(
listOf(TYPE, mimeType) listOf(TYPE, mimeType)
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class FileStorageHeaderEvent( class FileStorageHeaderEvent(
@@ -52,7 +52,7 @@ class FileStorageHeaderEvent(
encryptionKey: AESGCM? = null, encryptionKey: AESGCM? = null,
sensitiveContent: Boolean? = null, sensitiveContent: Boolean? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): FileStorageHeaderEvent { ): FileStorageHeaderEvent {
val tags = listOfNotNull( val tags = listOfNotNull(
listOf("e", storageEvent.id), listOf("e", storageEvent.id),
@@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class GenericRepostEvent( class GenericRepostEvent(
@@ -29,7 +29,7 @@ class GenericRepostEvent(
companion object { companion object {
const val kind = 16 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 content = boostedPost.toJson()
val replyToPost = listOf("e", boostedPost.id()) val replyToPost = listOf("e", boostedPost.id())
@@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Date
@Immutable @Immutable
class HTTPAuthorizationEvent( class HTTPAuthorizationEvent(
@@ -25,7 +25,7 @@ class HTTPAuthorizationEvent(
method: String, method: String,
body: String? = null, body: String? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): HTTPAuthorizationEvent { ): HTTPAuthorizationEvent {
val sha256 = MessageDigest.getInstance("SHA-256") val sha256 = MessageDigest.getInstance("SHA-256")
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class HighlightEvent( class HighlightEvent(
@@ -26,7 +26,7 @@ class HighlightEvent(
fun create( fun create(
msg: String, msg: String,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): PollNoteEvent { ): PollNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class LiveActivitiesChatMessageEvent( class LiveActivitiesChatMessageEvent(
@@ -49,7 +49,7 @@ class LiveActivitiesChatMessageEvent(
mentions: List<String>? = null, mentions: List<String>? = null,
zapReceiver: String?, zapReceiver: String?,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000, createdAt: Long = TimeUtils.now(),
markAsSensitive: Boolean, markAsSensitive: Boolean,
zapRaiserAmount: Long? zapRaiserAmount: Long?
): LiveActivitiesChatMessageEvent { ): LiveActivitiesChatMessageEvent {
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class LiveActivitiesEvent( 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 participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
fun checkStatus(eventStatus: String?): String? { 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 STATUS_ENDED
} else { } else {
eventStatus eventStatus
@@ -48,7 +48,7 @@ class LiveActivitiesEvent(
fun create( fun create(
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): LiveActivitiesEvent { ): LiveActivitiesEvent {
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
@@ -7,11 +7,11 @@ import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.google.gson.JsonParseException import com.google.gson.JsonParseException
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.lang.reflect.Type import java.lang.reflect.Type
import java.util.Date
@Immutable @Immutable
class LnZapPaymentRequestEvent( class LnZapPaymentRequestEvent(
@@ -57,7 +57,7 @@ class LnZapPaymentRequestEvent(
lnInvoice: String, lnInvoice: String,
walletServicePubkey: String, walletServicePubkey: String,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): LnZapPaymentRequestEvent { ): LnZapPaymentRequestEvent {
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey)
val serializedRequest = gson.toJson(PayInvoiceMethod.create(lnInvoice)) val serializedRequest = gson.toJson(PayInvoiceMethod.create(lnInvoice))
@@ -64,7 +64,7 @@ class LnZapRequestEvent(
pollOption: Int?, pollOption: Int?,
message: String, message: String,
zapType: LnZapEvent.ZapType, zapType: LnZapEvent.ZapType,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): LnZapRequestEvent { ): LnZapRequestEvent {
var content = message var content = message
var privkey = privateKey var privkey = privateKey
@@ -104,7 +104,7 @@ class LnZapRequestEvent(
privateKey: ByteArray, privateKey: ByteArray,
message: String, message: String,
zapType: LnZapEvent.ZapType, zapType: LnZapEvent.ZapType,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): LnZapRequestEvent { ): LnZapRequestEvent {
var content = message var content = message
var privkey = privateKey var privkey = privateKey
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class LongTextNoteEvent( class LongTextNoteEvent(
@@ -32,7 +32,7 @@ class LongTextNoteEvent(
companion object { companion object {
const val kind = 30023 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 pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
replyTos?.forEach { replyTos?.forEach {
@@ -7,11 +7,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.gson.Gson import com.google.gson.Gson
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.UserMetadata import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.util.Date
@Stable @Stable
abstract class IdentityClaim( abstract class IdentityClaim(
@@ -171,7 +171,7 @@ class MetadataEvent(
.readerFor(UserMetadata::class.java) .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 pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
@@ -4,10 +4,10 @@ import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.hexToByteArray
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class MuteListEvent( class MuteListEvent(
@@ -71,7 +71,7 @@ class MuteListEvent(
privAddresses: List<ATag>? = null, privAddresses: List<ATag>? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): MuteListEvent { ): MuteListEvent {
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey)
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class PeopleListEvent( class PeopleListEvent(
@@ -30,7 +30,7 @@ class PeopleListEvent(
privAddresses: List<ATag>? = null, privAddresses: List<ATag>? = null,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): PeopleListEvent { ): PeopleListEvent {
val pubKey = Utils.pubkeyCreate(privateKey) val pubKey = Utils.pubkeyCreate(privateKey)
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey) val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class PinListEvent( class PinListEvent(
@@ -27,7 +27,7 @@ class PinListEvent(
fun create( fun create(
pins: List<String>, pins: List<String>,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): PinListEvent { ): PinListEvent {
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
pins.forEach { pins.forEach {
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
const val POLL_OPTION = "poll_option" const val POLL_OPTION = "poll_option"
const val VALUE_MAXIMUM = "value_maximum" const val VALUE_MAXIMUM = "value_maximum"
@@ -45,7 +45,7 @@ class PollNoteEvent(
mentions: List<String>?, mentions: List<String>?,
addresses: List<ATag>?, addresses: List<ATag>?,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000, createdAt: Long = TimeUtils.now(),
pollOptions: Map<Int, String>, pollOptions: Map<Int, String>,
valueMaximum: Int?, valueMaximum: Int?,
valueMinimum: Int?, valueMinimum: Int?,
@@ -3,12 +3,12 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.HexValidator import com.vitorpamplona.amethyst.service.HexValidator
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import nostr.postr.Utils import nostr.postr.Utils
import nostr.postr.toHex import nostr.postr.toHex
import java.util.Date
@Immutable @Immutable
class PrivateDmEvent( class PrivateDmEvent(
@@ -83,7 +83,7 @@ class PrivateDmEvent(
mentions: List<String>? = null, mentions: List<String>? = null,
zapReceiver: String?, zapReceiver: String?,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000, createdAt: Long = TimeUtils.now(),
publishedRecipientPubKey: ByteArray? = null, publishedRecipientPubKey: ByteArray? = null,
advertiseNip18: Boolean = true, advertiseNip18: Boolean = true,
markAsSensitive: Boolean, markAsSensitive: Boolean,
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class ReactionEvent( class ReactionEvent(
@@ -22,15 +22,15 @@ class ReactionEvent(
companion object { companion object {
const val kind = 7 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) 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) 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() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey())) var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))
@@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.net.URI import java.net.URI
import java.util.Date
@Immutable @Immutable
class RecommendRelayEvent( class RecommendRelayEvent(
@@ -27,7 +27,7 @@ class RecommendRelayEvent(
companion object { companion object {
const val kind = 2 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 content = relay.toString()
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf<List<String>>() val tags = listOf<List<String>>()
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class RelayAuthEvent( class RelayAuthEvent(
@@ -21,7 +21,7 @@ class RelayAuthEvent(
companion object { companion object {
const val kind = 22242 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 content = ""
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf( val tags = listOf(
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class RelaySetEvent( class RelaySetEvent(
@@ -28,7 +28,7 @@ class RelaySetEvent(
fun create( fun create(
relays: List<String>, relays: List<String>,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): RelaySetEvent { ): RelaySetEvent {
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
relays.forEach { relays.forEach {
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType) data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
@@ -58,7 +58,7 @@ class ReportEvent(
type: ReportType, type: ReportType,
privateKey: ByteArray, privateKey: ByteArray,
content: String = "", content: String = "",
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): ReportEvent { ): ReportEvent {
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase()) val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
val reportAuthorTag = listOf("p", reportedPost.pubKey(), 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()) 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 content = ""
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase()) val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())
@@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.Client
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class RepostEvent( class RepostEvent(
@@ -29,7 +29,7 @@ class RepostEvent(
companion object { companion object {
const val kind = 6 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 content = boostedPost.toJson()
val replyToPost = listOf("e", boostedPost.id()) val replyToPost = listOf("e", boostedPost.id())
@@ -4,10 +4,10 @@ import androidx.compose.runtime.Immutable
import com.linkedin.urls.detection.UrlDetector import com.linkedin.urls.detection.UrlDetector
import com.linkedin.urls.detection.UrlDetectorOptions import com.linkedin.urls.detection.UrlDetectorOptions
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags
import nostr.postr.Utils import nostr.postr.Utils
import java.util.Date
@Immutable @Immutable
class TextNoteEvent( class TextNoteEvent(
@@ -40,7 +40,7 @@ class TextNoteEvent(
directMentions: Set<HexKey>, directMentions: Set<HexKey>,
privateKey: ByteArray, privateKey: ByteArray,
createdAt: Long = Date().time / 1000 createdAt: Long = TimeUtils.now()
): TextNoteEvent { ): TextNoteEvent {
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = mutableListOf<List<String>>() val tags = mutableListOf<List<String>>()
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.relays
import android.util.Log import android.util.Log
import com.google.gson.JsonElement import com.google.gson.JsonElement
import com.vitorpamplona.amethyst.BuildConfig import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.checkNotInMainThread import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.model.EventInterface import com.vitorpamplona.amethyst.service.model.EventInterface
@@ -14,7 +15,6 @@ import okhttp3.WebSocket
import okhttp3.WebSocketListener import okhttp3.WebSocketListener
import java.net.Proxy import java.net.Proxy
import java.time.Duration import java.time.Duration
import java.util.Date
enum class FeedType { enum class FeedType {
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH, WALLET_CONNECT FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH, WALLET_CONNECT
@@ -179,7 +179,7 @@ class Relay(
socket = null socket = null
isReady = false isReady = false
afterEOSE = false afterEOSE = false
closingTime = Date().time / 1000 closingTime = TimeUtils.now()
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) } listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) }
} }
@@ -193,7 +193,7 @@ class Relay(
socket = null socket = null
isReady = false isReady = false
afterEOSE = false afterEOSE = false
closingTime = Date().time / 1000 closingTime = TimeUtils.now()
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response") Log.w("Relay", "Relay onFailure $url, ${response?.message} $response")
t.printStackTrace() t.printStackTrace()
@@ -208,7 +208,7 @@ class Relay(
errorCounter++ errorCounter++
isReady = false isReady = false
afterEOSE = false afterEOSE = false
closingTime = Date().time / 1000 closingTime = TimeUtils.now()
Log.e("Relay", "Relay Invalid $url") Log.e("Relay", "Relay Invalid $url")
e.printStackTrace() e.printStackTrace()
} }
@@ -216,7 +216,7 @@ class Relay(
fun disconnect() { fun disconnect() {
// httpClient.dispatcher.executorService.shutdown() // httpClient.dispatcher.executorService.shutdown()
closingTime = Date().time / 1000 closingTime = TimeUtils.now()
socket?.close(1000, "Normal close") socket?.close(1000, "Normal close")
socket = null socket = null
isReady = false isReady = false
@@ -241,7 +241,7 @@ class Relay(
} }
} else { } else {
// waits 60 seconds to reconnect after disconnected. // 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. // sends all filters after connection is successful.
requestAndWatch() requestAndWatch()
} }
@@ -254,7 +254,7 @@ class Relay(
if (socket == null) { if (socket == null) {
// waits 60 seconds to reconnect after disconnected. // waits 60 seconds to reconnect after disconnected.
if (Date().time / 1000 > closingTime + 60) { if (TimeUtils.now() > closingTime + 60) {
// println("sendfilter Only if Disconnected ${url} ") // println("sendfilter Only if Disconnected ${url} ")
requestAndWatch() requestAndWatch()
} }
@@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() { 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> { 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 isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
@@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() { 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> { 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 isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
@@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.ParticipantListBuilder import com.vitorpamplona.amethyst.model.ParticipantListBuilder
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.* 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_ENDED
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE 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> { 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 isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
val followingKeySet = val followingKeySet =
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note 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.ChannelMessageEvent
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
import com.vitorpamplona.amethyst.service.model.PollNoteEvent 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 followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet() val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
val now = System.currentTimeMillis() / 1000 val now = TimeUtils.now()
return collection return collection
.asSequence() .asSequence()
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note 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.AudioTrackEvent
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
import com.vitorpamplona.amethyst.service.model.HighlightEvent 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.PollNoteEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.service.model.TextNoteEvent import com.vitorpamplona.amethyst.service.model.TextNoteEvent
import java.util.Date
class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() { class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
override fun feedKey(): String { override fun feedKey(): String {
@@ -35,7 +35,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
val followingTagSet = account.selectedTagsFollowList(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 val oneHr = 60 * 60
return collection return collection
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() { 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> { private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
val now = System.currentTimeMillis() / 1000 val now = TimeUtils.now()
val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet() val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()
@@ -5,29 +5,19 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape 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.MaterialTheme
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@@ -36,7 +26,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.map import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.RelayInformation
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists 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.actions.toImmutableListOfLists
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy 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.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel 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.ChatBubbleShapeThem
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat 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.Size15dp
import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer 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.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -567,7 +547,7 @@ private fun StatusRow(
Column(modifier = ReactionRowHeightChat) { Column(modifier = ReactionRowHeightChat) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) { Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
ChatTimeAgo(baseNote) ChatTimeAgo(baseNote)
RelayBadges(baseNote, accountViewModel, nav = nav) RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav)
Spacer(modifier = DoubleHorzSpacer) Spacer(modifier = DoubleHorzSpacer)
} }
} }
@@ -806,131 +786,3 @@ private fun DisplayMessageUsername(
Spacer(modifier = StdHorzSpacer) Spacer(modifier = StdHorzSpacer)
DrawPlayName(userDisplayName) 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
)
}
@@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.map import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.model.UserMetadata import com.vitorpamplona.amethyst.model.UserMetadata
import com.vitorpamplona.amethyst.service.Nip05Verifier 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 com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.Date
@Composable @Composable
fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableState<Boolean?> { fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
val nip05Verified = remember(user.nip05) { val nip05Verified = remember(userMetadata.nip05) {
// starts with null if must verify or already filled in if verified in the last hour // 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 val default = if ((userMetadata.nip05LastVerificationTime ?: 0) > TimeUtils.oneHourAgo()) {
user.nip05Verified userMetadata.nip05Verified
} else { } else {
null null
} }
@@ -55,23 +55,23 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableSta
} }
if (nip05Verified.value == null) { if (nip05Verified.value == null) {
LaunchedEffect(key1 = user.nip05) { LaunchedEffect(key1 = userMetadata.nip05) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
user.nip05?.ifBlank { null }?.let { nip05 -> userMetadata.nip05?.ifBlank { null }?.let { nip05 ->
Nip05Verifier().verifyNip05( Nip05Verifier().verifyNip05(
nip05, nip05,
onSuccess = { onSuccess = {
// Marks user as verified // Marks user as verified
if (it == pubkeyHex) { if (it == pubkeyHex) {
user.nip05Verified = true userMetadata.nip05Verified = true
user.nip05LastVerificationTime = Date().time / 1000 userMetadata.nip05LastVerificationTime = TimeUtils.now()
if (nip05Verified.value != true) { if (nip05Verified.value != true) {
nip05Verified.value = true nip05Verified.value = true
} }
} else { } else {
user.nip05Verified = false userMetadata.nip05Verified = false
user.nip05LastVerificationTime = 0 userMetadata.nip05LastVerificationTime = 0
if (nip05Verified.value != false) { if (nip05Verified.value != false) {
nip05Verified.value = false nip05Verified.value = false
@@ -79,8 +79,8 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableSta
} }
}, },
onError = { onError = {
user.nip05LastVerificationTime = 0 userMetadata.nip05LastVerificationTime = 0
user.nip05Verified = false userMetadata.nip05Verified = false
if (nip05Verified.value != false) { if (nip05Verified.value != false) {
nip05Verified.value = false nip05Verified.value = false
@@ -1,6 +1,5 @@
package com.vitorpamplona.amethyst.ui.note package com.vitorpamplona.amethyst.ui.note
import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.util.Log import android.util.Log
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
@@ -10,7 +9,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.shape.CutCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Divider import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton import androidx.compose.material.IconButton
import androidx.compose.material.LocalTextStyle import androidx.compose.material.LocalTextStyle
@@ -38,12 +34,10 @@ import androidx.compose.material.Text
import androidx.compose.material.darkColors import androidx.compose.material.darkColors
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt 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.Link
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.PushPin import androidx.compose.material.icons.filled.PushPin
import androidx.compose.material.lightColors import androidx.compose.material.lightColors
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -58,7 +52,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Alignment.Companion.TopEnd
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush 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.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.get import androidx.core.graphics.get
import androidx.lifecycle.distinctUntilChanged 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.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status 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.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton 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.JoinCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag 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.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import com.vitorpamplona.amethyst.ui.theme.DividerThickness 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.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder 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.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
import com.vitorpamplona.amethyst.ui.theme.Size25dp 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
)
}
}
}
}
@@ -7,6 +7,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.TimeUtils
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.* import com.vitorpamplona.amethyst.service.model.*
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@@ -86,7 +87,7 @@ class PollNoteViewModel : ViewModel() {
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
fun isPollClosed(): Boolean = closedAt?.let { // allow 2 minute leeway for zap to propagate 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 } == true
fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) { fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) {
@@ -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
)
}
}
}
@@ -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
)
}
@@ -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
)
}
}
}
}