refactor nostr service
This commit is contained in:
@@ -32,7 +32,8 @@ class NostrForegroundService : Service() {
|
|||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
val notification = createNotification("Connecting to Nostr...")
|
|
||||||
|
val notification = createNotification()
|
||||||
startForeground(1, notification)
|
startForeground(1, notification)
|
||||||
|
|
||||||
serviceScope.launch {
|
serviceScope.launch {
|
||||||
@@ -44,11 +45,25 @@ class NostrForegroundService : Service() {
|
|||||||
// Connect to bootstrap relays
|
// Connect to bootstrap relays
|
||||||
nostr.connectBootstrapRelays()
|
nostr.connectBootstrapRelays()
|
||||||
// Handle notifications
|
// Handle notifications
|
||||||
nostr.handleLiteNotifications { event ->
|
nostr.handleNotifications(
|
||||||
|
onMetadataUpdate = { pubkey, metadata ->
|
||||||
|
serviceScope.launch { nostr.emitMetadataUpdate(pubkey, metadata) }
|
||||||
|
},
|
||||||
|
onContactListUpdate = { contacts ->
|
||||||
|
serviceScope.launch { nostr.emitContactListUpdate(contacts) }
|
||||||
|
},
|
||||||
|
onSubscriptionClose = {
|
||||||
|
serviceScope.launch { nostr.emitSubscriptionClosed() }
|
||||||
|
},
|
||||||
|
onNewMessage = { event ->
|
||||||
|
serviceScope.launch {
|
||||||
if (!isUserInApp()) {
|
if (!isUserInApp()) {
|
||||||
showNewMessageNotification(event.roomId(), event.content())
|
showNewMessageNotification(event.roomId(), event.content())
|
||||||
}
|
}
|
||||||
|
nostr.emitNewEvent(event)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to start Nostr in background: ${e.message}")
|
println("Failed to start Nostr in background: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -59,22 +74,40 @@ class NostrForegroundService : Service() {
|
|||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
val channel = NotificationChannel(
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
"nostr_service",
|
|
||||||
"Nostr Background Service",
|
val serviceChannel = NotificationChannel(
|
||||||
|
"nostr_service_silent",
|
||||||
|
"Nostr Background Status",
|
||||||
|
NotificationManager.IMPORTANCE_MIN
|
||||||
|
).apply {
|
||||||
|
setShowBadge(false)
|
||||||
|
}
|
||||||
|
manager?.createNotificationChannel(serviceChannel)
|
||||||
|
|
||||||
|
val messageChannel = NotificationChannel(
|
||||||
|
"nostr_messages",
|
||||||
|
"New Messages",
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
)
|
)
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
manager?.createNotificationChannel(messageChannel)
|
||||||
manager?.createNotificationChannel(channel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createNotification(content: String): Notification {
|
private fun createNotification(content: String? = null): Notification {
|
||||||
return NotificationCompat.Builder(this, "nostr_service")
|
val builder = NotificationCompat.Builder(this, "nostr_service")
|
||||||
.setContentTitle("Coop")
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentText(content)
|
|
||||||
.setSmallIcon(android.R.drawable.ic_menu_send)
|
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.build()
|
.setPriority(NotificationCompat.PRIORITY_MIN)
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
|
||||||
|
if (content != null) {
|
||||||
|
builder.setContentTitle("Coop")
|
||||||
|
builder.setContentText(content)
|
||||||
|
} else {
|
||||||
|
builder.setContentTitle("Coop is active")
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showNewMessageNotification(roomId: Long, message: String) {
|
private fun showNewMessageNotification(roomId: Long, message: String) {
|
||||||
@@ -90,11 +123,13 @@ class NostrForegroundService : Service() {
|
|||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, "nostr_service")
|
val notification = NotificationCompat.Builder(this, "nostr_messages")
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle("You received a new message")
|
.setContentTitle("You received a new message")
|
||||||
.setContentText(message)
|
.setContentText(message)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
|
.setCategory(Notification.CATEGORY_MESSAGE)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
val manager = getSystemService(NotificationManager::class.java)
|
||||||
|
|||||||
BIN
composeApp/src/androidMain/res/drawable-hdpi/ic_notification.png
Normal file
BIN
composeApp/src/androidMain/res/drawable-hdpi/ic_notification.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 551 B |
BIN
composeApp/src/androidMain/res/drawable-mdpi/ic_notification.png
Normal file
BIN
composeApp/src/androidMain/res/drawable-mdpi/ic_notification.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 391 B |
Binary file not shown.
|
After Width: | Height: | Size: 727 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -8,9 +8,9 @@ import io.ktor.client.statement.HttpResponse
|
|||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import rust.nostr.sdk.AckPolicy
|
import rust.nostr.sdk.AckPolicy
|
||||||
@@ -62,9 +62,6 @@ object NostrManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class Nostr {
|
class Nostr {
|
||||||
private val _isInitialized = MutableStateFlow(false)
|
|
||||||
val isInitialized: StateFlow<Boolean> = _isInitialized.asStateFlow()
|
|
||||||
|
|
||||||
var client: Client? = null
|
var client: Client? = null
|
||||||
private set
|
private set
|
||||||
var signer: UniversalSigner = UniversalSigner(Keys.generate())
|
var signer: UniversalSigner = UniversalSigner(Keys.generate())
|
||||||
@@ -76,9 +73,35 @@ class Nostr {
|
|||||||
var rumorMap: MutableMap<EventId, EventId> = mutableMapOf()
|
var rumorMap: MutableMap<EventId, EventId> = mutableMapOf()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
private val isInitialized = MutableStateFlow(false)
|
||||||
|
|
||||||
|
// Add these to the Nostr class
|
||||||
|
private val _newEvents = MutableSharedFlow<UnsignedEvent>(extraBufferCapacity = 100)
|
||||||
|
val newEvents = _newEvents.asSharedFlow()
|
||||||
|
|
||||||
|
private val _metadataUpdates =
|
||||||
|
MutableSharedFlow<Pair<PublicKey, Metadata>>(extraBufferCapacity = 100)
|
||||||
|
val metadataUpdates = _metadataUpdates.asSharedFlow()
|
||||||
|
|
||||||
|
private val _contactListUpdates = MutableSharedFlow<List<PublicKey>>(extraBufferCapacity = 100)
|
||||||
|
val contactListUpdates = _contactListUpdates.asSharedFlow()
|
||||||
|
|
||||||
|
private val _subscriptionClosed = MutableSharedFlow<Unit>(extraBufferCapacity = 10)
|
||||||
|
val subscriptionClosed = _subscriptionClosed.asSharedFlow()
|
||||||
|
|
||||||
|
suspend fun emitNewEvent(event: UnsignedEvent) = _newEvents.emit(event)
|
||||||
|
|
||||||
|
suspend fun emitSubscriptionClosed() = _subscriptionClosed.emit(Unit)
|
||||||
|
|
||||||
|
suspend fun emitMetadataUpdate(pubkey: PublicKey, metadata: Metadata) =
|
||||||
|
_metadataUpdates.emit(pubkey to metadata)
|
||||||
|
|
||||||
|
suspend fun emitContactListUpdate(contacts: List<PublicKey>) =
|
||||||
|
_contactListUpdates.emit(contacts)
|
||||||
|
|
||||||
suspend fun init(dbPath: String) {
|
suspend fun init(dbPath: String) {
|
||||||
try {
|
try {
|
||||||
if (_isInitialized.value) return
|
if (isInitialized.value) return
|
||||||
|
|
||||||
// Initialize the logger for nostr client
|
// Initialize the logger for nostr client
|
||||||
initLogger(LogLevel.DEBUG)
|
initLogger(LogLevel.DEBUG)
|
||||||
@@ -108,14 +131,14 @@ class Nostr {
|
|||||||
.sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
|
.sleepWhenIdle(SleepWhenIdle.Enabled(idleTimeout))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
_isInitialized.value = true
|
isInitialized.value = true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to initialize Nostr client: ${e.message}", e)
|
throw IllegalStateException("Failed to initialize Nostr client: ${e.message}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun waitUntilInitialized() {
|
suspend fun waitUntilInitialized() {
|
||||||
_isInitialized.first { it }
|
isInitialized.first { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun connectBootstrapRelays() {
|
suspend fun connectBootstrapRelays() {
|
||||||
@@ -216,61 +239,6 @@ class Nostr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun handleLiteNotifications(
|
|
||||||
onNewMessage: (UnsignedEvent) -> Unit,
|
|
||||||
) {
|
|
||||||
val now = Timestamp.now()
|
|
||||||
val processedEvent = mutableSetOf<EventId>()
|
|
||||||
val notifications = client?.notifications() ?: return
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
val notification = notifications.next() ?: continue
|
|
||||||
|
|
||||||
when (notification) {
|
|
||||||
is ClientNotification.Message -> {
|
|
||||||
val relayUrl = notification.relayUrl
|
|
||||||
|
|
||||||
when (val message = notification.message.asEnum()) {
|
|
||||||
is RelayMessageEnum.EventMsg -> {
|
|
||||||
val event = message.event
|
|
||||||
val subscriptionId = message.subscriptionId
|
|
||||||
|
|
||||||
// Ignore events not from the newest gift wraps subscription
|
|
||||||
if (subscriptionId != "newest-gift-wraps") continue
|
|
||||||
|
|
||||||
// Prevent processing duplicate events
|
|
||||||
if (processedEvent.contains(event.id())) continue
|
|
||||||
processedEvent.add(event.id())
|
|
||||||
|
|
||||||
if (event.kind().asStd()?.equals(KindStandard.GIFT_WRAP) == true) {
|
|
||||||
try {
|
|
||||||
val rumor = extractRumor(event)
|
|
||||||
|
|
||||||
// Handle new message
|
|
||||||
rumor?.createdAt()?.asSecs()?.let {
|
|
||||||
if (it >= now.asSecs()) {
|
|
||||||
onNewMessage(rumor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println("Failed to extract rumor: $e")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
/* Ignore other event kinds */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
/* Ignore other message types */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun handleNotifications(
|
suspend fun handleNotifications(
|
||||||
onMetadataUpdate: (PublicKey, Metadata) -> Unit,
|
onMetadataUpdate: (PublicKey, Metadata) -> Unit,
|
||||||
onContactListUpdate: (List<PublicKey>) -> Unit,
|
onContactListUpdate: (List<PublicKey>) -> Unit,
|
||||||
@@ -293,7 +261,6 @@ class Nostr {
|
|||||||
when (val message = notification.message.asEnum()) {
|
when (val message = notification.message.asEnum()) {
|
||||||
is RelayMessageEnum.EventMsg -> {
|
is RelayMessageEnum.EventMsg -> {
|
||||||
val event = message.event
|
val event = message.event
|
||||||
val id = message.subscriptionId
|
|
||||||
|
|
||||||
// Prevent processing duplicate events
|
// Prevent processing duplicate events
|
||||||
if (processedEvent.contains(event.id())) continue
|
if (processedEvent.contains(event.id())) continue
|
||||||
|
|||||||
@@ -71,11 +71,20 @@ class NostrViewModel(
|
|||||||
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
startNotificationHandler()
|
// Check local stored secret (secret key or bunker)
|
||||||
startMetadataBatchHandler()
|
|
||||||
getCacheMetadata()
|
|
||||||
login()
|
login()
|
||||||
|
|
||||||
|
// Observe the signer state and verify the relay list
|
||||||
observeSignerAndCheckRelays()
|
observeSignerAndCheckRelays()
|
||||||
|
|
||||||
|
// Get all local stored metadata
|
||||||
|
getCacheMetadata()
|
||||||
|
|
||||||
|
// Observe new events from the Nostr client
|
||||||
|
runObserver()
|
||||||
|
|
||||||
|
// Wait and merge metadata requests into a single batch
|
||||||
|
runMetadataBatching()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
@@ -95,24 +104,11 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startNotificationHandler() {
|
private fun runObserver() {
|
||||||
viewModelScope.launch {
|
|
||||||
// Wait until the client is ready
|
|
||||||
nostr.waitUntilInitialized()
|
|
||||||
|
|
||||||
nostr.handleNotifications(
|
|
||||||
onMetadataUpdate = { pubkey, metadata ->
|
|
||||||
updateMetadata(pubkey, metadata)
|
|
||||||
},
|
|
||||||
onContactListUpdate = { contactList ->
|
|
||||||
_contactList.value = contactList.toSet()
|
|
||||||
},
|
|
||||||
onSubscriptionClose = {
|
|
||||||
getChatRooms()
|
|
||||||
_isPartialProcessedGiftWrap.value = true
|
|
||||||
},
|
|
||||||
onNewMessage = { event ->
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
// Observe new messages
|
||||||
|
launch {
|
||||||
|
nostr.newEvents.collect { event ->
|
||||||
val roomId = event.roomId()
|
val roomId = event.roomId()
|
||||||
val existingRoom = _chatRooms.value.firstOrNull { it.id == roomId }
|
val existingRoom = _chatRooms.value.firstOrNull { it.id == roomId }
|
||||||
|
|
||||||
@@ -120,9 +116,7 @@ class NostrViewModel(
|
|||||||
val currentUser = nostr.signer.currentUser
|
val currentUser = nostr.signer.currentUser
|
||||||
if (currentUser != null) {
|
if (currentUser != null) {
|
||||||
val newRoom = Room.new(event, currentUser)
|
val newRoom = Room.new(event, currentUser)
|
||||||
_chatRooms.update { currentRooms ->
|
_chatRooms.update { (it + newRoom).sortedDescending().toSet() }
|
||||||
currentRooms + newRoom
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateRoomList(roomId, event)
|
updateRoomList(roomId, event)
|
||||||
@@ -130,12 +124,33 @@ class NostrViewModel(
|
|||||||
|
|
||||||
_newEvents.emit(event)
|
_newEvents.emit(event)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
)
|
|
||||||
|
// Observe metadata updates
|
||||||
|
launch {
|
||||||
|
nostr.metadataUpdates.collect { (pubkey, metadata) ->
|
||||||
|
updateMetadata(pubkey, metadata)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startMetadataBatchHandler() {
|
// Observe contact list updates
|
||||||
|
launch {
|
||||||
|
nostr.contactListUpdates.collect { contacts ->
|
||||||
|
_contactList.value = contacts.toSet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observes subscription close
|
||||||
|
launch {
|
||||||
|
nostr.subscriptionClosed.collect {
|
||||||
|
getChatRooms()
|
||||||
|
_isPartialProcessedGiftWrap.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun runMetadataBatching() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
// Wait until the client is ready
|
// Wait until the client is ready
|
||||||
nostr.waitUntilInitialized()
|
nostr.waitUntilInitialized()
|
||||||
@@ -210,8 +225,7 @@ class NostrViewModel(
|
|||||||
val appKeys = getOrInitAppKeys()
|
val appKeys = getOrInitAppKeys()
|
||||||
val bunker = NostrConnectUri.parse(secret)
|
val bunker = NostrConnectUri.parse(secret)
|
||||||
val timeout = Duration.parse("50s") // 50 seconds timeout
|
val timeout = Duration.parse("50s") // 50 seconds timeout
|
||||||
val remote =
|
val remote = NostrConnect(uri = bunker, appKeys, timeout, opts = null)
|
||||||
NostrConnect(uri = bunker, appKeys = appKeys, timeout = timeout, null)
|
|
||||||
nostr.setSigner(remote)
|
nostr.setSigner(remote)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
showError("Error: ${e.message}")
|
showError("Error: ${e.message}")
|
||||||
@@ -435,11 +449,13 @@ class NostrViewModel(
|
|||||||
if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in")
|
if (nostr.signer.currentUser == null) throw IllegalStateException("User not signed in")
|
||||||
if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required")
|
if (to.isEmpty()) throw IllegalArgumentException("At least one recipient is required")
|
||||||
|
|
||||||
|
val currentUser = nostr.signer.currentUser!!
|
||||||
|
|
||||||
// Construct the rumor event
|
// Construct the rumor event
|
||||||
val rumor = EventBuilder
|
val rumor = EventBuilder
|
||||||
.privateMsgRumor(to.first(), "")
|
.privateMsgRumor(to.first(), "")
|
||||||
.tags(to.map { Tag.publicKey(it) })
|
.tags(to.map { Tag.publicKey(it) })
|
||||||
.build(nostr.signer.currentUser!!)
|
.build(currentUser)
|
||||||
|
|
||||||
// Check if the room already exists
|
// Check if the room already exists
|
||||||
val id = rumor.roomId()
|
val id = rumor.roomId()
|
||||||
@@ -451,7 +467,7 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a room from the rumor event
|
// Create a room from the rumor event
|
||||||
val room = Room.new(rumor, nostr.signer.currentUser!!)
|
val room = Room.new(rumor, currentUser)
|
||||||
|
|
||||||
// Update the chat rooms state
|
// Update the chat rooms state
|
||||||
_chatRooms.update { currentRooms ->
|
_chatRooms.update { currentRooms ->
|
||||||
@@ -546,13 +562,18 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun updateRoomList(roomId: Long, newMessage: UnsignedEvent) {
|
private fun updateRoomList(roomId: Long, newMessage: UnsignedEvent) {
|
||||||
_chatRooms.value = _chatRooms.value.map { room ->
|
_chatRooms.update { currentRooms ->
|
||||||
|
currentRooms.map { room ->
|
||||||
if (room.id == roomId) {
|
if (room.id == roomId) {
|
||||||
room.copy(lastMessage = newMessage.content(), createdAt = newMessage.createdAt())
|
room.copy(
|
||||||
|
lastMessage = newMessage.content(),
|
||||||
|
createdAt = newMessage.createdAt()
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
room
|
room
|
||||||
}
|
}
|
||||||
}.toSet()
|
}.sortedDescending().toSet()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun searchByAddress(query: String): PublicKey? {
|
suspend fun searchByAddress(query: String): PublicKey? {
|
||||||
|
|||||||
@@ -40,10 +40,10 @@ data class Room(
|
|||||||
val subject = rumor.tags().find(TagKind.Subject)?.content()
|
val subject = rumor.tags().find(TagKind.Subject)?.content()
|
||||||
|
|
||||||
// Collect the author's public key and all public keys from tags
|
// Collect the author's public key and all public keys from tags
|
||||||
// Also remove the user's public key from the list, current user is always a member
|
|
||||||
val pubkeys: MutableSet<PublicKey> = mutableSetOf()
|
val pubkeys: MutableSet<PublicKey> = mutableSetOf()
|
||||||
pubkeys.add(rumor.author())
|
pubkeys.add(rumor.author())
|
||||||
pubkeys.addAll(rumor.tags().publicKeys())
|
pubkeys.addAll(rumor.tags().publicKeys())
|
||||||
|
// Also remove the user's public key from the list, current user is always a member
|
||||||
pubkeys.remove(userPubkey)
|
pubkeys.remove(userPubkey)
|
||||||
|
|
||||||
// Create a new Room instance
|
// Create a new Room instance
|
||||||
|
|||||||
Reference in New Issue
Block a user