mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-08 20:08:06 +02:00
Refactoring of Badge Box codes and Time classes
This commit is contained in:
parent
9a7e678bbe
commit
7ea5be0152
@ -19,7 +19,6 @@ import java.math.BigDecimal
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Date
|
||||
import java.util.regex.Pattern
|
||||
|
||||
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
||||
@ -434,7 +433,7 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun hasAnyReports(): Boolean {
|
||||
val dayAgo = Date().time / 1000 - 24 * 60 * 60
|
||||
val dayAgo = TimeUtils.oneDayAgo()
|
||||
return reports.isNotEmpty() ||
|
||||
(
|
||||
author?.reports?.values?.any {
|
||||
@ -471,8 +470,7 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun hasBoostedInTheLast5Minutes(loggedIn: User): Boolean {
|
||||
val currentTime = Date().time / 1000
|
||||
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > currentTime - (60 * 5) } != null // 5 minute protection
|
||||
return boosts.firstOrNull { it.author == loggedIn && (it.createdAt() ?: 0) > TimeUtils.fiveMinutesAgo() } != null // 5 minute protection
|
||||
}
|
||||
|
||||
fun boostedBy(loggedIn: User): List<Note> {
|
||||
|
@ -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 com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
import com.vitorpamplona.amethyst.service.model.Event
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
@ -12,7 +13,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import kotlin.Error
|
||||
|
||||
@ -57,7 +57,7 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
|
||||
if (type == Relay.Type.EOSE && channel != null) {
|
||||
// updates a per subscripton since date
|
||||
subscriptions[channel]?.updateEOSE(Date().time / 1000, relay.url)
|
||||
subscriptions[channel]?.updateEOSE(TimeUtils.now(), relay.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class AudioTrackEvent(
|
||||
@ -42,7 +42,7 @@ class AudioTrackEvent(
|
||||
cover: String? = null,
|
||||
subject: String? = null,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): AudioTrackEvent {
|
||||
val tags = listOfNotNull(
|
||||
listOf(MEDIA, media),
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class BookmarkListEvent(
|
||||
@ -30,7 +30,7 @@ class BookmarkListEvent(
|
||||
privAddresses: List<ATag>? = null,
|
||||
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): BookmarkListEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
|
||||
|
@ -3,9 +3,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class ChannelCreateEvent(
|
||||
@ -26,7 +26,7 @@ class ChannelCreateEvent(
|
||||
companion object {
|
||||
const val kind = 40
|
||||
|
||||
fun create(channelInfo: ChannelData?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelCreateEvent {
|
||||
fun create(channelInfo: ChannelData?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelCreateEvent {
|
||||
val content = try {
|
||||
if (channelInfo != null) {
|
||||
gson.toJson(channelInfo)
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class ChannelHideMessageEvent(
|
||||
@ -26,7 +26,7 @@ class ChannelHideMessageEvent(
|
||||
companion object {
|
||||
const val kind = 43
|
||||
|
||||
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent {
|
||||
fun create(reason: String, messagesToHide: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelHideMessageEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags =
|
||||
messagesToHide?.map {
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class ChannelMessageEvent(
|
||||
@ -34,7 +34,7 @@ class ChannelMessageEvent(
|
||||
mentions: List<String>? = null,
|
||||
zapReceiver: String?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?
|
||||
): ChannelMessageEvent {
|
||||
|
@ -3,9 +3,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class ChannelMetadataEvent(
|
||||
@ -29,7 +29,7 @@ class ChannelMetadataEvent(
|
||||
companion object {
|
||||
const val kind = 41
|
||||
|
||||
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMetadataEvent {
|
||||
fun create(newChannelInfo: ChannelCreateEvent.ChannelData?, originalChannelIdHex: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMetadataEvent {
|
||||
val content =
|
||||
if (newChannelInfo != null) {
|
||||
gson.toJson(newChannelInfo)
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class ChannelMuteUserEvent(
|
||||
@ -26,7 +26,7 @@ class ChannelMuteUserEvent(
|
||||
companion object {
|
||||
const val kind = 44
|
||||
|
||||
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent {
|
||||
fun create(reason: String, usersToMute: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMuteUserEvent {
|
||||
val content = reason
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags =
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class CommunityDefinitionEvent(
|
||||
@ -30,7 +30,7 @@ class CommunityDefinitionEvent(
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): CommunityDefinitionEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
|
@ -3,10 +3,10 @@ package com.vitorpamplona.amethyst.service.model
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class CommunityPostApprovalEvent(
|
||||
@ -45,7 +45,7 @@ class CommunityPostApprovalEvent(
|
||||
companion object {
|
||||
const val kind = 4550
|
||||
|
||||
fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = Date().time / 1000): GenericRepostEvent {
|
||||
fun create(approvedPost: Event, community: CommunityDefinitionEvent, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
|
||||
val content = approvedPost.toJson()
|
||||
|
||||
val communities = listOf("a", community.address().toTag())
|
||||
|
@ -5,10 +5,10 @@ import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.decodePublicKey
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
data class Contact(val pubKeyHex: String, val relayUri: String?)
|
||||
@ -73,7 +73,7 @@ class ContactListEvent(
|
||||
companion object {
|
||||
const val kind = 3
|
||||
|
||||
fun create(follows: List<Contact>, followTags: List<String>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ContactListEvent {
|
||||
fun create(follows: List<Contact>, followTags: List<String>, relayUse: Map<String, ReadWrite>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent {
|
||||
val content = if (relayUse != null) {
|
||||
gson.toJson(relayUse)
|
||||
} else {
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class DeletionEvent(
|
||||
@ -20,7 +20,7 @@ class DeletionEvent(
|
||||
companion object {
|
||||
const val kind = 5
|
||||
|
||||
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): DeletionEvent {
|
||||
fun create(deleteEvents: List<String>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): DeletionEvent {
|
||||
val content = ""
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = deleteEvents.map { listOf("e", it) }
|
||||
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.Immutable
|
||||
import com.google.gson.*
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import fr.acinq.secp256k1.Secp256k1
|
||||
@ -314,7 +315,7 @@ open class Event(
|
||||
return MessageDigest.getInstance("SHA-256").digest(rawEventJson.toByteArray())
|
||||
}
|
||||
|
||||
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = Date().time / 1000): Event {
|
||||
fun create(privateKey: ByteArray, kind: Int, tags: List<List<String>> = emptyList(), content: String = "", createdAt: Long = TimeUtils.now()): Event {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = Companion.generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey).toHexKey()
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class FileHeaderEvent(
|
||||
@ -52,7 +52,7 @@ class FileHeaderEvent(
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): FileHeaderEvent {
|
||||
val tags = listOfNotNull(
|
||||
listOf(URL, url),
|
||||
|
@ -3,10 +3,10 @@ package com.vitorpamplona.amethyst.service.model
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Base64
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class FileStorageEvent(
|
||||
@ -44,7 +44,7 @@ class FileStorageEvent(
|
||||
mimeType: String,
|
||||
data: ByteArray,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): FileStorageEvent {
|
||||
val tags = listOfNotNull(
|
||||
listOf(TYPE, mimeType)
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class FileStorageHeaderEvent(
|
||||
@ -52,7 +52,7 @@ class FileStorageHeaderEvent(
|
||||
encryptionKey: AESGCM? = null,
|
||||
sensitiveContent: Boolean? = null,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): FileStorageHeaderEvent {
|
||||
val tags = listOfNotNull(
|
||||
listOf("e", storageEvent.id),
|
||||
|
@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class GenericRepostEvent(
|
||||
@ -29,7 +29,7 @@ class GenericRepostEvent(
|
||||
companion object {
|
||||
const val kind = 16
|
||||
|
||||
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): GenericRepostEvent {
|
||||
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): GenericRepostEvent {
|
||||
val content = boostedPost.toJson()
|
||||
|
||||
val replyToPost = listOf("e", boostedPost.id())
|
||||
|
@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.security.MessageDigest
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class HTTPAuthorizationEvent(
|
||||
@ -25,7 +25,7 @@ class HTTPAuthorizationEvent(
|
||||
method: String,
|
||||
body: String? = null,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): HTTPAuthorizationEvent {
|
||||
val sha256 = MessageDigest.getInstance("SHA-256")
|
||||
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class HighlightEvent(
|
||||
@ -26,7 +26,7 @@ class HighlightEvent(
|
||||
fun create(
|
||||
msg: String,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): PollNoteEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = mutableListOf<List<String>>()
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class LiveActivitiesChatMessageEvent(
|
||||
@ -49,7 +49,7 @@ class LiveActivitiesChatMessageEvent(
|
||||
mentions: List<String>? = null,
|
||||
zapReceiver: String?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
markAsSensitive: Boolean,
|
||||
zapRaiserAmount: Long?
|
||||
): LiveActivitiesChatMessageEvent {
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class LiveActivitiesEvent(
|
||||
@ -32,7 +32,7 @@ class LiveActivitiesEvent(
|
||||
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(3)) }
|
||||
|
||||
fun checkStatus(eventStatus: String?): String? {
|
||||
return if (eventStatus == STATUS_LIVE && createdAt < Date().time / 1000 - (60 * 60 * 8)) { // 2 hours {
|
||||
return if (eventStatus == STATUS_LIVE && createdAt < TimeUtils.eightHoursAgo()) {
|
||||
STATUS_ENDED
|
||||
} else {
|
||||
eventStatus
|
||||
@ -48,7 +48,7 @@ class LiveActivitiesEvent(
|
||||
|
||||
fun create(
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): LiveActivitiesEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
|
@ -7,11 +7,11 @@ import com.google.gson.JsonDeserializer
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.lang.reflect.Type
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class LnZapPaymentRequestEvent(
|
||||
@ -57,7 +57,7 @@ class LnZapPaymentRequestEvent(
|
||||
lnInvoice: String,
|
||||
walletServicePubkey: String,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): LnZapPaymentRequestEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||
val serializedRequest = gson.toJson(PayInvoiceMethod.create(lnInvoice))
|
||||
|
@ -64,7 +64,7 @@ class LnZapRequestEvent(
|
||||
pollOption: Int?,
|
||||
message: String,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): LnZapRequestEvent {
|
||||
var content = message
|
||||
var privkey = privateKey
|
||||
@ -104,7 +104,7 @@ class LnZapRequestEvent(
|
||||
privateKey: ByteArray,
|
||||
message: String,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): LnZapRequestEvent {
|
||||
var content = message
|
||||
var privkey = privateKey
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class LongTextNoteEvent(
|
||||
@ -32,7 +32,7 @@ class LongTextNoteEvent(
|
||||
companion object {
|
||||
const val kind = 30023
|
||||
|
||||
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent {
|
||||
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): LongTextNoteEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = mutableListOf<List<String>>()
|
||||
replyTos?.forEach {
|
||||
|
@ -7,11 +7,11 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.google.gson.Gson
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.util.Date
|
||||
|
||||
@Stable
|
||||
abstract class IdentityClaim(
|
||||
@ -171,7 +171,7 @@ class MetadataEvent(
|
||||
.readerFor(UserMetadata::class.java)
|
||||
}
|
||||
|
||||
fun create(contactMetaData: String, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
|
||||
fun create(contactMetaData: String, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): MetadataEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = mutableListOf<List<String>>()
|
||||
|
||||
|
@ -4,10 +4,10 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class MuteListEvent(
|
||||
@ -71,7 +71,7 @@ class MuteListEvent(
|
||||
privAddresses: List<ATag>? = null,
|
||||
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): MuteListEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class PeopleListEvent(
|
||||
@ -30,7 +30,7 @@ class PeopleListEvent(
|
||||
privAddresses: List<ATag>? = null,
|
||||
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): PeopleListEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||
val content = createPrivateTags(privEvents, privUsers, privAddresses, privateKey, pubKey)
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class PinListEvent(
|
||||
@ -27,7 +27,7 @@ class PinListEvent(
|
||||
fun create(
|
||||
pins: List<String>,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): PinListEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
pins.forEach {
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
const val POLL_OPTION = "poll_option"
|
||||
const val VALUE_MAXIMUM = "value_maximum"
|
||||
@ -45,7 +45,7 @@ class PollNoteEvent(
|
||||
mentions: List<String>?,
|
||||
addresses: List<ATag>?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
pollOptions: Map<Int, String>,
|
||||
valueMaximum: Int?,
|
||||
valueMinimum: Int?,
|
||||
|
@ -3,12 +3,12 @@ package com.vitorpamplona.amethyst.service.model
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.HexValidator
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import nostr.postr.Utils
|
||||
import nostr.postr.toHex
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class PrivateDmEvent(
|
||||
@ -83,7 +83,7 @@ class PrivateDmEvent(
|
||||
mentions: List<String>? = null,
|
||||
zapReceiver: String?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
publishedRecipientPubKey: ByteArray? = null,
|
||||
advertiseNip18: Boolean = true,
|
||||
markAsSensitive: Boolean,
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class ReactionEvent(
|
||||
@ -22,15 +22,15 @@ class ReactionEvent(
|
||||
companion object {
|
||||
const val kind = 7
|
||||
|
||||
fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
|
||||
fun createWarning(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
|
||||
return create("\u26A0\uFE0F", originalNote, privateKey, createdAt)
|
||||
}
|
||||
|
||||
fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
|
||||
fun createLike(originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
|
||||
return create("+", originalNote, privateKey, createdAt)
|
||||
}
|
||||
|
||||
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReactionEvent {
|
||||
fun create(content: String, originalNote: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReactionEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
|
||||
var tags = listOf(listOf("e", originalNote.id()), listOf("p", originalNote.pubKey()))
|
||||
|
@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.net.URI
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class RecommendRelayEvent(
|
||||
@ -27,7 +27,7 @@ class RecommendRelayEvent(
|
||||
companion object {
|
||||
const val kind = 2
|
||||
|
||||
fun create(relay: URI, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RecommendRelayEvent {
|
||||
fun create(relay: URI, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RecommendRelayEvent {
|
||||
val content = relay.toString()
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = listOf<List<String>>()
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class RelayAuthEvent(
|
||||
@ -21,7 +21,7 @@ class RelayAuthEvent(
|
||||
companion object {
|
||||
const val kind = 22242
|
||||
|
||||
fun create(relay: String, challenge: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RelayAuthEvent {
|
||||
fun create(relay: String, challenge: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RelayAuthEvent {
|
||||
val content = ""
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = listOf(
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class RelaySetEvent(
|
||||
@ -28,7 +28,7 @@ class RelaySetEvent(
|
||||
fun create(
|
||||
relays: List<String>,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): RelaySetEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
relays.forEach {
|
||||
|
@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
|
||||
@ -58,7 +58,7 @@ class ReportEvent(
|
||||
type: ReportType,
|
||||
privateKey: ByteArray,
|
||||
content: String = "",
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): ReportEvent {
|
||||
val reportPostTag = listOf("e", reportedPost.id(), type.name.lowercase())
|
||||
val reportAuthorTag = listOf("p", reportedPost.pubKey(), type.name.lowercase())
|
||||
@ -75,7 +75,7 @@ class ReportEvent(
|
||||
return ReportEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||
}
|
||||
|
||||
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
|
||||
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ReportEvent {
|
||||
val content = ""
|
||||
|
||||
val reportAuthorTag = listOf("p", reportedUser, type.name.lowercase())
|
||||
|
@ -2,10 +2,10 @@ package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class RepostEvent(
|
||||
@ -29,7 +29,7 @@ class RepostEvent(
|
||||
companion object {
|
||||
const val kind = 6
|
||||
|
||||
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = Date().time / 1000): RepostEvent {
|
||||
fun create(boostedPost: EventInterface, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): RepostEvent {
|
||||
val content = boostedPost.toJson()
|
||||
|
||||
val replyToPost = listOf("e", boostedPost.id())
|
||||
|
@ -4,10 +4,10 @@ import androidx.compose.runtime.Immutable
|
||||
import com.linkedin.urls.detection.UrlDetector
|
||||
import com.linkedin.urls.detection.UrlDetectorOptions
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.findHashtags
|
||||
import nostr.postr.Utils
|
||||
import java.util.Date
|
||||
|
||||
@Immutable
|
||||
class TextNoteEvent(
|
||||
@ -40,7 +40,7 @@ class TextNoteEvent(
|
||||
directMentions: Set<HexKey>,
|
||||
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = Date().time / 1000
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): TextNoteEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val tags = mutableListOf<List<String>>()
|
||||
|
@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service.relays
|
||||
import android.util.Log
|
||||
import com.google.gson.JsonElement
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.service.model.Event
|
||||
import com.vitorpamplona.amethyst.service.model.EventInterface
|
||||
@ -14,7 +15,6 @@ import okhttp3.WebSocket
|
||||
import okhttp3.WebSocketListener
|
||||
import java.net.Proxy
|
||||
import java.time.Duration
|
||||
import java.util.Date
|
||||
|
||||
enum class FeedType {
|
||||
FOLLOWS, PUBLIC_CHATS, PRIVATE_DMS, GLOBAL, SEARCH, WALLET_CONNECT
|
||||
@ -179,7 +179,7 @@ class Relay(
|
||||
socket = null
|
||||
isReady = false
|
||||
afterEOSE = false
|
||||
closingTime = Date().time / 1000
|
||||
closingTime = TimeUtils.now()
|
||||
listeners.forEach { it.onRelayStateChange(this@Relay, Type.DISCONNECT, null) }
|
||||
}
|
||||
|
||||
@ -193,7 +193,7 @@ class Relay(
|
||||
socket = null
|
||||
isReady = false
|
||||
afterEOSE = false
|
||||
closingTime = Date().time / 1000
|
||||
closingTime = TimeUtils.now()
|
||||
|
||||
Log.w("Relay", "Relay onFailure $url, ${response?.message} $response")
|
||||
t.printStackTrace()
|
||||
@ -208,7 +208,7 @@ class Relay(
|
||||
errorCounter++
|
||||
isReady = false
|
||||
afterEOSE = false
|
||||
closingTime = Date().time / 1000
|
||||
closingTime = TimeUtils.now()
|
||||
Log.e("Relay", "Relay Invalid $url")
|
||||
e.printStackTrace()
|
||||
}
|
||||
@ -216,7 +216,7 @@ class Relay(
|
||||
|
||||
fun disconnect() {
|
||||
// httpClient.dispatcher.executorService.shutdown()
|
||||
closingTime = Date().time / 1000
|
||||
closingTime = TimeUtils.now()
|
||||
socket?.close(1000, "Normal close")
|
||||
socket = null
|
||||
isReady = false
|
||||
@ -241,7 +241,7 @@ class Relay(
|
||||
}
|
||||
} else {
|
||||
// waits 60 seconds to reconnect after disconnected.
|
||||
if (Date().time / 1000 > closingTime + 60) {
|
||||
if (TimeUtils.now() > closingTime + 60) {
|
||||
// sends all filters after connection is successful.
|
||||
requestAndWatch()
|
||||
}
|
||||
@ -254,7 +254,7 @@ class Relay(
|
||||
|
||||
if (socket == null) {
|
||||
// waits 60 seconds to reconnect after disconnected.
|
||||
if (Date().time / 1000 > closingTime + 60) {
|
||||
if (TimeUtils.now() > closingTime + 60) {
|
||||
// println("sendfilter Only if Disconnected ${url} ")
|
||||
requestAndWatch()
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
|
||||
open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
@ -25,7 +26,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
}
|
||||
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
|
||||
open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
@ -25,7 +26,7 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte
|
||||
}
|
||||
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
||||
|
@ -5,6 +5,7 @@ import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.ParticipantListBuilder
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_ENDED
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||
@ -30,7 +31,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
}
|
||||
|
||||
protected open fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||
|
||||
val followingKeySet =
|
||||
|
@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
@ -29,7 +30,7 @@ class HomeConversationsFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val now = TimeUtils.now()
|
||||
|
||||
return collection
|
||||
.asSequence()
|
||||
|
@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
@ -11,7 +12,6 @@ import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PollNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
import java.util.Date
|
||||
|
||||
class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
override fun feedKey(): String {
|
||||
@ -35,7 +35,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
val followingTagSet = account.selectedTagsFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||
|
||||
val oneMinuteInTheFuture = Date().time / 1000 + (1 * 60) // one minute in the future.
|
||||
val oneMinuteInTheFuture = TimeUtils.now() + (1 * 60) // one minute in the future.
|
||||
val oneHr = 60 * 60
|
||||
|
||||
return collection
|
||||
|
@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
|
||||
class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
@ -22,7 +23,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
||||
}
|
||||
|
||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||
val now = System.currentTimeMillis() / 1000
|
||||
val now = TimeUtils.now()
|
||||
val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS
|
||||
|
||||
val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet()
|
||||
|
@ -5,29 +5,19 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@ -36,7 +26,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -48,26 +37,21 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.RelayInformation
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||
import com.vitorpamplona.amethyst.ui.actions.ImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.actions.RelayInformationDialog
|
||||
import com.vitorpamplona.amethyst.ui.actions.loadRelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@ -76,13 +60,9 @@ import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeMe
|
||||
import com.vitorpamplona.amethyst.ui.theme.ChatBubbleShapeThem
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat
|
||||
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size13dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@ -567,7 +547,7 @@ private fun StatusRow(
|
||||
Column(modifier = ReactionRowHeightChat) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
|
||||
ChatTimeAgo(baseNote)
|
||||
RelayBadges(baseNote, accountViewModel, nav = nav)
|
||||
RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav)
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
}
|
||||
}
|
||||
@ -806,131 +786,3 @@ private fun DisplayMessageUsername(
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
DrawPlayName(userDisplayName)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val expanded = remember { mutableStateOf(false) }
|
||||
|
||||
RenderRelayList(baseNote, expanded, accountViewModel, nav)
|
||||
|
||||
RenderExpandButton(baseNote, expanded) {
|
||||
ChatRelayExpandButton { expanded.value = true }
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun RenderRelayList(baseNote: Note, expanded: MutableState<Boolean>, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val noteRelays by baseNote.live().relays.map {
|
||||
it.note.relays
|
||||
}.observeAsState(baseNote.relays)
|
||||
|
||||
FlowRow(StdStartPadding) {
|
||||
val relaysToDisplay = remember(noteRelays, expanded.value) {
|
||||
if (expanded.value) noteRelays else noteRelays.take(3)
|
||||
}
|
||||
relaysToDisplay.forEach {
|
||||
RenderRelay(it, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderExpandButton(
|
||||
baseNote: Note,
|
||||
expanded: MutableState<Boolean>,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val showExpandButton by baseNote.live().relays.map {
|
||||
it.note.relays.size > 3
|
||||
}.observeAsState(baseNote.relays.size > 3)
|
||||
|
||||
if (showExpandButton && !expanded.value) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChatRelayExpandButton(onClick: () -> Unit) {
|
||||
IconButton(
|
||||
modifier = Size15Modifier,
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
null,
|
||||
modifier = Size15Modifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderRelay(dirtyUrl: String, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
val iconUrl by remember(dirtyUrl) {
|
||||
derivedStateOf {
|
||||
val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/")
|
||||
"https://$cleanUrl/favicon.ico"
|
||||
}
|
||||
}
|
||||
|
||||
var relayInfo: RelayInformation? by remember { mutableStateOf(null) }
|
||||
|
||||
if (relayInfo != null) {
|
||||
RelayInformationDialog(
|
||||
onClose = {
|
||||
relayInfo = null
|
||||
},
|
||||
relayInfo = relayInfo!!,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val ripple = rememberRipple(bounded = false, radius = Size15dp)
|
||||
|
||||
val clickableModifier = remember(dirtyUrl) {
|
||||
Modifier
|
||||
.padding(1.dp)
|
||||
.size(Size15dp)
|
||||
.clickable(
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple,
|
||||
onClick = {
|
||||
loadRelayInfo(dirtyUrl, context, scope) {
|
||||
relayInfo = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = clickableModifier
|
||||
) {
|
||||
RenderRelayIcon(iconUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderRelayIcon(iconUrl: String) {
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
|
||||
val iconModifier = remember {
|
||||
Modifier
|
||||
.size(Size13dp)
|
||||
.clip(shape = CircleShape)
|
||||
.background(backgroundColor)
|
||||
}
|
||||
|
||||
RobohashFallbackAsyncImage(
|
||||
robot = iconUrl,
|
||||
model = iconUrl,
|
||||
contentDescription = stringResource(id = R.string.relay_icon),
|
||||
colorFilter = RelayIconFilter,
|
||||
modifier = iconModifier
|
||||
)
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||
import com.vitorpamplona.amethyst.service.Nip05Verifier
|
||||
@ -39,14 +40,13 @@ import com.vitorpamplona.amethyst.ui.theme.Nip05
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
|
||||
val nip05Verified = remember(user.nip05) {
|
||||
fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState<Boolean?> {
|
||||
val nip05Verified = remember(userMetadata.nip05) {
|
||||
// starts with null if must verify or already filled in if verified in the last hour
|
||||
val default = if ((user.nip05LastVerificationTime ?: 0) > (Date().time / 1000 - 60 * 60)) { // 1hour
|
||||
user.nip05Verified
|
||||
val default = if ((userMetadata.nip05LastVerificationTime ?: 0) > TimeUtils.oneHourAgo()) {
|
||||
userMetadata.nip05Verified
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -55,23 +55,23 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableSta
|
||||
}
|
||||
|
||||
if (nip05Verified.value == null) {
|
||||
LaunchedEffect(key1 = user.nip05) {
|
||||
LaunchedEffect(key1 = userMetadata.nip05) {
|
||||
launch(Dispatchers.IO) {
|
||||
user.nip05?.ifBlank { null }?.let { nip05 ->
|
||||
userMetadata.nip05?.ifBlank { null }?.let { nip05 ->
|
||||
Nip05Verifier().verifyNip05(
|
||||
nip05,
|
||||
onSuccess = {
|
||||
// Marks user as verified
|
||||
if (it == pubkeyHex) {
|
||||
user.nip05Verified = true
|
||||
user.nip05LastVerificationTime = Date().time / 1000
|
||||
userMetadata.nip05Verified = true
|
||||
userMetadata.nip05LastVerificationTime = TimeUtils.now()
|
||||
|
||||
if (nip05Verified.value != true) {
|
||||
nip05Verified.value = true
|
||||
}
|
||||
} else {
|
||||
user.nip05Verified = false
|
||||
user.nip05LastVerificationTime = 0
|
||||
userMetadata.nip05Verified = false
|
||||
userMetadata.nip05LastVerificationTime = 0
|
||||
|
||||
if (nip05Verified.value != false) {
|
||||
nip05Verified.value = false
|
||||
@ -79,8 +79,8 @@ fun nip05VerificationAsAState(user: UserMetadata, pubkeyHex: String): MutableSta
|
||||
}
|
||||
},
|
||||
onError = {
|
||||
user.nip05LastVerificationTime = 0
|
||||
user.nip05Verified = false
|
||||
userMetadata.nip05LastVerificationTime = 0
|
||||
userMetadata.nip05Verified = false
|
||||
|
||||
if (nip05Verified.value != false) {
|
||||
nip05Verified.value = false
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.Crossfade
|
||||
@ -10,7 +9,6 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -28,8 +26,6 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CutCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
@ -38,12 +34,10 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.material.icons.filled.ExpandMore
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@ -58,7 +52,6 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Alignment.Companion.CenterVertically
|
||||
import androidx.compose.ui.Alignment.Companion.TopEnd
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@ -71,15 +64,12 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.graphics.get
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
@ -133,7 +123,6 @@ import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
|
||||
import com.vitorpamplona.amethyst.ui.components.LoadThumbAndThenVideoView
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
|
||||
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
|
||||
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
|
||||
@ -154,7 +143,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.JoinCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LeaveCommunityButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ReportNoteDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||
@ -165,9 +153,6 @@ import com.vitorpamplona.amethyst.ui.theme.HalfPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonBoxModifer
|
||||
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconButtonModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.ShowMoreRelaysButtonIconModifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size24Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||
@ -3317,529 +3302,3 @@ fun CreateImageHeader(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RelayBadges(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var showShowMore by remember { mutableStateOf(false) }
|
||||
|
||||
var lazyRelayList by remember {
|
||||
val baseNumber = baseNote.relays.map {
|
||||
it.removePrefix("wss://").removePrefix("ws://")
|
||||
}.toImmutableList()
|
||||
|
||||
mutableStateOf(baseNumber)
|
||||
}
|
||||
var shortRelayList by remember {
|
||||
mutableStateOf(lazyRelayList.take(3).toImmutableList())
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
WatchRelayLists(baseNote) { relayList ->
|
||||
if (!equalImmutableLists(relayList, lazyRelayList)) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
lazyRelayList = relayList
|
||||
shortRelayList = relayList.take(3).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
val nextShowMore = relayList.size > 3
|
||||
if (nextShowMore != showShowMore) {
|
||||
scope.launch(Dispatchers.Main) {
|
||||
// only triggers recomposition when actually different
|
||||
showShowMore = nextShowMore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(DoubleVertSpacer)
|
||||
|
||||
if (expanded) {
|
||||
VerticalRelayPanelWithFlow(lazyRelayList, accountViewModel, nav)
|
||||
} else {
|
||||
VerticalRelayPanelWithFlow(shortRelayList, accountViewModel, nav)
|
||||
}
|
||||
|
||||
if (showShowMore && !expanded) {
|
||||
ShowMoreRelaysButton {
|
||||
expanded = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WatchRelayLists(baseNote: Note, onListChanges: (ImmutableList<String>) -> Unit) {
|
||||
val noteRelaysState by baseNote.live().relays.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = noteRelaysState) {
|
||||
launch(Dispatchers.IO) {
|
||||
val relayList = noteRelaysState?.note?.relays?.map {
|
||||
it.removePrefix("wss://").removePrefix("ws://")
|
||||
} ?: emptyList()
|
||||
|
||||
onListChanges(relayList.toImmutableList())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
@Stable
|
||||
private fun VerticalRelayPanelWithFlow(
|
||||
relays: ImmutableList<String>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
// FlowRow Seems to be a lot faster than LazyVerticalGrid
|
||||
FlowRow() {
|
||||
relays.forEach { url ->
|
||||
RenderRelay(url, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = ShowMoreRelaysButtonBoxModifer,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
IconButton(
|
||||
modifier = ShowMoreRelaysButtonIconButtonModifier,
|
||||
onClick = onClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ExpandMore,
|
||||
null,
|
||||
modifier = ShowMoreRelaysButtonIconModifier,
|
||||
tint = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteAuthorPicture(
|
||||
baseNote: Note,
|
||||
nav: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier = Modifier
|
||||
) {
|
||||
NoteAuthorPicture(baseNote, size, accountViewModel, pictureModifier) {
|
||||
nav("User/${it.pubkeyHex}")
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteAuthorPicture(
|
||||
baseNote: Note,
|
||||
size: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: ((User) -> Unit)? = null
|
||||
) {
|
||||
val author by baseNote.live().metadata.map {
|
||||
it.note.author
|
||||
}.distinctUntilChanged().observeAsState(baseNote.author)
|
||||
|
||||
Crossfade(targetState = author) {
|
||||
if (it == null) {
|
||||
DisplayBlankAuthor(size, modifier)
|
||||
} else {
|
||||
ClickableUserPicture(it, size, accountViewModel, modifier, onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DisplayBlankAuthor(size: Dp, modifier: Modifier = Modifier) {
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
|
||||
val nullModifier = remember {
|
||||
modifier
|
||||
.size(size)
|
||||
.clip(shape = CircleShape)
|
||||
.background(backgroundColor)
|
||||
}
|
||||
|
||||
RobohashAsyncImage(
|
||||
robot = "authornotfound",
|
||||
contentDescription = stringResource(R.string.unknown_author),
|
||||
modifier = nullModifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserPicture(
|
||||
user: User,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier = remember { Modifier },
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val route by remember {
|
||||
derivedStateOf {
|
||||
"User/${user.pubkeyHex}"
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
ClickableUserPicture(
|
||||
baseUser = user,
|
||||
size = size,
|
||||
accountViewModel = accountViewModel,
|
||||
modifier = pictureModifier,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
nav(route)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ClickableUserPicture(
|
||||
baseUser: User,
|
||||
size: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = remember { Modifier },
|
||||
onClick: ((User) -> Unit)? = null,
|
||||
onLongClick: ((User) -> Unit)? = null
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val ripple = rememberRipple(bounded = false, radius = size)
|
||||
|
||||
// BaseUser is the same reference as accountState.user
|
||||
val myModifier = remember {
|
||||
if (onClick != null && onLongClick != null) {
|
||||
Modifier
|
||||
.size(size)
|
||||
.combinedClickable(
|
||||
onClick = { onClick(baseUser) },
|
||||
onLongClick = { onLongClick(baseUser) },
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple
|
||||
)
|
||||
} else if (onClick != null) {
|
||||
Modifier
|
||||
.size(size)
|
||||
.clickable(
|
||||
onClick = { onClick(baseUser) },
|
||||
role = Role.Button,
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple
|
||||
)
|
||||
} else {
|
||||
Modifier.size(size)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = myModifier, contentAlignment = TopEnd) {
|
||||
BaseUserPicture(baseUser, size, accountViewModel, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NonClickableUserPicture(
|
||||
baseUser: User,
|
||||
size: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = remember { Modifier }
|
||||
) {
|
||||
val myBoxModifier = remember {
|
||||
Modifier.size(size)
|
||||
}
|
||||
|
||||
Box(myBoxModifier, contentAlignment = TopEnd) {
|
||||
BaseUserPicture(baseUser, size, accountViewModel, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BaseUserPicture(
|
||||
baseUser: User,
|
||||
size: Dp,
|
||||
accountViewModel: AccountViewModel,
|
||||
modifier: Modifier = remember { Modifier }
|
||||
) {
|
||||
val userPubkey = remember {
|
||||
baseUser.pubkeyHex
|
||||
}
|
||||
|
||||
val userProfile by baseUser.live().metadata.map {
|
||||
it.user.profilePicture()
|
||||
}.distinctUntilChanged().observeAsState(baseUser.profilePicture())
|
||||
|
||||
val myBoxModifier = remember {
|
||||
Modifier.size(size)
|
||||
}
|
||||
|
||||
Box(myBoxModifier, contentAlignment = TopEnd) {
|
||||
PictureAndFollowingMark(
|
||||
userHex = userPubkey,
|
||||
userPicture = userProfile,
|
||||
size = size,
|
||||
modifier = modifier,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PictureAndFollowingMark(
|
||||
userHex: String,
|
||||
userPicture: String?,
|
||||
size: Dp,
|
||||
modifier: Modifier,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val backgroundColor = MaterialTheme.colors.background
|
||||
val myImageModifier = remember {
|
||||
modifier
|
||||
.size(size)
|
||||
.clip(shape = CircleShape)
|
||||
.background(backgroundColor)
|
||||
}
|
||||
|
||||
RobohashAsyncImageProxy(
|
||||
robot = userHex,
|
||||
model = userPicture,
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = myImageModifier,
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
|
||||
val myIconSize by remember(size) {
|
||||
derivedStateOf {
|
||||
size.div(3.5f)
|
||||
}
|
||||
}
|
||||
ObserveAndDisplayFollowingMark(userHex, myIconSize, accountViewModel)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ObserveAndDisplayFollowingMark(userHex: String, iconSize: Dp, accountViewModel: AccountViewModel) {
|
||||
WatchFollows(userHex, accountViewModel) {
|
||||
Crossfade(targetState = it) {
|
||||
if (it) {
|
||||
Box(contentAlignment = TopEnd) {
|
||||
FollowingIcon(iconSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchFollows(userHex: String, accountViewModel: AccountViewModel, onFollowChanges: @Composable (Boolean) -> Unit) {
|
||||
val showFollowingMark by accountViewModel.userFollows.map {
|
||||
it.user.isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
}.distinctUntilChanged().observeAsState(
|
||||
accountViewModel.account.userProfile().isFollowingCached(userHex) || (userHex == accountViewModel.account.userProfile().pubkeyHex)
|
||||
)
|
||||
|
||||
onFollowChanges(showFollowingMark)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FollowingIcon(iconSize: Dp) {
|
||||
val modifier = remember {
|
||||
Modifier.size(iconSize)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.verified_follow_shield),
|
||||
contentDescription = stringResource(id = R.string.following),
|
||||
modifier = modifier,
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
data class DropDownParams(
|
||||
val isFollowingAuthor: Boolean,
|
||||
val isPrivateBookmarkNote: Boolean,
|
||||
val isPublicBookmarkNote: Boolean,
|
||||
val isLoggedUser: Boolean,
|
||||
val isSensitive: Boolean,
|
||||
val showSensitiveContent: Boolean?
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
|
||||
var reportDialogShowing by remember { mutableStateOf(false) }
|
||||
|
||||
var state by remember {
|
||||
mutableStateOf<DropDownParams>(
|
||||
DropDownParams(
|
||||
isFollowingAuthor = false,
|
||||
isPrivateBookmarkNote = false,
|
||||
isPublicBookmarkNote = false,
|
||||
isLoggedUser = false,
|
||||
isSensitive = false,
|
||||
showSensitiveContent = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = popupExpanded,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val appContext = LocalContext.current.applicationContext
|
||||
val actContext = LocalContext.current
|
||||
|
||||
WatchBookmarksFollowsAndAccount(note, accountViewModel) { newState ->
|
||||
if (state != newState) {
|
||||
state = newState
|
||||
}
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
if (!state.isFollowingAuthor) {
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.follow(
|
||||
note.author ?: return@DropdownMenuItem
|
||||
); onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.follow))
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: ""))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.copy_text))
|
||||
}
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:${note.author?.pubkeyNpub()}"))
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.copy_user_pubkey))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
clipboardManager.setText(AnnotatedString("nostr:" + note.toNEvent()))
|
||||
onDismiss()
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.copy_note_id))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
externalLinkForNote(note)
|
||||
)
|
||||
putExtra(Intent.EXTRA_TITLE, actContext.getString(R.string.quick_action_share_browser_link))
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, appContext.getString(R.string.quick_action_share))
|
||||
ContextCompat.startActivity(actContext, shareIntent, null)
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.quick_action_share))
|
||||
}
|
||||
Divider()
|
||||
if (state.isPrivateBookmarkNote) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePrivateBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.remove_from_private_bookmarks))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPrivateBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.add_to_private_bookmarks))
|
||||
}
|
||||
}
|
||||
if (state.isPublicBookmarkNote) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.removePublicBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.remove_from_public_bookmarks))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.addPublicBookmark(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.add_to_public_bookmarks))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.broadcast(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.broadcast))
|
||||
}
|
||||
Divider()
|
||||
if (state.isLoggedUser) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.delete(note); onDismiss() } }) {
|
||||
Text(stringResource(R.string.request_deletion))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
|
||||
Text("Block / Report")
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
if (state.showSensitiveContent == null || state.showSensitiveContent == true) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.hideSensitiveContent(); onDismiss() } }) {
|
||||
Text(stringResource(R.string.content_warning_hide_all_sensitive_content))
|
||||
}
|
||||
}
|
||||
if (state.showSensitiveContent == null || state.showSensitiveContent == false) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.disableContentWarnings(); onDismiss() } }) {
|
||||
Text(stringResource(R.string.content_warning_show_all_sensitive_content))
|
||||
}
|
||||
}
|
||||
if (state.showSensitiveContent != null) {
|
||||
DropdownMenuItem(onClick = { scope.launch(Dispatchers.IO) { accountViewModel.seeContentWarnings(); onDismiss() } }) {
|
||||
Text(stringResource(R.string.content_warning_see_warnings))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reportDialogShowing) {
|
||||
ReportNoteDialog(note = note, accountViewModel = accountViewModel) {
|
||||
reportDialogShowing = false
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WatchBookmarksFollowsAndAccount(note: Note, accountViewModel: AccountViewModel, onNew: (DropDownParams) -> Unit) {
|
||||
val followState by accountViewModel.userProfile().live().follows.observeAsState()
|
||||
val bookmarkState by accountViewModel.userProfile().live().bookmarks.observeAsState()
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
|
||||
LaunchedEffect(key1 = followState, key2 = bookmarkState, key3 = accountState) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newState = DropDownParams(
|
||||
isFollowingAuthor = accountViewModel.isFollowing(note.author),
|
||||
isPrivateBookmarkNote = accountViewModel.isInPrivateBookmarks(note),
|
||||
isPublicBookmarkNote = accountViewModel.isInPublicBookmarks(note),
|
||||
isLoggedUser = accountViewModel.isLoggedUser(note.author),
|
||||
isSensitive = note.event?.isSensitive() ?: false,
|
||||
showSensitiveContent = accountState?.account?.showSensitiveContent
|
||||
)
|
||||
|
||||
launch(Dispatchers.Main) {
|
||||
onNew(
|
||||
newState
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -86,7 +87,7 @@ class PollNoteViewModel : ViewModel() {
|
||||
fun isVoteAmountAtomic() = valueMaximum != null && valueMinimum != null && valueMinimum == valueMaximum
|
||||
|
||||
fun isPollClosed(): Boolean = closedAt?.let { // allow 2 minute leeway for zap to propagate
|
||||
pollNote?.createdAt()?.plus(it * (86400 + 120))!! < Date().time / 1000
|
||||
pollNote?.createdAt()?.plus(it * (86400 + 120))!! < TimeUtils.now()
|
||||
} == true
|
||||
|
||||
fun voteAmountPlaceHolderText(sats: String): String = if (valueMinimum == null && valueMaximum == null) {
|
||||
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user