diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt index d499b35e..4f8d15a2 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseChatRepository.kt @@ -13,6 +13,10 @@ class DatabaseChatRepository( ): ChatRepository { val logger = Logger.withTag(TAG) + override suspend fun getChatRoomListByPublicKey(publicKey: String): List { + return database.chatRoomDao().getChatRoomListByUserPublicKey(publicKey) + } + override suspend fun getChatRoomByIdentifier(id: String): LocalChatRoom? { return database.chatRoomDao().findChatRoomById(id) } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt index 95e1292a..3750c116 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/database/repository/DatabaseNostrRepository.kt @@ -692,10 +692,6 @@ class DatabaseNostrRepository( return database.profileDao().observeProfileWithFollowingByPublicKey(publicKey) } - override suspend fun getChatRoomListByPublicKey(publicKey: String): List { - return database.chatRoomDao().getChatRoomListByUserPublicKey(publicKey) - } - override suspend fun getRecentSearches(): List { return database.recentSearchDao().getAllRecentSearches() } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt index eef8b58c..44d07b64 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/ChatRepository.kt @@ -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 + 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 { + return emptyList() + } + override suspend fun getChatRoomByIdentifier(id: String): LocalChatRoom? { - TODO("Not yet implemented") + return null } override suspend fun getOrCreateChatRoom(publicKey: String, defaultSubject: String?): LocalChatRoom? { diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt index d17c1c64..b9359324 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/repository/NostrRepository.kt @@ -124,8 +124,6 @@ interface NostrRepository { suspend fun observeProfileWithFollowing(publicKey: String): Flow - suspend fun getChatRoomListByPublicKey(publicKey: String): List - suspend fun getRecentSearches(): List suspend fun removeRecentSearch(recentSearch: RecentSearch) @@ -298,10 +296,6 @@ interface NostrRepository { TODO("Not yet implemented") } - override suspend fun getChatRoomListByPublicKey(publicKey: String): List { - TODO("Not yet implemented") - } - override suspend fun getRecentSearches(): List { TODO("Not yet implemented") } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt index 87c2bfdf..eba086ba 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/ChatRoomDetailScreen.kt @@ -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() diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt index 946a4c01..9f2689ce 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/HomeScreen.kt @@ -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 ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt index c322e772..1fa0c313 100644 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/composable/navigation/AuxNavHost.kt @@ -344,7 +344,8 @@ fun AuxNavHost( route = chatRoomDetailRoute ) }, - nostrRepository = databaseNostrRepository + nostrRepository = databaseNostrRepository, + chatRepository = databaseChatRepository ) } composable { diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt new file mode 100755 index 00000000..e09e1a19 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomDetailMessageListViewModel.kt @@ -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 + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomListViewModel.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomListViewModel.kt index 3ae41fe4..43b9b913 100755 --- a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomListViewModel.kt +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/model/ChatRoomListViewModel.kt @@ -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 ) } } diff --git a/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/ChatRoomDetailMessageListUIState.kt b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/ChatRoomDetailMessageListUIState.kt new file mode 100755 index 00000000..141b3028 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/ac/cord/auxiliary/compose/ui/view/state/ChatRoomDetailMessageListUIState.kt @@ -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 + ): ChatRoomDetailMessageListUIState + + data object Error: ChatRoomDetailMessageListUIState + data object Loading: ChatRoomDetailMessageListUIState +} +