Add a placeholder chatmessagelist view mode.

This commit is contained in:
Kgothatso Ngako
2026-06-01 23:46:08 +02:00
parent e7501d5d7b
commit 740f42b522
10 changed files with 271 additions and 22 deletions

View File

@@ -13,6 +13,10 @@ class DatabaseChatRepository(
): ChatRepository {
val logger = Logger.withTag(TAG)
override suspend fun getChatRoomListByPublicKey(publicKey: String): List<LocalChatRoom> {
return database.chatRoomDao().getChatRoomListByUserPublicKey(publicKey)
}
override suspend fun getChatRoomByIdentifier(id: String): LocalChatRoom? {
return database.chatRoomDao().findChatRoomById(id)
}

View File

@@ -692,10 +692,6 @@ class DatabaseNostrRepository(
return database.profileDao().observeProfileWithFollowingByPublicKey(publicKey)
}
override suspend fun getChatRoomListByPublicKey(publicKey: String): List<LocalChatRoom> {
return database.chatRoomDao().getChatRoomListByUserPublicKey(publicKey)
}
override suspend fun getRecentSearches(): List<RecentSearch> {
return database.recentSearchDao().getAllRecentSearches()
}

View File

@@ -4,6 +4,8 @@ import ac.cord.auxiliary.compose.database.model.ChatRoom
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
interface ChatRepository {
suspend fun getChatRoomListByPublicKey(publicKey: String): List<LocalChatRoom>
suspend fun getChatRoomByIdentifier(id: String): LocalChatRoom?
suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String? = null): LocalChatRoom?
@@ -11,9 +13,13 @@ interface ChatRepository {
fun updateChatRoomSubject(publicKey: String, subject: String): ChatRoom?
companion object {
val NO_OP_CHAT_REPOSITORY = object: ChatRepository {
val NO_OP_CHAT_REPOSITORY = object: ChatRepository {
override suspend fun getChatRoomListByPublicKey(publicKey: String): List<LocalChatRoom> {
return emptyList()
}
override suspend fun getChatRoomByIdentifier(id: String): LocalChatRoom? {
TODO("Not yet implemented")
return null
}
override suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String?): LocalChatRoom? {

View File

@@ -124,8 +124,6 @@ interface NostrRepository {
suspend fun observeProfileWithFollowing(publicKey: String): Flow<LocalProfileWithFollowing?>
suspend fun getChatRoomListByPublicKey(publicKey: String): List<LocalChatRoom>
suspend fun getRecentSearches(): List<RecentSearch>
suspend fun removeRecentSearch(recentSearch: RecentSearch)
@@ -298,10 +296,6 @@ interface NostrRepository {
TODO("Not yet implemented")
}
override suspend fun getChatRoomListByPublicKey(publicKey: String): List<LocalChatRoom> {
TODO("Not yet implemented")
}
override suspend fun getRecentSearches(): List<RecentSearch> {
TODO("Not yet implemented")
}

View File

@@ -7,7 +7,9 @@ import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.widgets.LoadingDataIndicator
import ac.cord.auxiliary.compose.ui.theme.AuxTheme
import ac.cord.auxiliary.compose.ui.view.model.ChatRoomDetailMessageListViewModel
import ac.cord.auxiliary.compose.ui.view.model.ChatRoomDetailViewModel
import ac.cord.auxiliary.compose.ui.view.state.ChatRoomDetailMessageListUIState
import ac.cord.auxiliary.compose.ui.view.state.ChatRoomDetailUIState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -53,6 +55,7 @@ import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -96,6 +99,14 @@ fun ChatRoomDetailScreen(
is ChatRoomDetailUIState.Loaded -> {
val textFieldState = rememberTextFieldState()
val chatRoomDetailMessageListViewModel: ChatRoomDetailMessageListViewModel = viewModel(
factory = ChatRoomDetailMessageListViewModel.factory(
localChatRoom = chatRoomDetailUIState.localChatRoom,
nostrRepository = nostrRepository,
chatRepository = chatRepository
)
)
Scaffold(
topBar = {
TopAppBar(
@@ -118,12 +129,19 @@ fun ChatRoomDetailScreen(
modifier = Modifier.padding(innerPadding).fillMaxSize()
) {
Column(
modifier = Modifier.weight(1f)
modifier = Modifier.weight(1f).fillMaxWidth()
) {
Text(
text = "Messages will appear here..."
)
key(true) {
chatRoomDetailMessageListViewModel.RenderMessages(
onNavigateToDirectMessageDetail = {}
)
}
}
key(true) {
chatRoomDetailMessageListViewModel.initiate()
}
// TODO: Check that we have direct message relays for this user...
Column(
modifier = Modifier.fillMaxWidth()

View File

@@ -3,6 +3,7 @@ package ac.cord.auxiliary.compose.ui.composable
import ac.cord.auxiliary.compose.database.model.NostrEvent
import ac.cord.auxiliary.compose.database.model.Profile
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalProfileWithFollowing
import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.ChatRoomDetailRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.NostrEventDetailRoute
@@ -61,6 +62,7 @@ fun HomeScreen(
onNavigateToWriteNewNote: () -> Unit,
onNavigateToSearch: () -> Unit,
nostrRepository: NostrRepository,
chatRepository: ChatRepository
) {
val scope = rememberCoroutineScope()
@@ -221,7 +223,8 @@ fun HomeScreen(
homeScreenTypeType,
profileWithFollowing = homeScreenUIState.profileWithFollowing
),
nostrRepository = nostrRepository
nostrRepository = nostrRepository,
chatRepository = chatRepository
),
)
@@ -285,7 +288,8 @@ It has survived not only five centuries, but also the leap into electronic types
onNavigateToWriteNewNote = {},
onNavigateToSearch = {},
onNavigateToDirectMessageDetail = {},
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY
nostrRepository = NostrRepository.NO_OP_NOSTR_REPOSITORY,
chatRepository = ChatRepository.NO_OP_CHAT_REPOSITORY
)
}
}

View File

@@ -344,7 +344,8 @@ fun AuxNavHost(
route = chatRoomDetailRoute
)
},
nostrRepository = databaseNostrRepository
nostrRepository = databaseNostrRepository,
chatRepository = databaseChatRepository
)
}
composable<BlankRoute> {

View File

@@ -0,0 +1,209 @@
package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.ChatRoomDetailRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.Route
import ac.cord.auxiliary.compose.ui.view.state.ChatRoomDetailMessageListUIState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import co.touchlab.kermit.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.launch
class ChatRoomDetailMessageListViewModel(
initialChatRoomDetailMessageListUIState: ChatRoomDetailMessageListUIState,
val localChatRoom: LocalChatRoom,
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository
): ViewModel() {
val logger = Logger.withTag(TAG)
var chatRoomDetailMessageListUIState: ChatRoomDetailMessageListUIState by mutableStateOf(initialChatRoomDetailMessageListUIState)
private set
fun initiate() {
logger.d("init")
scheduleSynchronization()
observeChatRoomFeed()
}
fun observeChatRoomFeed() {
logger.d("observeChatRoomFeed")
viewModelScope.launch(Dispatchers.IO) {
chatRepository.getChatRoomListByPublicKey(
localChatRoom.chatRoom.userPublicKey
).let { chatRooms ->
logger.d("ChatRoomList: $chatRooms")
chatRoomDetailMessageListUIState = ChatRoomDetailMessageListUIState.Loaded(
chatRoomList = chatRooms
)
}
}
}
fun scheduleSynchronization() {
logger.d("scheduleSynchronization")
// viewModelScope.launch(Dispatchers.IO) {
// // Sync Notifications... might want to also run this in the background
// nostrRepository.queueNegentropySynchronizeRequest(
// Relays.DefaultDMRelayList.shuffled().map { normalizedRelayUrl ->
// NegentropySynchronizeRequest(
// id = NegentropySynchronizeRequest.computeId(
// relayURL = normalizedRelayUrl.url,
// synchronizationFilter = synchronizationFilter
// ),
// purpose = "chat",
// synchronizationFilter = synchronizationFilter,
// relayURL = normalizedRelayUrl.url,
// level = 0
// )
// }
// )
// }
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun RenderMessages(
onNavigateToDirectMessageDetail: (Route) -> Unit
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
when (val chatRoomDetailMessageListUIState = this@ChatRoomDetailMessageListViewModel.chatRoomDetailMessageListUIState) {
ChatRoomDetailMessageListUIState.Error -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = "Something went wrong",
)
}
}
is ChatRoomDetailMessageListUIState.Loaded -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
if (chatRoomDetailMessageListUIState.chatRoomList.isEmpty()) {
Text(
text = "No messages. Go to a profile and send them a message."
)
} else {
LazyColumn(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(10.dp),
reverseLayout = true
) {
items(
items = chatRoomDetailMessageListUIState.chatRoomList,
key = { it.chatRoom.id }
) { localChatRoom ->
Card(
modifier = Modifier.fillMaxWidth(),
onClick = {
onNavigateToDirectMessageDetail.invoke(
ChatRoomDetailRoute(
directMessageIdentifier = localChatRoom.chatRoom.id
)
)
}
) {
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (localChatRoom.participants.size == 1) {
Text(
text = "Note to Self (${localChatRoom.participants.first().profile?.humanReadableNameOrPubkey()})"
)
} else {
// Remove the active user publicKey from participants...
val participantNames = localChatRoom.participants.filter { it.participant.participantPublicKey != localChatRoom.chatRoom.userPublicKey }.map { it.profile?.humanReadableNameOrPubkey() ?: "unknown participant" }
Text(
text = localChatRoom.chatRoom.subject ?: participantNames.joinToString()
)
}
}
}
}
}
}
}
}
ChatRoomDetailMessageListUIState.Loading -> {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(
modifier = Modifier.height(50.dp)
)
Text(
text = "Loading",
textAlign = TextAlign.Center
)
Spacer(
modifier = Modifier.height(50.dp)
)
LoadingIndicator()
}
}
}
}
}
companion object {
const val TAG = "ChatRoomDetailMessageListViewModel"
fun factory(
initialChatRoomDetailMessageListUIState: ChatRoomDetailMessageListUIState = ChatRoomDetailMessageListUIState.Loading,
localChatRoom: LocalChatRoom,
nostrRepository: NostrRepository,
chatRepository: ChatRepository
): ViewModelProvider.Factory = viewModelFactory {
initializer {
ChatRoomDetailMessageListViewModel(
initialChatRoomDetailMessageListUIState = initialChatRoomDetailMessageListUIState,
localChatRoom = localChatRoom,
nostrRepository = nostrRepository,
chatRepository = chatRepository
)
}
}
}
}

View File

@@ -3,6 +3,7 @@ package ac.cord.auxiliary.compose.ui.view.model
import ac.cord.auxiliary.compose.database.model.NegentropySynchronizeRequest
import ac.cord.auxiliary.compose.database.model.types.SynchronizationFilter
import ac.cord.auxiliary.compose.nostr.Relays
import ac.cord.auxiliary.compose.repository.ChatRepository
import ac.cord.auxiliary.compose.repository.NostrRepository
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.ChatRoomDetailRoute
import ac.cord.auxiliary.compose.ui.composable.navigation.routes.Route
@@ -42,7 +43,8 @@ class ChatRoomListViewModel(
initialChatRoomListUIState: ChatRoomListUIState,
val publicKey: String,
val synchronizationFilter: SynchronizationFilter,
val nostrRepository: NostrRepository
val nostrRepository: NostrRepository,
val chatRepository: ChatRepository
): ViewModel() {
val logger = Logger.withTag(TAG)
var chatRoomListUIState: ChatRoomListUIState by mutableStateOf(initialChatRoomListUIState)
@@ -57,7 +59,7 @@ class ChatRoomListViewModel(
fun observeChatRoomFeed() {
logger.d("observeChatRoomFeed")
viewModelScope.launch(Dispatchers.IO) {
nostrRepository.getChatRoomListByPublicKey(
chatRepository.getChatRoomListByPublicKey(
publicKey
).let { chatRooms ->
logger.d("ChatRoomList: $chatRooms")
@@ -202,13 +204,15 @@ class ChatRoomListViewModel(
publicKey: String,
synchronizationFilter: SynchronizationFilter,
nostrRepository: NostrRepository,
chatRepository: ChatRepository
): ViewModelProvider.Factory = viewModelFactory {
initializer {
ChatRoomListViewModel(
initialChatRoomListUIState = initialChatRoomListUIState,
publicKey = publicKey,
synchronizationFilter = synchronizationFilter,
nostrRepository = nostrRepository
nostrRepository = nostrRepository,
chatRepository = chatRepository
)
}
}

View File

@@ -0,0 +1,13 @@
package ac.cord.auxiliary.compose.ui.view.state
import ac.cord.auxiliary.compose.database.model.intermdiate.LocalChatRoom
sealed interface ChatRoomDetailMessageListUIState {
data class Loaded(
val chatRoomList: List<LocalChatRoom>
): ChatRoomDetailMessageListUIState
data object Error: ChatRoomDetailMessageListUIState
data object Loading: ChatRoomDetailMessageListUIState
}