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