Moves Channels (public chats, ephemeral channels and live streams) Account modules to commons
This commit is contained in:
Vendored
+15
-1
@@ -20,9 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.cache
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
/**
|
||||
@@ -81,7 +84,16 @@ interface ICacheProvider {
|
||||
* @param hexKey The note's ID in hex format
|
||||
* @return The Note (existing or newly created)
|
||||
*/
|
||||
fun checkGetOrCreateNote(hexKey: HexKey): Any?
|
||||
fun checkGetOrCreateNote(hexKey: HexKey): Note?
|
||||
|
||||
/**
|
||||
* Gets an existing AddressableNote or creates a new one if it doesn't exist.
|
||||
* Used by ThreadAssembler for building thread structures.
|
||||
*
|
||||
* @param address The note's ID in address format
|
||||
* @return The AddressableNote (existing or newly created)
|
||||
*/
|
||||
fun getOrCreateAddressableNote(key: Address): AddressableNote
|
||||
|
||||
/**
|
||||
* Gets the event stream for cache updates.
|
||||
@@ -99,4 +111,6 @@ interface ICacheProvider {
|
||||
* @return true if the event has been deleted, false otherwise
|
||||
*/
|
||||
fun hasBeenDeleted(event: Any): Boolean
|
||||
|
||||
fun justConsumeMyOwnEvent(event: Event): Boolean
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.emphChat
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
|
||||
@Stable
|
||||
class EphemeralChatChannel(
|
||||
val roomId: RoomId,
|
||||
) : Channel() {
|
||||
override fun relays() = setOf(roomId.relayUrl)
|
||||
|
||||
override fun toBestDisplayName() = roomId.toDisplayKey()
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean = roomId.id.contains(prefix, true)
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.emphChat
|
||||
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.roomSet
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.rooms
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
|
||||
class EphemeralChatListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<EphemeralChatListEvent>(signer)
|
||||
|
||||
fun cachedRoomSet(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).roomSet()
|
||||
|
||||
fun cachedRooms(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagListPrecached(event).rooms()
|
||||
|
||||
suspend fun roomSet(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagList(event).roomSet()
|
||||
|
||||
suspend fun rooms(event: EphemeralChatListEvent) = cachedPrivateLists.mergeTagList(event).rooms()
|
||||
}
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.emphChat
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.chat.RoomId
|
||||
import com.vitorpamplona.quartz.experimental.ephemChat.list.EphemeralChatListEvent
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface EphemeralChatRepository {
|
||||
fun ephemeralChatList(): EphemeralChatListEvent?
|
||||
|
||||
fun updateEphemeralChatListTo(newEphemeralChatList: EphemeralChatListEvent?)
|
||||
}
|
||||
|
||||
class EphemeralChatListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: ICacheProvider,
|
||||
val decryptionCache: EphemeralChatListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: EphemeralChatRepository,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val ephemeralChatListNote = cache.getOrCreateAddressableNote(getEphemeralChatListAddress())
|
||||
|
||||
fun getEphemeralChatListAddress() = EphemeralChatListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getEphemeralChatListFlow(): StateFlow<NoteState> = ephemeralChatListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getEphemeralChatList(): EphemeralChatListEvent? = ephemeralChatListNote.event as? EphemeralChatListEvent
|
||||
|
||||
suspend fun ephemeralChatListWithBackup(note: Note): Set<RoomId> {
|
||||
val event = note.event as? EphemeralChatListEvent ?: settings.ephemeralChatList()
|
||||
return event?.let { decryptionCache.roomSet(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val liveEphemeralChatList: StateFlow<Set<RoomId>> =
|
||||
getEphemeralChatListFlow()
|
||||
.transformLatest { noteState ->
|
||||
emit(ephemeralChatListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(ephemeralChatListWithBackup(ephemeralChatListNote))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
suspend fun follow(channel: EphemeralChatChannel): EphemeralChatListEvent {
|
||||
val ephemeralChatList = getEphemeralChatList()
|
||||
|
||||
return if (ephemeralChatList == null) {
|
||||
EphemeralChatListEvent.create(
|
||||
room = channel.roomId,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
EphemeralChatListEvent.add(
|
||||
earlierVersion = ephemeralChatList,
|
||||
room = channel.roomId,
|
||||
isPrivate = true,
|
||||
signer = signer,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unfollow(channel: EphemeralChatChannel): EphemeralChatListEvent? {
|
||||
val ephemeralChatList = getEphemeralChatList()
|
||||
return if (ephemeralChatList != null) {
|
||||
EphemeralChatListEvent.remove(
|
||||
earlierVersion = ephemeralChatList,
|
||||
room = channel.roomId,
|
||||
signer = signer,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.ephemeralChatList()?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved ephemeral chat list")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "EphemeralChatList Collector Start")
|
||||
getEphemeralChatListFlow().collect { noteState ->
|
||||
Log.d("AccountRegisterObservers", "EphemeralChatList List for ${signer.pubKey}")
|
||||
(noteState.note.event as? EphemeralChatListEvent)?.let {
|
||||
settings.updateEphemeralChatListTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip28PublicChats
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
import com.vitorpamplona.quartz.nip01Core.core.EmptyTagList
|
||||
import com.vitorpamplona.quartz.nip01Core.core.toImmutableListOfLists
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.types.EventIdHintOptional
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelDataNorm
|
||||
|
||||
@Stable
|
||||
class PublicChatChannel(
|
||||
val idHex: String,
|
||||
) : Channel() {
|
||||
var creator: User? = null
|
||||
var event: ChannelCreateEvent? = null
|
||||
|
||||
// Important to keep this long-term reference because LocalCache uses WeakReferences.
|
||||
var creationEventNote: Note? = null
|
||||
var updateEventNote: Note? = null
|
||||
|
||||
var info = ChannelDataNorm(null, null, null, null)
|
||||
var infoTags = EmptyTagList
|
||||
var updatedMetadataAt: Long = 0
|
||||
|
||||
override fun relays() = info.relays?.toSet() ?: super.relays()
|
||||
|
||||
fun relayHintUrls() = relays().take(3)
|
||||
|
||||
fun relayHintUrl() = relays().firstOrNull()
|
||||
|
||||
fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, relayHintUrls())
|
||||
|
||||
fun toNostrUri() = "nostr:${toNEvent()}"
|
||||
|
||||
fun toEventHint() = event?.let { EventHintBundle(it, relayHintUrl(), null) }
|
||||
|
||||
fun toEventId() = EventIdHintOptional(idHex, relayHintUrl())
|
||||
|
||||
fun updateChannelInfo(
|
||||
creator: User,
|
||||
event: ChannelCreateEvent,
|
||||
eventNote: Note? = null,
|
||||
) {
|
||||
this.creator = creator
|
||||
this.event = event
|
||||
this.info = event.channelInfo()
|
||||
|
||||
this.infoTags = event.tags.toImmutableListOfLists()
|
||||
this.updatedMetadataAt = event.createdAt
|
||||
this.creationEventNote = eventNote
|
||||
|
||||
updateChannelInfo()
|
||||
}
|
||||
|
||||
fun updateChannelInfo(
|
||||
creator: User,
|
||||
event: ChannelMetadataEvent,
|
||||
eventNote: Note? = null,
|
||||
) {
|
||||
this.creator = creator
|
||||
this.info = event.channelInfo()
|
||||
|
||||
this.infoTags = event.tags.toImmutableListOfLists()
|
||||
this.updatedMetadataAt = event.createdAt
|
||||
this.updateEventNote = eventNote
|
||||
|
||||
super.updateChannelInfo()
|
||||
}
|
||||
|
||||
override fun toBestDisplayName(): String = info.name ?: toNEvent().toShortDisplay()
|
||||
|
||||
fun summary(): String? = info.about
|
||||
|
||||
fun profilePicture(): String? {
|
||||
if (info.picture.isNullOrBlank()) return creator?.info?.banner
|
||||
return info.picture ?: creator?.info?.banner
|
||||
}
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean =
|
||||
idHex.startsWith(prefix) ||
|
||||
info.name?.contains(prefix, true) == true ||
|
||||
info.about?.contains(prefix, true) == true
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip28PublicChats
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.channelSet
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.channels
|
||||
import com.vitorpamplona.quartz.nip51Lists.PrivateTagArrayEventCache
|
||||
|
||||
class PublicChatListDecryptionCache(
|
||||
val signer: NostrSigner,
|
||||
) {
|
||||
val cachedPrivateLists = PrivateTagArrayEventCache<ChannelListEvent>(signer)
|
||||
|
||||
fun cachedChannelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channelSet()
|
||||
|
||||
fun cachedChannels(event: ChannelListEvent) = cachedPrivateLists.mergeTagListPrecached(event).channels()
|
||||
|
||||
suspend fun channelSet(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channelSet()
|
||||
|
||||
suspend fun channels(event: ChannelListEvent) = cachedPrivateLists.mergeTagList(event).channels()
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip28PublicChats
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.NoteState
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.ICacheProvider
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.ChannelListEvent
|
||||
import com.vitorpamplona.quartz.nip28PublicChat.list.tags.ChannelTag
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
interface PublicChatListRepository {
|
||||
fun channelList(): ChannelListEvent?
|
||||
|
||||
fun updateChannelListTo(newChannelList: ChannelListEvent?)
|
||||
}
|
||||
|
||||
class PublicChatListState(
|
||||
val signer: NostrSigner,
|
||||
val cache: ICacheProvider,
|
||||
val decryptionCache: PublicChatListDecryptionCache,
|
||||
val scope: CoroutineScope,
|
||||
val settings: PublicChatListRepository,
|
||||
) {
|
||||
// Creates a long-term reference for this note so that the GC doesn't collect the note it self
|
||||
val publicChatListNote = cache.getOrCreateAddressableNote(getChannelListAddress())
|
||||
|
||||
fun getChannelListAddress() = ChannelListEvent.createAddress(signer.pubKey)
|
||||
|
||||
fun getChannelListFlow(): StateFlow<NoteState> = publicChatListNote.flow().metadata.stateFlow
|
||||
|
||||
fun getChannelList(): ChannelListEvent? = publicChatListNote.event as? ChannelListEvent
|
||||
|
||||
suspend fun publicChatListWithBackup(note: Note): Set<ChannelTag> {
|
||||
val event = note.event as? ChannelListEvent ?: settings.channelList()
|
||||
return event?.let { decryptionCache.channelSet(it) } ?: emptySet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flow: StateFlow<Set<ChannelTag>> =
|
||||
getChannelListFlow()
|
||||
.transformLatest { noteState ->
|
||||
emit(publicChatListWithBackup(noteState.note))
|
||||
}.onStart {
|
||||
emit(publicChatListWithBackup(publicChatListNote))
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
val flowSet: StateFlow<Set<HexKey>> =
|
||||
flow
|
||||
.map {
|
||||
it.mapTo(mutableSetOf()) { it.eventId }
|
||||
}.onStart {
|
||||
emit(flow.value.mapTo(mutableSetOf()) { it.eventId })
|
||||
}.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
emptySet(),
|
||||
)
|
||||
|
||||
suspend fun follow(channel: PublicChatChannel): ChannelListEvent {
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
return if (publicChatList == null) {
|
||||
ChannelListEvent.create(ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer)
|
||||
} else {
|
||||
ChannelListEvent.add(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun follow(channels: List<PublicChatChannel>): ChannelListEvent {
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
val channelTags = channels.map { ChannelTag(it.idHex, it.relayHintUrl()) }
|
||||
return if (publicChatList == null) {
|
||||
ChannelListEvent.create(channelTags, true, signer)
|
||||
} else {
|
||||
ChannelListEvent.add(publicChatList, channelTags, true, signer)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unfollow(channel: PublicChatChannel): ChannelListEvent? {
|
||||
val publicChatList = getChannelList()
|
||||
|
||||
return if (publicChatList != null) {
|
||||
ChannelListEvent.remove(publicChatList, ChannelTag(channel.idHex, channel.relayHintUrl()), signer)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
settings.channelList()?.let { event ->
|
||||
Log.d("AccountRegisterObservers", "Loading saved channel list ${event.toJson()}")
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
Log.d("AccountRegisterObservers", "Channel List Collector Start")
|
||||
getChannelListFlow().collect {
|
||||
Log.d("AccountRegisterObservers", "Channel List for ${signer.pubKey}")
|
||||
(it.note.event as? ChannelListEvent)?.let {
|
||||
settings.updateChannelListTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip38UserStatuses
|
||||
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.nip09Deletions.DeletionEvent
|
||||
import com.vitorpamplona.quartz.nip38UserStatus.StatusEvent
|
||||
|
||||
class UserStatusAction {
|
||||
companion object {
|
||||
suspend fun create(
|
||||
newStatus: String,
|
||||
signer: NostrSigner,
|
||||
): StatusEvent = StatusEvent.create(newStatus, "general", expiration = null, signer)
|
||||
|
||||
suspend fun update(
|
||||
oldStatus: AddressableNote,
|
||||
newStatus: String,
|
||||
signer: NostrSigner,
|
||||
): StatusEvent {
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event")
|
||||
|
||||
return StatusEvent.update(oldEvent, newStatus, signer)
|
||||
}
|
||||
|
||||
suspend fun delete(
|
||||
oldStatus: AddressableNote,
|
||||
signer: NostrSigner,
|
||||
): List<Event> {
|
||||
val oldEvent = oldStatus.event as? StatusEvent ?: throw IllegalStateException("Tried to update a non-status event")
|
||||
|
||||
val event = StatusEvent.clear(oldEvent, signer)
|
||||
|
||||
val deletion =
|
||||
signer.sign(
|
||||
DeletionEvent.buildForVersionOnly(listOf(event)),
|
||||
)
|
||||
|
||||
return listOf(event, deletion)
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.model.nip53LiveActivities
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.util.toShortDisplay
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.aTag.ATag
|
||||
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
|
||||
|
||||
@Stable
|
||||
class LiveActivitiesChannel(
|
||||
val address: Address,
|
||||
) : Channel() {
|
||||
var creator: User? = null
|
||||
var info: LiveActivitiesEvent? = null
|
||||
|
||||
// Important to keep this long-term reference because LocalCache uses WeakReferences.
|
||||
var infoNote: Note? = null
|
||||
|
||||
fun address() = address
|
||||
|
||||
override fun relays() = info?.allRelayUrls()?.toSet()?.ifEmpty { null } ?: super.relays()
|
||||
|
||||
fun relayHintUrl() = relays().firstOrNull()
|
||||
|
||||
fun relayHintUrls() = relays().take(3)
|
||||
|
||||
fun updateChannelInfo(
|
||||
creator: User,
|
||||
channelInfo: LiveActivitiesEvent,
|
||||
channelInfoNote: Note,
|
||||
) {
|
||||
this.info = channelInfo
|
||||
this.creator = creator
|
||||
this.infoNote = channelInfoNote
|
||||
super.updateChannelInfo()
|
||||
}
|
||||
|
||||
override fun toBestDisplayName(): String = info?.title() ?: creatorName() ?: toNAddr().toShortDisplay()
|
||||
|
||||
fun creatorName(): String? = creator?.toBestDisplayName()
|
||||
|
||||
fun summary(): String? = info?.summary()
|
||||
|
||||
fun profilePicture(): String? = info?.image()?.ifBlank { null }
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean =
|
||||
info?.title()?.contains(prefix, true) == true ||
|
||||
info?.summary()?.contains(prefix, true) == true
|
||||
|
||||
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrls())
|
||||
|
||||
fun toATag() = ATag(address, relayHintUrl())
|
||||
|
||||
fun toNostrUri() = "nostr:${toNAddr()}"
|
||||
}
|
||||
Reference in New Issue
Block a user