Merge branch 'main' into amber

This commit is contained in:
greenart7c3
2023-08-23 07:28:58 -03:00
committed by GitHub
66 changed files with 820 additions and 670 deletions
+4 -4
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 276
versionName "0.74.3"
versionCode 278
versionName "0.74.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -88,7 +88,7 @@ android {
dependencies {
implementation project(path: ':quartz')
implementation 'androidx.core:core-ktx:1.10.1'
implementation "androidx.core:core-ktx:$core_ktx_version"
implementation 'androidx.activity:activity-compose:1.7.2'
implementation "androidx.compose.ui:ui:$compose_ui_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
@@ -112,7 +112,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Zoomable images
implementation "net.engawapg.lib:zoomable:1.4.3"
implementation "net.engawapg.lib:zoomable:1.5.0"
// Biometrics
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
@@ -21,7 +21,7 @@ class ImageUploadTesting {
val image = "R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw=="
fun testBase(server: FileServer) {
private fun testBase(server: FileServer) {
val bytes = Base64.getDecoder().decode(image)
val inputStream = bytes.inputStream()
@@ -39,7 +39,7 @@ class ImageUploadTesting {
"image/gif",
server,
onSuccess = { newUrl, contentType ->
println("Uploaded to $url")
println("Uploaded $contentType to $url")
url = newUrl
countDownLatch.countDown()
},
@@ -4128,7 +4128,7 @@ https://nostr.build/i/fd53fcf5ad950fbe45127e4bcee1b59e8301d41de6beee211f45e344db
}
private fun printStateForDebug(state: RichTextViewerState) {
state.paragraphs.forEachIndexed { index, paragraph ->
state.paragraphs.forEach { paragraph ->
paragraph.words.forEach { seg ->
println(
"\"${
@@ -205,6 +205,10 @@ object LocalPreferences {
}
}
fun allLocalAccountNPubs(): Set<String> {
return savedAccounts().toSet()
}
fun saveToEncryptedStorage(account: Account) {
val prefs = encryptedPreferences(account.userProfile().pubkeyNpub())
prefs.edit().apply {
@@ -31,6 +31,7 @@ import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.actions.ImageUploader
import com.vitorpamplona.quartz.encoders.decodePublicKeyAsHexOrNull
import java.io.File
object ServiceManager {
@@ -124,11 +125,14 @@ object ServiceManager {
fun cleanUp() {
LocalCache.cleanObservers()
val accounts = LocalPreferences.allLocalAccountNPubs().mapNotNull { decodePublicKeyAsHexOrNull(it) }.toSet()
account?.let {
LocalCache.pruneOldAndHiddenMessages(it)
LocalCache.pruneHiddenMessages(it)
LocalCache.pruneContactLists(it)
LocalCache.pruneRepliesAndReactions(it)
LocalCache.pruneContactLists(accounts)
LocalCache.pruneRepliesAndReactions(accounts)
LocalCache.prunePastVersionsOfReplaceables()
}
}
}
@@ -204,46 +204,6 @@ object LocalCache {
}
}
fun consume(event: PeopleListEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
private fun consume(event: AdvertisedRelayListEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
fun formattedDateTime(timestamp: Long): String {
return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault())
.format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a"))
@@ -349,25 +309,6 @@ object LocalCache {
refreshObservers(note)
}
private fun consume(event: CommunityDefinitionEvent, relay: Relay?) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
private fun consume(event: LiveActivitiesEvent, relay: Relay?) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
@@ -392,140 +333,17 @@ object LocalCache {
}
}
fun consume(event: EmojiPackSelectionEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
fun consume(event: PeopleListEvent) { consumeBaseReplaceable(event) }
private fun consume(event: AdvertisedRelayListEvent) { consumeBaseReplaceable(event) }
private fun consume(event: CommunityDefinitionEvent, relay: Relay?) { consumeBaseReplaceable(event) }
fun consume(event: EmojiPackSelectionEvent) { consumeBaseReplaceable(event) }
private fun consume(event: EmojiPackEvent) { consumeBaseReplaceable(event) }
private fun consume(event: ClassifiedsEvent) { consumeBaseReplaceable(event) }
private fun consume(event: PinListEvent) { consumeBaseReplaceable(event) }
private fun consume(event: RelaySetEvent) { consumeBaseReplaceable(event) }
private fun consume(event: AudioTrackEvent) { consumeBaseReplaceable(event) }
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
private fun consume(event: EmojiPackEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
private fun consume(event: ClassifiedsEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
private fun consume(event: PinListEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
private fun consume(event: RelaySetEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
private fun consume(event: AudioTrackEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
fun consume(event: BadgeDefinitionEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList<Note>())
refreshObservers(note)
}
}
fun consume(event: BadgeDefinitionEvent) { consumeBaseReplaceable(event) }
fun consume(event: BadgeProfilesEvent) {
val version = getOrCreateNote(event.id)
@@ -541,7 +359,7 @@ object LocalCache {
if (note.event?.id() == event.id()) return
val replyTo = event.badgeAwardEvents().mapNotNull { checkGetOrCreateNote(it) } +
event.badgeAwardDefinitions().mapNotNull { getOrCreateAddressableNote(it) }
event.badgeAwardDefinitions().map { getOrCreateAddressableNote(it) }
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, replyTo)
@@ -571,7 +389,14 @@ object LocalCache {
refreshObservers(note)
}
private fun comsume(event: NNSEvent) {
private fun comsume(event: NNSEvent) { consumeBaseReplaceable(event) }
fun consume(event: AppDefinitionEvent) { consumeBaseReplaceable(event) }
private fun consume(event: CalendarEvent) { consumeBaseReplaceable(event) }
private fun consume(event: CalendarDateSlotEvent) { consumeBaseReplaceable(event) }
private fun consume(event: CalendarTimeSlotEvent) { consumeBaseReplaceable(event) }
private fun consume(event: CalendarRSVPEvent) { consumeBaseReplaceable(event) }
private fun consumeBaseReplaceable(event: BaseAddressableEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
@@ -591,45 +416,7 @@ object LocalCache {
}
}
fun consume(event: AppDefinitionEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
fun consume(event: AppRecommendationEvent) {
val version = getOrCreateNote(event.id)
val note = getOrCreateAddressableNote(event.address())
val author = getOrCreateUser(event.pubKey)
if (version.event == null) {
version.loadEvent(event, author, emptyList())
version.moveAllReferencesTo(note)
}
// Already processed this event.
if (note.event?.id() == event.id()) return
if (event.createdAt > (note.createdAt() ?: 0)) {
note.loadEvent(event, author, emptyList())
refreshObservers(note)
}
}
fun consume(event: AppRecommendationEvent) { consumeBaseReplaceable(event) }
@Suppress("UNUSED_PARAMETER")
fun consume(event: RecommendRelayEvent) {
@@ -729,7 +516,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey)
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, repliesTo)
@@ -751,7 +538,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey)
val repliesTo = event.boostedPost().mapNotNull { checkGetOrCreateNote(it) } +
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
event.taggedAddresses().map { getOrCreateAddressableNote(it) }
note.loadEvent(event, author, repliesTo)
@@ -1365,8 +1152,8 @@ object LocalCache {
// Doesn't need to clean up the replies and mentions.. Too small to matter.
// Counts the replies
it.replyTo?.forEach { _ ->
it.removeReply(it)
it.replyTo?.forEach { parent ->
parent.removeReply(it)
}
childrenToBeRemoved.addAll(it.removeAllChildNotes())
@@ -1389,8 +1176,8 @@ object LocalCache {
notes.remove(it.idHex)
// Counts the replies
it.replyTo?.forEach { _ ->
it.removeReply(it)
it.replyTo?.forEach { parent ->
parent.removeReply(it)
}
childrenToBeRemoved.addAll(it.removeAllChildNotes())
@@ -1405,9 +1192,46 @@ object LocalCache {
}
}
fun pruneRepliesAndReactions(account: Account) {
fun prunePastVersionsOfReplaceables() {
val toBeRemoved = notes.filter {
val noteEvent = it.value.event
if (noteEvent is AddressableEvent) {
noteEvent.createdAt() < (addressables[noteEvent.address().toTag()]?.event?.createdAt() ?: 0)
} else {
false
}
}.values
val childrenToBeRemoved = mutableListOf<Note>()
toBeRemoved.forEach {
notes.remove(it.idHex)
val newerVersion = addressables[(it.event as? AddressableEvent)?.address()?.toTag()]
if (newerVersion != null) {
it.moveAllReferencesTo(newerVersion)
}
it.replyTo?.forEach { masterNote ->
masterNote.removeReply(it)
masterNote.removeBoost(it)
masterNote.removeReaction(it)
masterNote.removeZap(it)
masterNote.clearEOSE() // allows reloading of these events
}
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
removeChildrenOf(childrenToBeRemoved)
if (toBeRemoved.size > 1) {
println("PRUNE: ${toBeRemoved.size} old version of addressables removed.")
}
}
fun pruneRepliesAndReactions(accounts: Set<HexKey>) {
checkNotInMainThread()
val user = account.userProfile()
val toBeRemoved = notes.filter {
(
@@ -1416,8 +1240,8 @@ object LocalCache {
it.value.event is ReportEvent || it.value.event is GenericRepostEvent
) &&
it.value.liveSet?.isInUse() != true && // don't delete if observing.
it.value.author != user && // don't delete if it is the logged in account
it.value.event?.isTaggedUser(user.pubkeyHex) != true // don't delete if it's a notification to the logged in user
it.value.author?.pubkeyHex !in accounts && // don't delete if it is the logged in account
it.value.event?.isTaggedUsers(accounts) != true // don't delete if it's a notification to the logged in user
}.values
val childrenToBeRemoved = mutableListOf<Note>()
@@ -1430,12 +1254,15 @@ object LocalCache {
masterNote.removeBoost(it)
masterNote.removeReaction(it)
masterNote.removeZap(it)
masterNote.removeReport(it)
masterNote.clearEOSE() // allows reloading of these events
}
childrenToBeRemoved.addAll(it.removeAllChildNotes())
}
removeChildrenOf(childrenToBeRemoved)
if (toBeRemoved.size > 1) {
println("PRUNE: ${toBeRemoved.size} thread replies removed.")
}
@@ -1491,12 +1318,15 @@ object LocalCache {
}
}
fun pruneContactLists(userAccount: Account) {
fun pruneContactLists(loggedIn: Set<HexKey>) {
checkNotInMainThread()
var removingContactList = 0
users.values.forEach {
if (it != userAccount.userProfile() && (it.liveSet == null || it.liveSet?.isInUse() == false) && it.latestContactList != null) {
if (it.pubkeyHex !in loggedIn &&
(it.liveSet == null || it.liveSet?.isInUse() == false) &&
it.latestContactList != null
) {
it.latestContactList = null
removingContactList++
}
@@ -1547,6 +1377,10 @@ object LocalCache {
is BadgeDefinitionEvent -> consume(event)
is BadgeProfilesEvent -> consume(event)
is BookmarkListEvent -> consume(event)
is CalendarEvent -> consume(event)
is CalendarDateSlotEvent -> consume(event)
is CalendarTimeSlotEvent -> consume(event)
is CalendarRSVPEvent -> consume(event)
is ChannelCreateEvent -> consume(event)
is ChannelHideMessageEvent -> consume(event)
is ChannelMessageEvent -> consume(event, relay)
@@ -72,7 +72,22 @@ open class Note(val idHex: String) {
open fun idNote() = id().toNote()
open fun toNEvent(): String {
return Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
val myEvent = event
return if (myEvent is WrappedEvent) {
val host = myEvent.host
if (host != null) {
Nip19.createNEvent(
host.id,
host.pubKey,
host.kind(),
relays.firstOrNull()
)
} else {
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
}
} else {
Nip19.createNEvent(idHex, author?.pubkeyHex, event?.kind(), relays.firstOrNull())
}
}
fun toNostrUri(): String {
@@ -238,8 +253,7 @@ open class Note(val idHex: String) {
zaps = zaps.minus(note)
liveSet?.zaps?.invalidateData()
} else if (zaps.containsValue(note)) {
val toRemove = zaps.filterValues { it == note }
zaps = zaps.minus(toRemove.keys)
zaps = zaps.filterValues { it != note }
liveSet?.zaps?.invalidateData()
}
}
@@ -22,7 +22,7 @@ class ThreadAssembler {
}
}
val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true }
val hasNoReplyTo = note.replyTo?.reversed()?.firstOrNull { it.replyTo?.isEmpty() == true }
if (hasNoReplyTo != null) return hasNoReplyTo
// recursive
@@ -10,12 +10,15 @@ import com.vitorpamplona.quartz.events.Event
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import java.util.UUID
import kotlin.Error
abstract class NostrDataSource(val debugName: String) {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private var subscriptions = mapOf<String, Subscription>()
data class Counter(var counter: Int)
@@ -76,6 +79,7 @@ abstract class NostrDataSource(val debugName: String) {
fun destroy() {
stop()
Client.unsubscribe(clientListener)
scope.cancel()
bundler.cancel()
}
@@ -117,8 +121,7 @@ abstract class NostrDataSource(val debugName: String) {
}
fun resetFilters() {
val scope = CoroutineScope(Job() + Dispatchers.IO)
scope.launch {
scope.launch(Dispatchers.IO) {
resetFiltersSuspend()
}
}
@@ -284,16 +284,36 @@ class Relay(
checkNotInMainThread()
if (signedEvent is RelayAuthEvent) {
// specific protocol for this event.
val event = """["AUTH",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
}
} else {
if (write) {
if (isConnected()) {
if (isReady) {
val event = """["EVENT",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
}
} else {
// waits 60 seconds to reconnect after disconnected.
if (TimeUtils.now() > closingTimeInSeconds + RECONNECTING_IN_SECONDS) {
// sends all filters after connection is successful.
connectAndRun {
checkNotInMainThread()
if (write) {
if (signedEvent !is RelayAuthEvent) {
val event = """["EVENT",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
val event = """["EVENT",${signedEvent.toJson()}]"""
socket?.send(event)
eventUploadCounterInBytes += event.bytesUsedInMemory()
// Sends everything.
Client.allSubscriptions().forEach {
sendFilter(requestId = it)
}
}
}
}
}
}
}
@@ -42,6 +42,7 @@ import androidx.compose.runtime.remember
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@@ -910,19 +911,18 @@ fun DisplayLocationInTitle(geohash: String) {
fun Notifying(baseMentions: ImmutableList<User>?, onClick: (User) -> Unit) {
val mentions = baseMentions?.toSet()
FlowRow(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 10.dp)) {
FlowRow(horizontalArrangement = Arrangement.spacedBy(5.dp)) {
if (!mentions.isNullOrEmpty()) {
Text(
stringResource(R.string.reply_notify),
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.placeholderText
color = MaterialTheme.colors.placeholderText,
modifier = Modifier.align(CenterVertically)
)
mentions.forEachIndexed { idx, user ->
val innerUserState by user.live().metadata.observeAsState()
innerUserState?.user?.let { myUser ->
Spacer(modifier = Modifier.width(5.dp))
val tags = remember(innerUserState) {
myUser.info?.latestMetadata?.tags?.toImmutableListOfLists()
}
@@ -35,7 +35,7 @@ class BundledUpdate(
return
}
scope.launch {
scope.launch(dispatcher) {
try {
onUpdate()
delay(delay)
@@ -77,7 +77,7 @@ class BundledInsert<T>(
return
}
scope.launch(Dispatchers.IO) {
scope.launch(dispatcher) {
try {
val mySet = mutableSetOf<T>()
queue.drainTo(mySet)
@@ -18,7 +18,6 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -30,9 +29,8 @@ import com.vitorpamplona.amethyst.ui.theme.placeholderText
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SlidingCarousel(
pagerState: PagerState,
modifier: Modifier = Modifier,
pagerState: PagerState = remember { PagerState() },
itemsCount: Int,
itemContent: @Composable (index: Int) -> Unit
) {
val isDragged by pagerState.interactionSource.collectIsDraggedAsState()
@@ -40,7 +38,7 @@ fun SlidingCarousel(
Box(
modifier = modifier.fillMaxWidth()
) {
HorizontalPager(pageCount = itemsCount, state = pagerState) { page ->
HorizontalPager(state = pagerState) { page ->
itemContent(page)
}
@@ -55,7 +53,7 @@ fun SlidingCarousel(
) {
DotsIndicator(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
totalDots = itemsCount,
totalDots = pagerState.pageCount,
selectedIndex = if (isDragged) pagerState.currentPage else pagerState.targetPage,
dotSize = 8.dp
)
@@ -637,7 +637,7 @@ fun ZoomableImageDialog(
) {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) {
val pagerState: PagerState = rememberPagerState()
val pagerState: PagerState = rememberPagerState() { allImages.size }
LaunchedEffect(key1 = pagerState, key2 = imageUrl) {
val page = allImages.indexOf(imageUrl)
@@ -648,12 +648,10 @@ fun ZoomableImageDialog(
if (allImages.size > 1) {
SlidingCarousel(
pagerState = pagerState,
itemsCount = allImages.size,
itemContent = { index ->
RenderImageOrVideo(allImages[index], false, accountViewModel)
}
)
pagerState = pagerState
) { index ->
RenderImageOrVideo(allImages[index], false, accountViewModel)
}
} else {
RenderImageOrVideo(imageUrl, false, accountViewModel)
}
@@ -15,6 +15,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.AppBarDefaults
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Divider
@@ -91,6 +93,7 @@ import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader
import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures
import com.vitorpamplona.amethyst.ui.note.SearchIcon
import com.vitorpamplona.amethyst.ui.note.ShortCommunityHeader
import com.vitorpamplona.amethyst.ui.note.UserCompose
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -107,8 +110,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowVideoStreaming
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.Size40dp
@@ -231,7 +234,9 @@ private fun CommunityTopBar(
ShortCommunityHeader(baseNote, fontWeight = FontWeight.Medium, accountViewModel, nav)
},
extendableRow = {
LongCommunityHeader(baseNote, accountViewModel, nav)
Column(Modifier.verticalScroll(rememberScrollState())) {
LongCommunityHeader(baseNote = baseNote, accountViewModel = accountViewModel, nav = nav)
}
},
popBack = navPopBack
)
@@ -297,6 +302,17 @@ private fun RenderRoomTopBar(
}
}
},
extendableRow = {
LoadUser(baseUserHex = room.users.first()) {
if (it != null) {
UserCompose(
baseUser = it,
accountViewModel = accountViewModel,
nav = nav
)
}
}
},
popBack = navPopBack
)
} else {
@@ -308,10 +324,17 @@ private fun RenderRoomTopBar(
size = Size34dp
)
RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp).weight(1f), fontWeight = FontWeight.Medium, accountViewModel.userProfile())
RoomNameOnlyDisplay(
room,
Modifier
.padding(start = 10.dp)
.weight(1f),
fontWeight = FontWeight.Medium,
accountViewModel.userProfile()
)
},
extendableRow = {
LongRoomHeader(room, accountViewModel, nav)
LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav)
},
popBack = navPopBack
)
@@ -342,7 +365,7 @@ private fun ChannelTopBar(
)
},
extendableRow = {
LongChannelHeader(baseChannel, accountViewModel, nav)
LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav)
},
popBack = navPopBack
)
@@ -680,6 +703,7 @@ fun FlexibleTopBarWithBackButton(
},
actions = {}
)
Spacer(modifier = HalfVertSpacer)
Divider(thickness = 0.25.dp)
}
}
@@ -861,9 +885,7 @@ fun MyExtensibleTopAppBar(
if (expanded.value && extendableRow != null) {
Row(
Modifier
.fillMaxWidth()
.padding(bottom = Size10dp),
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
@@ -770,7 +770,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Gallery(users: List<User>, accountViewModel: AccountViewModel) {
FlowRow(verticalAlignment = CenterVertically) {
FlowRow(verticalArrangement = Arrangement.Center) {
users.take(6).forEach {
ClickableUserPicture(it, Size35dp, accountViewModel)
}
@@ -23,9 +23,11 @@ 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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CutCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Divider
@@ -113,6 +115,7 @@ 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.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -124,6 +127,7 @@ import com.vitorpamplona.amethyst.ui.theme.HalfStartPadding
import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
import com.vitorpamplona.amethyst.ui.theme.Size16Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
@@ -517,7 +521,7 @@ fun CommunityHeader(
Column(Modifier.fillMaxWidth()) {
Column(
verticalArrangement = Arrangement.Center,
modifier = modifier.clickable {
modifier = Modifier.clickable {
if (sendToCommunity) {
routeFor(baseNote, accountViewModel.userProfile())?.let {
nav(it)
@@ -534,7 +538,14 @@ fun CommunityHeader(
)
if (expanded.value) {
LongCommunityHeader(baseNote, accountViewModel, nav)
Column(Modifier.verticalScroll(rememberScrollState())) {
LongCommunityHeader(
baseNote = baseNote,
lineModifier = modifier,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
@@ -547,23 +558,41 @@ fun CommunityHeader(
}
@Composable
fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun LongCommunityHeader(
baseNote: AddressableNote,
lineModifier: Modifier = Modifier.padding(horizontal = Size10dp, vertical = Size5dp),
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteState by baseNote.live().metadata.observeAsState()
val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return
Row(
Modifier
.fillMaxWidth()
.padding(top = 10.dp)
lineModifier
) {
val rulesLabel = stringResource(id = R.string.rules)
val summary = remember(noteState) {
noteEvent.description()?.ifBlank { null }
val subject = noteEvent.subject()?.ifEmpty { null }
val body = noteEvent.description()?.ifBlank { null }
val rules = noteEvent.rules()?.ifBlank { null }
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
if (rules == null) {
"## $subject\n$body"
} else {
"## $subject\n$body\n\n## $rulesLabel\n\n$rules"
}
} else {
if (rules == null) {
body
} else {
"$body\n\n$rulesLabel\n$rules"
}
}
}
Column(
Modifier
.weight(1f)
.padding(start = 10.dp)
Modifier.weight(1f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
val defaultBackground = MaterialTheme.colors.background
@@ -593,40 +622,8 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
}
}
val rules = remember(noteState) {
noteEvent.rules()?.ifBlank { null }
}
rules?.let {
Spacer(DoubleVertSpacer)
Row(
Modifier
.fillMaxWidth()
.padding(start = 10.dp)
) {
val defaultBackground = MaterialTheme.colors.background
val background = remember {
mutableStateOf(defaultBackground)
}
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
TranslatableRichTextViewer(
content = it,
canPreview = false,
tags = tags,
backgroundColor = background,
accountViewModel = accountViewModel,
nav = nav
)
}
}
Spacer(DoubleVertSpacer)
Row(
Modifier
.fillMaxWidth()
.padding(start = 10.dp),
lineModifier,
verticalAlignment = Alignment.CenterVertically
) {
Text(
@@ -639,8 +636,6 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
NoteAuthorPicture(baseNote, nav, accountViewModel, Size25dp)
Spacer(DoubleHorzSpacer)
NoteUsernameDisplay(baseNote, remember { Modifier.weight(1f) })
TimeAgo(baseNote)
MoreOptionsButton(baseNote, accountViewModel)
}
var participantUsers by remember(baseNote) {
@@ -664,12 +659,9 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
participantUsers.forEach {
Row(
Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 10.dp)
.clickable {
nav("User/${it.second.pubkeyHex}")
},
lineModifier.clickable {
nav("User/${it.second.pubkeyHex}")
},
verticalAlignment = Alignment.CenterVertically
) {
it.first.role?.let { it1 ->
@@ -686,6 +678,21 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView
UsernameDisplay(it.second, remember { Modifier.weight(1f) })
}
}
Row(
lineModifier,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(id = R.string.created_at),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.width(75.dp)
)
Spacer(DoubleHorzSpacer)
NormalTimeAgo(baseNote = baseNote, Modifier.weight(1f))
MoreOptionsButton(baseNote, accountViewModel)
}
}
@Composable
@@ -3424,7 +3431,12 @@ fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: Accou
}
content?.let {
Row(verticalAlignment = CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) {
Row(
verticalAlignment = CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(top = 5.dp)
) {
TranslatableRichTextViewer(
content = it,
canPreview = true,
@@ -4,7 +4,7 @@ import android.content.Context
import android.util.Log
import android.widget.Toast
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.Crossfade
import androidx.compose.animation.ExperimentalAnimationApi
@@ -57,6 +57,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
@@ -119,7 +120,6 @@ import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
import kotlin.math.roundToInt
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
@Composable
@@ -146,7 +146,6 @@ fun ReactionsRow(
Spacer(modifier = HalfDoubleVertSpacer)
}
@OptIn(ExperimentalTime::class)
@Composable
private fun InnerReactionRow(
baseNote: Note,
@@ -621,14 +620,14 @@ private fun SlidingAnimationCount(baseCount: MutableState<Int>, textColor: Color
private fun SlidingAnimationCount(baseCount: Int, textColor: Color) {
AnimatedContent<Int>(
targetState = baseCount,
transitionSpec = AnimatedContentScope<Int>::transitionSpec
transitionSpec = AnimatedContentTransitionScope<Int>::transitionSpec
) { count ->
TextCount(count, textColor)
}
}
@OptIn(ExperimentalAnimationApi::class)
private fun <S> AnimatedContentScope<S>.transitionSpec(): ContentTransform {
private fun <S> AnimatedContentTransitionScope<S>.transitionSpec(): ContentTransform {
return slideAnimation
}
@@ -655,7 +654,7 @@ private fun TextCount(count: Int, textColor: Color) {
private fun SlidingAnimationAmount(amount: MutableState<String>, textColor: Color) {
AnimatedContent(
targetState = amount.value,
transitionSpec = AnimatedContentScope<String>::transitionSpec
transitionSpec = AnimatedContentTransitionScope<String>::transitionSpec
) { count ->
Text(
text = count,
@@ -736,25 +735,26 @@ fun BoostReaction(
}
) {
BoostIcon(baseNote, iconSize, grayTint, accountViewModel)
}
if (wantsToBoost) {
BoostTypeChoicePopup(
baseNote,
accountViewModel,
onDismiss = {
wantsToBoost = false
},
onQuote = {
wantsToBoost = false
onQuotePress()
},
onRepost = {
scope.launch(Dispatchers.IO) {
event = accountViewModel.boost(baseNote, false)
if (wantsToBoost) {
BoostTypeChoicePopup(
baseNote,
iconSize,
accountViewModel,
onDismiss = {
wantsToBoost = false
},
onQuote = {
wantsToBoost = false
onQuotePress()
}
}
)
onRepost = {
scope.launch(Dispatchers.IO) {
event = accountViewModel.boost(baseNote, false)
}
}
)
}
}
BoostText(baseNote, grayTint)
@@ -871,6 +871,7 @@ fun LikeReaction(
if (wantsToReact) {
ReactionChoicePopup(
baseNote,
iconSize,
accountViewModel,
onDismiss = {
wantsToReact = false
@@ -1306,10 +1307,14 @@ private fun DrawViewCount(
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit, onRepost: () -> Unit) {
private fun BoostTypeChoicePopup(baseNote: Note, iconSize: Dp, accountViewModel: AccountViewModel, onDismiss: () -> Unit, onQuote: () -> Unit, onRepost: () -> Unit) {
val iconSizePx = with(LocalDensity.current) {
-iconSize.toPx().toInt()
}
Popup(
alignment = Alignment.BottomCenter,
offset = IntOffset(0, -50),
offset = IntOffset(0, iconSizePx),
onDismissRequest = { onDismiss() }
) {
FlowRow {
@@ -1355,6 +1360,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
@Composable
fun ReactionChoicePopup(
baseNote: Note,
iconSize: Dp,
accountViewModel: AccountViewModel,
onDismiss: () -> Unit,
onChangeAmount: () -> Unit
@@ -1366,9 +1372,13 @@ fun ReactionChoicePopup(
baseNote.reactedBy(account.userProfile()).toSet()
}
val iconSizePx = with(LocalDensity.current) {
-iconSize.toPx().toInt()
}
Popup(
alignment = Alignment.BottomCenter,
offset = IntOffset(0, -50),
offset = IntOffset(0, iconSizePx),
onDismissRequest = { onDismiss() }
) {
FlowRow(horizontalArrangement = Arrangement.Center) {
@@ -13,18 +13,12 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.Size55dp
import com.vitorpamplona.amethyst.ui.theme.StdPadding
@Composable
fun UserCompose(
baseUser: User,
overallModifier: Modifier = remember {
Modifier
.padding(
start = 12.dp,
end = 12.dp,
top = 10.dp
)
},
overallModifier: Modifier = StdPadding,
showDiviser: Boolean = true,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
@@ -56,7 +50,6 @@ fun UserCompose(
if (showDiviser) {
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
}
@@ -57,6 +57,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.time.Instant
@@ -109,26 +110,58 @@ fun UserReactionsRow(
@Composable
private fun UserZapModel(model: UserReactionsViewModel) {
val zaps by model.zaps.collectAsState()
UserZapReaction(showAmountAxis(zaps[model.today()]))
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Size24Modifier,
tint = BitcoinOrange
)
Spacer(modifier = Modifier.width(8.dp))
UserZapReaction(model)
}
@Composable
private fun UserReactionModel(model: UserReactionsViewModel) {
val reactions by model.reactions.collectAsState()
UserLikeReaction(reactions[model.today()])
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = Size20Modifier,
tint = Color.Unspecified
)
Spacer(modifier = StdHorzSpacer)
UserLikeReaction(model)
}
@Composable
private fun UserBoostModel(model: UserReactionsViewModel) {
val boosts by model.boosts.collectAsState()
UserBoostReaction(boosts[model.today()])
Icon(
painter = painterResource(R.drawable.ic_retweeted),
null,
modifier = Size24Modifier,
tint = Color.Unspecified
)
Spacer(modifier = StdHorzSpacer)
UserBoostReaction(model)
}
@Composable
private fun UserReplyModel(model: UserReactionsViewModel) {
val replies by model.replies.collectAsState()
UserReplyReaction(replies[model.today()])
Icon(
painter = painterResource(R.drawable.ic_comment),
null,
modifier = Size20Modifier,
tint = RoyalBlue
)
Spacer(modifier = StdHorzSpacer)
UserReplyReaction(model)
}
@Stable
@@ -154,6 +187,11 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
private var takenIntoAccount = setOf<HexKey>()
private val sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd") // SimpleDateFormat()
val todaysReplyCount = _replies.map { showCount(it[today()]) }
val todaysBoostCount = _boosts.map { showCount(it[today()]) }
val todaysReactionCount = _reactions.map { showCount(it[today()]) }
val todaysZapAmount = _zaps.map { showAmountAxis(it[today()]) }
fun formatDate(createAt: Long): String {
return sdf.format(
Instant.ofEpochSecond(createAt)
@@ -339,18 +377,9 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
@Composable
fun UserReplyReaction(
replyCount: Int?
model: UserReactionsViewModel
) {
val showCounts = remember(replyCount) { showCount(replyCount) }
Icon(
painter = painterResource(R.drawable.ic_comment),
null,
modifier = Size20Modifier,
tint = RoyalBlue
)
Spacer(modifier = StdHorzSpacer)
val showCounts by model.todaysReplyCount.collectAsState("")
Text(
showCounts,
@@ -361,21 +390,12 @@ fun UserReplyReaction(
@Composable
fun UserBoostReaction(
boostCount: Int?
model: UserReactionsViewModel
) {
val showCounts = remember(boostCount) { showCount(boostCount) }
Icon(
painter = painterResource(R.drawable.ic_retweeted),
null,
modifier = Size24Modifier,
tint = Color.Unspecified
)
Spacer(modifier = StdHorzSpacer)
val boosts by model.todaysBoostCount.collectAsState("")
Text(
showCounts,
boosts,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
@@ -383,21 +403,12 @@ fun UserBoostReaction(
@Composable
fun UserLikeReaction(
likeCount: Int?
model: UserReactionsViewModel
) {
val showCounts = remember(likeCount) { showCount(likeCount) }
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = Size20Modifier,
tint = Color.Unspecified
)
Spacer(modifier = StdHorzSpacer)
val reactions by model.todaysReactionCount.collectAsState("")
Text(
text = showCounts,
text = reactions,
fontWeight = FontWeight.Bold,
fontSize = 18.sp
)
@@ -405,17 +416,9 @@ fun UserLikeReaction(
@Composable
fun UserZapReaction(
amount: String
model: UserReactionsViewModel
) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Size24Modifier,
tint = BitcoinOrange
)
Spacer(modifier = Modifier.width(8.dp))
val amount by model.todaysZapAmount.collectAsState("")
Text(
amount,
fontWeight = FontWeight.Bold,
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.saveable.rememberSaveable
@@ -61,17 +62,19 @@ fun rememberForeverLazyListState(
fun rememberForeverPagerState(
key: String,
initialFirstVisibleItemIndex: Int = 0,
initialFirstVisibleItemScrollOffset: Float = 0.0f
initialFirstVisibleItemScrollOffset: Float = 0.0f,
pageCount: () -> Int
): PagerState {
val scrollState = rememberSaveable(saver = PagerState.Saver) {
val savedValue = savedScrollStates[key]
val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex
val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset
PagerState(
savedIndex,
savedOffset
)
}
val savedValue = savedScrollStates[key]
val savedIndex = savedValue?.index ?: initialFirstVisibleItemIndex
val savedOffset = savedValue?.scrollOffsetFraction ?: initialFirstVisibleItemScrollOffset
val scrollState = rememberPagerState(
savedIndex,
savedOffset,
pageCount
)
DisposableEffect(scrollState) {
onDispose {
val lastIndex = scrollState.currentPage
@@ -79,5 +82,6 @@ fun rememberForeverPagerState(
savedScrollStates[key] = ScrollState(lastIndex, lastOffset)
}
}
return scrollState
}
@@ -50,7 +50,7 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
Column(Modifier.fillMaxHeight()) {
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
val pagerState = rememberPagerState()
val pagerState = rememberPagerState() { 2 }
val coroutineScope = rememberCoroutineScope()
TabRow(
@@ -73,7 +73,7 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit
}
)
}
HorizontalPager(pageCount = 2, state = pagerState) { page ->
HorizontalPager(state = pagerState) { page ->
when (page) {
0 -> RefresheableFeedView(
privateFeedViewModel,
@@ -108,12 +108,12 @@ import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.note.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.TimeAgo
import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.note.routeFor
import com.vitorpamplona.amethyst.ui.note.timeAgo
import com.vitorpamplona.amethyst.ui.note.timeAgoShort
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists
@@ -584,7 +584,7 @@ fun ChannelHeader(
)
if (expanded.value) {
LongChannelHeader(baseChannel, accountViewModel, nav)
LongChannelHeader(baseChannel = baseChannel, accountViewModel = accountViewModel, nav = nav)
}
}
@@ -726,6 +726,7 @@ fun ShortChannelHeader(
@Composable
fun LongChannelHeader(
baseChannel: Channel,
lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
@@ -735,18 +736,14 @@ fun LongChannelHeader(
} ?: return
Row(
Modifier
.fillMaxWidth()
.padding(top = 10.dp)
lineModifier
) {
val summary = remember(channelState) {
channel.summary()?.ifBlank { null }
}
Column(
Modifier
.weight(1f)
.padding(start = 10.dp)
Modifier.weight(1f)
) {
Row(verticalAlignment = Alignment.CenterVertically) {
val defaultBackground = MaterialTheme.colors.background
@@ -773,7 +770,9 @@ fun LongChannelHeader(
}
if (baseChannel is LiveActivitiesChannel) {
val hashtags = remember(baseChannel.info) { baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf() }
val hashtags = remember(baseChannel.info) {
baseChannel.info?.hashtags()?.toImmutableList() ?: persistentListOf()
}
DisplayUncitedHashtags(hashtags, summary ?: "", nav)
}
}
@@ -788,28 +787,37 @@ fun LongChannelHeader(
}
}
Spacer(DoubleVertSpacer)
Row(
Modifier
.fillMaxWidth()
.padding(start = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
LoadNote(baseNoteHex = channel.idHex) {
it?.let {
LoadNote(baseNoteHex = channel.idHex) { loadingNote ->
loadingNote?.let { note ->
Row(
lineModifier,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(id = R.string.owner),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.width(55.dp)
modifier = Modifier.width(75.dp)
)
Spacer(DoubleHorzSpacer)
NoteAuthorPicture(it, nav, accountViewModel, Size25dp)
NoteAuthorPicture(note, nav, accountViewModel, Size25dp)
Spacer(DoubleHorzSpacer)
NoteUsernameDisplay(it, remember { Modifier.weight(1f) })
TimeAgo(it)
MoreOptionsButton(it, accountViewModel)
NoteUsernameDisplay(note, remember { Modifier.weight(1f) })
}
Row(
lineModifier,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(id = R.string.created_at),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.width(75.dp)
)
Spacer(DoubleHorzSpacer)
NormalTimeAgo(note, remember { Modifier.weight(1f) })
MoreOptionsButton(note, accountViewModel)
}
}
}
@@ -835,9 +843,7 @@ fun LongChannelHeader(
participantUsers.forEach {
Row(
Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 10.dp)
lineModifier
.clickable {
nav("User/${it.second.pubkeyHex}")
},
@@ -860,6 +866,24 @@ fun LongChannelHeader(
}
}
@Composable
fun NormalTimeAgo(baseNote: Note, modifier: Modifier) {
val nowStr = stringResource(id = R.string.now)
val time by remember(baseNote) {
derivedStateOf {
timeAgoShort(baseNote.createdAt() ?: 0, nowStr)
}
}
Text(
text = time,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = modifier
)
}
@Composable
private fun ShortChannelActionOptions(
channel: PublicChatChannel,
@@ -57,7 +57,7 @@ fun ChatroomListScreen(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val pagerState = rememberPagerState()
val pagerState = rememberPagerState() { 2 }
val coroutineScope = rememberCoroutineScope()
var moreActionsExpanded by remember { mutableStateOf(false) }
@@ -136,7 +136,6 @@ fun ChatroomListScreen(
}
HorizontalPager(
pageCount = 2,
state = pagerState,
modifier = Modifier.fillMaxSize()
) { page ->
@@ -5,7 +5,6 @@ import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
@@ -603,7 +602,7 @@ fun GroupChatroomHeader(
}
if (expanded.value) {
LongRoomHeader(room, accountViewModel, nav)
LongRoomHeader(room = room, accountViewModel = accountViewModel, nav = nav)
}
}
@@ -740,15 +739,18 @@ fun NewSubjectView(onClose: () -> Unit, accountViewModel: AccountViewModel, room
}
@Composable
fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun LongRoomHeader(
room: ChatroomKey,
lineModifier: Modifier = StdPadding,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val list = remember(room) {
room.users.toPersistentList()
}
Row(
modifier = Modifier
.padding(top = 10.dp)
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
@@ -765,16 +767,18 @@ fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: (
}
LazyColumn(
modifier = Modifier.fillMaxHeight(),
contentPadding = PaddingValues(
bottom = 10.dp
),
modifier = Modifier,
state = rememberLazyListState()
) {
itemsIndexed(list, key = { _, item -> item }) { _, item ->
LoadUser(baseUserHex = item) {
if (it != null) {
UserCompose(baseUser = it, accountViewModel = accountViewModel, nav = nav)
UserCompose(
baseUser = it,
overallModifier = lineModifier,
accountViewModel = accountViewModel,
nav = nav
)
}
}
}
@@ -69,7 +69,17 @@ fun DiscoverScreen(
) {
val lifeCycleOwner = LocalLifecycleOwner.current
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN)
val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) {
mutableStateOf(
listOf(
TabItem(R.string.discover_live, discoveryLiveFeedViewModel, Route.Discover.base + "Live", ScrollStateKeys.DISCOVER_LIVE, LiveActivitiesEvent.kind),
TabItem(R.string.discover_community, discoveryCommunityFeedViewModel, Route.Discover.base + "Community", ScrollStateKeys.DISCOVER_COMMUNITY, CommunityDefinitionEvent.kind),
TabItem(R.string.discover_chat, discoveryChatFeedViewModel, Route.Discover.base + "Chats", ScrollStateKeys.DISCOVER_CHATS, ChannelCreateEvent.kind)
).toImmutableList()
)
}
val pagerState = rememberForeverPagerState(key = PagerStateKeys.DISCOVER_SCREEN) { tabs.size }
WatchAccountForDiscoveryScreen(
discoveryLiveFeedViewModel = discoveryLiveFeedViewModel,
@@ -92,16 +102,6 @@ fun DiscoverScreen(
}
}
val tabs by remember(discoveryLiveFeedViewModel, discoveryCommunityFeedViewModel, discoveryChatFeedViewModel) {
mutableStateOf(
listOf(
TabItem(R.string.discover_live, discoveryLiveFeedViewModel, Route.Discover.base + "Live", ScrollStateKeys.DISCOVER_LIVE, LiveActivitiesEvent.kind),
TabItem(R.string.discover_community, discoveryCommunityFeedViewModel, Route.Discover.base + "Community", ScrollStateKeys.DISCOVER_COMMUNITY, CommunityDefinitionEvent.kind),
TabItem(R.string.discover_chat, discoveryChatFeedViewModel, Route.Discover.base + "Chats", ScrollStateKeys.DISCOVER_CHATS, ChannelCreateEvent.kind)
).toImmutableList()
)
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
@@ -139,7 +139,7 @@ private fun DiscoverPages(
}
}
HorizontalPager(pageCount = 3, state = pagerState) { page ->
HorizontalPager(state = pagerState) { page ->
RefresheableView(tabs[page].viewModel, true) {
SaveableFeedState(tabs[page].viewModel, scrollStateKey = tabs[page].scrollStateKey) { listState ->
RenderDiscoverFeed(
@@ -78,7 +78,7 @@ fun HiddenUsersScreen(
Column(Modifier.fillMaxHeight()) {
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
val pagerState = rememberPagerState()
val pagerState = rememberPagerState() { 2 }
val coroutineScope = rememberCoroutineScope()
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.warnAboutPostsWithReports) }
var filterSpam by remember { mutableStateOf(accountViewModel.account.filterSpamFromStrangers) }
@@ -129,7 +129,7 @@ fun HiddenUsersScreen(
}
)
}
HorizontalPager(pageCount = 2, state = pagerState) { page ->
HorizontalPager(state = pagerState) { page ->
when (page) {
0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel, nav)
1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel, nav)
@@ -56,7 +56,7 @@ fun HomeScreen(
) {
var wantsToAddNip47 by remember(nip47) { mutableStateOf(nip47) }
val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN)
val pagerState = rememberForeverPagerState(key = PagerStateKeys.HOME_SCREEN) { 2 }
WatchAccountForHomeScreen(homeFeedViewModel, repliesFeedViewModel, accountViewModel)
@@ -124,7 +124,7 @@ private fun HomePages(
}
}
HorizontalPager(pageCount = 2, state = pagerState) { page ->
HorizontalPager(state = pagerState) { page ->
RefresheableFeedView(
viewModel = tabs[page].viewModel,
routeForLastRead = tabs[page].routeForLastRead,
@@ -25,6 +25,8 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.quartz.events.ChannelCreateEvent
import com.vitorpamplona.quartz.events.ChatroomKeyable
import com.vitorpamplona.quartz.events.GiftWrapEvent
import com.vitorpamplona.quartz.events.SealedGossipEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -75,9 +77,17 @@ fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav:
LaunchedEffect(key1 = noteState) {
scope.launch {
val note = noteState?.note
val event = note?.event
val channelHex = note?.channelHex()
val note = noteState?.note ?: return@launch
var event = note.event
val channelHex = note.channelHex()
if (event is GiftWrapEvent) {
event = accountViewModel.unwrap(event)
}
if (event is SealedGossipEvent) {
event = accountViewModel.unseal(event)
}
if (event == null) {
// stay here, loading
@@ -85,7 +95,7 @@ fun LoadRedirectScreen(baseNote: Note, accountViewModel: AccountViewModel, nav:
nav("Channel/${note.idHex}")
} else if (event is ChatroomKeyable) {
note.author?.let {
val withKey = (note.event as ChatroomKeyable)
val withKey = (event as ChatroomKeyable)
.chatroomKey(accountViewModel.userProfile().pubkeyHex)
withContext(Dispatchers.IO) {
@@ -113,12 +113,12 @@ var hasAlreadyAskedNotificationPermissions = false
@OptIn(ExperimentalPermissionsApi::class)
@Composable
fun CheckifItNeedsToRequestNotificationPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !hasAlreadyAskedNotificationPermissions) {
val notificationPermissionState = rememberPermissionState(
Manifest.permission.POST_NOTIFICATIONS
)
if (!notificationPermissionState.status.isGranted && !hasAlreadyAskedNotificationPermissions) {
if (!notificationPermissionState.status.isGranted) {
hasAlreadyAskedNotificationPermissions = true
// This will pause the APP, including the connection with relays.
@@ -313,7 +313,6 @@ private fun RenderSurface(
columnSize = it
}
) {
val pagerState = rememberPagerState()
val coroutineScope = rememberCoroutineScope()
val scrollState = rememberScrollState()
@@ -352,7 +351,6 @@ private fun RenderSurface(
) {
RenderScreen(
baseUser,
pagerState,
tabRowModifier,
pagerModifier,
threadsViewModel,
@@ -375,7 +373,6 @@ private fun RenderSurface(
@OptIn(ExperimentalFoundationApi::class)
private fun RenderScreen(
baseUser: User,
pagerState: PagerState,
tabRowModifier: Modifier,
pagerModifier: Modifier,
threadsViewModel: NostrUserProfileNewThreadsFeedViewModel,
@@ -389,6 +386,8 @@ private fun RenderScreen(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val pagerState = rememberPagerState { 9 }
Column() {
ProfileHeader(baseUser, appRecommendations, nav, accountViewModel)
ScrollableTabRow(
@@ -400,7 +399,6 @@ private fun RenderScreen(
CreateAndRenderTabs(baseUser, pagerState)
}
HorizontalPager(
pageCount = 9,
state = pagerState,
modifier = pagerModifier
) { page ->
@@ -1125,7 +1123,7 @@ private fun DisplayAppRecommendations(
Text(stringResource(id = R.string.recommended_apps))
FlowRow(
verticalAlignment = Alignment.CenterVertically,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(vertical = 5.dp)
) {
state.feed.value.forEach { app ->
@@ -1210,7 +1208,7 @@ private fun RenderBadgeList(
nav: (String) -> Unit
) {
FlowRow(
verticalAlignment = Alignment.CenterVertically,
verticalArrangement = Arrangement.Center,
modifier = Modifier.padding(vertical = 5.dp)
) {
list.forEach { badgeAwardEvent ->
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
@@ -45,6 +46,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrVideoDataSource
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.note.BoostReaction
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.HiddenNote
@@ -54,6 +56,7 @@ import com.vitorpamplona.amethyst.ui.note.NoteComposeReportState
import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.RenderRelay
import com.vitorpamplona.amethyst.ui.note.ReplyReaction
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
import com.vitorpamplona.amethyst.ui.note.WatchForReports
import com.vitorpamplona.amethyst.ui.note.ZapReaction
@@ -63,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.screen.FeedState
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
import com.vitorpamplona.amethyst.ui.screen.LoadingFeed
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState
import com.vitorpamplona.amethyst.ui.theme.Size35dp
@@ -131,15 +135,7 @@ private fun SaveableFeedState(
nav: (String) -> Unit,
scrollStateKey: String? = null
) {
val pagerState = if (scrollStateKey != null) {
rememberForeverPagerState(scrollStateKey)
} else {
remember { PagerState() }
}
WatchScrollToTop(videoFeedView, pagerState)
RenderPage(videoFeedView, accountViewModel, pagerState, nav)
RenderPage(videoFeedView, accountViewModel, scrollStateKey, nav)
}
@OptIn(ExperimentalFoundationApi::class)
@@ -163,7 +159,7 @@ public fun WatchScrollToTop(
fun RenderPage(
videoFeedView: NostrVideoFeedViewModel,
accountViewModel: AccountViewModel,
pagerState: PagerState,
pagerStateKey: String?,
nav: (String) -> Unit
) {
val feedState by videoFeedView.feedContent.collectAsState()
@@ -184,12 +180,7 @@ fun RenderPage(
}
is FeedState.Loaded -> {
SlidingCarousel(
state.feed,
pagerState,
accountViewModel,
nav
)
LoadedState(state, pagerStateKey, videoFeedView, accountViewModel, nav)
}
is FeedState.Loading -> {
@@ -201,6 +192,33 @@ fun RenderPage(
}
}
@Composable
@OptIn(ExperimentalFoundationApi::class)
private fun LoadedState(
state: FeedState.Loaded,
pagerStateKey: String?,
videoFeedView: NostrVideoFeedViewModel,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val pagerState = if (pagerStateKey != null) {
rememberForeverPagerState(pagerStateKey) { state.feed.value.size }
} else {
rememberPagerState { state.feed.value.size }
}
WatchScrollToTop(videoFeedView, pagerState)
RefresheableView(viewModel = videoFeedView) {
SlidingCarousel(
state.feed,
pagerState,
accountViewModel,
nav
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SlidingCarousel(
@@ -210,7 +228,6 @@ fun SlidingCarousel(
nav: (String) -> Unit
) {
VerticalPager(
pageCount = feed.value.size,
state = pagerState,
beyondBoundsPageCount = 1,
modifier = Modifier.fillMaxSize(1f),
@@ -434,13 +451,12 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
Spacer(modifier = Modifier.height(8.dp))
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(bottom = 75.dp, end = 20.dp)) {
/*
ReplyReaction(baseNote, accountViewModel, iconSize = 40.dp) {
ReplyReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp) {
wantsToReplyTo = baseNote
}
BoostReaction(baseNote, accountViewModel, iconSize = 40.dp) {
BoostReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp) {
wantsToQuote = baseNote
}*/
}
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp)
ViewCountReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colors.onBackgroundColorFilter)
+3
View File
@@ -548,4 +548,7 @@
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
<string name="copy_the_note_id_to_the_clipboard">Copy Note ID to clipboard</string>
<string name="created_at">Created at</string>
<string name="rules">Rules</string>
</resources>
@@ -38,7 +38,7 @@ class CryptoBenchmark {
val keyPair2 = KeyPair()
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.getSharedSecretNIP24(keyPair1.privKey!!, keyPair2.pubKey))
assertNotNull(CryptoUtils.getSharedSecretNIP44(keyPair1.privKey!!, keyPair2.pubKey))
}
}
@@ -58,7 +58,7 @@ class CryptoBenchmark {
val keyPair2 = KeyPair()
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.computeSharedSecretNIP24(keyPair1.privKey!!, keyPair2.pubKey))
assertNotNull(CryptoUtils.computeSharedSecretNIP44(keyPair1.privKey!!, keyPair2.pubKey))
}
}
@@ -166,7 +166,7 @@ class GiftWrapReceivingBenchmark {
val toDecrypt = decodeNIP44(wrappedEvent.content) ?: return
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.decryptNIP24(toDecrypt, sender.privKey!!, wrappedEvent.pubKey.hexToByteArray()))
assertNotNull(CryptoUtils.decryptNIP44(toDecrypt, sender.privKey!!, wrappedEvent.pubKey.hexToByteArray()))
}
}
@@ -191,7 +191,7 @@ class GiftWrapReceivingBenchmark {
)
val toDecrypt = decodeNIP44(wrappedEvent.content) ?: return
val innerJson = CryptoUtils.decryptNIP24(toDecrypt, sender.privKey!!, wrappedEvent.pubKey.hexToByteArray())
val innerJson = CryptoUtils.decryptNIP44(toDecrypt, sender.privKey!!, wrappedEvent.pubKey.hexToByteArray())
benchmarkRule.measureRepeated {
assertNotNull(innerJson?.let { Event.fromJson(it) })
@@ -236,7 +236,7 @@ class GiftWrapReceivingBenchmark {
val toDecrypt = decodeNIP44(seal.content) ?: return
benchmarkRule.measureRepeated {
assertNotNull(CryptoUtils.decryptNIP24(toDecrypt, sender.privKey!!, seal.pubKey.hexToByteArray()))
assertNotNull(CryptoUtils.decryptNIP44(toDecrypt, sender.privKey!!, seal.pubKey.hexToByteArray()))
}
}
@@ -256,7 +256,7 @@ class GiftWrapReceivingBenchmark {
)
val toDecrypt = decodeNIP44(seal.content) ?: return
val innerJson = CryptoUtils.decryptNIP24(toDecrypt, sender.privKey!!, seal.pubKey.hexToByteArray())
val innerJson = CryptoUtils.decryptNIP44(toDecrypt, sender.privKey!!, seal.pubKey.hexToByteArray())
benchmarkRule.measureRepeated {
assertNotNull(innerJson?.let { Gossip.fromJson(it) })
+9 -7
View File
@@ -4,22 +4,24 @@ buildscript {
ext {
fragment_version = "1.6.1"
lifecycle_version = '2.6.1'
compose_ui_version = '1.4.3'
nav_version = "2.6.0"
compose_ui_version = '1.5.0'
nav_version = "2.7.0"
room_version = "2.4.3"
accompanist_version = '0.30.1'
coil_version = '2.4.0'
vico_version = '1.7.3'
exoplayer_version = '1.1.0'
vico_version = '1.9.2'
exoplayer_version = '1.1.1'
media3_version = '1.1.0'
core_ktx_version = '1.10.1'
}
dependencies {
classpath 'com.google.gms:google-services:4.3.15'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
}
plugins {
id 'com.android.application' version '8.1.0' apply false
id 'com.android.library' version '8.1.0' apply false
id 'com.android.application' version '8.1.1' apply false
id 'com.android.library' version '8.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
id 'org.jetbrains.kotlin.jvm' version '1.9.0' apply false
id 'androidx.benchmark' version '1.1.1' apply false
+1 -1
View File
@@ -32,7 +32,7 @@ android {
}
dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation "androidx.core:core-ktx:$core_ktx_version"
// @Immutable and @Stable
implementation "androidx.compose.runtime:runtime:$compose_ui_version"
@@ -24,7 +24,7 @@ class CryptoUtilsTest {
val privateKey = "f410f88bcec6cbfda04d6a273c7b1dd8bba144cd45b71e87109cfa11dd7ed561"
val publicKey = "765cd7cf91d3ad07423d114d5a39c61d52b2cdbc18ba055ddbbeec71fbe2aa2f"
val key = CryptoUtils.getSharedSecretNIP24(privateKey = privateKey.hexToByteArray(), pubKey = publicKey.hexToByteArray())
val key = CryptoUtils.getSharedSecretNIP44(privateKey = privateKey.hexToByteArray(), pubKey = publicKey.hexToByteArray())
assertEquals("577c966f499dddd8e8dcc34e8f352e283cc177e53ae372794947e0b8ede7cfd8", key.toHexKey())
}
@@ -34,8 +34,8 @@ class CryptoUtilsTest {
val sender = KeyPair()
val receiver = KeyPair()
val sharedSecret1 = CryptoUtils.getSharedSecretNIP24(sender.privKey!!, receiver.pubKey)
val sharedSecret2 = CryptoUtils.getSharedSecretNIP24(receiver.privKey!!, sender.pubKey)
val sharedSecret1 = CryptoUtils.getSharedSecretNIP44(sender.privKey!!, receiver.pubKey)
val sharedSecret2 = CryptoUtils.getSharedSecretNIP44(receiver.privKey!!, sender.pubKey)
assertEquals(sharedSecret1.toHexKey(), sharedSecret2.toHexKey())
@@ -73,14 +73,14 @@ class CryptoUtilsTest {
}
@Test
fun encryptDecryptNIP24Test() {
fun encryptDecryptNIP44Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val encrypted = CryptoUtils.encryptNIP24(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP24(encrypted, privateKey, publicKey)
val encrypted = CryptoUtils.encryptNIP44(msg, privateKey, publicKey)
val decrypted = CryptoUtils.decryptNIP44(encrypted, privateKey, publicKey)
assertEquals(msg, decrypted)
}
@@ -99,15 +99,15 @@ class CryptoUtilsTest {
}
@Test
fun encryptSharedSecretDecryptNIP24Test() {
fun encryptSharedSecretDecryptNIP44Test() {
val msg = "Hi"
val privateKey = CryptoUtils.privkeyCreate()
val publicKey = CryptoUtils.pubkeyCreate(privateKey)
val sharedSecret = CryptoUtils.getSharedSecretNIP24(privateKey, publicKey)
val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, publicKey)
val encrypted = CryptoUtils.encryptNIP24(msg, sharedSecret)
val decrypted = CryptoUtils.decryptNIP24(encrypted, sharedSecret)
val encrypted = CryptoUtils.encryptNIP44(msg, sharedSecret)
val decrypted = CryptoUtils.decryptNIP44(encrypted, sharedSecret)
assertEquals(msg, decrypted)
}
@@ -17,7 +17,7 @@ import javax.crypto.spec.SecretKeySpec
object CryptoUtils {
private val sharedKeyCache04 = LruCache<Int, ByteArray>(200)
private val sharedKeyCache24 = LruCache<Int, ByteArray>(200)
private val sharedKeyCache44 = LruCache<Int, ByteArray>(200)
private val secp256k1 = Secp256k1.get()
private val libSodium = SodiumAndroid()
@@ -26,7 +26,7 @@ object CryptoUtils {
fun clearCache() {
sharedKeyCache04.evictAll()
sharedKeyCache24.evictAll()
sharedKeyCache44.evictAll()
}
fun randomInt(bound: Int): Int {
@@ -115,12 +115,12 @@ object CryptoUtils {
return String(cipher.doFinal(encryptedMsg))
}
fun encryptNIP24(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
val sharedSecret = getSharedSecretNIP24(privateKey, pubKey)
return encryptNIP24(msg, sharedSecret)
fun encryptNIP44(msg: String, privateKey: ByteArray, pubKey: ByteArray): EncryptedInfo {
val sharedSecret = getSharedSecretNIP44(privateKey, pubKey)
return encryptNIP44(msg, sharedSecret)
}
fun encryptNIP24(msg: String, sharedSecret: ByteArray): EncryptedInfo {
fun encryptNIP44(msg: String, sharedSecret: ByteArray): EncryptedInfo {
val nonce = ByteArray(24)
random.nextBytes(nonce)
@@ -134,16 +134,16 @@ object CryptoUtils {
return EncryptedInfo(
ciphertext = cipher ?: ByteArray(0),
nonce = nonce,
v = Nip44Version.NIP24.versionCode
v = Nip44Version.NIP44.versionCode
)
}
fun decryptNIP24(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? {
val sharedSecret = getSharedSecretNIP24(privateKey, pubKey)
return decryptNIP24(encryptedInfo, sharedSecret)
fun decryptNIP44(encryptedInfo: EncryptedInfo, privateKey: ByteArray, pubKey: ByteArray): String? {
val sharedSecret = getSharedSecretNIP44(privateKey, pubKey)
return decryptNIP44(encryptedInfo, sharedSecret)
}
fun decryptNIP24(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? {
fun decryptNIP44(encryptedInfo: EncryptedInfo, sharedSecret: ByteArray): String? {
return cryptoStreamXChaCha20Xor(
libSodium = libSodium,
messageBytes = encryptedInfo.ciphertext,
@@ -174,20 +174,20 @@ object CryptoUtils {
/**
* @return 32B shared secret
*/
fun getSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
fun getSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
val hash = combinedHashCode(privateKey, pubKey)
val preComputed = sharedKeyCache24[hash]
val preComputed = sharedKeyCache44[hash]
if (preComputed != null) return preComputed
val computed = computeSharedSecretNIP24(privateKey, pubKey)
sharedKeyCache24.put(hash, computed)
val computed = computeSharedSecretNIP44(privateKey, pubKey)
sharedKeyCache44.put(hash, computed)
return computed
}
/**
* @return 32B shared secret
*/
fun computeSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
fun computeSharedSecretNIP44(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
sha256(secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33))
}
@@ -196,7 +196,7 @@ data class EncryptedInfoString(val ciphertext: String, val nonce: String, val v:
enum class Nip44Version(val versionCode: Int) {
NIP04(0),
NIP24(1)
NIP44(1)
}
@@ -234,10 +234,10 @@ fun decodeByteArray(base64: String): EncryptedInfo? {
fun encodeJackson(info: EncryptedInfo): String {
return Event.mapper.writeValueAsString(
EncryptedInfoString(
v = info.v,
nonce = Base64.getEncoder().encodeToString(info.nonce),
ciphertext = Base64.getEncoder().encodeToString(info.ciphertext)
)
v = info.v,
nonce = Base64.getEncoder().encodeToString(info.nonce),
ciphertext = Base64.getEncoder().encodeToString(info.ciphertext)
)
)
}
@@ -15,9 +15,8 @@ class AdvertisedRelayListEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
override fun dTag() = fixedDTag
override fun address() = ATag(kind, pubKey, dTag(), null)
fun relays(): List<AdvertisedRelayInfo> {
return tags.mapNotNull {
@@ -14,10 +14,7 @@ class AppDefinitionEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun appMetaData() = try {
mapper.readValue(
ByteArrayInputStream(content.toByteArray(Charsets.UTF_8)),
@@ -12,16 +12,11 @@ class AppRecommendationEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun recommendations() = tags.filter { it.size > 1 && it[0] == "a" }.mapNotNull {
ATag.parse(it[1], it.getOrNull(2))
}
fun forKind() = runCatching { dTag().toInt() }.getOrNull()
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
companion object {
const val kind = 31989
}
@@ -15,10 +15,7 @@ class AudioTrackEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun participants() = tags.filter { it.size > 1 && it[0] == "p" }.map { Participant(it[1], it.getOrNull(2)) }
fun type() = tags.firstOrNull { it.size > 1 && it[0] == TYPE }?.get(1)
@@ -12,10 +12,7 @@ class BadgeDefinitionEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun name() = tags.firstOrNull { it.size > 1 && it[0] == "name" }?.get(1)
fun thumb() = tags.firstOrNull { it.size > 1 && it[0] == "thumb" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
@@ -12,7 +12,7 @@ class BadgeProfilesEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun badgeAwardEvents() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
fun badgeAwardDefinitions() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
val aTagValue = it.getOrNull(1)
@@ -21,9 +21,6 @@ class BadgeProfilesEvent(
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
}
override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
companion object {
const val kind = 30008
const val standardDTAg = "profile_badges"
@@ -0,0 +1,41 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CalendarDateSlotEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1)
fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1)
// ["start", "<YYYY-MM-DD>"],
// ["end", "<YYYY-MM-DD>"],
companion object {
const val kind = 31922
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CalendarDateSlotEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CalendarDateSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -0,0 +1,33 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CalendarEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 31924
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CalendarEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CalendarEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -0,0 +1,43 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CalendarRSVPEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun status() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1)
fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1)
// ["L", "status"],
// ["l", "<accepted/declined/tentative>", "status"],
// ["L", "freebusy"],
// ["l", "<free/busy>", "freebusy"]
companion object {
const val kind = 31925
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CalendarRSVPEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CalendarRSVPEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -0,0 +1,45 @@
package com.vitorpamplona.quartz.events
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.utils.TimeUtils
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.ATag
import com.vitorpamplona.quartz.encoders.HexKey
@Immutable
class CalendarTimeSlotEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
fun start() = tags.firstOrNull { it.size > 1 && it[0] == "start" }?.get(1)?.toLongOrNull()
fun end() = tags.firstOrNull { it.size > 1 && it[0] == "end" }?.get(1)?.toLongOrNull()
fun startTmz() = tags.firstOrNull { it.size > 1 && it[0] == "start_tzid" }?.get(1)?.toLongOrNull()
fun endTmz() = tags.firstOrNull { it.size > 1 && it[0] == "end_tzid" }?.get(1)?.toLongOrNull()
// ["start", "<Unix timestamp in seconds>"],
// ["end", "<Unix timestamp in seconds>"],
// ["start_tzid", "<IANA Time Zone Database identifier>"],
// ["end_tzid", "<IANA Time Zone Database identifier>"],
companion object {
const val kind = 31923
fun create(
privateKey: ByteArray,
createdAt: Long = TimeUtils.now()
): CalendarTimeSlotEvent {
val tags = mutableListOf<List<String>>()
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, "")
val sig = CryptoUtils.sign(id, privateKey)
return CalendarTimeSlotEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
}
}
}
@@ -71,5 +71,5 @@ class ChannelMessageEvent(
}
interface IsInPublicChatChannel {
open fun channel(): String?
fun channel(): String?
}
@@ -15,11 +15,7 @@ class ClassifiedsEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
@@ -15,11 +15,7 @@ class CommunityDefinitionEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun description() = tags.firstOrNull { it.size > 1 && it[0] == "description" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
fun rules() = tags.firstOrNull { it.size > 1 && it[0] == "rules" }?.get(1)
@@ -15,11 +15,7 @@ class EmojiPackSelectionEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 10030
@@ -98,6 +98,7 @@ open class Event(
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
override fun isTaggedUsers(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "p" && it[1] in idHexes }
override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex }
@@ -199,7 +200,7 @@ open class Event(
return try {
hasCorrectIDHash() && hasVerifedSignature()
} catch (e: Exception) {
Log.e("Event", "Fail checking if event $id has a valid signature", e)
Log.e("Event", "Event $id does not have a valid signature: ${toJson()}", e)
false
}
}
@@ -384,6 +385,20 @@ interface AddressableEvent {
fun address(): ATag
}
@Immutable
open class BaseAddressableEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
kind: Int,
tags: List<List<String>>,
content: String,
sig: HexKey
): Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
}
fun String.bytesUsedInMemory(): Int {
return (8 * ((((this.length) * 2) + 45) / 8))
}
@@ -22,6 +22,10 @@ class EventFactory {
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
BadgeProfilesEvent.kind -> BadgeProfilesEvent(id, pubKey, createdAt, tags, content, sig)
BookmarkListEvent.kind -> BookmarkListEvent(id, pubKey, createdAt, tags, content, sig)
CalendarEvent.kind -> CalendarEvent(id, pubKey, createdAt, tags, content, sig)
CalendarDateSlotEvent.kind -> CalendarDateSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarTimeSlotEvent.kind -> CalendarTimeSlotEvent(id, pubKey, createdAt, tags, content, sig)
CalendarRSVPEvent.kind -> CalendarRSVPEvent(id, pubKey, createdAt, tags, content, sig)
ChannelCreateEvent.kind -> ChannelCreateEvent(id, pubKey, createdAt, tags, content, sig)
ChannelHideMessageEvent.kind -> ChannelHideMessageEvent(id, pubKey, createdAt, tags, content, sig)
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
@@ -41,9 +45,6 @@ class EventFactory {
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
DeletionEvent.kind -> DeletionEvent(id, pubKey, createdAt, tags, content, sig)
// Will never happen.
// DirectMessageEvent.kind -> DirectMessageEvent(createdAt, tags, content)
EmojiPackEvent.kind -> EmojiPackEvent(id, pubKey, createdAt, tags, content, sig)
EmojiPackSelectionEvent.kind -> EmojiPackSelectionEvent(id, pubKey, createdAt, tags, content, sig)
SealedGossipEvent.kind -> SealedGossipEvent(id, pubKey, createdAt, tags, content, sig)
@@ -59,6 +60,7 @@ class EventFactory {
LnZapEvent.kind -> LnZapEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentRequestEvent.kind -> LnZapPaymentRequestEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPaymentResponseEvent.kind -> LnZapPaymentResponseEvent(id, pubKey, createdAt, tags, content, sig)
LnZapPrivateEvent.kind -> LnZapPrivateEvent(id, pubKey, createdAt, tags, content, sig)
LnZapRequestEvent.kind -> LnZapRequestEvent(id, pubKey, createdAt, tags, content, sig)
LongTextNoteEvent.kind -> LongTextNoteEvent(id, pubKey, createdAt, tags, content, sig)
MetadataEvent.kind -> MetadataEvent(id, pubKey, createdAt, tags, content, sig)
@@ -30,6 +30,7 @@ interface EventInterface {
fun hasValidSignature(): Boolean
fun isTaggedUser(idHex: String): Boolean
fun isTaggedUsers(idHex: Set<String>): Boolean
fun isTaggedEvent(idHex: String): Boolean
@@ -17,10 +17,7 @@ abstract class GeneralListEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun category() = dTag()
fun bookmarkedPosts() = taggedEvents()
fun bookmarkedPeople() = taggedUsers()
@@ -51,7 +51,7 @@ class GiftWrapEvent(
return when (toDecrypt.v) {
Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray())
Nip44Version.NIP24.versionCode -> CryptoUtils.decryptNIP24(toDecrypt, privKey, pubKey.hexToByteArray())
Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray())
else -> null
}
} catch (e: Exception) {
@@ -71,10 +71,10 @@ class GiftWrapEvent(
createdAt: Long = TimeUtils.randomWithinAWeek()
): GiftWrapEvent {
val privateKey = CryptoUtils.privkeyCreate() // GiftWrap is always a random key
val sharedSecret = CryptoUtils.getSharedSecretNIP24(privateKey, recipientPubKey.hexToByteArray())
val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, recipientPubKey.hexToByteArray())
val content = encodeNIP44(
CryptoUtils.encryptNIP24(
CryptoUtils.encryptNIP44(
toJson(event),
sharedSecret
)
@@ -15,11 +15,7 @@ class LiveActivitiesEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
@@ -0,0 +1,43 @@
package com.vitorpamplona.quartz.events
import android.util.Log
import androidx.compose.runtime.Immutable
import com.vitorpamplona.quartz.crypto.CryptoUtils
import com.vitorpamplona.quartz.encoders.Bech32
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.encoders.LnInvoiceUtil
import com.vitorpamplona.quartz.encoders.toHexKey
import com.vitorpamplona.quartz.utils.TimeUtils
import java.nio.charset.Charset
import java.security.SecureRandom
import javax.crypto.BadPaddingException
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
@Immutable
class LnZapPrivateEvent(
id: HexKey,
pubKey: HexKey,
createdAt: Long,
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
companion object {
const val kind = 9733
fun create(
privateKey: ByteArray,
tags: List<List<String>> = emptyList(),
content: String = "",
createdAt: Long = TimeUtils.now()
): Event {
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = CryptoUtils.sign(id, privateKey).toHexKey()
return Event(id.toHexKey(), pubKey, createdAt, kind, tags, content, sig)
}
}
}
@@ -86,9 +86,9 @@ class LnZapRequestEvent(
privkey = CryptoUtils.privkeyCreate()
pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey()
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt)
var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson()
var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().hexToByteArray())
val encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), originalNote.id(), createdAt)
val noteJson = (LnZapPrivateEvent.create(privkey, listOf(tags[0], tags[1]), message)).toJson()
val encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, originalNote.pubKey().hexToByteArray())
tags = tags + listOf(listOf("anon", encryptedContent))
content = "" // make sure public content is empty, as the content is encrypted
privkey = encryptionPrivateKey // sign event with generated privkey
@@ -119,9 +119,9 @@ class LnZapRequestEvent(
pubKey = CryptoUtils.pubkeyCreate(privkey).toHexKey()
tags = tags + listOf(listOf("anon", ""))
} else if (zapType == LnZapEvent.ZapType.PRIVATE) {
var encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt)
var noteJson = (create(privkey, 9733, listOf(tags[0], tags[1]), message)).toJson()
var encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.hexToByteArray())
val encryptionPrivateKey = createEncryptionPrivateKey(privateKey.toHexKey(), userHex, createdAt)
val noteJson = LnZapPrivateEvent.create(privkey, listOf(tags[0], tags[1]), message).toJson()
val encryptedContent = encryptPrivateZapMessage(noteJson, encryptionPrivateKey, userHex.hexToByteArray())
tags = tags + listOf(listOf("anon", encryptedContent))
content = ""
privkey = encryptionPrivateKey
@@ -132,21 +132,22 @@ class LnZapRequestEvent(
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
}
fun createEncryptionPrivateKey(privkey: String, id: String, createdAt: Long): ByteArray {
var str = privkey + id + createdAt.toString()
var strbyte = str.toByteArray(Charset.forName("utf-8"))
val str = privkey + id + createdAt.toString()
val strbyte = str.toByteArray(Charset.forName("utf-8"))
return CryptoUtils.sha256(strbyte)
}
private fun encryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
var sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey)
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey)
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
val keySpec = SecretKeySpec(sharedSecret, "AES")
val ivSpec = IvParameterSpec(iv)
var utf8message = msg.toByteArray(Charset.forName("utf-8"))
val utf8message = msg.toByteArray(Charset.forName("utf-8"))
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
val encryptedMsg = cipher.doFinal(utf8message)
@@ -158,7 +159,7 @@ class LnZapRequestEvent(
}
private fun decryptPrivateZapMessage(msg: String, privkey: ByteArray, pubkey: ByteArray): String {
var sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey)
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privkey, pubkey)
if (sharedSecret.size != 16 && sharedSecret.size != 32) {
throw IllegalArgumentException("Invalid shared secret size")
}
@@ -170,8 +171,10 @@ class LnZapRequestEvent(
val encryptedMsg = parts.first().run { Bech32.decode(this).second }
val encryptedBytes = Bech32.five2eight(encryptedMsg, 0)
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(
Bech32.five2eight(iv, 0)))
cipher.init(
Cipher.DECRYPT_MODE, SecretKeySpec(sharedSecret, "AES"), IvParameterSpec(
Bech32.five2eight(iv, 0))
)
try {
val decryptedMsgBytes = cipher.doFinal(encryptedBytes)
@@ -18,10 +18,7 @@ class MuteListEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.filter { it.firstOrNull() == "d" }.mapNotNull { it.getOrNull(1) }.firstOrNull() ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun plainContent(privKey: ByteArray): String? {
return try {
val sharedSecret = CryptoUtils.getSharedSecretNIP04(privKey, pubKey.hexToByteArray())
@@ -15,11 +15,7 @@ class NNSEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun ip4() = tags.firstOrNull { it.size > 1 && it[0] == "ip4" }?.get(1)
fun ip6() = tags.firstOrNull { it.size > 1 && it[0] == "ip6" }?.get(1)
fun version() = tags.firstOrNull { it.size > 1 && it[0] == "version" }?.get(1)
@@ -15,10 +15,7 @@ class PinListEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun pins() = tags.filter { it.size > 1 && it[0] == "pin" }.map { it[1] }
@@ -15,11 +15,7 @@ class RelaySetEvent(
tags: List<List<String>>,
content: String,
sig: HexKey
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
) : BaseAddressableEvent(id, pubKey, createdAt, kind, tags, content, sig) {
fun relays() = tags.filter { it.size > 1 && it[0] == "r" }.map { it[1] }
fun description() = tags.firstOrNull() { it.size > 1 && it[0] == "description" }?.get(1)
@@ -53,7 +53,7 @@ class SealedGossipEvent(
return when (toDecrypt.v) {
Nip44Version.NIP04.versionCode -> CryptoUtils.decryptNIP04(toDecrypt, privKey, pubKey.hexToByteArray())
Nip44Version.NIP24.versionCode -> CryptoUtils.decryptNIP24(toDecrypt, privKey, pubKey.hexToByteArray())
Nip44Version.NIP44.versionCode -> CryptoUtils.decryptNIP44(toDecrypt, privKey, pubKey.hexToByteArray())
else -> null
}
} catch (e: Exception) {
@@ -81,10 +81,10 @@ class SealedGossipEvent(
privateKey: ByteArray,
createdAt: Long = TimeUtils.randomWithinAWeek()
): SealedGossipEvent {
val sharedSecret = CryptoUtils.getSharedSecretNIP24(privateKey, encryptTo.hexToByteArray())
val sharedSecret = CryptoUtils.getSharedSecretNIP44(privateKey, encryptTo.hexToByteArray())
val content = encodeNIP44(
CryptoUtils.encryptNIP24(
CryptoUtils.encryptNIP44(
Gossip.toJson(gossip),
sharedSecret
)