Compare commits
2 Commits
43618533a5
...
2d830b778f
| Author | SHA1 | Date | |
|---|---|---|---|
| 2d830b778f | |||
| fcc30157bb |
@@ -6,14 +6,11 @@ import android.os.Build
|
|||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialExpressiveTheme
|
import androidx.compose.material3.MaterialExpressiveTheme
|
||||||
import androidx.compose.material3.MotionScheme
|
import androidx.compose.material3.MotionScheme
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.dynamicDarkColorScheme
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
@@ -27,8 +24,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.util.Consumer
|
import androidx.core.util.Consumer
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -235,34 +230,26 @@ fun App(
|
|||||||
NewIdentityScreen(accountViewModel)
|
NewIdentityScreen(accountViewModel)
|
||||||
}
|
}
|
||||||
entry<Screen.Chat> { key ->
|
entry<Screen.Chat> { key ->
|
||||||
val initialRoom = remember(key.id) { chatRepository.getChatRoom(key.id) }
|
val factory = remember(key) {
|
||||||
|
object : ViewModelProvider.Factory {
|
||||||
if (initialRoom != null) {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
val factory = remember(initialRoom) {
|
@Suppress("UNCHECKED_CAST")
|
||||||
object : ViewModelProvider.Factory {
|
return ChatScreenViewModel(
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
key.id,
|
||||||
return ChatScreenViewModel(
|
key.screening,
|
||||||
initialRoom,
|
accountRepository,
|
||||||
key.screening,
|
chatRepository
|
||||||
accountRepository,
|
) as T
|
||||||
chatRepository
|
|
||||||
) as T
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ChatScreen(
|
|
||||||
viewModel<ChatScreenViewModel>(
|
|
||||||
key = key.id.toString(),
|
|
||||||
factory = factory
|
|
||||||
),
|
|
||||||
accountViewModel
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Handle the rare case where the room isn't in DB (e.g., invalid deep link)
|
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
Text("Room not found")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
ChatScreen(
|
||||||
|
viewModel<ChatScreenViewModel>(
|
||||||
|
key = key.id.toString(),
|
||||||
|
factory = factory
|
||||||
|
),
|
||||||
|
accountViewModel
|
||||||
|
)
|
||||||
}
|
}
|
||||||
entry<Screen.NewChat> {
|
entry<Screen.NewChat> {
|
||||||
NewChatScreen(accountViewModel, chatViewModel)
|
NewChatScreen(accountViewModel, chatViewModel)
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ import su.reya.coop.RoomKind
|
|||||||
import su.reya.coop.RoomUiState
|
import su.reya.coop.RoomUiState
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
import su.reya.coop.ago
|
import su.reya.coop.ago
|
||||||
import su.reya.coop.flow
|
|
||||||
import su.reya.coop.shared.Avatar
|
import su.reya.coop.shared.Avatar
|
||||||
import su.reya.coop.shared.getExpressiveFontFamily
|
import su.reya.coop.shared.getExpressiveFontFamily
|
||||||
|
import su.reya.coop.uiStateFlow
|
||||||
import su.reya.coop.viewmodel.AccountViewModel
|
import su.reya.coop.viewmodel.AccountViewModel
|
||||||
import su.reya.coop.viewmodel.ChatViewModel
|
import su.reya.coop.viewmodel.ChatViewModel
|
||||||
|
|
||||||
@@ -620,9 +620,9 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
val firstRoom = requests.getOrNull(0)
|
val firstRoom = requests.getOrNull(0)
|
||||||
val secondRoom = requests.getOrNull(1)
|
val secondRoom = requests.getOrNull(1)
|
||||||
|
|
||||||
val firstRoomState by (firstRoom as Room).flow(profileCache)
|
val firstRoomState by (firstRoom as Room).uiStateFlow(profileCache)
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
.collectAsStateWithLifecycle(RoomUiState())
|
||||||
val secondRoomState by (secondRoom ?: firstRoom).flow(profileCache)
|
val secondRoomState by (secondRoom ?: firstRoom).uiStateFlow(profileCache)
|
||||||
.collectAsStateWithLifecycle(RoomUiState())
|
.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
val supportingText = when {
|
val supportingText = when {
|
||||||
@@ -695,7 +695,7 @@ fun NewRequests(requests: List<Room>) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
fun ChatRoom(room: Room, onClick: () -> Unit) {
|
||||||
val profileCache = LocalProfileCache.current
|
val profileCache = LocalProfileCache.current
|
||||||
val roomState by room.flow(profileCache).collectAsStateWithLifecycle(RoomUiState())
|
val roomState by room.uiStateFlow(profileCache).collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
modifier = Modifier.clickable(onClick = onClick),
|
||||||
|
|||||||
@@ -397,7 +397,7 @@ fun ContactListItem(
|
|||||||
supportingContent = { Text(text = pubkey.short()) },
|
supportingContent = { Text(text = pubkey.short()) },
|
||||||
content = {
|
content = {
|
||||||
Text(
|
Text(
|
||||||
text = profile?.name ?: "",
|
text = profile?.name ?: "Unknown",
|
||||||
style = MaterialTheme.typography.titleMediumEmphasized,
|
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,11 +94,12 @@ import rust.nostr.sdk.UnsignedEvent
|
|||||||
import su.reya.coop.LocalNavigator
|
import su.reya.coop.LocalNavigator
|
||||||
import su.reya.coop.LocalProfileCache
|
import su.reya.coop.LocalProfileCache
|
||||||
import su.reya.coop.LocalSnackbarHostState
|
import su.reya.coop.LocalSnackbarHostState
|
||||||
|
import su.reya.coop.Room
|
||||||
import su.reya.coop.RoomUiState
|
import su.reya.coop.RoomUiState
|
||||||
import su.reya.coop.Screen
|
import su.reya.coop.Screen
|
||||||
import su.reya.coop.flow
|
|
||||||
import su.reya.coop.formatAsGroup
|
import su.reya.coop.formatAsGroup
|
||||||
import su.reya.coop.shared.Avatar
|
import su.reya.coop.shared.Avatar
|
||||||
|
import su.reya.coop.uiStateFlow
|
||||||
import su.reya.coop.viewmodel.AccountViewModel
|
import su.reya.coop.viewmodel.AccountViewModel
|
||||||
import su.reya.coop.viewmodel.ChatScreenViewModel
|
import su.reya.coop.viewmodel.ChatScreenViewModel
|
||||||
|
|
||||||
@@ -117,11 +118,25 @@ fun ChatScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
val id = viewModel.id
|
||||||
val currentUser by viewModel.currentUser.collectAsStateWithLifecycle()
|
val currentUser by viewModel.currentUser.collectAsStateWithLifecycle()
|
||||||
val pubkey = currentUser?.publicKey
|
val chatRooms by viewModel.chatRooms.collectAsStateWithLifecycle()
|
||||||
|
val room by remember(id) { derivedStateOf { chatRooms.firstOrNull { it.id == id } } }
|
||||||
|
|
||||||
val room by viewModel.room.collectAsStateWithLifecycle()
|
// Show empty screen
|
||||||
val roomState by room.flow(profileCache, pubkey).collectAsStateWithLifecycle(RoomUiState())
|
if (room == null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Something went wrong.",
|
||||||
|
style = MaterialTheme.typography.titleMediumEmphasized,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val loading = viewModel.loading
|
val loading = viewModel.loading
|
||||||
val newOtherMessages = viewModel.newOtherMessages
|
val newOtherMessages = viewModel.newOtherMessages
|
||||||
@@ -131,6 +146,9 @@ fun ChatScreen(
|
|||||||
val groupedMessages =
|
val groupedMessages =
|
||||||
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } }
|
remember { derivedStateOf { messages.groupBy { it.createdAt().formatAsGroup() } } }
|
||||||
|
|
||||||
|
val roomState by (room as Room).uiStateFlow(profileCache, currentUser?.publicKey)
|
||||||
|
.collectAsStateWithLifecycle(RoomUiState())
|
||||||
|
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf("") }
|
||||||
var selectedMessage by remember { mutableStateOf<Pair<MessageModel, Rect>?>(null) }
|
var selectedMessage by remember { mutableStateOf<Pair<MessageModel, Rect>?>(null) }
|
||||||
var replyingTo by remember { mutableStateOf<MessageModel?>(null) }
|
var replyingTo by remember { mutableStateOf<MessageModel?>(null) }
|
||||||
@@ -175,7 +193,9 @@ fun ChatScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.blur(blurAmount),
|
modifier = Modifier.blur(blurAmount),
|
||||||
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime),
|
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime),
|
||||||
@@ -187,7 +207,7 @@ fun ChatScreen(
|
|||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
room.members.firstOrNull()?.let { pubkey ->
|
room?.members?.firstOrNull()?.let { pubkey ->
|
||||||
navigator.navigate(Screen.Profile(pubkey.toBech32()))
|
navigator.navigate(Screen.Profile(pubkey.toBech32()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +265,7 @@ fun ChatScreen(
|
|||||||
.padding(bottom = innerPadding.calculateBottomPadding())
|
.padding(bottom = innerPadding.calculateBottomPadding())
|
||||||
) {
|
) {
|
||||||
if (requireScreening) {
|
if (requireScreening) {
|
||||||
ScreenerCard(accountViewModel, room)
|
room?.let { ScreenerCard(accountViewModel, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
when (messages.isNotEmpty()) {
|
when (messages.isNotEmpty()) {
|
||||||
@@ -263,7 +283,8 @@ fun ChatScreen(
|
|||||||
items = messagesInGroup,
|
items = messagesInGroup,
|
||||||
key = { it.ensureId().id()?.toHex()!! }
|
key = { it.ensureId().id()?.toHex()!! }
|
||||||
) { event ->
|
) { event ->
|
||||||
val model = rememberMessageModel(event, pubkey)
|
val model =
|
||||||
|
rememberMessageModel(event, currentUser?.publicKey)
|
||||||
|
|
||||||
val replyPreview =
|
val replyPreview =
|
||||||
remember(model.replyEventIds, messages.size) {
|
remember(model.replyEventIds, messages.size) {
|
||||||
@@ -277,9 +298,7 @@ fun ChatScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
) {
|
) {
|
||||||
replyPreview?.let {
|
replyPreview?.let { ReplyPreview(it, model.isMine) }
|
||||||
ReplyPreview(it, model.isMine)
|
|
||||||
}
|
|
||||||
ChatMessage(
|
ChatMessage(
|
||||||
model = model,
|
model = model,
|
||||||
modifier = Modifier.graphicsLayer {
|
modifier = Modifier.graphicsLayer {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ data class RoomUiState(
|
|||||||
val isGroup: Boolean = false
|
val isGroup: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Room.flow(
|
fun Room.uiStateFlow(
|
||||||
profileCache: ProfileCache,
|
profileCache: ProfileCache,
|
||||||
currentUser: PublicKey? = null
|
currentUser: PublicKey? = null
|
||||||
): Flow<RoomUiState> {
|
): Flow<RoomUiState> {
|
||||||
@@ -113,12 +113,7 @@ fun UnsignedEvent.roomId(): Long {
|
|||||||
pubkeys.add(this.author())
|
pubkeys.add(this.author())
|
||||||
pubkeys.addAll(this.tags().publicKeys())
|
pubkeys.addAll(this.tags().publicKeys())
|
||||||
|
|
||||||
// Sort and hash the list of public keys
|
return pubkeys.map { it.toBech32() }.distinct().sorted().hashCode().toLong()
|
||||||
val sortedUniqueKeys = pubkeys
|
|
||||||
.distinctBy { it.toBech32() }
|
|
||||||
.sortedBy { it.toBech32() }
|
|
||||||
|
|
||||||
return sortedUniqueKeys.hashCode().toLong()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Timestamp.formatAsTime(): String {
|
fun Timestamp.formatAsTime(): String {
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
target = ReqTarget.manual(target),
|
target = ReqTarget.manual(target),
|
||||||
id = "gift-wraps"
|
id = "gift-wraps"
|
||||||
)
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch user messages: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch user messages: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -225,6 +227,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
// Filter out events without public keys (receivers)
|
// Filter out events without public keys (receivers)
|
||||||
?.filter { it.tags().publicKeys().isNotEmpty() }
|
?.filter { it.tags().publicKeys().isNotEmpty() }
|
||||||
?.sortedByDescending { it.createdAt().asSecs() } ?: emptyList()
|
?.sortedByDescending { it.createdAt().asSecs() } ?: emptyList()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get chat room messages: ${e.message}", e)
|
throw IllegalStateException("Failed to get chat room messages: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -248,6 +252,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
connectMsgRelays(event)
|
connectMsgRelays(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch relays: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch relays: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -340,6 +346,8 @@ class MessageManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to send message: ${e.message}", e)
|
throw IllegalStateException("Failed to send message: ${e.message}", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow
|
|||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.withTimeoutOrNull
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.supervisorScope
|
import kotlinx.coroutines.supervisorScope
|
||||||
|
import kotlinx.coroutines.withTimeoutOrNull
|
||||||
import rust.nostr.sdk.AsyncNostrSigner
|
import rust.nostr.sdk.AsyncNostrSigner
|
||||||
import rust.nostr.sdk.Client
|
import rust.nostr.sdk.Client
|
||||||
import rust.nostr.sdk.ClientBuilder
|
import rust.nostr.sdk.ClientBuilder
|
||||||
@@ -108,14 +108,6 @@ class Nostr(
|
|||||||
relays.connectBootstrapRelays()
|
relays.connectBootstrapRelays()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun reconnect() {
|
|
||||||
relays.reconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun disconnect() {
|
|
||||||
relays.disconnect()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun prune() {
|
suspend fun prune() {
|
||||||
try {
|
try {
|
||||||
client?.database()?.wipe()
|
client?.database()?.wipe()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import rust.nostr.sdk.ReqTarget
|
|||||||
import rust.nostr.sdk.SendEventTarget
|
import rust.nostr.sdk.SendEventTarget
|
||||||
import rust.nostr.sdk.SubscribeAutoCloseOptions
|
import rust.nostr.sdk.SubscribeAutoCloseOptions
|
||||||
import rust.nostr.sdk.Timestamp
|
import rust.nostr.sdk.Timestamp
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
class ProfileManager(private val nostr: Nostr) {
|
class ProfileManager(private val nostr: Nostr) {
|
||||||
@@ -80,6 +81,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
val opts = SubscribeAutoCloseOptions().exitPolicy(ReqExitPolicy.ExitOnEose)
|
||||||
|
|
||||||
client?.subscribe(target = target, id = "user-metadata", closeOn = opts)
|
client?.subscribe(target = target, id = "user-metadata", closeOn = opts)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch user metadata: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch user metadata: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -92,6 +95,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val relays = RelayManager.BOOTSTRAP_RELAYS.map { RelayUrl.parse(it) }
|
val relays = RelayManager.BOOTSTRAP_RELAYS.map { RelayUrl.parse(it) }
|
||||||
|
|
||||||
client?.sync(filter, relays)
|
client?.sync(filter, relays)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to sync mutual contacts: ${e.message}")
|
println("Failed to sync mutual contacts: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -174,6 +179,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return newMetadata
|
return newMetadata
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to update identity: ${e.message}", e)
|
throw IllegalStateException("Failed to update identity: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -186,6 +193,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val event = client?.database()?.query(filter)?.first() ?: return null
|
val event = client?.database()?.query(filter)?.first() ?: return null
|
||||||
|
|
||||||
Metadata.fromJson(event.content())
|
Metadata.fromJson(event.content())
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to get latest metadata: ${e.message}")
|
println("Failed to get latest metadata: ${e.message}")
|
||||||
null
|
null
|
||||||
@@ -208,6 +217,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
println("Failed to get all cache metadata: ${e.message}")
|
println("Failed to get all cache metadata: ${e.message}")
|
||||||
return emptyMap()
|
return emptyMap()
|
||||||
@@ -232,6 +243,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client?.subscribe(target = ReqTarget.manual(target), closeOn = opts)
|
client?.subscribe(target = ReqTarget.manual(target), closeOn = opts)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch metadata batch: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch metadata batch: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -247,6 +260,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
target = SendEventTarget.broadcast(),
|
target = SendEventTarget.broadcast(),
|
||||||
ackPolicy = AckPolicy.none(),
|
ackPolicy = AckPolicy.none(),
|
||||||
)
|
)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to set contact list: ${e.message}", e)
|
throw IllegalStateException("Failed to set contact list: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -258,6 +273,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val bodyString: String = response.body()
|
val bodyString: String = response.body()
|
||||||
|
|
||||||
return Nip05Profile.fromJson(address, bodyString)
|
return Nip05Profile.fromJson(address, bodyString)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to fetch profile from address: ${e.message}", e)
|
throw IllegalStateException("Failed to fetch profile from address: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -269,6 +286,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val profile = profileFromAddress(httpClient, address)
|
val profile = profileFromAddress(httpClient, address)
|
||||||
|
|
||||||
return profile.publicKey()
|
return profile.publicKey()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to search address: ${e.message}", e)
|
throw IllegalStateException("Failed to search address: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -304,6 +323,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
|
throw IllegalStateException("Failed to search nostr: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -323,6 +344,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return events?.first()?.createdAt()
|
return events?.first()?.createdAt()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get latest activity: ${e.message}", e)
|
throw IllegalStateException("Failed to get latest activity: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -340,6 +363,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
val pubkeys = events?.first()?.tags()?.publicKeys() ?: listOf()
|
val pubkeys = events?.first()?.tags()?.publicKeys() ?: listOf()
|
||||||
|
|
||||||
return pubkeys.contains(pubkey)
|
return pubkeys.contains(pubkey)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
|
throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
|
||||||
}
|
}
|
||||||
@@ -361,6 +386,8 @@ class ProfileManager(private val nostr: Nostr) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return contacts.toSet()
|
return contacts.toSet()
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
|
throw IllegalStateException("Failed to get mutual contacts: ${e.message}", e)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,13 +51,16 @@ class ChatRepository(
|
|||||||
)
|
)
|
||||||
val newEvents = _newEvents.asSharedFlow()
|
val newEvents = _newEvents.asSharedFlow()
|
||||||
|
|
||||||
val chatRooms = state.map { it.rooms.values.sortedByDescending { it.createdAt.asSecs() } }
|
val chatRooms = state
|
||||||
|
.map { it.rooms.values.sortedByDescending { it.createdAt.asSecs() } }
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(5000), emptyList())
|
.stateIn(scope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||||
|
|
||||||
val isSyncing = nostr.messages.messageSyncState.map { it.isSyncing }
|
val isSyncing = nostr.messages.messageSyncState
|
||||||
|
.map { it.isSyncing }
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(5000), false)
|
.stateIn(scope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
val isPartialProcessedGiftWrap = state.map { it.isPartialProcessedGiftWrap }
|
val isPartialProcessedGiftWrap = state
|
||||||
|
.map { it.isPartialProcessedGiftWrap }
|
||||||
.stateIn(scope, SharingStarted.WhileSubscribed(5000), false)
|
.stateIn(scope, SharingStarted.WhileSubscribed(5000), false)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -7,40 +7,30 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import rust.nostr.sdk.EventId
|
import rust.nostr.sdk.EventId
|
||||||
import rust.nostr.sdk.UnsignedEvent
|
import rust.nostr.sdk.UnsignedEvent
|
||||||
|
import su.reya.coop.Profile
|
||||||
import su.reya.coop.Room
|
import su.reya.coop.Room
|
||||||
import su.reya.coop.repository.AccountRepository
|
import su.reya.coop.repository.AccountRepository
|
||||||
import su.reya.coop.repository.ChatRepository
|
import su.reya.coop.repository.ChatRepository
|
||||||
import su.reya.coop.roomId
|
import su.reya.coop.roomId
|
||||||
|
|
||||||
class ChatScreenViewModel(
|
class ChatScreenViewModel(
|
||||||
initialRoom: Room,
|
val id: Long,
|
||||||
screening: Boolean,
|
screening: Boolean,
|
||||||
accountRepository: AccountRepository,
|
accountRepository: AccountRepository,
|
||||||
private val chatRepository: ChatRepository,
|
private val chatRepository: ChatRepository,
|
||||||
) : ViewModel(), ErrorHost by chatRepository {
|
) : ViewModel(), ErrorHost by chatRepository {
|
||||||
|
val currentUser: StateFlow<Profile?> = accountRepository.currentUserProfile
|
||||||
|
val chatRooms: StateFlow<List<Room>> = chatRepository.chatRooms
|
||||||
|
|
||||||
var loading by mutableStateOf(true)
|
var loading by mutableStateOf(true)
|
||||||
var newOtherMessages by mutableIntStateOf(0)
|
var newOtherMessages by mutableIntStateOf(0)
|
||||||
var requireScreening by mutableStateOf(screening)
|
var requireScreening by mutableStateOf(screening)
|
||||||
val messages = mutableStateListOf<UnsignedEvent>()
|
val messages = mutableStateListOf<UnsignedEvent>()
|
||||||
|
|
||||||
val currentUser = accountRepository.currentUserProfile
|
|
||||||
val id = initialRoom.id
|
|
||||||
|
|
||||||
val room: StateFlow<Room> = chatRepository.chatRooms
|
|
||||||
.map { rooms -> rooms.find { it.id == id } ?: initialRoom }
|
|
||||||
.stateIn(
|
|
||||||
scope = viewModelScope,
|
|
||||||
started = SharingStarted.WhileSubscribed(5000),
|
|
||||||
initialValue = initialRoom
|
|
||||||
)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadMessages()
|
loadMessages()
|
||||||
connect()
|
connect()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import kotlin.time.Duration.Companion.milliseconds
|
|||||||
|
|
||||||
class ProfileCache(
|
class ProfileCache(
|
||||||
private val nostr: Nostr,
|
private val nostr: Nostr,
|
||||||
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
defaultDispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||||
) : ErrorHost by createErrorHost() {
|
) : ErrorHost by createErrorHost() {
|
||||||
private val scope = CoroutineScope(SupervisorJob() + defaultDispatcher)
|
private val scope = CoroutineScope(SupervisorJob() + defaultDispatcher)
|
||||||
private val profiles = MutableStateFlow<Map<PublicKey, MutableStateFlow<Profile?>>>(emptyMap())
|
private val profiles = MutableStateFlow<Map<PublicKey, MutableStateFlow<Profile?>>>(emptyMap())
|
||||||
|
|||||||
Reference in New Issue
Block a user