From 7ea5be01521ce5cfbfd6bbaa22860c900b4687b4 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 10 Jul 2023 13:50:49 -0400 Subject: [PATCH] Refactoring of Badge Box codes and Time classes --- .../com/vitorpamplona/amethyst/model/Note.kt | 6 +- .../vitorpamplona/amethyst/model/TimeUtils.kt | 13 + .../amethyst/service/NostrDataSource.kt | 4 +- .../amethyst/service/model/AudioTrackEvent.kt | 4 +- .../service/model/BookmarkListEvent.kt | 4 +- .../service/model/ChannelCreateEvent.kt | 4 +- .../service/model/ChannelHideMessageEvent.kt | 4 +- .../service/model/ChannelMessageEvent.kt | 4 +- .../service/model/ChannelMetadataEvent.kt | 4 +- .../service/model/ChannelMuteUserEvent.kt | 4 +- .../service/model/CommunityDefinitionEvent.kt | 4 +- .../model/CommunityPostApprovalEvent.kt | 4 +- .../service/model/ContactListEvent.kt | 4 +- .../amethyst/service/model/DeletionEvent.kt | 4 +- .../amethyst/service/model/Event.kt | 3 +- .../amethyst/service/model/FileHeaderEvent.kt | 4 +- .../service/model/FileStorageEvent.kt | 4 +- .../service/model/FileStorageHeaderEvent.kt | 4 +- .../service/model/GenericRepostEvent.kt | 4 +- .../service/model/HTTPAuthorizationEvent.kt | 4 +- .../amethyst/service/model/HighlightEvent.kt | 4 +- .../model/LiveActivitiesChatMessageEvent.kt | 4 +- .../service/model/LiveActivitiesEvent.kt | 6 +- .../service/model/LnZapPaymentRequestEvent.kt | 4 +- .../service/model/LnZapRequestEvent.kt | 4 +- .../service/model/LongTextNoteEvent.kt | 4 +- .../amethyst/service/model/MetadataEvent.kt | 4 +- .../amethyst/service/model/MuteListEvent.kt | 4 +- .../amethyst/service/model/PeopleListEvent.kt | 4 +- .../amethyst/service/model/PinListEvent.kt | 4 +- .../amethyst/service/model/PollNoteEvent.kt | 4 +- .../amethyst/service/model/PrivateDmEvent.kt | 4 +- .../amethyst/service/model/ReactionEvent.kt | 8 +- .../service/model/RecommendRelayEvent.kt | 4 +- .../amethyst/service/model/RelayAuthEvent.kt | 4 +- .../amethyst/service/model/RelaySetEvent.kt | 4 +- .../amethyst/service/model/ReportEvent.kt | 6 +- .../amethyst/service/model/RepostEvent.kt | 4 +- .../amethyst/service/model/TextNoteEvent.kt | 4 +- .../amethyst/service/relays/Relay.kt | 14 +- .../amethyst/ui/dal/DiscoverChatFeedFilter.kt | 3 +- .../ui/dal/DiscoverCommunityFeedFilter.kt | 3 +- .../amethyst/ui/dal/DiscoverLiveFeedFilter.kt | 3 +- .../ui/dal/HomeConversationsFeedFilter.kt | 3 +- .../ui/dal/HomeNewThreadFeedFilter.kt | 4 +- .../amethyst/ui/dal/VideoFeedFilter.kt | 3 +- .../ui/note/ChatroomMessageCompose.kt | 150 +---- .../ui/note/NIP05VerificationDisplay.kt | 26 +- .../amethyst/ui/note/NoteCompose.kt | 541 ------------------ .../amethyst/ui/note/PollNoteViewModel.kt | 3 +- .../amethyst/ui/note/RelayListBox.kt | 136 +++++ .../amethyst/ui/note/RelayListRow.kt | 174 ++++++ .../amethyst/ui/note/UserProfilePicture.kt | 483 ++++++++++++++++ 53 files changed, 921 insertions(+), 799 deletions(-) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index cbc421b71..69758e4dc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -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 { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt new file mode 100644 index 000000000..348a46837 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/TimeUtils.kt @@ -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) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt index 3f8ad9386..1803d1423 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrDataSource.kt @@ -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) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt index e7c543dd2..063802c46 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AudioTrackEvent.kt @@ -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), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt index d11b079a1..d342e52e1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/BookmarkListEvent.kt @@ -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? = 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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt index 8b237c06d..205e2674c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelCreateEvent.kt @@ -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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt index 825f9d1a2..7379df593 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelHideMessageEvent.kt @@ -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?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelHideMessageEvent { + fun create(reason: String, messagesToHide: List?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelHideMessageEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = messagesToHide?.map { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt index 6b71d8692..03ab09130 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMessageEvent.kt @@ -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? = null, zapReceiver: String?, privateKey: ByteArray, - createdAt: Long = Date().time / 1000, + createdAt: Long = TimeUtils.now(), markAsSensitive: Boolean, zapRaiserAmount: Long? ): ChannelMessageEvent { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt index 94bb3585d..ea5540c4b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMetadataEvent.kt @@ -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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt index d1dd832c7..9525d55be 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ChannelMuteUserEvent.kt @@ -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?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ChannelMuteUserEvent { + fun create(reason: String, usersToMute: List?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ChannelMuteUserEvent { val content = reason val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityDefinitionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityDefinitionEvent.kt index 9c0901c8d..bfe7dd8a6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityDefinitionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityDefinitionEvent.kt @@ -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>() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityPostApprovalEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityPostApprovalEvent.kt index 7e7f1ca3a..9f4aac622 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityPostApprovalEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/CommunityPostApprovalEvent.kt @@ -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()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt index a0ebcc36a..5a8ad5d70 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ContactListEvent.kt @@ -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, followTags: List, relayUse: Map?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ContactListEvent { + fun create(follows: List, followTags: List, relayUse: Map?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): ContactListEvent { val content = if (relayUse != null) { gson.toJson(relayUse) } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt index 9c3613569..ed6316e42 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/DeletionEvent.kt @@ -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, privateKey: ByteArray, createdAt: Long = Date().time / 1000): DeletionEvent { + fun create(deleteEvents: List, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): DeletionEvent { val content = "" val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = deleteEvents.map { listOf("e", it) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt index c7b60b80c..ddc21ee35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/Event.kt @@ -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> = emptyList(), content: String = "", createdAt: Long = Date().time / 1000): Event { + fun create(privateKey: ByteArray, kind: Int, tags: List> = 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() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt index 85b6557e3..1a95c7595 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt @@ -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), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageEvent.kt index 13fc5d03d..533432102 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageEvent.kt @@ -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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt index c8c5e2c9d..5d5dc6f14 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileStorageHeaderEvent.kt @@ -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), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GenericRepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GenericRepostEvent.kt index a1994c825..bd816408b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/GenericRepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/GenericRepostEvent.kt @@ -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()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HTTPAuthorizationEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HTTPAuthorizationEvent.kt index ebca0b1a9..57c7989d6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HTTPAuthorizationEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HTTPAuthorizationEvent.kt @@ -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") diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt index 6bb49cb08..412d2087c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt @@ -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>() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesChatMessageEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesChatMessageEvent.kt index 60a4c0868..e5995e8f8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesChatMessageEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesChatMessageEvent.kt @@ -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? = null, zapReceiver: String?, privateKey: ByteArray, - createdAt: Long = Date().time / 1000, + createdAt: Long = TimeUtils.now(), markAsSensitive: Boolean, zapRaiserAmount: Long? ): LiveActivitiesChatMessageEvent { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesEvent.kt index 084e6a277..17fdc331f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LiveActivitiesEvent.kt @@ -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>() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentRequestEvent.kt index 9975ae814..b5f759c3a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapPaymentRequestEvent.kt @@ -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)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt index bdb77b3d4..cc6a7c667 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapRequestEvent.kt @@ -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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt index f1206678c..d37d4e58a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt @@ -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?, mentions: List?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent { + fun create(msg: String, replyTos: List?, mentions: List?, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): LongTextNoteEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() replyTos?.forEach { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt index 786e1058e..e73772582 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt @@ -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, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { + fun create(contactMetaData: String, identities: List, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): MetadataEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt index afd65cfb7..6f1237d92 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MuteListEvent.kt @@ -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? = null, privateKey: ByteArray, - createdAt: Long = Date().time / 1000 + createdAt: Long = TimeUtils.now() ): MuteListEvent { val pubKey = Utils.pubkeyCreate(privateKey) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt index 51d9fa305..69e4a7f31 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PeopleListEvent.kt @@ -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? = 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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt index e5f6e69a2..d1319593e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PinListEvent.kt @@ -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, privateKey: ByteArray, - createdAt: Long = Date().time / 1000 + createdAt: Long = TimeUtils.now() ): PinListEvent { val tags = mutableListOf>() pins.forEach { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt index b6ccc01f6..5c9b72764 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PollNoteEvent.kt @@ -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?, addresses: List?, privateKey: ByteArray, - createdAt: Long = Date().time / 1000, + createdAt: Long = TimeUtils.now(), pollOptions: Map, valueMaximum: Int?, valueMinimum: Int?, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt index 1144c4f3a..43cbc1174 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/PrivateDmEvent.kt @@ -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? = null, zapReceiver: String?, privateKey: ByteArray, - createdAt: Long = Date().time / 1000, + createdAt: Long = TimeUtils.now(), publishedRecipientPubKey: ByteArray? = null, advertiseNip18: Boolean = true, markAsSensitive: Boolean, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt index e2a23ecb7..bfde96a4c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReactionEvent.kt @@ -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())) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt index 8cdc833dc..6f1e7fb17 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RecommendRelayEvent.kt @@ -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>() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt index 388daefdc..cbc8ae6ed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelayAuthEvent.kt @@ -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( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelaySetEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelaySetEvent.kt index 2aa96885b..260adaacd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelaySetEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RelaySetEvent.kt @@ -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, privateKey: ByteArray, - createdAt: Long = Date().time / 1000 + createdAt: Long = TimeUtils.now() ): RelaySetEvent { val tags = mutableListOf>() relays.forEach { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt index cdb075f3a..d137f3e9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ReportEvent.kt @@ -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()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt index e58f6b7bb..9fb0da2d2 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/RepostEvent.kt @@ -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()) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt index b3c7fbb28..577c557bc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/TextNoteEvent.kt @@ -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, privateKey: ByteArray, - createdAt: Long = Date().time / 1000 + createdAt: Long = TimeUtils.now() ): TextNoteEvent { val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 9fc4624f5..4fe979957 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -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() } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt index b5399c8e9..ca6fd4b80 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverChatFeedFilter.kt @@ -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() { @@ -25,7 +26,7 @@ open class DiscoverChatFeedFilter(val account: Account) : AdditiveFeedFilter): Set { - val now = System.currentTimeMillis() / 1000 + val now = TimeUtils.now() val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt index 3810b9a8c..1c89d9544 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverCommunityFeedFilter.kt @@ -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() { @@ -25,7 +26,7 @@ open class DiscoverCommunityFeedFilter(val account: Account) : AdditiveFeedFilte } protected open fun innerApplyFilter(collection: Collection): Set { - val now = System.currentTimeMillis() / 1000 + val now = TimeUtils.now() val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt index e9be4ea1a..a53667f69 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/DiscoverLiveFeedFilter.kt @@ -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): Set { - val now = System.currentTimeMillis() / 1000 + val now = TimeUtils.now() val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS val followingKeySet = diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt index e4861cfe6..af5686bde 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/HomeConversationsFeedFilter.kt @@ -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() { override fun feedKey(): String { @@ -35,7 +35,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter() 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index c47f1e2a7..d8f34198d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -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() { @@ -22,7 +23,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { } private fun innerApplyFilter(collection: Collection): Set { - val now = System.currentTimeMillis() / 1000 + val now = TimeUtils.now() val isGlobal = account.defaultStoriesFollowList == GLOBAL_FOLLOWS val followingKeySet = account.selectedUsersFollowList(account.defaultStoriesFollowList) ?: emptySet() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index d3e3f7327..e829302d6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -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, 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, - 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 - ) -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt index 8734b6c5d..0b72a65a8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NIP05VerificationDisplay.kt @@ -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 { - val nip05Verified = remember(user.nip05) { +fun nip05VerificationAsAState(userMetadata: UserMetadata, pubkeyHex: String): MutableState { + 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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 53e941cc7..5d1f51c25 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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) -> 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, - 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( - 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 - ) - } - } - } -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt index 59b6fbb78..828568ea8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/PollNoteViewModel.kt @@ -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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt new file mode 100644 index 000000000..1796f8171 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListBox.kt @@ -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) -> 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, + 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 + ) + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt new file mode 100644 index 000000000..4c738c091 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/RelayListRow.kt @@ -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, 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, + 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 + ) +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt new file mode 100644 index 000000000..c59f54aa4 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UserProfilePicture.kt @@ -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( + 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 + ) + } + } + } +}