Merge branch 'main' into amber
This commit is contained in:
+13
-9
@@ -7,20 +7,20 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.vitorpamplona.amethyst'
|
namespace 'com.vitorpamplona.amethyst'
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "com.vitorpamplona.amethyst"
|
applicationId "com.vitorpamplona.amethyst"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
versionCode 272
|
versionCode 276
|
||||||
versionName "0.73.3"
|
versionName "0.74.3"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary true
|
useSupportLibrary true
|
||||||
}
|
}
|
||||||
resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW', 'ja']
|
resourceConfigurations += ['ar', 'cs', 'de', 'eo', 'es', 'fa', 'fr', 'hu', 'ja', 'night', 'nl', 'pt-rBR', 'ru', 'sv-rSE', 'ta', 'tr', 'uk', 'zh', 'sh-rHK', 'zh-rTW']
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -111,6 +111,7 @@ dependencies {
|
|||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||||
|
|
||||||
|
// Zoomable images
|
||||||
implementation "net.engawapg.lib:zoomable:1.4.3"
|
implementation "net.engawapg.lib:zoomable:1.4.3"
|
||||||
|
|
||||||
// Biometrics
|
// Biometrics
|
||||||
@@ -119,7 +120,7 @@ dependencies {
|
|||||||
// Websockets API
|
// Websockets API
|
||||||
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.11'
|
||||||
|
|
||||||
// link preview
|
// HTML Parsing for Link Preview
|
||||||
implementation 'org.jsoup:jsoup:1.16.1'
|
implementation 'org.jsoup:jsoup:1.16.1'
|
||||||
|
|
||||||
// Encrypted Key Storage
|
// Encrypted Key Storage
|
||||||
@@ -159,9 +160,9 @@ dependencies {
|
|||||||
|
|
||||||
// Language picker and Theme chooser
|
// Language picker and Theme chooser
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||||
implementation 'androidx.media3:media3-exoplayer-hls:1.1.0'
|
implementation "androidx.media3:media3-exoplayer-hls:$media3_version"
|
||||||
implementation 'androidx.media3:media3-ui:1.1.0'
|
implementation "androidx.media3:media3-ui:$media3_version"
|
||||||
implementation 'androidx.media3:media3-session:1.1.0'
|
implementation "androidx.media3:media3-session:$media3_version"
|
||||||
|
|
||||||
// Local model for language identification
|
// Local model for language identification
|
||||||
playImplementation 'com.google.mlkit:language-id:17.0.4'
|
playImplementation 'com.google.mlkit:language-id:17.0.4'
|
||||||
@@ -182,6 +183,9 @@ dependencies {
|
|||||||
// GeoHash
|
// GeoHash
|
||||||
implementation 'com.github.drfonfon:android-kotlin-geohash:1.0'
|
implementation 'com.github.drfonfon:android-kotlin-geohash:1.0'
|
||||||
|
|
||||||
|
// Waveform visualizer
|
||||||
|
implementation 'com.github.lincollincol:compose-audiowaveform:1.1.1'
|
||||||
|
|
||||||
// Video compression lib
|
// Video compression lib
|
||||||
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
|
implementation 'com.github.AbedElazizShe:LightCompressor:1.3.1'
|
||||||
// Image compression lib
|
// Image compression lib
|
||||||
|
|||||||
@@ -299,7 +299,8 @@ object LocalPreferences {
|
|||||||
val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf()
|
val followingCommunities = getStringSet(PrefKeys.FOLLOWING_COMMUNITIES, null) ?: setOf()
|
||||||
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf()
|
val hiddenUsers = getStringSet(PrefKeys.HIDDEN_USERS, emptySet()) ?: setOf()
|
||||||
val localRelays = getString(PrefKeys.RELAYS, "[]")?.let {
|
val localRelays = getString(PrefKeys.RELAYS, "[]")?.let {
|
||||||
Event.mapper.readValue<Set<RelaySetupInfo>>(it)
|
println("LocalRelays: $it")
|
||||||
|
Event.mapper.readValue<Set<RelaySetupInfo>?>(it)
|
||||||
} ?: setOf<RelaySetupInfo>()
|
} ?: setOf<RelaySetupInfo>()
|
||||||
|
|
||||||
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
val dontTranslateFrom = getStringSet(PrefKeys.DONT_TRANSLATE_FROM, null) ?: setOf()
|
||||||
@@ -310,24 +311,24 @@ object LocalPreferences {
|
|||||||
val defaultDiscoveryFollowList = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
val defaultDiscoveryFollowList = getString(PrefKeys.DEFAULT_DISCOVERY_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||||
|
|
||||||
val zapAmountChoices = getString(PrefKeys.ZAP_AMOUNTS, "[]")?.let {
|
val zapAmountChoices = getString(PrefKeys.ZAP_AMOUNTS, "[]")?.let {
|
||||||
Event.mapper.readValue<List<Long>>(it)
|
Event.mapper.readValue<List<Long>?>(it)
|
||||||
}?.ifEmpty { listOf(500L, 1000L, 5000L) } ?: listOf(500L, 1000L, 5000L)
|
}?.ifEmpty { listOf(500L, 1000L, 5000L) } ?: listOf(500L, 1000L, 5000L)
|
||||||
|
|
||||||
val reactionChoices = getString(PrefKeys.REACTION_CHOICES, "[]")?.let {
|
val reactionChoices = getString(PrefKeys.REACTION_CHOICES, "[]")?.let {
|
||||||
Event.mapper.readValue<List<String>>(it)
|
Event.mapper.readValue<List<String>?>(it)
|
||||||
}?.ifEmpty { listOf("+") } ?: listOf("+")
|
}?.ifEmpty { listOf("+") } ?: listOf("+")
|
||||||
|
|
||||||
val defaultZapType = getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
|
val defaultZapType = getString(PrefKeys.DEFAULT_ZAPTYPE, "")?.let { serverName ->
|
||||||
LnZapEvent.ZapType.values().firstOrNull { it.name == serverName }
|
LnZapEvent.ZapType.values().firstOrNull() { it.name == serverName }
|
||||||
} ?: LnZapEvent.ZapType.PUBLIC
|
} ?: LnZapEvent.ZapType.PUBLIC
|
||||||
|
|
||||||
val defaultFileServer = getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
|
val defaultFileServer = getString(PrefKeys.DEFAULT_FILE_SERVER, "")?.let { serverName ->
|
||||||
ServersAvailable.values().firstOrNull { it.name == serverName }
|
ServersAvailable.values().firstOrNull() { it.name == serverName }
|
||||||
} ?: ServersAvailable.NOSTR_BUILD
|
} ?: ServersAvailable.NOSTR_BUILD
|
||||||
|
|
||||||
val zapPaymentRequestServer = try {
|
val zapPaymentRequestServer = try {
|
||||||
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
getString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, null)?.let {
|
||||||
Event.mapper.readValue<Nip47URI>(it)
|
Event.mapper.readValue<Nip47URI?>(it)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -345,7 +346,7 @@ object LocalPreferences {
|
|||||||
|
|
||||||
val languagePreferences = try {
|
val languagePreferences = try {
|
||||||
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
|
getString(PrefKeys.LANGUAGE_PREFS, null)?.let {
|
||||||
Event.mapper.readValue<Map<String, String>>(it)
|
Event.mapper.readValue<Map<String, String>?>(it)
|
||||||
} ?: mapOf()
|
} ?: mapOf()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -369,7 +370,7 @@ object LocalPreferences {
|
|||||||
|
|
||||||
val lastReadPerRoute = try {
|
val lastReadPerRoute = try {
|
||||||
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
|
getString(PrefKeys.LAST_READ_PER_ROUTE, null)?.let {
|
||||||
Event.mapper.readValue<Map<String, Long>>(it)
|
Event.mapper.readValue<Map<String, Long>?>(it)
|
||||||
} ?: mapOf()
|
} ?: mapOf()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|||||||
@@ -26,15 +26,35 @@ class PlaybackService : MediaSessionService() {
|
|||||||
return ProgressiveMediaSource.Factory(VideoCache.get(Amethyst.instance, HttpClient.getHttpClient()))
|
return ProgressiveMediaSource.Factory(VideoCache.get(Amethyst.instance, HttpClient.getHttpClient()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun lazyHlsDS(): MultiPlayerPlaybackManager {
|
||||||
|
managerHls?.let { return it }
|
||||||
|
|
||||||
|
val newInstance = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
|
||||||
|
managerHls = newInstance
|
||||||
|
return newInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lazyProgressiveDS(): MultiPlayerPlaybackManager {
|
||||||
|
managerProgressive?.let { return it }
|
||||||
|
|
||||||
|
val newInstance = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
|
||||||
|
managerProgressive = newInstance
|
||||||
|
return newInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
fun lazyLocalDS(): MultiPlayerPlaybackManager {
|
||||||
|
managerLocal?.let { return it }
|
||||||
|
|
||||||
|
val newInstance = MultiPlayerPlaybackManager(cachedPositions = videoViewedPositionCache)
|
||||||
|
managerLocal = newInstance
|
||||||
|
return newInstance
|
||||||
|
}
|
||||||
|
|
||||||
// Create your Player and MediaSession in the onCreate lifecycle event
|
// Create your Player and MediaSession in the onCreate lifecycle event
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache)
|
|
||||||
managerProgressive = MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache)
|
|
||||||
managerLocal = MultiPlayerPlaybackManager(cachedPositions = videoViewedPositionCache)
|
|
||||||
|
|
||||||
// Stop all videos and recreates all managers when the proxy changes.
|
// Stop all videos and recreates all managers when the proxy changes.
|
||||||
HttpClient.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
|
HttpClient.proxyChangeListeners.add(this@PlaybackService::onProxyUpdated)
|
||||||
}
|
}
|
||||||
@@ -62,11 +82,11 @@ class PlaybackService : MediaSessionService() {
|
|||||||
|
|
||||||
fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager? {
|
fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager? {
|
||||||
return if (fileName.startsWith("file")) {
|
return if (fileName.startsWith("file")) {
|
||||||
managerLocal
|
lazyLocalDS()
|
||||||
} else if (fileName.endsWith("m3u8")) {
|
} else if (fileName.endsWith("m3u8")) {
|
||||||
managerHls
|
lazyHlsDS()
|
||||||
} else {
|
} else {
|
||||||
managerProgressive
|
lazyProgressiveDS()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ fun getLanguagesSpokenByUser(): Set<String> {
|
|||||||
return codedList
|
return codedList
|
||||||
}
|
}
|
||||||
|
|
||||||
val GLOBAL_FOLLOWS = " Global "
|
val GLOBAL_FOLLOWS = " Global " // This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
val KIND3_FOLLOWS = " All Follows "
|
val KIND3_FOLLOWS = " All Follows " // This has spaces to avoid mixing with a potential NIP-51 list with the same name.
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
@Stable
|
@Stable
|
||||||
@@ -492,9 +492,15 @@ class Account(
|
|||||||
|
|
||||||
fun broadcast(note: Note) {
|
fun broadcast(note: Note) {
|
||||||
note.event?.let {
|
note.event?.let {
|
||||||
|
if (it is WrappedEvent && it.host != null) {
|
||||||
|
it.host?.let { hostEvent ->
|
||||||
|
Client.send(hostEvent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun migrateCommunitiesAndChannelsIfNeeded(latestContactList: ContactListEvent?): ContactListEvent? {
|
private fun migrateCommunitiesAndChannelsIfNeeded(latestContactList: ContactListEvent?): ContactListEvent? {
|
||||||
if (latestContactList == null) return latestContactList
|
if (latestContactList == null) return latestContactList
|
||||||
@@ -908,6 +914,23 @@ class Account(
|
|||||||
|
|
||||||
Client.send(signedEvent, relayList = relayList)
|
Client.send(signedEvent, relayList = relayList)
|
||||||
LocalCache.consume(signedEvent)
|
LocalCache.consume(signedEvent)
|
||||||
|
|
||||||
|
// broadcast replied notes
|
||||||
|
replyingTo?.let {
|
||||||
|
LocalCache.getNoteIfExists(replyingTo)?.event?.let {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
replyTo?.forEach {
|
||||||
|
it.event?.let {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addresses?.forEach {
|
||||||
|
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,6 +981,17 @@ class Account(
|
|||||||
|
|
||||||
Client.send(signedEvent, relayList = relayList)
|
Client.send(signedEvent, relayList = relayList)
|
||||||
LocalCache.consume(signedEvent)
|
LocalCache.consume(signedEvent)
|
||||||
|
|
||||||
|
replyTo?.forEach {
|
||||||
|
it.event?.let {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addresses?.forEach {
|
||||||
|
LocalCache.getAddressableNoteIfExists(it.toTag())?.event?.let {
|
||||||
|
Client.send(it, relayList = relayList)
|
||||||
|
}
|
||||||
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1800,8 +1834,8 @@ class Account(
|
|||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun convertGlobalRelays(): Array<String> {
|
fun activeGlobalRelays(): Array<String> {
|
||||||
return localRelays.filter { it.feedTypes.contains(FeedType.GLOBAL) }
|
return (activeRelays() ?: convertLocalRelays()).filter { it.activeTypes.contains(FeedType.GLOBAL) }
|
||||||
.map { it.url }
|
.map { it.url }
|
||||||
.toTypedArray()
|
.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1051,6 +1051,23 @@ object LocalCache {
|
|||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: AudioHeaderEvent, relay: Relay?) {
|
||||||
|
val note = getOrCreateNote(event.id)
|
||||||
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
|
if (relay != null) {
|
||||||
|
author.addRelayBeingUsed(relay, event.createdAt)
|
||||||
|
note.addRelay(relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already processed this event.
|
||||||
|
if (note.event != null) return
|
||||||
|
|
||||||
|
note.loadEvent(event, author, emptyList())
|
||||||
|
|
||||||
|
refreshObservers(note)
|
||||||
|
}
|
||||||
|
|
||||||
fun consume(event: FileHeaderEvent, relay: Relay?) {
|
fun consume(event: FileHeaderEvent, relay: Relay?) {
|
||||||
val note = getOrCreateNote(event.id)
|
val note = getOrCreateNote(event.id)
|
||||||
val author = getOrCreateUser(event.pubKey)
|
val author = getOrCreateUser(event.pubKey)
|
||||||
@@ -1284,7 +1301,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return notes.values.filter {
|
return notes.values.filter {
|
||||||
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent) &&
|
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent) &&
|
||||||
(
|
(
|
||||||
it.event?.content()?.contains(text, true) ?: false ||
|
it.event?.content()?.contains(text, true) ?: false ||
|
||||||
it.event?.matchTag1With(text) ?: false ||
|
it.event?.matchTag1With(text) ?: false ||
|
||||||
@@ -1292,7 +1309,7 @@ object LocalCache {
|
|||||||
it.idNote().startsWith(text, true)
|
it.idNote().startsWith(text, true)
|
||||||
)
|
)
|
||||||
} + addressables.values.filter {
|
} + addressables.values.filter {
|
||||||
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent) &&
|
(it.event !is GenericRepostEvent && it.event !is RepostEvent && it.event !is CommunityPostApprovalEvent && it.event !is ReactionEvent && it.event !is GiftWrapEvent) &&
|
||||||
(
|
(
|
||||||
it.event?.content()?.contains(text, true) ?: false ||
|
it.event?.content()?.contains(text, true) ?: false ||
|
||||||
it.event?.matchTag1With(text) ?: false ||
|
it.event?.matchTag1With(text) ?: false ||
|
||||||
@@ -1524,6 +1541,7 @@ object LocalCache {
|
|||||||
is AdvertisedRelayListEvent -> consume(event)
|
is AdvertisedRelayListEvent -> consume(event)
|
||||||
is AppDefinitionEvent -> consume(event)
|
is AppDefinitionEvent -> consume(event)
|
||||||
is AppRecommendationEvent -> consume(event)
|
is AppRecommendationEvent -> consume(event)
|
||||||
|
is AudioHeaderEvent -> consume(event, relay)
|
||||||
is AudioTrackEvent -> consume(event)
|
is AudioTrackEvent -> consume(event)
|
||||||
is BadgeAwardEvent -> consume(event)
|
is BadgeAwardEvent -> consume(event)
|
||||||
is BadgeDefinitionEvent -> consume(event)
|
is BadgeDefinitionEvent -> consume(event)
|
||||||
|
|||||||
@@ -129,12 +129,14 @@ open class Note(val idHex: String) {
|
|||||||
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
return "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
||||||
}
|
}
|
||||||
|
|
||||||
val mySignature = replyTo
|
val mySignature = (
|
||||||
|
replyTo
|
||||||
.filter { it in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
|
.filter { it in eventsToConsider } // This forces the signature to be based on a branch, avoiding two roots
|
||||||
.map {
|
.map {
|
||||||
cachedSignatures[it] ?: it.replyLevelSignature(eventsToConsider, cachedSignatures).apply { cachedSignatures.put(it, this) }
|
cachedSignatures[it] ?: it.replyLevelSignature(eventsToConsider, cachedSignatures).apply { cachedSignatures.put(it, this) }
|
||||||
}
|
}
|
||||||
.maxBy { it.length }.removeSuffix(";") + "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
.maxByOrNull { it.length }?.removeSuffix(";") ?: ""
|
||||||
|
) + "/" + formattedDateTime(createdAt() ?: 0) + ";"
|
||||||
|
|
||||||
cachedSignatures[this] = mySignature
|
cachedSignatures[this] = mySignature
|
||||||
return mySignature
|
return mySignature
|
||||||
@@ -613,6 +615,7 @@ open class Note(val idHex: String) {
|
|||||||
|
|
||||||
fun clearLive() {
|
fun clearLive() {
|
||||||
if (liveSet != null && liveSet?.isInUse() == false) {
|
if (liveSet != null && liveSet?.isInUse() == false) {
|
||||||
|
liveSet?.destroy()
|
||||||
liveSet = null
|
liveSet = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -654,12 +657,26 @@ class NoteLiveSet(u: Note) {
|
|||||||
relays.hasObservers() ||
|
relays.hasObservers() ||
|
||||||
zaps.hasObservers()
|
zaps.hasObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
metadata.destroy()
|
||||||
|
reactions.destroy()
|
||||||
|
boosts.destroy()
|
||||||
|
replies.destroy()
|
||||||
|
reports.destroy()
|
||||||
|
relays.destroy()
|
||||||
|
zaps.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
bundler.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
if (!hasObservers()) return
|
if (!hasObservers()) return
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,12 @@ class ThreadAssembler {
|
|||||||
testedNotes.add(note)
|
testedNotes.add(note)
|
||||||
|
|
||||||
val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1)
|
val markedAsRoot = note.event?.tags()?.firstOrNull { it[0] == "e" && it.size > 3 && it[3] == "root" }?.getOrNull(1)
|
||||||
if (markedAsRoot != null) return LocalCache.checkGetOrCreateNote(markedAsRoot)
|
if (markedAsRoot != null) {
|
||||||
|
// Check to ssee if there is an error in the tag and the root has replies
|
||||||
|
if (LocalCache.getNoteIfExists(markedAsRoot)?.replyTo?.isEmpty() == true) {
|
||||||
|
return LocalCache.checkGetOrCreateNote(markedAsRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true }
|
val hasNoReplyTo = note.replyTo?.firstOrNull { it.replyTo?.isEmpty() == true }
|
||||||
if (hasNoReplyTo != null) return hasNoReplyTo
|
if (hasNoReplyTo != null) return hasNoReplyTo
|
||||||
|
|||||||
@@ -372,6 +372,7 @@ class User(val pubkeyHex: String) {
|
|||||||
|
|
||||||
fun clearLive() {
|
fun clearLive() {
|
||||||
if (liveSet != null && liveSet?.isInUse() == false) {
|
if (liveSet != null && liveSet?.isInUse() == false) {
|
||||||
|
liveSet?.destroy()
|
||||||
liveSet = null
|
liveSet = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,6 +401,18 @@ class UserLiveSet(u: User) {
|
|||||||
zaps.hasObservers() ||
|
zaps.hasObservers() ||
|
||||||
bookmarks.hasObservers()
|
bookmarks.hasObservers()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
follows.destroy()
|
||||||
|
followers.destroy()
|
||||||
|
reports.destroy()
|
||||||
|
messages.destroy()
|
||||||
|
relays.destroy()
|
||||||
|
relayInfo.destroy()
|
||||||
|
metadata.destroy()
|
||||||
|
zaps.destroy()
|
||||||
|
bookmarks.destroy()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
@@ -470,6 +483,10 @@ class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
|||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
private val bundler = BundledUpdate(500, Dispatchers.IO)
|
||||||
|
|
||||||
|
fun destroy() {
|
||||||
|
bundler.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
if (!hasObservers()) return
|
if (!hasObservers()) return
|
||||||
checkNotInMainThread()
|
checkNotInMainThread()
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
fun destroy() {
|
fun destroy() {
|
||||||
stop()
|
stop()
|
||||||
Client.unsubscribe(clientListener)
|
Client.unsubscribe(clientListener)
|
||||||
|
bundler.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun start() {
|
open fun start() {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
@@ -26,7 +27,7 @@ object NostrGeohashDataSource : NostrDataSource("SingleGeoHashFeed") {
|
|||||||
hashToLoad
|
hashToLoad
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, AudioHeaderEvent.kind),
|
||||||
limit = 200
|
limit = 200
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
import com.vitorpamplona.amethyst.service.relays.COMMON_FEED_TYPES
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
@@ -29,7 +30,7 @@ object NostrHashtagDataSource : NostrDataSource("SingleHashtagFeed") {
|
|||||||
hashToLoad.capitalize()
|
hashToLoad.capitalize()
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, ChannelMessageEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, LiveActivitiesChatMessageEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, AudioHeaderEvent.kind),
|
||||||
limit = 200
|
limit = 200
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.service.relays.EOSEAccount
|
|||||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||||
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
import com.vitorpamplona.amethyst.service.relays.JsonFilter
|
||||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
import com.vitorpamplona.quartz.events.CommunityPostApprovalEvent
|
||||||
@@ -56,10 +57,10 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
|
val follows = account.selectedUsersFollowList(account.defaultHomeFollowList)
|
||||||
|
|
||||||
val followKeys = follows?.map {
|
val followKeys = follows?.map {
|
||||||
it.substring(0, 6)
|
it.substring(0, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
val followSet = followKeys?.plus(account.userProfile().pubkeyHex.substring(0, 6))
|
val followSet = followKeys?.plus(account.userProfile().pubkeyHex.substring(0, 8))
|
||||||
|
|
||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = setOf(FeedType.FOLLOWS),
|
types = setOf(FeedType.FOLLOWS),
|
||||||
@@ -73,6 +74,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
PollNoteEvent.kind,
|
PollNoteEvent.kind,
|
||||||
HighlightEvent.kind,
|
HighlightEvent.kind,
|
||||||
AudioTrackEvent.kind,
|
AudioTrackEvent.kind,
|
||||||
|
AudioHeaderEvent.kind,
|
||||||
PinListEvent.kind,
|
PinListEvent.kind,
|
||||||
LiveActivitiesChatMessageEvent.kind,
|
LiveActivitiesChatMessageEvent.kind,
|
||||||
LiveActivitiesEvent.kind
|
LiveActivitiesEvent.kind
|
||||||
@@ -92,7 +94,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = setOf(FeedType.FOLLOWS),
|
types = setOf(FeedType.FOLLOWS),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioHeaderEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
||||||
tags = mapOf(
|
tags = mapOf(
|
||||||
"t" to hashToLoad.map {
|
"t" to hashToLoad.map {
|
||||||
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||||
@@ -112,7 +114,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
return TypedFilter(
|
return TypedFilter(
|
||||||
types = setOf(FeedType.FOLLOWS),
|
types = setOf(FeedType.FOLLOWS),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioHeaderEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
||||||
tags = mapOf(
|
tags = mapOf(
|
||||||
"g" to hashToLoad.map {
|
"g" to hashToLoad.map {
|
||||||
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||||
@@ -137,6 +139,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
LongTextNoteEvent.kind,
|
LongTextNoteEvent.kind,
|
||||||
ClassifiedsEvent.kind,
|
ClassifiedsEvent.kind,
|
||||||
HighlightEvent.kind,
|
HighlightEvent.kind,
|
||||||
|
AudioHeaderEvent.kind,
|
||||||
AudioTrackEvent.kind,
|
AudioTrackEvent.kind,
|
||||||
PinListEvent.kind,
|
PinListEvent.kind,
|
||||||
CommunityPostApprovalEvent.kind
|
CommunityPostApprovalEvent.kind
|
||||||
|
|||||||
+3
-2
@@ -56,8 +56,9 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SearchEventFeed") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(
|
kinds = listOf(
|
||||||
TextNoteEvent.kind, LongTextNoteEvent.kind, BadgeDefinitionEvent.kind,
|
TextNoteEvent.kind, LongTextNoteEvent.kind, BadgeDefinitionEvent.kind,
|
||||||
PeopleListEvent.kind, BookmarkListEvent.kind, AudioTrackEvent.kind, PinListEvent.kind,
|
PeopleListEvent.kind, BookmarkListEvent.kind, AudioHeaderEvent.kind,
|
||||||
PollNoteEvent.kind, ChannelCreateEvent.kind
|
AudioTrackEvent.kind, PinListEvent.kind, PollNoteEvent.kind,
|
||||||
|
ChannelCreateEvent.kind
|
||||||
),
|
),
|
||||||
search = mySearchString,
|
search = mySearchString,
|
||||||
limit = 100
|
limit = 100
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
|||||||
RepostEvent.kind,
|
RepostEvent.kind,
|
||||||
LongTextNoteEvent.kind,
|
LongTextNoteEvent.kind,
|
||||||
AudioTrackEvent.kind,
|
AudioTrackEvent.kind,
|
||||||
|
AudioHeaderEvent.kind,
|
||||||
PinListEvent.kind,
|
PinListEvent.kind,
|
||||||
PollNoteEvent.kind,
|
PollNoteEvent.kind,
|
||||||
HighlightEvent.kind
|
HighlightEvent.kind
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import androidx.compose.material.Surface
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.BuildConfig
|
|
||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.ServiceManager
|
import com.vitorpamplona.amethyst.ServiceManager
|
||||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||||
@@ -102,9 +101,9 @@ class MainActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
if (BuildConfig.DEBUG) {
|
// if (BuildConfig.DEBUG) {
|
||||||
debugState(this)
|
debugState(this)
|
||||||
}
|
// }
|
||||||
|
|
||||||
ServiceManager.pause()
|
ServiceManager.pause()
|
||||||
super.onPause()
|
super.onPause()
|
||||||
|
|||||||
@@ -241,6 +241,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewMo
|
|||||||
postViewModel.galleryUri?.let {
|
postViewModel.galleryUri?.let {
|
||||||
VideoView(
|
VideoView(
|
||||||
videoUri = it.toString(),
|
videoUri = it.toString(),
|
||||||
|
roundedCorner = false,
|
||||||
accountViewModel = accountViewModel
|
accountViewModel = accountViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,6 @@ import kotlinx.coroutines.flow.Flow
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewPostView(
|
fun NewPostView(
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
@@ -409,7 +408,7 @@ fun NewPostView(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||||
VideoView(myUrlPreview, accountViewModel = accountViewModel)
|
VideoView(myUrlPreview, roundedCorner = true, accountViewModel = accountViewModel)
|
||||||
} else {
|
} else {
|
||||||
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
|
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
|
||||||
}
|
}
|
||||||
@@ -1389,7 +1388,7 @@ fun ImageVideoDescription(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
VideoView(uri.toString(), accountViewModel = accountViewModel)
|
VideoView(uri.toString(), roundedCorner = true, accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit, navScrollToTop: (Route, Boolean) -> Unit) {
|
||||||
var wantsToPost by remember {
|
var wantsToPost by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
@@ -62,11 +62,9 @@ fun NewImageButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
|||||||
val postViewModel: NewMediaModel = viewModel()
|
val postViewModel: NewMediaModel = viewModel()
|
||||||
postViewModel.onceUploaded {
|
postViewModel.onceUploaded {
|
||||||
scope.launch(Dispatchers.Default) {
|
scope.launch(Dispatchers.Default) {
|
||||||
// awaits an refresh on the list
|
delay(500)
|
||||||
delay(250)
|
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
val route = Route.Video.route.replace("{scrollToTop}", "true")
|
navScrollToTop(Route.Video, true)
|
||||||
nav(route)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.AnimationSpec
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.*
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawStyle
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Fill
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.coerceIn
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.linc.audiowaveform.model.AmplitudeType
|
||||||
|
import com.linc.audiowaveform.model.WaveformAlignment
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private val MinSpikeWidthDp: Dp = 1.dp
|
||||||
|
private val MaxSpikeWidthDp: Dp = 24.dp
|
||||||
|
private val MinSpikePaddingDp: Dp = 0.dp
|
||||||
|
private val MaxSpikePaddingDp: Dp = 12.dp
|
||||||
|
private val MinSpikeRadiusDp: Dp = 0.dp
|
||||||
|
private val MaxSpikeRadiusDp: Dp = 12.dp
|
||||||
|
|
||||||
|
private const val MinProgress: Float = 0F
|
||||||
|
private const val MaxProgress: Float = 1F
|
||||||
|
|
||||||
|
private const val MinSpikeHeight: Float = 1F
|
||||||
|
private const val DefaultGraphicsLayerAlpha: Float = 0.99F
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AudioWaveformReadOnly(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: DrawStyle = Fill,
|
||||||
|
waveformBrush: Brush = SolidColor(Color.White),
|
||||||
|
progressBrush: Brush = SolidColor(Color.Blue),
|
||||||
|
waveformAlignment: WaveformAlignment = WaveformAlignment.Center,
|
||||||
|
amplitudeType: AmplitudeType = AmplitudeType.Avg,
|
||||||
|
onProgressChangeFinished: (() -> Unit)? = null,
|
||||||
|
spikeAnimationSpec: AnimationSpec<Float> = tween(500),
|
||||||
|
spikeWidth: Dp = 3.dp,
|
||||||
|
spikeRadius: Dp = 2.dp,
|
||||||
|
spikePadding: Dp = 2.dp,
|
||||||
|
progress: Float = 0F,
|
||||||
|
amplitudes: List<Int>,
|
||||||
|
onProgressChange: (Float) -> Unit
|
||||||
|
) {
|
||||||
|
val backgroundColor = MaterialTheme.colors.background
|
||||||
|
val _progress = remember(progress) { progress.coerceIn(MinProgress, MaxProgress) }
|
||||||
|
val _spikeWidth = remember(spikeWidth) { spikeWidth.coerceIn(MinSpikeWidthDp, MaxSpikeWidthDp) }
|
||||||
|
val _spikePadding = remember(spikePadding) { spikePadding.coerceIn(MinSpikePaddingDp, MaxSpikePaddingDp) }
|
||||||
|
val _spikeRadius = remember(spikeRadius) { spikeRadius.coerceIn(MinSpikeRadiusDp, MaxSpikeRadiusDp) }
|
||||||
|
val _spikeTotalWidth = remember(spikeWidth, spikePadding) { _spikeWidth + _spikePadding }
|
||||||
|
var canvasSize by remember { mutableStateOf(Size(0f, 0f)) }
|
||||||
|
var spikes by remember { mutableStateOf(0F) }
|
||||||
|
val spikesAmplitudes = remember(amplitudes, spikes, amplitudeType) {
|
||||||
|
amplitudes.toDrawableAmplitudes(
|
||||||
|
amplitudeType = amplitudeType,
|
||||||
|
spikes = spikes.toInt(),
|
||||||
|
minHeight = MinSpikeHeight,
|
||||||
|
maxHeight = canvasSize.height.coerceAtLeast(MinSpikeHeight)
|
||||||
|
)
|
||||||
|
}.map { animateFloatAsState(it, spikeAnimationSpec).value }
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.requiredHeight(48.dp)
|
||||||
|
.graphicsLayer(alpha = DefaultGraphicsLayerAlpha)
|
||||||
|
.then(modifier)
|
||||||
|
) {
|
||||||
|
canvasSize = size
|
||||||
|
spikes = size.width / _spikeTotalWidth.toPx()
|
||||||
|
spikesAmplitudes.forEachIndexed { index, amplitude ->
|
||||||
|
drawRoundRect(
|
||||||
|
brush = waveformBrush,
|
||||||
|
topLeft = Offset(
|
||||||
|
x = index * _spikeTotalWidth.toPx(),
|
||||||
|
y = when (waveformAlignment) {
|
||||||
|
WaveformAlignment.Top -> 0F
|
||||||
|
WaveformAlignment.Bottom -> size.height - amplitude
|
||||||
|
WaveformAlignment.Center -> size.height / 2F - amplitude / 2F
|
||||||
|
}
|
||||||
|
),
|
||||||
|
size = Size(
|
||||||
|
width = _spikeWidth.toPx(),
|
||||||
|
height = amplitude
|
||||||
|
),
|
||||||
|
cornerRadius = CornerRadius(_spikeRadius.toPx(), _spikeRadius.toPx()),
|
||||||
|
style = style
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
brush = progressBrush,
|
||||||
|
size = Size(
|
||||||
|
width = _progress * size.width,
|
||||||
|
height = size.height
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.SrcAtop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<Int>.toDrawableAmplitudes(
|
||||||
|
amplitudeType: AmplitudeType,
|
||||||
|
spikes: Int,
|
||||||
|
minHeight: Float,
|
||||||
|
maxHeight: Float
|
||||||
|
): List<Float> {
|
||||||
|
val amplitudes = map(Int::toFloat)
|
||||||
|
if (amplitudes.isEmpty() || spikes == 0) {
|
||||||
|
return List(spikes) { minHeight }
|
||||||
|
}
|
||||||
|
val transform = { data: List<Float> ->
|
||||||
|
when (amplitudeType) {
|
||||||
|
AmplitudeType.Avg -> data.average()
|
||||||
|
AmplitudeType.Max -> data.max()
|
||||||
|
AmplitudeType.Min -> data.min()
|
||||||
|
}.toFloat().coerceIn(minHeight, maxHeight)
|
||||||
|
}
|
||||||
|
return when {
|
||||||
|
spikes > amplitudes.count() -> amplitudes.fillToSize(spikes, transform)
|
||||||
|
else -> amplitudes.chunkToSize(spikes, transform)
|
||||||
|
}.normalize(minHeight, maxHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T> Iterable<T>.fillToSize(size: Int, transform: (List<T>) -> T): List<T> {
|
||||||
|
val capacity = ceil(size.safeDiv(count())).roundToInt()
|
||||||
|
return map { data -> List(capacity) { data } }.flatten().chunkToSize(size, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun <T> Iterable<T>.chunkToSize(size: Int, transform: (List<T>) -> T): List<T> {
|
||||||
|
val chunkSize = count() / size
|
||||||
|
val remainder = count() % size
|
||||||
|
val remainderIndex = ceil(count().safeDiv(remainder)).roundToInt()
|
||||||
|
val chunkIteration = filterIndexed { index, _ ->
|
||||||
|
remainderIndex == 0 || index % remainderIndex != 0
|
||||||
|
}.chunked(chunkSize, transform)
|
||||||
|
return when (size) {
|
||||||
|
chunkIteration.count() -> chunkIteration
|
||||||
|
else -> chunkIteration.chunkToSize(size, transform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun Iterable<Float>.normalize(min: Float, max: Float): List<Float> {
|
||||||
|
return map { (max - min) * ((it - min()) / (max() - min())) + min }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.safeDiv(value: Int): Float {
|
||||||
|
return if (value == 0) return 0F else this / value.toFloat()
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ import kotlinx.coroutines.CoroutineScope
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@@ -21,6 +23,8 @@ class BundledUpdate(
|
|||||||
val delay: Long,
|
val delay: Long,
|
||||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||||
) {
|
) {
|
||||||
|
val scope = CoroutineScope(dispatcher + SupervisorJob())
|
||||||
|
|
||||||
private var onlyOneInBlock = AtomicBoolean()
|
private var onlyOneInBlock = AtomicBoolean()
|
||||||
private var invalidatesAgain = false
|
private var invalidatesAgain = false
|
||||||
|
|
||||||
@@ -32,7 +36,6 @@ class BundledUpdate(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val scope = CoroutineScope(Job() + dispatcher)
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
try {
|
try {
|
||||||
onUpdate()
|
onUpdate()
|
||||||
@@ -48,6 +51,10 @@ class BundledUpdate(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,6 +65,8 @@ class BundledInsert<T>(
|
|||||||
val delay: Long,
|
val delay: Long,
|
||||||
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||||
) {
|
) {
|
||||||
|
val scope = CoroutineScope(dispatcher + SupervisorJob())
|
||||||
|
|
||||||
private var onlyOneInBlock = AtomicBoolean()
|
private var onlyOneInBlock = AtomicBoolean()
|
||||||
private var queue = LinkedBlockingQueue<T>()
|
private var queue = LinkedBlockingQueue<T>()
|
||||||
|
|
||||||
@@ -88,4 +97,8 @@ class BundledInsert<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cancel() {
|
||||||
|
scope.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ private fun ZoomableContentView(
|
|||||||
) {
|
) {
|
||||||
state.imagesForPager[word]?.let {
|
state.imagesForPager[word]?.let {
|
||||||
Box(modifier = HalfVertPadding) {
|
Box(modifier = HalfVertPadding) {
|
||||||
ZoomableContentView(it, state.imageList, accountViewModel)
|
ZoomableContentView(it, state.imageList, roundedCorner = true, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import android.view.ViewGroup
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -17,6 +19,7 @@ import androidx.compose.foundation.layout.BoxWithConstraints
|
|||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
@@ -34,6 +37,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.composed
|
import androidx.compose.ui.composed
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.LayoutCoordinates
|
import androidx.compose.ui.layout.LayoutCoordinates
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
@@ -54,6 +59,7 @@ import androidx.media3.ui.AspectRatioFrameLayout
|
|||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
import coil.imageLoader
|
import coil.imageLoader
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import com.linc.audiowaveform.infiniteLinearGradient
|
||||||
import com.vitorpamplona.amethyst.PlaybackClientController
|
import com.vitorpamplona.amethyst.PlaybackClientController
|
||||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||||
@@ -66,8 +72,12 @@ import com.vitorpamplona.amethyst.ui.theme.PinBottomIconSize
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size22Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size50Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
import com.vitorpamplona.amethyst.ui.theme.VolumeBottomIconSize
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.conflate
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -80,6 +90,7 @@ fun LoadThumbAndThenVideoView(
|
|||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumbUri: String,
|
thumbUri: String,
|
||||||
authorName: String? = null,
|
authorName: String? = null,
|
||||||
|
roundedCorner: Boolean,
|
||||||
nostrUriCallback: String? = null,
|
nostrUriCallback: String? = null,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
onDialog: ((Boolean) -> Unit)? = null
|
onDialog: ((Boolean) -> Unit)? = null
|
||||||
@@ -111,6 +122,7 @@ fun LoadThumbAndThenVideoView(
|
|||||||
videoUri = videoUri,
|
videoUri = videoUri,
|
||||||
title = title,
|
title = title,
|
||||||
thumb = VideoThumb(loadingFinished.second),
|
thumb = VideoThumb(loadingFinished.second),
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
artworkUri = thumbUri,
|
artworkUri = thumbUri,
|
||||||
authorName = authorName,
|
authorName = authorName,
|
||||||
nostrUriCallback = nostrUriCallback,
|
nostrUriCallback = nostrUriCallback,
|
||||||
@@ -122,6 +134,7 @@ fun LoadThumbAndThenVideoView(
|
|||||||
videoUri = videoUri,
|
videoUri = videoUri,
|
||||||
title = title,
|
title = title,
|
||||||
thumb = null,
|
thumb = null,
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
artworkUri = thumbUri,
|
artworkUri = thumbUri,
|
||||||
authorName = authorName,
|
authorName = authorName,
|
||||||
nostrUriCallback = nostrUriCallback,
|
nostrUriCallback = nostrUriCallback,
|
||||||
@@ -137,6 +150,8 @@ fun VideoView(
|
|||||||
videoUri: String,
|
videoUri: String,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumb: VideoThumb? = null,
|
thumb: VideoThumb? = null,
|
||||||
|
roundedCorner: Boolean,
|
||||||
|
waveform: ImmutableList<Int>? = null,
|
||||||
artworkUri: String? = null,
|
artworkUri: String? = null,
|
||||||
authorName: String? = null,
|
authorName: String? = null,
|
||||||
nostrUriCallback: String? = null,
|
nostrUriCallback: String? = null,
|
||||||
@@ -151,6 +166,8 @@ fun VideoView(
|
|||||||
defaultToStart,
|
defaultToStart,
|
||||||
title,
|
title,
|
||||||
thumb,
|
thumb,
|
||||||
|
roundedCorner,
|
||||||
|
waveform,
|
||||||
artworkUri,
|
artworkUri,
|
||||||
authorName,
|
authorName,
|
||||||
nostrUriCallback,
|
nostrUriCallback,
|
||||||
@@ -167,6 +184,8 @@ fun VideoViewInner(
|
|||||||
defaultToStart: Boolean = false,
|
defaultToStart: Boolean = false,
|
||||||
title: String? = null,
|
title: String? = null,
|
||||||
thumb: VideoThumb? = null,
|
thumb: VideoThumb? = null,
|
||||||
|
roundedCorner: Boolean,
|
||||||
|
waveform: ImmutableList<Int>? = null,
|
||||||
artworkUri: String? = null,
|
artworkUri: String? = null,
|
||||||
authorName: String? = null,
|
authorName: String? = null,
|
||||||
nostrUriCallback: String? = null,
|
nostrUriCallback: String? = null,
|
||||||
@@ -220,7 +239,16 @@ fun VideoViewInner(
|
|||||||
defaultToStart = defaultToStart,
|
defaultToStart = defaultToStart,
|
||||||
nostrUriCallback = nostrUriCallback
|
nostrUriCallback = nostrUriCallback
|
||||||
) { controller, keepPlaying ->
|
) { controller, keepPlaying ->
|
||||||
RenderVideoPlayer(controller, thumb, keepPlaying, automaticallyStartPlayback, activeOnScreen, onDialog)
|
RenderVideoPlayer(
|
||||||
|
controller = controller,
|
||||||
|
thumbData = thumb,
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
|
waveform = waveform,
|
||||||
|
keepPlaying = keepPlaying,
|
||||||
|
automaticallyStartPlayback = automaticallyStartPlayback,
|
||||||
|
activeOnScreen = activeOnScreen,
|
||||||
|
onDialog = onDialog
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -472,6 +500,8 @@ data class VideoThumb(
|
|||||||
private fun RenderVideoPlayer(
|
private fun RenderVideoPlayer(
|
||||||
controller: MediaController,
|
controller: MediaController,
|
||||||
thumbData: VideoThumb?,
|
thumbData: VideoThumb?,
|
||||||
|
roundedCorner: Boolean,
|
||||||
|
waveform: ImmutableList<Int>? = null,
|
||||||
keepPlaying: MutableState<Boolean>,
|
keepPlaying: MutableState<Boolean>,
|
||||||
automaticallyStartPlayback: MutableState<Boolean>,
|
automaticallyStartPlayback: MutableState<Boolean>,
|
||||||
activeOnScreen: MutableState<Boolean>,
|
activeOnScreen: MutableState<Boolean>,
|
||||||
@@ -487,10 +517,15 @@ private fun RenderVideoPlayer(
|
|||||||
|
|
||||||
BoxWithConstraints() {
|
BoxWithConstraints() {
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = Modifier
|
modifier = if (roundedCorner) {
|
||||||
.fillMaxWidth()
|
MaterialTheme.colors.imageModifier
|
||||||
.defaultMinSize(minHeight = 70.dp)
|
.defaultMinSize(minHeight = 100.dp)
|
||||||
.align(Alignment.Center),
|
.align(Alignment.Center)
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
.defaultMinSize(minHeight = 100.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
},
|
||||||
factory = {
|
factory = {
|
||||||
PlayerView(context).apply {
|
PlayerView(context).apply {
|
||||||
player = controller
|
player = controller
|
||||||
@@ -518,6 +553,10 @@ private fun RenderVideoPlayer(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
waveform?.let {
|
||||||
|
Waveform(it, controller, Modifier.align(Alignment.Center))
|
||||||
|
}
|
||||||
|
|
||||||
val startingMuteState = remember(controller) {
|
val startingMuteState = remember(controller) {
|
||||||
controller.volume < 0.001
|
controller.volume < 0.001
|
||||||
}
|
}
|
||||||
@@ -555,6 +594,69 @@ private fun RenderVideoPlayer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun pollCurrentDuration(controller: MediaController) = flow {
|
||||||
|
while (controller.currentPosition <= controller.duration) {
|
||||||
|
emit(controller.currentPosition / controller.duration.toFloat())
|
||||||
|
delay(100)
|
||||||
|
}
|
||||||
|
}.conflate()
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Waveform(
|
||||||
|
waveform: ImmutableList<Int>,
|
||||||
|
controller: MediaController,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
val waveformProgress = remember { mutableStateOf(0F) }
|
||||||
|
|
||||||
|
DrawWaveform(waveform, waveformProgress, modifier)
|
||||||
|
|
||||||
|
val restartFlow = remember {
|
||||||
|
mutableStateOf(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keeps the screen on while playing and viewing videos.
|
||||||
|
DisposableEffect(key1 = controller) {
|
||||||
|
val listener = object : Player.Listener {
|
||||||
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
// doesn't consider the mutex because the screen can turn off if the video
|
||||||
|
// being played in the mutex is not visible.
|
||||||
|
if (isPlaying) {
|
||||||
|
restartFlow.value += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.addListener(listener)
|
||||||
|
onDispose {
|
||||||
|
controller.removeListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(key1 = restartFlow.value) {
|
||||||
|
pollCurrentDuration(controller).collect() { value ->
|
||||||
|
waveformProgress.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawWaveform(waveform: ImmutableList<Int>, waveformProgress: MutableState<Float>, modifier: Modifier) {
|
||||||
|
AudioWaveformReadOnly(
|
||||||
|
modifier = modifier.padding(start = 10.dp, end = 10.dp),
|
||||||
|
amplitudes = waveform,
|
||||||
|
progress = waveformProgress.value,
|
||||||
|
progressBrush = Brush.infiniteLinearGradient(
|
||||||
|
colors = listOf(Color(0xff2598cf), Color(0xff652d80)),
|
||||||
|
animation = tween(durationMillis = 6000, easing = LinearEasing),
|
||||||
|
width = 128F
|
||||||
|
),
|
||||||
|
onProgressChange = {
|
||||||
|
waveformProgress.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ControlWhenPlayerIsActive(
|
fun ControlWhenPlayerIsActive(
|
||||||
controller: Player,
|
controller: Player,
|
||||||
|
|||||||
@@ -29,11 +29,18 @@ import androidx.compose.foundation.pager.PagerState
|
|||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.text.InlineTextContent
|
import androidx.compose.foundation.text.InlineTextContent
|
||||||
import androidx.compose.foundation.text.appendInlineContent
|
import androidx.compose.foundation.text.appendInlineContent
|
||||||
|
import androidx.compose.material.Button
|
||||||
|
import androidx.compose.material.ButtonDefaults
|
||||||
|
import androidx.compose.material.DropdownMenu
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.IconButton
|
import androidx.compose.material.IconButton
|
||||||
import androidx.compose.material.LocalTextStyle
|
import androidx.compose.material.LocalTextStyle
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Surface
|
import androidx.compose.material.Surface
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
@@ -46,10 +53,12 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.Placeholder
|
import androidx.compose.ui.text.Placeholder
|
||||||
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
import androidx.compose.ui.text.PlaceholderVerticalAlign
|
||||||
@@ -77,11 +86,15 @@ import com.vitorpamplona.amethyst.ui.note.DownloadForOfflineIcon
|
|||||||
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
import com.vitorpamplona.amethyst.ui.note.HashCheckFailedIcon
|
||||||
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
|
import com.vitorpamplona.amethyst.ui.note.HashCheckIcon
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
import com.vitorpamplona.amethyst.ui.theme.Size24dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
import com.vitorpamplona.amethyst.ui.theme.Size30dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -181,26 +194,38 @@ fun figureOutMimeType(fullUrl: String): ZoomableContent {
|
|||||||
fun ZoomableContentView(
|
fun ZoomableContentView(
|
||||||
content: ZoomableContent,
|
content: ZoomableContent,
|
||||||
images: ImmutableList<ZoomableContent> = listOf(content).toImmutableList(),
|
images: ImmutableList<ZoomableContent> = listOf(content).toImmutableList(),
|
||||||
|
roundedCorner: Boolean,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
val clipboardManager = LocalClipboardManager.current
|
|
||||||
|
|
||||||
// store the dialog open or close state
|
// store the dialog open or close state
|
||||||
var dialogOpen by remember {
|
var dialogOpen by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainImageModifier = MaterialTheme.colors.imageModifier
|
// store the dialog open or close state
|
||||||
|
val shareOpen = remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shareOpen.value) {
|
||||||
|
ShareImageAction(shareOpen, content) { shareOpen.value = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
var mainImageModifier = if (roundedCorner) {
|
||||||
|
MaterialTheme.colors.imageModifier
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
}
|
||||||
|
|
||||||
if (content is ZoomableUrlContent) {
|
if (content is ZoomableUrlContent) {
|
||||||
mainImageModifier = mainImageModifier.combinedClickable(
|
mainImageModifier = mainImageModifier.combinedClickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri ?: content.url)) }
|
onLongClick = { shareOpen.value = true }
|
||||||
)
|
)
|
||||||
} else if (content is ZoomablePreloadedContent) {
|
} else if (content is ZoomablePreloadedContent) {
|
||||||
mainImageModifier = mainImageModifier.combinedClickable(
|
mainImageModifier = mainImageModifier.combinedClickable(
|
||||||
onClick = { dialogOpen = true },
|
onClick = { dialogOpen = true },
|
||||||
onLongClick = { clipboardManager.setText(AnnotatedString(content.uri)) }
|
onLongClick = { shareOpen.value = true }
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
mainImageModifier = mainImageModifier.clickable {
|
mainImageModifier = mainImageModifier.clickable {
|
||||||
@@ -215,6 +240,7 @@ fun ZoomableContentView(
|
|||||||
title = content.description,
|
title = content.description,
|
||||||
artworkUri = content.artworkUri,
|
artworkUri = content.artworkUri,
|
||||||
authorName = content.authorName,
|
authorName = content.authorName,
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
nostrUriCallback = content.uri,
|
nostrUriCallback = content.uri,
|
||||||
onDialog = { dialogOpen = true },
|
onDialog = { dialogOpen = true },
|
||||||
accountViewModel = accountViewModel
|
accountViewModel = accountViewModel
|
||||||
@@ -228,6 +254,7 @@ fun ZoomableContentView(
|
|||||||
title = content.description,
|
title = content.description,
|
||||||
artworkUri = content.artworkUri,
|
artworkUri = content.artworkUri,
|
||||||
authorName = content.authorName,
|
authorName = content.authorName,
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
nostrUriCallback = content.uri,
|
nostrUriCallback = content.uri,
|
||||||
onDialog = { dialogOpen = true },
|
onDialog = { dialogOpen = true },
|
||||||
accountViewModel = accountViewModel
|
accountViewModel = accountViewModel
|
||||||
@@ -624,11 +651,11 @@ fun ZoomableImageDialog(
|
|||||||
pagerState = pagerState,
|
pagerState = pagerState,
|
||||||
itemsCount = allImages.size,
|
itemsCount = allImages.size,
|
||||||
itemContent = { index ->
|
itemContent = { index ->
|
||||||
RenderImageOrVideo(allImages[index], accountViewModel)
|
RenderImageOrVideo(allImages[index], false, accountViewModel)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
RenderImageOrVideo(imageUrl, accountViewModel)
|
RenderImageOrVideo(imageUrl, false, accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -642,7 +669,10 @@ fun ZoomableImageDialog(
|
|||||||
|
|
||||||
val myContent = allImages[pagerState.currentPage]
|
val myContent = allImages[pagerState.currentPage]
|
||||||
if (myContent is ZoomableUrlContent) {
|
if (myContent is ZoomableUrlContent) {
|
||||||
|
Row() {
|
||||||
|
CopyToClipboard(content = myContent)
|
||||||
SaveToGallery(url = myContent.url)
|
SaveToGallery(url = myContent.url)
|
||||||
|
}
|
||||||
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
|
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
|
||||||
SaveToGallery(
|
SaveToGallery(
|
||||||
localFile = myContent.localFile,
|
localFile = myContent.localFile,
|
||||||
@@ -656,7 +686,65 @@ fun ZoomableImageDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderImageOrVideo(content: ZoomableContent, accountViewModel: AccountViewModel) {
|
private fun CopyToClipboard(
|
||||||
|
content: ZoomableContent
|
||||||
|
) {
|
||||||
|
val popupExpanded = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(horizontal = Size5dp),
|
||||||
|
onClick = { popupExpanded.value = true },
|
||||||
|
shape = ButtonBorder,
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.placeholderText
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
tint = Color.White,
|
||||||
|
imageVector = Icons.Default.Share,
|
||||||
|
modifier = Size20Modifier,
|
||||||
|
contentDescription = stringResource(R.string.copy_url_to_clipboard)
|
||||||
|
)
|
||||||
|
|
||||||
|
ShareImageAction(popupExpanded, content) { popupExpanded.value = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShareImageAction(
|
||||||
|
popupExpanded:
|
||||||
|
MutableState<Boolean>,
|
||||||
|
content: ZoomableContent,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = popupExpanded.value,
|
||||||
|
onDismissRequest = onDismiss
|
||||||
|
) {
|
||||||
|
val clipboardManager = LocalClipboardManager.current
|
||||||
|
|
||||||
|
if (content is ZoomableUrlContent) {
|
||||||
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.url)); onDismiss() }) {
|
||||||
|
Text(stringResource(R.string.copy_url_to_clipboard))
|
||||||
|
}
|
||||||
|
if (content.uri != null) {
|
||||||
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.uri)); onDismiss() }) {
|
||||||
|
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content is ZoomablePreloadedContent) {
|
||||||
|
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(content.uri)); onDismiss() }) {
|
||||||
|
Text(stringResource(R.string.copy_the_note_id_to_the_clipboard))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderImageOrVideo(content: ZoomableContent, roundedCorner: Boolean, accountViewModel: AccountViewModel) {
|
||||||
val mainModifier = Modifier
|
val mainModifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.zoomable(rememberZoomState())
|
.zoomable(rememberZoomState())
|
||||||
@@ -670,6 +758,7 @@ private fun RenderImageOrVideo(content: ZoomableContent, accountViewModel: Accou
|
|||||||
title = content.description,
|
title = content.description,
|
||||||
artworkUri = content.artworkUri,
|
artworkUri = content.artworkUri,
|
||||||
authorName = content.authorName,
|
authorName = content.authorName,
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
alwaysShowVideo = true
|
alwaysShowVideo = true
|
||||||
)
|
)
|
||||||
@@ -684,6 +773,7 @@ private fun RenderImageOrVideo(content: ZoomableContent, accountViewModel: Accou
|
|||||||
title = content.description,
|
title = content.description,
|
||||||
artworkUri = content.artworkUri,
|
artworkUri = content.artworkUri,
|
||||||
authorName = content.authorName,
|
authorName = content.authorName,
|
||||||
|
roundedCorner = roundedCorner,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
alwaysShowVideo = true
|
alwaysShowVideo = true
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,13 +11,19 @@ import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
|
|||||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_PLANNED
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
|
||||||
open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Note>() {
|
open class DiscoverLiveFeedFilter(
|
||||||
|
val account: Account
|
||||||
|
) : AdditiveFeedFilter<Note>() {
|
||||||
override fun feedKey(): String {
|
override fun feedKey(): String {
|
||||||
return account.userProfile().pubkeyHex + "-" + account.defaultDiscoveryFollowList
|
return account.userProfile().pubkeyHex + "-" + followList()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun followList(): String {
|
||||||
|
return account.defaultDiscoveryFollowList
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun showHiddenKey(): Boolean {
|
override fun showHiddenKey(): Boolean {
|
||||||
return account.defaultDiscoveryFollowList == PeopleListEvent.blockList
|
return followList() == PeopleListEvent.blockList
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
@@ -39,15 +45,15 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
|||||||
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
val isGlobal = account.defaultDiscoveryFollowList == GLOBAL_FOLLOWS
|
||||||
val isHiddenList = showHiddenKey()
|
val isHiddenList = showHiddenKey()
|
||||||
|
|
||||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
val followingKeySet = account.selectedUsersFollowList(followList()) ?: emptySet()
|
||||||
val followingTagSet = account.selectedTagsFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
val followingTagSet = account.selectedTagsFollowList(followList()) ?: emptySet()
|
||||||
val followingGeohashSet = account.selectedGeohashesFollowList(account.defaultDiscoveryFollowList) ?: emptySet()
|
val followingGeohashSet = account.selectedGeohashesFollowList(followList()) ?: emptySet()
|
||||||
|
|
||||||
val activities = collection
|
val activities = collection
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.filter { it.event is LiveActivitiesEvent }
|
.filter { it.event is LiveActivitiesEvent }
|
||||||
.filter {
|
.filter {
|
||||||
isGlobal || it.author?.pubkeyHex in followingKeySet || it.event?.isTaggedHashes(
|
isGlobal || (it.event as LiveActivitiesEvent).participantsIntersect(followingKeySet) || it.event?.isTaggedHashes(
|
||||||
followingTagSet
|
followingTagSet
|
||||||
) == true || it.event?.isTaggedGeoHashes(
|
) == true || it.event?.isTaggedGeoHashes(
|
||||||
followingGeohashSet
|
followingGeohashSet
|
||||||
@@ -61,7 +67,7 @@ open class DiscoverLiveFeedFilter(val account: Account) : AdditiveFeedFilter<Not
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun sort(collection: Set<Note>): List<Note> {
|
override fun sort(collection: Set<Note>): List<Note> {
|
||||||
val followingKeySet = account.selectedUsersFollowList(account.defaultDiscoveryFollowList)
|
val followingKeySet = account.selectedUsersFollowList(followList())
|
||||||
|
|
||||||
val counter = ParticipantListBuilder()
|
val counter = ParticipantListBuilder()
|
||||||
val participantCounts = collection.associate {
|
val participantCounts = collection.associate {
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
|
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||||
import com.vitorpamplona.quartz.events.*
|
import com.vitorpamplona.quartz.events.*
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
|
import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE
|
||||||
|
|
||||||
class DiscoverLiveNowFeedFilter(account: Account) : DiscoverLiveFeedFilter(account) {
|
class DiscoverLiveNowFeedFilter(
|
||||||
|
account: Account
|
||||||
|
) : DiscoverLiveFeedFilter(account) {
|
||||||
|
override fun followList(): String {
|
||||||
|
// uses follows by default, but other lists if they were selected in the top bar
|
||||||
|
val currentList = super.followList()
|
||||||
|
return if (currentList == GLOBAL_FOLLOWS) {
|
||||||
|
KIND3_FOLLOWS
|
||||||
|
} else {
|
||||||
|
currentList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
override fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
val allItems = super.innerApplyFilter(collection)
|
val allItems = super.innerApplyFilter(collection)
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||||
@@ -32,7 +33,8 @@ class GeoHashFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
|
|||||||
it.event is TextNoteEvent ||
|
it.event is TextNoteEvent ||
|
||||||
it.event is LongTextNoteEvent ||
|
it.event is LongTextNoteEvent ||
|
||||||
it.event is ChannelMessageEvent ||
|
it.event is ChannelMessageEvent ||
|
||||||
it.event is PrivateDmEvent
|
it.event is PrivateDmEvent ||
|
||||||
|
it.event is AudioHeaderEvent
|
||||||
) &&
|
) &&
|
||||||
it.event?.isTaggedGeoHash(myTag) == true
|
it.event?.isTaggedGeoHash(myTag) == true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal
|
|||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
import com.vitorpamplona.quartz.events.ChannelMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
import com.vitorpamplona.quartz.events.LongTextNoteEvent
|
||||||
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
import com.vitorpamplona.quartz.events.PrivateDmEvent
|
||||||
@@ -32,7 +33,8 @@ class HashtagFeedFilter(val tag: String, val account: Account) : AdditiveFeedFil
|
|||||||
it.event is TextNoteEvent ||
|
it.event is TextNoteEvent ||
|
||||||
it.event is LongTextNoteEvent ||
|
it.event is LongTextNoteEvent ||
|
||||||
it.event is ChannelMessageEvent ||
|
it.event is ChannelMessageEvent ||
|
||||||
it.event is PrivateDmEvent
|
it.event is PrivateDmEvent ||
|
||||||
|
it.event is AudioHeaderEvent
|
||||||
) &&
|
) &&
|
||||||
it.event?.isTaggedHash(myTag) == true
|
it.event?.isTaggedHash(myTag) == true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
@@ -37,7 +38,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>, ignoreAddressables: Boolean): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>, ignoreAddressables: Boolean): Set<Note> {
|
||||||
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
|
val isGlobal = account.defaultHomeFollowList == GLOBAL_FOLLOWS
|
||||||
val gRelays = account.convertGlobalRelays()
|
val gRelays = account.activeGlobalRelays()
|
||||||
val isHiddenList = showHiddenKey()
|
val isHiddenList = showHiddenKey()
|
||||||
|
|
||||||
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
val followingKeySet = account.selectedUsersFollowList(account.defaultHomeFollowList) ?: emptySet()
|
||||||
@@ -53,7 +54,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||||||
.filter { it ->
|
.filter { it ->
|
||||||
val noteEvent = it.event
|
val noteEvent = it.event
|
||||||
val isGlobalRelay = it.relays.any { gRelays.contains(it) }
|
val isGlobalRelay = it.relays.any { gRelays.contains(it) }
|
||||||
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
|
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent || noteEvent is AudioHeaderEvent) &&
|
||||||
(!ignoreAddressables || noteEvent.kind() < 10000) &&
|
(!ignoreAddressables || noteEvent.kind() < 10000) &&
|
||||||
((isGlobal && isGlobalRelay) || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeoSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) &&
|
((isGlobal && isGlobalRelay) || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet) || noteEvent.isTaggedGeoHashes(followingGeoSet) || noteEvent.isTaggedAddressableNotes(followingCommunities)) &&
|
||||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class NotificationFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
|||||||
it.event !is BadgeDefinitionEvent &&
|
it.event !is BadgeDefinitionEvent &&
|
||||||
it.event !is BadgeProfilesEvent &&
|
it.event !is BadgeProfilesEvent &&
|
||||||
it.event !is GiftWrapEvent &&
|
it.event !is GiftWrapEvent &&
|
||||||
it.author !== loggedInUser &&
|
(it.event is LnZapEvent || it.author !== loggedInUser) &&
|
||||||
(isGlobal || it.author?.pubkeyHex in followingKeySet) &&
|
(isGlobal || it.author?.pubkeyHex in followingKeySet) &&
|
||||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||||
(isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
(isHiddenList || it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
||||||
|
|||||||
+3
-1
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
import com.vitorpamplona.quartz.events.ClassifiedsEvent
|
||||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
@@ -41,7 +42,8 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : Add
|
|||||||
it.event is LongTextNoteEvent ||
|
it.event is LongTextNoteEvent ||
|
||||||
it.event is PollNoteEvent ||
|
it.event is PollNoteEvent ||
|
||||||
it.event is HighlightEvent ||
|
it.event is HighlightEvent ||
|
||||||
it.event is AudioTrackEvent
|
it.event is AudioTrackEvent ||
|
||||||
|
it.event is AudioHeaderEvent
|
||||||
) &&
|
) &&
|
||||||
it.isNewThread() &&
|
it.isNewThread() &&
|
||||||
account.isAcceptable(it) == true
|
account.isAcceptable(it) == true
|
||||||
|
|||||||
@@ -732,9 +732,9 @@ fun debugState(context: Context) {
|
|||||||
Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
Log.d("STATE DUMP", "Image Disk Cache ${(imageLoader.diskCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.diskCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
||||||
Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
Log.d("STATE DUMP", "Image Memory Cache ${(imageLoader.memoryCache?.size ?: 0) / (1024 * 1024)}/${(imageLoader.memoryCache?.maxSize ?: 0) / (1024 * 1024)} MB")
|
||||||
|
|
||||||
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
|
Log.d("STATE DUMP", "Notes: " + LocalCache.notes.filter { it.value.liveSet != null }.size + " / " + LocalCache.notes.filter { it.value.event != null }.size + "/" + LocalCache.notes.size)
|
||||||
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
|
Log.d("STATE DUMP", "Addressables: " + LocalCache.addressables.filter { it.value.liveSet != null }.size + " / " + LocalCache.addressables.filter { it.value.event != null }.size + "/" + LocalCache.addressables.size)
|
||||||
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
|
Log.d("STATE DUMP", "Users: " + LocalCache.users.filter { it.value.liveSet != null }.size + " / " + LocalCache.users.filter { it.value.info?.latestMetadata != null }.size + "/" + LocalCache.users.size)
|
||||||
|
|
||||||
Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB")
|
Log.d("STATE DUMP", "Memory used by Events: " + LocalCache.notes.values.sumOf { it.event?.countMemory() ?: 0 } / (1024 * 1024) + " MB")
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -58,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.EndedFlag
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.LiveFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.OfflineFlag
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ScheduledFlag
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
@@ -361,6 +363,10 @@ fun InnerChannelCardWithReactions(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
thickness = DividerThickness
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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.material.Divider
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -25,6 +26,7 @@ import androidx.compose.ui.graphics.compositeOver
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -118,5 +120,9 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, routeForLastRead: String,
|
|||||||
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
thickness = DividerThickness
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ 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.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.Divider
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -54,6 +55,7 @@ import com.vitorpamplona.amethyst.ui.screen.CombinedZap
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.showAmountAxis
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
|
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
|
import com.vitorpamplona.amethyst.ui.theme.NotificationIconModifierSmaller
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
@@ -159,6 +161,10 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, showHi
|
|||||||
|
|
||||||
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
NoteDropDownMenu(baseNote, popupExpanded, accountViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
thickness = DividerThickness
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
|||||||
import com.vitorpamplona.quartz.encoders.ATag
|
import com.vitorpamplona.quartz.encoders.ATag
|
||||||
import com.vitorpamplona.quartz.encoders.toNpub
|
import com.vitorpamplona.quartz.encoders.toNpub
|
||||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
import com.vitorpamplona.quartz.events.BadgeAwardEvent
|
||||||
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
||||||
@@ -201,7 +202,6 @@ import java.io.File
|
|||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.time.ExperimentalTime
|
|
||||||
import kotlin.time.measureTimedValue
|
import kotlin.time.measureTimedValue
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@@ -480,8 +480,8 @@ fun NormalNote(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
|
||||||
is FileHeaderEvent -> FileHeaderDisplay(baseNote, accountViewModel)
|
is FileHeaderEvent -> FileHeaderDisplay(baseNote, isQuotedNote, accountViewModel)
|
||||||
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, accountViewModel)
|
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, isQuotedNote, accountViewModel)
|
||||||
else ->
|
else ->
|
||||||
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
|
||||||
CheckNewAndRenderNote(
|
CheckNewAndRenderNote(
|
||||||
@@ -896,7 +896,6 @@ fun ClickableNote(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalTime::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InnerNoteWithReactions(
|
fun InnerNoteWithReactions(
|
||||||
baseNote: Note,
|
baseNote: Note,
|
||||||
@@ -1045,6 +1044,14 @@ private fun RenderNoteRow(
|
|||||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is AudioTrackEvent -> {
|
||||||
|
RenderAudioTrack(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AudioHeaderEvent -> {
|
||||||
|
RenderAudioHeader(baseNote, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
is ReactionEvent -> {
|
is ReactionEvent -> {
|
||||||
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
|
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
@@ -1077,10 +1084,6 @@ private fun RenderNoteRow(
|
|||||||
DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
DisplayRelaySet(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
is AudioTrackEvent -> {
|
|
||||||
RenderAudioTrack(baseNote, accountViewModel, nav)
|
|
||||||
}
|
|
||||||
|
|
||||||
is PinListEvent -> {
|
is PinListEvent -> {
|
||||||
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
@@ -2251,6 +2254,17 @@ private fun RenderAudioTrack(
|
|||||||
AudioTrackHeader(noteEvent, note, accountViewModel, nav)
|
AudioTrackHeader(noteEvent, note, accountViewModel, nav)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderAudioHeader(
|
||||||
|
note: Note,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val noteEvent = note.event as? AudioHeaderEvent ?: return
|
||||||
|
|
||||||
|
AudioHeader(noteEvent, note, accountViewModel, nav)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun RenderLongFormContent(
|
private fun RenderLongFormContent(
|
||||||
note: Note,
|
note: Note,
|
||||||
@@ -3178,7 +3192,7 @@ private fun RenderBadge(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FileHeaderDisplay(note: Note, accountViewModel: AccountViewModel) {
|
fun FileHeaderDisplay(note: Note, isQuotedNote: Boolean, accountViewModel: AccountViewModel) {
|
||||||
val event = (note.event as? FileHeaderEvent) ?: return
|
val event = (note.event as? FileHeaderEvent) ?: return
|
||||||
val fullUrl = event.url() ?: return
|
val fullUrl = event.url() ?: return
|
||||||
|
|
||||||
@@ -3214,18 +3228,18 @@ fun FileHeaderDisplay(note: Note, accountViewModel: AccountViewModel) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||||
ZoomableContentView(content = content, accountViewModel = accountViewModel)
|
ZoomableContentView(content = content, roundedCorner = isQuotedNote, accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun FileStorageHeaderDisplay(baseNote: Note, accountViewModel: AccountViewModel) {
|
fun FileStorageHeaderDisplay(baseNote: Note, isQuotedNote: Boolean, accountViewModel: AccountViewModel) {
|
||||||
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
val eventHeader = (baseNote.event as? FileStorageHeaderEvent) ?: return
|
||||||
val dataEventId = eventHeader.dataEventId() ?: return
|
val dataEventId = eventHeader.dataEventId() ?: return
|
||||||
|
|
||||||
LoadNote(baseNoteHex = dataEventId) { contentNote ->
|
LoadNote(baseNoteHex = dataEventId) { contentNote ->
|
||||||
if (contentNote != null) {
|
if (contentNote != null) {
|
||||||
ObserverAndRenderNIP95(baseNote, contentNote, accountViewModel)
|
ObserverAndRenderNIP95(baseNote, contentNote, isQuotedNote, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3234,6 +3248,7 @@ fun FileStorageHeaderDisplay(baseNote: Note, accountViewModel: AccountViewModel)
|
|||||||
private fun ObserverAndRenderNIP95(
|
private fun ObserverAndRenderNIP95(
|
||||||
header: Note,
|
header: Note,
|
||||||
content: Note,
|
content: Note,
|
||||||
|
isQuotedNote: Boolean,
|
||||||
accountViewModel: AccountViewModel
|
accountViewModel: AccountViewModel
|
||||||
) {
|
) {
|
||||||
val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return
|
val eventHeader = (header.event as? FileStorageHeaderEvent) ?: return
|
||||||
@@ -3280,7 +3295,7 @@ private fun ObserverAndRenderNIP95(
|
|||||||
Crossfade(targetState = content) {
|
Crossfade(targetState = content) {
|
||||||
if (it != null) {
|
if (it != null) {
|
||||||
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
|
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
|
||||||
ZoomableContentView(content = it, accountViewModel = accountViewModel)
|
ZoomableContentView(content = it, roundedCorner = isQuotedNote, accountViewModel = accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3343,8 +3358,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
|
|||||||
|
|
||||||
media?.let { media ->
|
media?.let { media ->
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
modifier = Modifier.padding(10.dp)
|
|
||||||
) {
|
) {
|
||||||
cover?.let { cover ->
|
cover?.let { cover ->
|
||||||
LoadThumbAndThenVideoView(
|
LoadThumbAndThenVideoView(
|
||||||
@@ -3352,6 +3366,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
|
|||||||
title = noteEvent.subject(),
|
title = noteEvent.subject(),
|
||||||
thumbUri = cover,
|
thumbUri = cover,
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
|
roundedCorner = true,
|
||||||
nostrUriCallback = "nostr:${note.toNEvent()}",
|
nostrUriCallback = "nostr:${note.toNEvent()}",
|
||||||
accountViewModel = accountViewModel
|
accountViewModel = accountViewModel
|
||||||
)
|
)
|
||||||
@@ -3360,6 +3375,7 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
|
|||||||
videoUri = media,
|
videoUri = media,
|
||||||
title = noteEvent.subject(),
|
title = noteEvent.subject(),
|
||||||
authorName = note.author?.toBestDisplayName(),
|
authorName = note.author?.toBestDisplayName(),
|
||||||
|
roundedCorner = true,
|
||||||
accountViewModel = accountViewModel
|
accountViewModel = accountViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3368,6 +3384,66 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, note: Note, accountViewModel: A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AudioHeader(noteEvent: AudioHeaderEvent, note: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
|
val media = remember { noteEvent.stream() ?: noteEvent.download() }
|
||||||
|
val waveform = remember { noteEvent.wavefrom()?.toImmutableList()?.ifEmpty { null } }
|
||||||
|
val content = remember { noteEvent.content().ifBlank { null } }
|
||||||
|
|
||||||
|
val defaultBackground = MaterialTheme.colors.background
|
||||||
|
val background = remember { mutableStateOf(defaultBackground) }
|
||||||
|
val tags = remember(noteEvent) { noteEvent?.tags()?.toImmutableListOfLists() ?: ImmutableListOfLists() }
|
||||||
|
|
||||||
|
val eventContent = remember(note.event) {
|
||||||
|
val subject = (note.event as? TextNoteEvent)?.subject()?.ifEmpty { null }
|
||||||
|
val body = accountViewModel.decrypt(note)
|
||||||
|
|
||||||
|
if (!subject.isNullOrBlank() && body?.split("\n")?.get(0)?.contains(subject) == false) {
|
||||||
|
"## $subject\n$body"
|
||||||
|
} else {
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(modifier = Modifier.padding(top = 5.dp)) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
media?.let { media ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
VideoView(
|
||||||
|
videoUri = media,
|
||||||
|
waveform = waveform,
|
||||||
|
title = noteEvent.subject(),
|
||||||
|
authorName = note.author?.toBestDisplayName(),
|
||||||
|
roundedCorner = true,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nostrUriCallback = note.toNostrUri()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content?.let {
|
||||||
|
Row(verticalAlignment = CenterVertically, modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) {
|
||||||
|
TranslatableRichTextViewer(
|
||||||
|
content = it,
|
||||||
|
canPreview = true,
|
||||||
|
tags = tags,
|
||||||
|
backgroundColor = background,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
nav = nav
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
val hashtags = remember(noteEvent) { noteEvent.hashtags().toImmutableList() }
|
||||||
|
DisplayUncitedHashtags(hashtags, content ?: "", nav)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RenderLiveActivityEvent(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
fun RenderLiveActivityEvent(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||||
Row(modifier = Modifier.padding(top = 5.dp)) {
|
Row(modifier = Modifier.padding(top = 5.dp)) {
|
||||||
@@ -3486,6 +3562,7 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
|||||||
title = subject,
|
title = subject,
|
||||||
artworkUri = cover,
|
artworkUri = cover,
|
||||||
authorName = baseNote.author?.toBestDisplayName(),
|
authorName = baseNote.author?.toBestDisplayName(),
|
||||||
|
roundedCorner = true,
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
nostrUriCallback = "nostr:${baseNote.toNEvent()}"
|
nostrUriCallback = "nostr:${baseNote.toNEvent()}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
|||||||
takenIntoAccount.add(noteEvent.id())
|
takenIntoAccount.add(noteEvent.id())
|
||||||
}
|
}
|
||||||
} else if (noteEvent is LnZapEvent) {
|
} else if (noteEvent is LnZapEvent) {
|
||||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
|
if (noteEvent.isTaggedUser(currentUser)) { // the user might be sending his own receipts noteEvent.pubKey != currentUser
|
||||||
val netDate = formatDate(noteEvent.createdAt)
|
val netDate = formatDate(noteEvent.createdAt)
|
||||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
||||||
takenIntoAccount.add(noteEvent.id())
|
takenIntoAccount.add(noteEvent.id())
|
||||||
@@ -245,7 +245,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
|||||||
hasNewElements = true
|
hasNewElements = true
|
||||||
}
|
}
|
||||||
} else if (noteEvent is LnZapEvent) {
|
} else if (noteEvent is LnZapEvent) {
|
||||||
if (noteEvent.isTaggedUser(currentUser) && noteEvent.pubKey != currentUser) {
|
if (noteEvent.isTaggedUser(currentUser)) { // && noteEvent.pubKey != currentUser User might be sending his own receipts
|
||||||
val netDate = formatDate(noteEvent.createdAt)
|
val netDate = formatDate(noteEvent.createdAt)
|
||||||
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
zaps[netDate] = (zaps[netDate] ?: BigDecimal.ZERO) + (noteEvent.amount ?: BigDecimal.ZERO)
|
||||||
takenIntoAccount.add(noteEvent.id())
|
takenIntoAccount.add(noteEvent.id())
|
||||||
@@ -326,6 +326,7 @@ class UserReactionsViewModel(val account: Account) : ViewModel() {
|
|||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
collectorJob?.cancel()
|
collectorJob?.cancel()
|
||||||
|
bundlerInsert.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
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.material.Divider
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -22,6 +23,7 @@ import androidx.compose.ui.graphics.compositeOver
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
|
import com.vitorpamplona.amethyst.ui.screen.ZapUserSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
import com.vitorpamplona.amethyst.ui.theme.newItemBackgroundColor
|
||||||
@@ -89,5 +91,9 @@ fun ZapUserSetCompose(zapSetCard: ZapUserSetCard, isInnerNote: Boolean = false,
|
|||||||
UserCompose(baseUser = zapSetCard.user, accountViewModel = accountViewModel, nav = nav)
|
UserCompose(baseUser = zapSetCard.user, accountViewModel = accountViewModel, nav = nav)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Divider(
|
||||||
|
thickness = DividerThickness
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,6 +353,8 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
clear()
|
clear()
|
||||||
|
bundlerInsert.cancel()
|
||||||
|
bundler.cancel()
|
||||||
collectorJob?.cancel()
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -340,6 +340,8 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), I
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
bundlerInsert.cancel()
|
||||||
|
bundler.cancel()
|
||||||
collectorJob?.cancel()
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<ZapReqResponse>) : View
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
bundler.cancel()
|
||||||
collectorJob?.cancel()
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,6 +91,11 @@ class RelayFeedViewModel : ViewModel() {
|
|||||||
refreshSuspended()
|
refreshSuspended()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
bundler.cancel()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
|||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
||||||
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
import com.vitorpamplona.quartz.events.AppDefinitionEvent
|
||||||
|
import com.vitorpamplona.quartz.events.AudioHeaderEvent
|
||||||
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
import com.vitorpamplona.quartz.events.AudioTrackEvent
|
||||||
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
import com.vitorpamplona.quartz.events.BadgeDefinitionEvent
|
||||||
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
import com.vitorpamplona.quartz.events.ChannelCreateEvent
|
||||||
@@ -376,13 +377,15 @@ fun NoteMaster(
|
|||||||
nav = nav
|
nav = nav
|
||||||
)
|
)
|
||||||
} else if (noteEvent is FileHeaderEvent) {
|
} else if (noteEvent is FileHeaderEvent) {
|
||||||
FileHeaderDisplay(baseNote, accountViewModel)
|
FileHeaderDisplay(baseNote, true, accountViewModel)
|
||||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||||
FileStorageHeaderDisplay(baseNote, accountViewModel)
|
FileStorageHeaderDisplay(baseNote, true, accountViewModel)
|
||||||
} else if (noteEvent is PeopleListEvent) {
|
} else if (noteEvent is PeopleListEvent) {
|
||||||
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
|
||||||
} else if (noteEvent is AudioTrackEvent) {
|
} else if (noteEvent is AudioTrackEvent) {
|
||||||
AudioTrackHeader(noteEvent, baseNote, accountViewModel, nav)
|
AudioTrackHeader(noteEvent, baseNote, accountViewModel, nav)
|
||||||
|
} else if (noteEvent is AudioHeaderEvent) {
|
||||||
|
AudioHeader(noteEvent, baseNote, accountViewModel, nav)
|
||||||
} else if (noteEvent is CommunityPostApprovalEvent) {
|
} else if (noteEvent is CommunityPostApprovalEvent) {
|
||||||
RenderPostApproval(
|
RenderPostApproval(
|
||||||
baseNote,
|
baseNote,
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel(), In
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
bundler.cancel()
|
||||||
collectorJob?.cancel()
|
collectorJob?.cancel()
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -644,6 +644,7 @@ fun ShowVideoStreaming(
|
|||||||
|
|
||||||
ZoomableContentView(
|
ZoomableContentView(
|
||||||
content = zoomableUrlVideo,
|
content = zoomableUrlVideo,
|
||||||
|
roundedCorner = false,
|
||||||
accountViewModel = accountViewModel
|
accountViewModel = accountViewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingButtons(navState, accountViewModel, accountStateViewModel, nav)
|
FloatingButtons(navState, accountViewModel, accountStateViewModel, nav, navBottomRow)
|
||||||
},
|
},
|
||||||
scaffoldState = scaffoldState
|
scaffoldState = scaffoldState
|
||||||
) {
|
) {
|
||||||
@@ -249,7 +249,8 @@ fun FloatingButtons(
|
|||||||
navEntryState: State<NavBackStackEntry?>,
|
navEntryState: State<NavBackStackEntry?>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
accountStateViewModel: AccountStateViewModel,
|
accountStateViewModel: AccountStateViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit,
|
||||||
|
navScrollToTop: (Route, Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
val accountState by accountStateViewModel.accountContent.collectAsState()
|
val accountState by accountStateViewModel.accountContent.collectAsState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -265,7 +266,7 @@ fun FloatingButtons(
|
|||||||
// Does nothing.
|
// Does nothing.
|
||||||
}
|
}
|
||||||
is AccountState.LoggedIn -> {
|
is AccountState.LoggedIn -> {
|
||||||
WritePermissionButtons(navEntryState, accountViewModel, nav)
|
WritePermissionButtons(navEntryState, accountViewModel, nav, navScrollToTop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -275,7 +276,8 @@ fun FloatingButtons(
|
|||||||
private fun WritePermissionButtons(
|
private fun WritePermissionButtons(
|
||||||
navEntryState: State<NavBackStackEntry?>,
|
navEntryState: State<NavBackStackEntry?>,
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
nav: (String) -> Unit
|
nav: (String) -> Unit,
|
||||||
|
navScrollToTop: (Route, Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
val currentRoute by remember(navEntryState.value) {
|
val currentRoute by remember(navEntryState.value) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
@@ -286,7 +288,7 @@ private fun WritePermissionButtons(
|
|||||||
when (currentRoute) {
|
when (currentRoute) {
|
||||||
Route.Home.base -> NewNoteButton(accountViewModel, nav)
|
Route.Home.base -> NewNoteButton(accountViewModel, nav)
|
||||||
Route.Message.base -> ChannelFabColumn(accountViewModel, nav)
|
Route.Message.base -> ChannelFabColumn(accountViewModel, nav)
|
||||||
Route.Video.base -> NewImageButton(accountViewModel, nav)
|
Route.Video.base -> NewImageButton(accountViewModel, nav, navScrollToTop)
|
||||||
Route.Community.base -> {
|
Route.Community.base -> {
|
||||||
val communityId by remember(navEntryState.value) {
|
val communityId by remember(navEntryState.value) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
|
|||||||
@@ -690,9 +690,6 @@ private fun ProfileHeader(
|
|||||||
) {
|
) {
|
||||||
MessageButton(baseUser, accountViewModel, nav)
|
MessageButton(baseUser, accountViewModel, nav)
|
||||||
|
|
||||||
// No need for this button anymore
|
|
||||||
// NPubCopyButton(baseUser)
|
|
||||||
|
|
||||||
ProfileActions(baseUser, accountViewModel)
|
ProfileActions(baseUser, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,11 @@ class SearchBarViewModel(val account: Account) : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
bundler.cancel()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
fun isSearchingFun() = searchValue.isNotBlank()
|
fun isSearchingFun() = searchValue.isNotBlank()
|
||||||
|
|
||||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||||
|
|||||||
@@ -302,9 +302,9 @@ private fun RenderVideoOrPictureNote(
|
|||||||
Row(remember { Modifier.weight(1f) }, verticalAlignment = Alignment.CenterVertically) {
|
Row(remember { Modifier.weight(1f) }, verticalAlignment = Alignment.CenterVertically) {
|
||||||
val noteEvent = remember { note.event }
|
val noteEvent = remember { note.event }
|
||||||
if (noteEvent is FileHeaderEvent) {
|
if (noteEvent is FileHeaderEvent) {
|
||||||
FileHeaderDisplay(note, accountViewModel)
|
FileHeaderDisplay(note, false, accountViewModel)
|
||||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||||
FileStorageHeaderDisplay(note, accountViewModel)
|
FileStorageHeaderDisplay(note, false, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
<string name="uploading">Uploading…</string>
|
<string name="uploading">Uploading…</string>
|
||||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">User does not have a lightning address setup to receive sats</string>
|
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">User does not have a lightning address setup to receive sats</string>
|
||||||
<string name="reply_here">"reply here.. "</string>
|
<string name="reply_here">"reply here.. "</string>
|
||||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copies the Note ID to the clipboard for sharing</string>
|
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copies the Note ID to the clipboard for sharing in Nostr</string>
|
||||||
<string name="copy_channel_id_note_to_the_clipboard">Copy Channel ID (Note) to the Clipboard</string>
|
<string name="copy_channel_id_note_to_the_clipboard">Copy Channel ID (Note) to the Clipboard</string>
|
||||||
<string name="edits_the_channel_metadata">Edits the Channel Metadata</string>
|
<string name="edits_the_channel_metadata">Edits the Channel Metadata</string>
|
||||||
<string name="join">Join</string>
|
<string name="join">Join</string>
|
||||||
@@ -545,4 +545,7 @@
|
|||||||
<string name="automatically_play_videos_description">Automatically plays videos and GIFs</string>
|
<string name="automatically_play_videos_description">Automatically plays videos and GIFs</string>
|
||||||
<string name="automatically_show_url_preview_description">Show URL previews</string>
|
<string name="automatically_show_url_preview_description">Show URL previews</string>
|
||||||
<string name="load_image_description">When to load images</string>
|
<string name="load_image_description">When to load images</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>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -6,20 +6,19 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.vitorpamplona.amethyst.benchmark'
|
namespace 'com.vitorpamplona.amethyst.benchmark'
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = '17'
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
|
|
||||||
testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner'
|
testInstrumentationRunner 'androidx.benchmark.junit4.AndroidBenchmarkRunner'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.vitorpamplona.amethyst.benchmark
|
||||||
|
|
||||||
|
import androidx.benchmark.junit4.BenchmarkRule
|
||||||
|
import androidx.benchmark.junit4.measureRepeated
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.vitorpamplona.quartz.encoders.Bech32
|
||||||
|
import com.vitorpamplona.quartz.encoders.Hex
|
||||||
|
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||||
|
import com.vitorpamplona.quartz.encoders.toNpub
|
||||||
|
import com.vitorpamplona.quartz.events.Event
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class BechBenchmark {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val benchmarkRule = BenchmarkRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun npubEncoding() {
|
||||||
|
val myUser = Hex.decode("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c")
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertEquals("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z",myUser.toNpub())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun npubDecoding() {
|
||||||
|
val myUser = "npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z"
|
||||||
|
val expected = listOf(70, 12, 37, -26, -126, -3, -89, -125, 43, 82, -47, -14, 45, 61, 34, -77, 23, 109, -105, 47, 96, -36, -36, 50, 18, -19, -116, -110, -17, -123, 6, 92).map { it.toByte() }
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertEquals(expected, myUser.bechToBytes().toList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
package com.vitorpamplona.amethyst.benchmark
|
||||||
|
|
||||||
|
import androidx.benchmark.junit4.BenchmarkRule
|
||||||
|
import androidx.benchmark.junit4.measureRepeated
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
|
import com.vitorpamplona.quartz.encoders.Bech32
|
||||||
|
import com.vitorpamplona.quartz.encoders.Hex
|
||||||
|
import com.vitorpamplona.quartz.encoders.bechToBytes
|
||||||
|
import com.vitorpamplona.quartz.encoders.toNpub
|
||||||
|
import com.vitorpamplona.quartz.events.Event
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import junit.framework.TestCase.assertNotNull
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class CryptoBenchmark {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val benchmarkRule = BenchmarkRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getSharedKeyNip04() {
|
||||||
|
val keyPair1 = KeyPair()
|
||||||
|
val keyPair2 = KeyPair()
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertNotNull(CryptoUtils.getSharedSecretNIP04(keyPair1.privKey!!, keyPair2.pubKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun getSharedKeyNip44() {
|
||||||
|
val keyPair1 = KeyPair()
|
||||||
|
val keyPair2 = KeyPair()
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertNotNull(CryptoUtils.getSharedSecretNIP24(keyPair1.privKey!!, keyPair2.pubKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun computeSharedKeyNip04() {
|
||||||
|
val keyPair1 = KeyPair()
|
||||||
|
val keyPair2 = KeyPair()
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertNotNull(CryptoUtils.computeSharedSecretNIP04(keyPair1.privKey!!, keyPair2.pubKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun computeSharedKeyNip44() {
|
||||||
|
val keyPair1 = KeyPair()
|
||||||
|
val keyPair2 = KeyPair()
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertNotNull(CryptoUtils.computeSharedSecretNIP24(keyPair1.privKey!!, keyPair2.pubKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun random() {
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertNotNull(CryptoUtils.random(1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sha256() {
|
||||||
|
val keyPair = KeyPair()
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertNotNull(CryptoUtils.sha256(keyPair.pubKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sign() {
|
||||||
|
val keyPair = KeyPair()
|
||||||
|
val msg = CryptoUtils.sha256(CryptoUtils.random(1000))
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertNotNull(CryptoUtils.sign(msg, keyPair.privKey!!))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun verify() {
|
||||||
|
val keyPair = KeyPair()
|
||||||
|
val msg = CryptoUtils.sha256(CryptoUtils.random(1000))
|
||||||
|
val signature = CryptoUtils.sign(msg, keyPair.privKey!!)
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
assertNotNull(CryptoUtils.verifySignature(signature, msg, keyPair.pubKey))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+54
@@ -3,8 +3,11 @@ package com.vitorpamplona.amethyst.benchmark
|
|||||||
import androidx.benchmark.junit4.BenchmarkRule
|
import androidx.benchmark.junit4.BenchmarkRule
|
||||||
import androidx.benchmark.junit4.measureRepeated
|
import androidx.benchmark.junit4.measureRepeated
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
import com.vitorpamplona.quartz.encoders.toHexKey
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
|
import com.vitorpamplona.quartz.events.Event
|
||||||
|
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||||
import com.vitorpamplona.quartz.events.NIP24Factory
|
import com.vitorpamplona.quartz.events.NIP24Factory
|
||||||
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
import com.vitorpamplona.quartz.events.SealedGossipEvent
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
@@ -53,6 +56,38 @@ class GiftWrapBenchmark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun receivePerformanceTest(message: String) {
|
||||||
|
val sender = KeyPair()
|
||||||
|
val receiver = KeyPair()
|
||||||
|
|
||||||
|
val giftWrap = NIP24Factory().createMsgNIP24(
|
||||||
|
message,
|
||||||
|
listOf(receiver.pubKey.toHexKey()),
|
||||||
|
sender.privKey!!
|
||||||
|
).first()
|
||||||
|
|
||||||
|
val keyToUse = if (giftWrap.recipientPubKey() == sender.pubKey.toHexKey()) sender.privKey!! else receiver.privKey!!
|
||||||
|
val giftWrapJson = giftWrap.toJson()
|
||||||
|
|
||||||
|
// Simulate Receiver
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
CryptoUtils.clearCache()
|
||||||
|
|
||||||
|
val wrap = Event.fromJson(giftWrapJson) as GiftWrapEvent
|
||||||
|
wrap.checkSignature()
|
||||||
|
|
||||||
|
val seal = wrap.unwrap(keyToUse)
|
||||||
|
seal?.checkSignature()
|
||||||
|
|
||||||
|
if (seal is SealedGossipEvent) {
|
||||||
|
val innerData = seal.unseal(keyToUse)
|
||||||
|
Assert.assertEquals(message, innerData?.content)
|
||||||
|
} else {
|
||||||
|
Assert.fail("Wrong Event")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun tinyMessageHardCoded() {
|
fun tinyMessageHardCoded() {
|
||||||
@@ -77,6 +112,25 @@ class GiftWrapBenchmark {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun receivesTinyMessage() {
|
||||||
|
receivePerformanceTest("Hola, que tal?")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun receivesRegularMessage() {
|
||||||
|
receivePerformanceTest("Hi, honey, can you drop by the market and get some bread?")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun receivesLongMessageHardCoded() {
|
||||||
|
receivePerformanceTest(
|
||||||
|
"My queen, you are nothing short of royalty to me. You possess more beauty in the nail of your pinkie toe than everything else in this world combined. I am astounded by your grace, generosity, and graciousness. I am so lucky to know you. "
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@Test
|
@Test
|
||||||
fun tinyMessageHardCodedCompressed() {
|
fun tinyMessageHardCodedCompressed() {
|
||||||
|
|||||||
+44
@@ -74,6 +74,50 @@ class GiftWrapReceivingBenchmark {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkId() {
|
||||||
|
val sender = KeyPair()
|
||||||
|
val receiver = KeyPair()
|
||||||
|
|
||||||
|
val senderPublicKey = CryptoUtils.pubkeyCreate(sender.privKey!!).toHexKey()
|
||||||
|
val senderMessage = createMessage(sender, receiver)
|
||||||
|
|
||||||
|
val wrap = GiftWrapEvent.create(
|
||||||
|
event = SealedGossipEvent.create(
|
||||||
|
event = senderMessage,
|
||||||
|
encryptTo = senderPublicKey,
|
||||||
|
privateKey = sender.privKey!!
|
||||||
|
),
|
||||||
|
recipientPubKey = senderPublicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
wrap.hasCorrectIDHash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun checkSignature() {
|
||||||
|
val sender = KeyPair()
|
||||||
|
val receiver = KeyPair()
|
||||||
|
|
||||||
|
val senderPublicKey = CryptoUtils.pubkeyCreate(sender.privKey!!).toHexKey()
|
||||||
|
val senderMessage = createMessage(sender, receiver)
|
||||||
|
|
||||||
|
val wrap = GiftWrapEvent.create(
|
||||||
|
event = SealedGossipEvent.create(
|
||||||
|
event = senderMessage,
|
||||||
|
encryptTo = senderPublicKey,
|
||||||
|
privateKey = sender.privKey!!
|
||||||
|
),
|
||||||
|
recipientPubKey = senderPublicKey
|
||||||
|
)
|
||||||
|
|
||||||
|
benchmarkRule.measureRepeated {
|
||||||
|
wrap.hasVerifedSignature()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun decodeWrapEvent() {
|
fun decodeWrapEvent() {
|
||||||
val sender = KeyPair()
|
val sender = KeyPair()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ buildscript {
|
|||||||
coil_version = '2.4.0'
|
coil_version = '2.4.0'
|
||||||
vico_version = '1.7.3'
|
vico_version = '1.7.3'
|
||||||
exoplayer_version = '1.1.0'
|
exoplayer_version = '1.1.0'
|
||||||
|
media3_version = '1.1.0'
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.google.gms:google-services:4.3.15'
|
classpath 'com.google.gms:google-services:4.3.15'
|
||||||
|
|||||||
+2
-2
@@ -6,11 +6,11 @@ plugins {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'com.vitorpamplona.quartz'
|
namespace 'com.vitorpamplona.quartz'
|
||||||
compileSdk 33
|
compileSdk 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 34
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
consumerProguardFiles "consumer-rules.pro"
|
consumerProguardFiles "consumer-rules.pro"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.vitorpamplona.quartz.crypto
|
package com.vitorpamplona.quartz.crypto
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.LruCache
|
||||||
import com.goterl.lazysodium.SodiumAndroid
|
import com.goterl.lazysodium.SodiumAndroid
|
||||||
import com.goterl.lazysodium.utils.Key
|
import com.goterl.lazysodium.utils.Key
|
||||||
import com.vitorpamplona.quartz.events.Event
|
|
||||||
import com.vitorpamplona.quartz.encoders.Hex
|
import com.vitorpamplona.quartz.encoders.Hex
|
||||||
|
import com.vitorpamplona.quartz.events.Event
|
||||||
import fr.acinq.secp256k1.Secp256k1
|
import fr.acinq.secp256k1.Secp256k1
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
@@ -15,11 +16,19 @@ import javax.crypto.spec.SecretKeySpec
|
|||||||
|
|
||||||
|
|
||||||
object CryptoUtils {
|
object CryptoUtils {
|
||||||
|
private val sharedKeyCache04 = LruCache<Int, ByteArray>(200)
|
||||||
|
private val sharedKeyCache24 = LruCache<Int, ByteArray>(200)
|
||||||
|
|
||||||
private val secp256k1 = Secp256k1.get()
|
private val secp256k1 = Secp256k1.get()
|
||||||
private val libSodium = SodiumAndroid()
|
private val libSodium = SodiumAndroid()
|
||||||
private val random = SecureRandom()
|
private val random = SecureRandom()
|
||||||
private val h02 = Hex.decode("02")
|
private val h02 = Hex.decode("02")
|
||||||
|
|
||||||
|
fun clearCache() {
|
||||||
|
sharedKeyCache04.evictAll()
|
||||||
|
sharedKeyCache24.evictAll()
|
||||||
|
}
|
||||||
|
|
||||||
fun randomInt(bound: Int): Int {
|
fun randomInt(bound: Int): Int {
|
||||||
return random.nextInt(bound)
|
return random.nextInt(bound)
|
||||||
}
|
}
|
||||||
@@ -27,8 +36,10 @@ object CryptoUtils {
|
|||||||
/**
|
/**
|
||||||
* Provides a 32B "private key" aka random number
|
* Provides a 32B "private key" aka random number
|
||||||
*/
|
*/
|
||||||
fun privkeyCreate(): ByteArray {
|
fun privkeyCreate() = random(32)
|
||||||
val bytes = ByteArray(32)
|
|
||||||
|
fun random(size: Int): ByteArray {
|
||||||
|
val bytes = ByteArray(size)
|
||||||
random.nextBytes(bytes)
|
random.nextBytes(bytes)
|
||||||
return bytes
|
return bytes
|
||||||
}
|
}
|
||||||
@@ -52,12 +63,6 @@ object CryptoUtils {
|
|||||||
return MessageDigest.getInstance("SHA-256").digest(data)
|
return MessageDigest.getInstance("SHA-256").digest(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return 32B shared secret
|
|
||||||
*/
|
|
||||||
fun getSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
|
|
||||||
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
|
|
||||||
|
|
||||||
fun encryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
|
fun encryptNIP04(msg: String, privateKey: ByteArray, pubKey: ByteArray): String {
|
||||||
val info = encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey))
|
val info = encryptNIP04(msg, getSharedSecretNIP04(privateKey, pubKey))
|
||||||
val encryptionInfo = EncryptedInfoString(
|
val encryptionInfo = EncryptedInfoString(
|
||||||
@@ -150,7 +155,39 @@ object CryptoUtils {
|
|||||||
/**
|
/**
|
||||||
* @return 32B shared secret
|
* @return 32B shared secret
|
||||||
*/
|
*/
|
||||||
fun getSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
|
fun getSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
|
||||||
|
val hash = combinedHashCode(privateKey, pubKey)
|
||||||
|
val preComputed = sharedKeyCache04[hash]
|
||||||
|
if (preComputed != null) return preComputed
|
||||||
|
|
||||||
|
val computed = computeSharedSecretNIP04(privateKey, pubKey)
|
||||||
|
sharedKeyCache04.put(hash, computed)
|
||||||
|
return computed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 32B shared secret
|
||||||
|
*/
|
||||||
|
fun computeSharedSecretNIP04(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
|
||||||
|
secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 32B shared secret
|
||||||
|
*/
|
||||||
|
fun getSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray {
|
||||||
|
val hash = combinedHashCode(privateKey, pubKey)
|
||||||
|
val preComputed = sharedKeyCache24[hash]
|
||||||
|
if (preComputed != null) return preComputed
|
||||||
|
|
||||||
|
val computed = computeSharedSecretNIP24(privateKey, pubKey)
|
||||||
|
sharedKeyCache24.put(hash, computed)
|
||||||
|
return computed
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 32B shared secret
|
||||||
|
*/
|
||||||
|
fun computeSharedSecretNIP24(privateKey: ByteArray, pubKey: ByteArray): ByteArray =
|
||||||
sha256(secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33))
|
sha256(secp256k1.pubKeyTweakMul(h02 + pubKey, privateKey).copyOfRange(1, 33))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +250,13 @@ fun decodeJackson(json: String): EncryptedInfo {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun combinedHashCode(a: ByteArray, b: ByteArray): Int {
|
||||||
|
var result = 1
|
||||||
|
for (element in a) result = 31 * result + element
|
||||||
|
for (element in b) result = 31 * result + element
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
OLD Versions used for the Benchmark
|
OLD Versions used for the Benchmark
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ private typealias Int5 = Byte
|
|||||||
*/
|
*/
|
||||||
object Bech32 {
|
object Bech32 {
|
||||||
const val alphabet: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
const val alphabet: String = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||||
|
const val alphabetUpperCase: String = "QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L"
|
||||||
|
|
||||||
enum class Encoding(public val constant: Int) {
|
enum class Encoding(public val constant: Int) {
|
||||||
Bech32(1),
|
Bech32(1),
|
||||||
@@ -44,20 +45,26 @@ object Bech32 {
|
|||||||
for (i in 0..alphabet.lastIndex) {
|
for (i in 0..alphabet.lastIndex) {
|
||||||
map[alphabet[i].code] = i.toByte()
|
map[alphabet[i].code] = i.toByte()
|
||||||
}
|
}
|
||||||
|
for (i in 0..alphabetUpperCase.lastIndex) {
|
||||||
|
map[alphabetUpperCase[i].code] = i.toByte()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expand(hrp: String): Array<Int5> {
|
fun expand(hrp: String): Array<Int5> {
|
||||||
val result = Array<Int5>(hrp.length + 1 + hrp.length) { 0 }
|
val half = hrp.length + 1
|
||||||
for (i in hrp.indices) {
|
val size = half + hrp.length
|
||||||
result[i] = hrp[i].code.shr(5).toByte()
|
return Array<Int5>(size) {
|
||||||
result[hrp.length + 1 + i] = (hrp[i].code and 31).toByte()
|
when (it) {
|
||||||
|
in hrp.indices -> hrp[it].code.shr(5).toByte()
|
||||||
|
in half until size -> (hrp[it - half].code and 31).toByte()
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
result[hrp.length] = 0
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun polymod(values: Array<Int5>, values1: Array<Int5>): Int {
|
private val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
|
||||||
val GEN = arrayOf(0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3)
|
|
||||||
|
fun polymod(values: Array<Int5>, values1: Array<Int5>): Int {
|
||||||
var chk = 1
|
var chk = 1
|
||||||
values.forEach { v ->
|
values.forEach { v ->
|
||||||
val b = chk shr 25
|
val b = chk shr 25
|
||||||
@@ -83,14 +90,18 @@ object Bech32 {
|
|||||||
* @return hrp + data encoded as a Bech32 string
|
* @return hrp + data encoded as a Bech32 string
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
public fun encode(hrp: String, int5s: Array<Int5>, encoding: Encoding): String {
|
public fun encode(hrp: String, int5s: ArrayList<Int5>, encoding: Encoding): String {
|
||||||
require(hrp.lowercase() == hrp || hrp.uppercase() == hrp) { "mixed case strings are not valid bech32 prefixes" }
|
require(hrp.lowercase() == hrp || hrp.uppercase() == hrp) { "mixed case strings are not valid bech32 prefixes" }
|
||||||
val data = int5s.toByteArray().toTypedArray()
|
val dataWithChecksum = when (encoding) {
|
||||||
val checksum = when (encoding) {
|
Encoding.Beck32WithoutChecksum -> int5s
|
||||||
Encoding.Beck32WithoutChecksum -> arrayOf()
|
else -> addChecksum(hrp, int5s, encoding)
|
||||||
else -> checksum(hrp, data, encoding)
|
|
||||||
}
|
}
|
||||||
return hrp + "1" + (data + checksum).map { i -> alphabet[i.toInt()] }.toCharArray().concatToString()
|
|
||||||
|
val charArray = CharArray(dataWithChecksum.size) {
|
||||||
|
alphabet[dataWithChecksum[it].toInt()]
|
||||||
|
}.concatToString()
|
||||||
|
|
||||||
|
return hrp + "1" + charArray
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -110,14 +121,21 @@ object Bech32 {
|
|||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
public fun decode(bech32: String, noChecksum: Boolean = false): Triple<String, Array<Int5>, Encoding> {
|
public fun decode(bech32: String, noChecksum: Boolean = false): Triple<String, Array<Int5>, Encoding> {
|
||||||
require(bech32.lowercase() == bech32 || bech32.uppercase() == bech32) { "mixed case strings are not valid bech32" }
|
var pos = 0
|
||||||
bech32.forEach { require(it.code in 33..126) { "invalid character " } }
|
bech32.forEachIndexed { index, char ->
|
||||||
val input = bech32.lowercase()
|
require( char.code in 33..126 ) { "invalid character ${char}" }
|
||||||
val pos = input.lastIndexOf('1')
|
if (char == '1') {
|
||||||
val hrp = input.take(pos)
|
pos = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val hrp = bech32.take(pos)
|
||||||
require(hrp.length in 1..83) { "hrp must contain 1 to 83 characters" }
|
require(hrp.length in 1..83) { "hrp must contain 1 to 83 characters" }
|
||||||
val data = Array<Int5>(input.length - pos - 1) { 0 }
|
|
||||||
for (i in 0..data.lastIndex) data[i] = map[input[pos + 1 + i].code]
|
val data = Array(bech32.length - pos - 1) {
|
||||||
|
map[bech32[pos + 1 + it].code]
|
||||||
|
}
|
||||||
|
|
||||||
return if (noChecksum) {
|
return if (noChecksum) {
|
||||||
Triple(hrp, data, Encoding.Beck32WithoutChecksum)
|
Triple(hrp, data, Encoding.Beck32WithoutChecksum)
|
||||||
} else {
|
} else {
|
||||||
@@ -126,7 +144,7 @@ object Bech32 {
|
|||||||
Encoding.Bech32m.constant -> Encoding.Bech32m
|
Encoding.Bech32m.constant -> Encoding.Bech32m
|
||||||
else -> throw IllegalArgumentException("invalid checksum for $bech32")
|
else -> throw IllegalArgumentException("invalid checksum for $bech32")
|
||||||
}
|
}
|
||||||
Triple(hrp, data.dropLast(6).toTypedArray(), encoding)
|
Triple(hrp, data.copyOfRange(0, data.size-6), encoding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,16 +160,23 @@ object Bech32 {
|
|||||||
return Triple(hrp, five2eight(int5s, 0), encoding)
|
return Triple(hrp, five2eight(int5s, 0), encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val ZEROS = arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param hrp Human Readable Part
|
* @param hrp Human Readable Part
|
||||||
* @param data data (a sequence of 5 bits integers)
|
* @param data data (a sequence of 5 bits integers)
|
||||||
* @param encoding encoding to use (bech32 or bech32m)
|
* @param encoding encoding to use (bech32 or bech32m)
|
||||||
* @return a checksum computed over hrp and data
|
* @return a checksum computed over hrp and data
|
||||||
*/
|
*/
|
||||||
private fun checksum(hrp: String, data: Array<Int5>, encoding: Encoding): Array<Int5> {
|
private fun addChecksum(hrp: String, data: ArrayList<Int5>, encoding: Encoding): ArrayList<Int5> {
|
||||||
val values = expand(hrp) + data
|
val values = expand(hrp) + data
|
||||||
val poly = polymod(values, arrayOf(0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(), 0.toByte())) xor encoding.constant
|
val poly = polymod(values, ZEROS) xor encoding.constant
|
||||||
return Array(6) { i -> (poly.shr(5 * (5 - i)) and 31).toByte() }
|
|
||||||
|
for (i in 0 until 6) {
|
||||||
|
data.add((poly.shr(5 * (5 - i)) and 31).toByte())
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,9 +184,9 @@ object Bech32 {
|
|||||||
* @return a sequence of 5 bits integers
|
* @return a sequence of 5 bits integers
|
||||||
*/
|
*/
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
public fun eight2five(input: ByteArray): Array<Int5> {
|
public fun eight2five(input: ByteArray): ArrayList<Int5> {
|
||||||
var buffer = 0L
|
var buffer = 0L
|
||||||
val output = ArrayList<Int5>()
|
val output = ArrayList<Int5>(input.size * 2) //larger array on purpose. Checksum is added later.
|
||||||
var count = 0
|
var count = 0
|
||||||
input.forEach { b ->
|
input.forEach { b ->
|
||||||
buffer = (buffer shl 8) or (b.toLong() and 0xff)
|
buffer = (buffer shl 8) or (b.toLong() and 0xff)
|
||||||
@@ -172,7 +197,7 @@ object Bech32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (count > 0) output.add(((buffer shl (5 - count)) and 31).toByte())
|
if (count > 0) output.add(((buffer shl (5 - count)) and 31).toByte())
|
||||||
return output.toTypedArray()
|
return output
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -182,7 +207,7 @@ object Bech32 {
|
|||||||
@JvmStatic
|
@JvmStatic
|
||||||
public fun five2eight(input: Array<Int5>, offset: Int): ByteArray {
|
public fun five2eight(input: Array<Int5>, offset: Int): ByteArray {
|
||||||
var buffer = 0L
|
var buffer = 0L
|
||||||
val output = ArrayList<Byte>()
|
val output = ArrayList<Byte>(input.size)
|
||||||
var count = 0
|
var count = 0
|
||||||
for (i in offset..input.lastIndex) {
|
for (i in offset..input.lastIndex) {
|
||||||
val b = input[i]
|
val b = input[i]
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.vitorpamplona.quartz.events
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
||||||
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
||||||
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
class AudioHeaderEvent(
|
||||||
|
id: HexKey,
|
||||||
|
pubKey: HexKey,
|
||||||
|
createdAt: Long,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
|
||||||
|
fun download() = tags.firstOrNull { it.size > 1 && it[0] == DOWNLOAD_URL }?.get(1)
|
||||||
|
fun stream() = tags.firstOrNull { it.size > 1 && it[0] == STREAM_URL }?.get(1)
|
||||||
|
fun wavefrom() = tags.firstOrNull { it.size > 1 && it[0] == WAVEFORM }?.get(1)?.let {
|
||||||
|
mapper.readValue<List<Int>>(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val kind = 1808
|
||||||
|
|
||||||
|
private const val DOWNLOAD_URL = "download_url"
|
||||||
|
private const val STREAM_URL = "stream_url"
|
||||||
|
private const val WAVEFORM = "waveform"
|
||||||
|
|
||||||
|
fun create(
|
||||||
|
description: String,
|
||||||
|
downloadUrl: String,
|
||||||
|
streamUrl: String? = null,
|
||||||
|
wavefront: String? = null,
|
||||||
|
sensitiveContent: Boolean? = null,
|
||||||
|
privateKey: ByteArray,
|
||||||
|
createdAt: Long = TimeUtils.now()
|
||||||
|
): AudioHeaderEvent {
|
||||||
|
val tags = listOfNotNull(
|
||||||
|
downloadUrl.let { listOf(DOWNLOAD_URL, it) },
|
||||||
|
streamUrl?.let { listOf(STREAM_URL, it) },
|
||||||
|
wavefront?.let { listOf(WAVEFORM, it) },
|
||||||
|
sensitiveContent?.let {
|
||||||
|
if (it) {
|
||||||
|
listOf("content-warning", "")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val content = description
|
||||||
|
val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey()
|
||||||
|
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||||
|
val sig = CryptoUtils.sign(id, privateKey)
|
||||||
|
return AudioHeaderEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ class ChatMessageEvent(
|
|||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig), ChatroomKeyable {
|
) : WrappedEvent(id, pubKey, createdAt, kind, tags, content, sig), ChatroomKeyable {
|
||||||
/**
|
/**
|
||||||
* Recepients intended to receive this conversation
|
* Recepients intended to receive this conversation
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ class ContactListEvent(
|
|||||||
|
|
||||||
return create(
|
return create(
|
||||||
content = earlierVersion.content,
|
content = earlierVersion.content,
|
||||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != hashtag },
|
tags = earlierVersion.tags.filter { it.size > 1 && !it[1].equals(hashtag, true) },
|
||||||
privateKey = privateKey,
|
privateKey = privateKey,
|
||||||
createdAt = createdAt
|
createdAt = createdAt
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -362,6 +362,22 @@ open class Event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
open class WrappedEvent(
|
||||||
|
id: HexKey,
|
||||||
|
@JsonProperty("pubkey")
|
||||||
|
pubKey: HexKey,
|
||||||
|
@JsonProperty("created_at")
|
||||||
|
createdAt: Long,
|
||||||
|
kind: Int,
|
||||||
|
tags: List<List<String>>,
|
||||||
|
content: String,
|
||||||
|
sig: HexKey
|
||||||
|
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
|
@Transient
|
||||||
|
var host: Event? = null // host event to broadcast when needed
|
||||||
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
interface AddressableEvent {
|
interface AddressableEvent {
|
||||||
fun dTag(): String
|
fun dTag(): String
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class EventFactory {
|
|||||||
AdvertisedRelayListEvent.kind -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
AdvertisedRelayListEvent.kind -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
|
AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
AudioHeaderEvent.kind -> AudioHeaderEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
|
BadgeAwardEvent.kind -> BadgeAwardEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
BadgeDefinitionEvent.kind -> BadgeDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class GiftWrapEvent(
|
|||||||
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
|
if (cachedInnerEvent.contains(hex)) return cachedInnerEvent[hex]
|
||||||
|
|
||||||
val myInnerEvent = unwrap(privKey = privKey)
|
val myInnerEvent = unwrap(privKey = privKey)
|
||||||
|
if (myInnerEvent is WrappedEvent) {
|
||||||
|
myInnerEvent.host = this
|
||||||
|
}
|
||||||
|
|
||||||
cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent)
|
cachedInnerEvent = cachedInnerEvent + Pair(hex, myInnerEvent)
|
||||||
return myInnerEvent
|
return myInnerEvent
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ class LiveActivitiesEvent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun participantsIntersect(keySet: Set<String>): Boolean {
|
||||||
|
return tags.any { it.size > 1 && it[0] == "p" && it[1] in keySet }
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val kind = 30311
|
const val kind = 30311
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class SealedGossipEvent(
|
|||||||
tags: List<List<String>>,
|
tags: List<List<String>>,
|
||||||
content: String,
|
content: String,
|
||||||
sig: HexKey
|
sig: HexKey
|
||||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
): WrappedEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||||
@Transient
|
@Transient
|
||||||
private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
private var cachedInnerEvent: Map<HexKey, Event?> = mapOf()
|
||||||
|
|
||||||
@@ -30,6 +30,10 @@ class SealedGossipEvent(
|
|||||||
|
|
||||||
val gossip = unseal(privKey = privKey)
|
val gossip = unseal(privKey = privKey)
|
||||||
val event = gossip?.mergeWith(this)
|
val event = gossip?.mergeWith(this)
|
||||||
|
if (event is WrappedEvent) {
|
||||||
|
event.host = host ?: this
|
||||||
|
}
|
||||||
|
|
||||||
cachedInnerEvent = cachedInnerEvent + Pair(hex, event)
|
cachedInnerEvent = cachedInnerEvent + Pair(hex, event)
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user