chore: merge the develop branch into master #1
@@ -30,10 +30,13 @@ import su.reya.coop.screens.OnboardingScreen
|
|||||||
@Composable
|
@Composable
|
||||||
fun App(dbPath: String) {
|
fun App(dbPath: String) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
// Initialize Nostr and SecretStore
|
||||||
val nostr = remember { Nostr() }
|
val nostr = remember { Nostr() }
|
||||||
val secretStore = remember { SecretStore(context) }
|
val secretStore = remember { SecretStore(context) }
|
||||||
val viewModel: NostrViewModel = viewModel { NostrViewModel(nostr, secretStore) }
|
val viewModel: NostrViewModel = viewModel { NostrViewModel(nostr, secretStore) }
|
||||||
|
|
||||||
|
// Dynamic color scheme
|
||||||
val darkMode = isSystemInDarkTheme()
|
val darkMode = isSystemInDarkTheme()
|
||||||
val colorScheme = when {
|
val colorScheme = when {
|
||||||
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> {
|
android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S -> {
|
||||||
@@ -46,6 +49,7 @@ fun App(dbPath: String) {
|
|||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.initAndConnect(dbPath)
|
viewModel.initAndConnect(dbPath)
|
||||||
|
viewModel.getChatRooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
MaterialExpressiveTheme(
|
MaterialExpressiveTheme(
|
||||||
@@ -60,7 +64,8 @@ fun App(dbPath: String) {
|
|||||||
if (hasSecret == true) {
|
if (hasSecret == true) {
|
||||||
// Start a background notification handler
|
// Start a background notification handler
|
||||||
viewModel.startNotificationHandler()
|
viewModel.startNotificationHandler()
|
||||||
|
// Get chat rooms
|
||||||
|
viewModel.getChatRooms()
|
||||||
// Navigate to the home screen
|
// Navigate to the home screen
|
||||||
navController.navigate(Screen.Home) {
|
navController.navigate(Screen.Home) {
|
||||||
popUpTo(Screen.Onboarding) { inclusive = true }
|
popUpTo(Screen.Onboarding) { inclusive = true }
|
||||||
|
|||||||
@@ -38,7 +38,10 @@ fun HomeScreen(onOpenChat: (String) -> Unit) {
|
|||||||
searchBarState = searchState,
|
searchBarState = searchState,
|
||||||
onSearch = { scope.launch { searchState.animateToCollapsed() } },
|
onSearch = { scope.launch { searchState.animateToCollapsed() } },
|
||||||
placeholder = {
|
placeholder = {
|
||||||
Text(modifier = Modifier.clearAndSetSemantics() {}, text = "Search")
|
Text(
|
||||||
|
modifier = Modifier.clearAndSetSemantics() {},
|
||||||
|
text = "Find or start a conversation"
|
||||||
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ kotlin {
|
|||||||
commonMain.dependencies {
|
commonMain.dependencies {
|
||||||
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
|
||||||
implementation("su.reya:nostr-sdk-kmp:0.1.1")
|
implementation("su.reya:nostr-sdk-kmp:0.1.2")
|
||||||
}
|
}
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
implementation(libs.kotlin.test)
|
implementation(libs.kotlin.test)
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class Nostr {
|
|||||||
private set
|
private set
|
||||||
var deviceSigner: NostrSigner? = null
|
var deviceSigner: NostrSigner? = null
|
||||||
private set
|
private set
|
||||||
|
var contactList: List<PublicKey> = emptyList()
|
||||||
|
private set
|
||||||
|
|
||||||
suspend fun init(dbPath: String) {
|
suspend fun init(dbPath: String) {
|
||||||
val lmdb = NostrDatabase.lmdb(dbPath)
|
val lmdb = NostrDatabase.lmdb(dbPath)
|
||||||
@@ -211,7 +213,7 @@ class Nostr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCachedRumor(giftId: EventId): UnsignedEvent? {
|
private suspend fun getCachedRumor(giftId: EventId): UnsignedEvent? {
|
||||||
try {
|
try {
|
||||||
val filter = Filter().identifier(giftId.toBech32())
|
val filter = Filter().identifier(giftId.toBech32())
|
||||||
val event = client?.database()?.query(filter)?.first()
|
val event = client?.database()?.query(filter)?.first()
|
||||||
@@ -223,20 +225,22 @@ class Nostr {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setCachedRumor(giftId: EventId, rumor: UnsignedEvent) {
|
private suspend fun setCachedRumor(giftId: EventId, rumor: UnsignedEvent) {
|
||||||
if (rumor.id() == null) return
|
if (rumor.id() == null) return
|
||||||
try {
|
try {
|
||||||
|
val rngKeys = Keys.generate()
|
||||||
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA);
|
val kind = Kind.fromStd(KindStandard.APPLICATION_SPECIFIC_DATA);
|
||||||
val tags = listOf(Tag.identifier(giftId.toBech32()), Tag.event(rumor.id()!!))
|
val tags = listOf(Tag.identifier(giftId.toBech32()), Tag.event(rumor.id()!!))
|
||||||
val event = EventBuilder(kind, rumor.asJson()).tags(tags).signWithKeys(Keys.generate())
|
val event = EventBuilder(kind, rumor.asJson()).tags(tags).signWithKeys(rngKeys)
|
||||||
|
|
||||||
client?.database()?.saveEvent(event)
|
client?.database()?.saveEvent(event)
|
||||||
|
client?.database()?.saveEvent(rumor.signWithKeys(rngKeys))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// TODO: log error
|
// TODO: log error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun extractRumor(event: Event): UnsignedEvent? {
|
private suspend fun extractRumor(event: Event): UnsignedEvent? {
|
||||||
if (event.kind().asStd() != KindStandard.GIFT_WRAP) return null
|
if (event.kind().asStd() != KindStandard.GIFT_WRAP) return null
|
||||||
|
|
||||||
// Check if the rumor is already cached
|
// Check if the rumor is already cached
|
||||||
@@ -266,7 +270,7 @@ class Nostr {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun conversationId(rumor: UnsignedEvent): Long {
|
private fun conversationId(rumor: UnsignedEvent): Long {
|
||||||
val pubkeys: MutableList<PublicKey> = rumor.tags().publicKeys().toMutableList()
|
val pubkeys: MutableList<PublicKey> = rumor.tags().publicKeys().toMutableList()
|
||||||
pubkeys.add(rumor.author())
|
pubkeys.add(rumor.author())
|
||||||
|
|
||||||
@@ -278,7 +282,8 @@ class Nostr {
|
|||||||
return uniqueSortedKeys.hashCode().toLong()
|
return uniqueSortedKeys.hashCode().toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDefaultRelayList(): Map<RelayUrl, RelayMetadata> {
|
|
||||||
|
private suspend fun getDefaultRelayList(): Map<RelayUrl, RelayMetadata> {
|
||||||
// Construct a list of relays
|
// Construct a list of relays
|
||||||
val relayList = mapOf<RelayUrl, RelayMetadata>(
|
val relayList = mapOf<RelayUrl, RelayMetadata>(
|
||||||
RelayUrl.parse("wss://relay.damus.io") to RelayMetadata.READ,
|
RelayUrl.parse("wss://relay.damus.io") to RelayMetadata.READ,
|
||||||
@@ -302,7 +307,7 @@ class Nostr {
|
|||||||
return relayList
|
return relayList
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getMsgRelayList(): List<RelayUrl> {
|
private suspend fun getMsgRelayList(): List<RelayUrl> {
|
||||||
// Construct a list of messaging relays
|
// Construct a list of messaging relays
|
||||||
val msgRelayList = listOf(
|
val msgRelayList = listOf(
|
||||||
RelayUrl.parse("wss://relay.0xchat.com"),
|
RelayUrl.parse("wss://relay.0xchat.com"),
|
||||||
@@ -344,4 +349,67 @@ class Nostr {
|
|||||||
val contactListEvent = EventBuilder.contactList(defaultContact).sign(signer!!)
|
val contactListEvent = EventBuilder.contactList(defaultContact).sign(signer!!)
|
||||||
client?.sendEventNoWait(contactListEvent)
|
client?.sendEventNoWait(contactListEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun fetchMetadataBatch(keys: List<PublicKey>) {
|
||||||
|
val filter =
|
||||||
|
Filter()
|
||||||
|
.kind(Kind.fromStd(KindStandard.METADATA))
|
||||||
|
.authors(keys)
|
||||||
|
.limit(keys.size.toULong())
|
||||||
|
val target =
|
||||||
|
ReqTarget.manual(mapOf(RelayUrl.parse("wss://user.kindpag.es") to listOf(filter)))
|
||||||
|
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
||||||
|
|
||||||
|
client?.subscribe(target = target, id = "metadata-reqs", closeOn = opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getChatRooms(): Set<Room>? {
|
||||||
|
try {
|
||||||
|
val userPubkey = signer?.getPublicKey() ?: return null
|
||||||
|
val kind = Kind.fromStd(KindStandard.PRIVATE_DIRECT_MESSAGE)
|
||||||
|
|
||||||
|
// Get all events sent by the user
|
||||||
|
val sendFilter = Filter().kind(kind).author(userPubkey)
|
||||||
|
val sendEvents = client?.database()?.query(sendFilter);
|
||||||
|
|
||||||
|
// Get all events sent to the user
|
||||||
|
val recvFilter = Filter().kind(kind).pubkey(userPubkey)
|
||||||
|
val recvEvents = client?.database()?.query(recvFilter);
|
||||||
|
|
||||||
|
// Collect all events
|
||||||
|
val events = sendEvents?.merge(recvEvents!!)?.toVec();
|
||||||
|
val rooms: MutableSet<Room> = mutableSetOf()
|
||||||
|
|
||||||
|
events
|
||||||
|
?.filter { it.tags().publicKeys().isNotEmpty() }
|
||||||
|
?.sortedByDescending { it.createdAt().asSecs() }
|
||||||
|
?.forEach { event ->
|
||||||
|
val room = Room.new(rumor = event, userPubkey = userPubkey)
|
||||||
|
|
||||||
|
// Check if the room already exists
|
||||||
|
if (rooms.contains(room)) return@forEach
|
||||||
|
|
||||||
|
val filter =
|
||||||
|
Filter().kind(kind).author(userPubkey).pubkeys(room.members.toList());
|
||||||
|
|
||||||
|
// Check if the user is interacting with the room's members
|
||||||
|
val isInteracting = client?.database()?.query(filter)?.isEmpty() == false;
|
||||||
|
|
||||||
|
// Check if the room's members are in the contact list
|
||||||
|
val isContact = contactList.containsAll(room.members)
|
||||||
|
|
||||||
|
// Set the room kind based on interaction status
|
||||||
|
if (isInteracting || isContact) {
|
||||||
|
room.kind(RoomKind.Ongoing)
|
||||||
|
}
|
||||||
|
|
||||||
|
rooms.add(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rooms
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Failed to get chat rooms: ${e.message}")
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ package su.reya.coop
|
|||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.NonCancellable
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import rust.nostr.sdk.Keys
|
import rust.nostr.sdk.Keys
|
||||||
import rust.nostr.sdk.Metadata
|
import rust.nostr.sdk.Metadata
|
||||||
import rust.nostr.sdk.NostrConnect
|
import rust.nostr.sdk.NostrConnect
|
||||||
import rust.nostr.sdk.NostrConnectUri
|
import rust.nostr.sdk.NostrConnectUri
|
||||||
import rust.nostr.sdk.PublicKey
|
import rust.nostr.sdk.PublicKey
|
||||||
import su.reya.coop.storage.SecretStorage
|
import su.reya.coop.storage.SecretStorage
|
||||||
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
class NostrViewModel(
|
class NostrViewModel(
|
||||||
@@ -26,11 +29,61 @@ class NostrViewModel(
|
|||||||
private val _isCreating = MutableStateFlow(false)
|
private val _isCreating = MutableStateFlow(false)
|
||||||
val isCreating = _isCreating.asStateFlow()
|
val isCreating = _isCreating.asStateFlow()
|
||||||
|
|
||||||
// User metadata store
|
private val _chatRooms = MutableStateFlow<Set<Room>>(emptySet())
|
||||||
|
val chatRooms = _chatRooms.asStateFlow()
|
||||||
|
|
||||||
private val _metadataStore = mutableMapOf<PublicKey, MutableStateFlow<Metadata?>>()
|
private val _metadataStore = mutableMapOf<PublicKey, MutableStateFlow<Metadata?>>()
|
||||||
|
private val metadataRequestChannel = Channel<PublicKey>(Channel.UNLIMITED)
|
||||||
|
private val seenPublicKeys = mutableSetOf<PublicKey>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
startMetadataBatchProcessor()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startMetadataBatchProcessor() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val batch = mutableSetOf<PublicKey>()
|
||||||
|
val timeout = 500L // 500ms timeout for batching
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
val firstKey = metadataRequestChannel.receive()
|
||||||
|
batch.add(firstKey)
|
||||||
|
val lastFlushTime = Clock.System.now().toEpochMilliseconds()
|
||||||
|
|
||||||
|
while (batch.isNotEmpty()) {
|
||||||
|
val nextKey = withTimeoutOrNull(timeout) {
|
||||||
|
metadataRequestChannel.receive()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextKey != null) {
|
||||||
|
batch.add(nextKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
val now = Clock.System.now().toEpochMilliseconds()
|
||||||
|
if (batch.size >= 20 || (now - lastFlushTime) >= timeout || nextKey == null) {
|
||||||
|
val keysToRequest = batch.toList()
|
||||||
|
batch.clear()
|
||||||
|
nostr.fetchMetadataBatch(keysToRequest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun requestMetadata(pubkey: PublicKey) {
|
||||||
|
if (seenPublicKeys.add(pubkey)) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
metadataRequestChannel.send(pubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getMetadata(pubkey: PublicKey): StateFlow<Metadata?> {
|
fun getMetadata(pubkey: PublicKey): StateFlow<Metadata?> {
|
||||||
return _metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }.asStateFlow()
|
val flow = _metadataStore.getOrPut(pubkey) { MutableStateFlow(null) }
|
||||||
|
if (flow.value == null) {
|
||||||
|
requestMetadata(pubkey)
|
||||||
|
}
|
||||||
|
return flow.asStateFlow()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateMetadata(pubkey: PublicKey, metadata: Metadata) {
|
fun updateMetadata(pubkey: PublicKey, metadata: Metadata) {
|
||||||
@@ -42,37 +95,10 @@ class NostrViewModel(
|
|||||||
try {
|
try {
|
||||||
// Initialize nostr client
|
// Initialize nostr client
|
||||||
nostr.init(dbPath)
|
nostr.init(dbPath)
|
||||||
|
|
||||||
// Connect to bootstrap relays
|
// Connect to bootstrap relays
|
||||||
nostr.connect()
|
nostr.connect()
|
||||||
|
// Get user's secret
|
||||||
// Get user's signer secret
|
getUserSecret()
|
||||||
val secret = secretStore.get("user_signer")
|
|
||||||
|
|
||||||
// If no secret is found, show onboarding screen
|
|
||||||
if (secret == null) {
|
|
||||||
_hasSecret.value = false
|
|
||||||
return@launch
|
|
||||||
}
|
|
||||||
_hasSecret.value = true
|
|
||||||
|
|
||||||
// Handle different signer types
|
|
||||||
if (secret.startsWith("nsec1")) {
|
|
||||||
val keys = Keys.parse(secret)
|
|
||||||
nostr.setKeySigner(keys)
|
|
||||||
} else if (secret.startsWith("bunker://")) {
|
|
||||||
val appKeys = getOrInitAppKeys()
|
|
||||||
val bunker = NostrConnectUri.parse(secret)
|
|
||||||
val remote = NostrConnect(
|
|
||||||
uri = bunker,
|
|
||||||
appKeys = appKeys,
|
|
||||||
timeout = Duration.parse("5"),
|
|
||||||
opts = null
|
|
||||||
)
|
|
||||||
nostr.setRemoteSigner(remote)
|
|
||||||
} else {
|
|
||||||
throw IllegalArgumentException("Invalid secret format: $secret")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to connect: ${e.message}")
|
println("Failed to connect: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -87,6 +113,36 @@ class NostrViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getUserSecret() {
|
||||||
|
// Get user's signer secret
|
||||||
|
val secret = secretStore.get("user_signer")
|
||||||
|
|
||||||
|
// If no secret is found, show onboarding screen
|
||||||
|
if (secret == null) {
|
||||||
|
_hasSecret.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_hasSecret.value = true
|
||||||
|
|
||||||
|
// Handle different signer types
|
||||||
|
if (secret.startsWith("nsec1")) {
|
||||||
|
val keys = Keys.parse(secret)
|
||||||
|
nostr.setKeySigner(keys)
|
||||||
|
} else if (secret.startsWith("bunker://")) {
|
||||||
|
val appKeys = getOrInitAppKeys()
|
||||||
|
val bunker = NostrConnectUri.parse(secret)
|
||||||
|
val remote = NostrConnect(
|
||||||
|
uri = bunker,
|
||||||
|
appKeys = appKeys,
|
||||||
|
timeout = Duration.parse("5"),
|
||||||
|
opts = null
|
||||||
|
)
|
||||||
|
nostr.setRemoteSigner(remote)
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("Invalid secret format: $secret")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getOrInitAppKeys(): Keys {
|
suspend fun getOrInitAppKeys(): Keys {
|
||||||
val secret = secretStore.get("app_keys")
|
val secret = secretStore.get("app_keys")
|
||||||
|
|
||||||
@@ -123,6 +179,16 @@ class NostrViewModel(
|
|||||||
// TODO: Implement import
|
// TODO: Implement import
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getChatRooms() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
_chatRooms.value = nostr.getChatRooms() ?: emptySet()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Failed to get chat rooms: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
// Ensure all relays are disconnect
|
// Ensure all relays are disconnect
|
||||||
|
|||||||
76
shared/src/commonMain/kotlin/su/reya/coop/Room.kt
Normal file
76
shared/src/commonMain/kotlin/su/reya/coop/Room.kt
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
package su.reya.coop
|
||||||
|
|
||||||
|
import rust.nostr.sdk.Event
|
||||||
|
import rust.nostr.sdk.PublicKey
|
||||||
|
import rust.nostr.sdk.TagKind
|
||||||
|
import rust.nostr.sdk.Timestamp
|
||||||
|
|
||||||
|
enum class RoomKind {
|
||||||
|
Ongoing,
|
||||||
|
Request;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun default(): RoomKind = Request
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Room(
|
||||||
|
val id: Long,
|
||||||
|
val createdAt: Timestamp,
|
||||||
|
val subject: String?,
|
||||||
|
val members: Set<PublicKey>,
|
||||||
|
val kind: RoomKind = RoomKind.default()
|
||||||
|
) : Comparable<Room> {
|
||||||
|
override fun hashCode(): Int = id.hashCode()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Room) return false
|
||||||
|
return id == other.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun compareTo(other: Room): Int {
|
||||||
|
return this.createdAt.asSecs().compareTo(other.createdAt.asSecs())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun new(rumor: Event, userPubkey: PublicKey): Room {
|
||||||
|
val id = rumor.roomId()
|
||||||
|
val createdAt = rumor.createdAt()
|
||||||
|
val subject = rumor.tags().find(TagKind.Subject)?.content()
|
||||||
|
|
||||||
|
// Collect the author's public key and all public keys from tags
|
||||||
|
// Also remove the user's public key from the list
|
||||||
|
val pubkeys: MutableSet<PublicKey> = mutableSetOf()
|
||||||
|
pubkeys.add(rumor.author())
|
||||||
|
pubkeys.addAll(rumor.tags().publicKeys())
|
||||||
|
pubkeys.remove(userPubkey)
|
||||||
|
|
||||||
|
// Create a new Room instance
|
||||||
|
return Room(
|
||||||
|
id = id,
|
||||||
|
createdAt = createdAt,
|
||||||
|
subject = subject,
|
||||||
|
members = pubkeys as Set<PublicKey>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun kind(kind: RoomKind): Room {
|
||||||
|
return this.copy(kind = kind)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Event.roomId(): Long {
|
||||||
|
// Collect the author's public key and all public keys from tags
|
||||||
|
val pubkeys: MutableList<PublicKey> = mutableListOf()
|
||||||
|
pubkeys.add(this.author())
|
||||||
|
pubkeys.addAll(this.tags().publicKeys())
|
||||||
|
|
||||||
|
// Sort and hash the list of public keys
|
||||||
|
val sortedUniqueKeys = pubkeys
|
||||||
|
.distinctBy { it.toBech32() }
|
||||||
|
.sortedBy { it.toBech32() }
|
||||||
|
|
||||||
|
return sortedUniqueKeys.hashCode().toLong()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user