diff --git a/core/websocket/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/websocket/impl/WebsocketProviderImpl.kt b/core/websocket/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/websocket/impl/WebsocketProviderImpl.kt index d3b95821a..596c8cf90 100644 --- a/core/websocket/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/websocket/impl/WebsocketProviderImpl.kt +++ b/core/websocket/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/websocket/impl/WebsocketProviderImpl.kt @@ -220,7 +220,7 @@ class WebsocketProviderImpl( Log.d(TAG, "unsubscribe! $topic") } .catch { e -> - Log.d(TAG, "Subscription $topic reported error: ${e.localizedMessage}") + Log.e(TAG, "Subscription $topic reported error: ${e.localizedMessage}") } .map { WebsocketProvider.WebsocketData.Message(it) diff --git a/feature/metis-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metistest/MetisServiceStub.kt b/feature/metis-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metistest/MetisServiceStub.kt index cbba57727..3c307113a 100644 --- a/feature/metis-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metistest/MetisServiceStub.kt +++ b/feature/metis-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metistest/MetisServiceStub.kt @@ -1,13 +1,9 @@ package de.tum.informatics.www1.artemis.native_app.feature.metistest import de.tum.informatics.www1.artemis.native_app.core.data.NetworkResponse -import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.network.MetisService import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisPostDTO import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.StandalonePost -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf class MetisServiceStub( var posts: List = emptyList() @@ -31,11 +27,4 @@ class MetisServiceStub( ): NetworkResponse { return NetworkResponse.Response(posts.first()) } - - override fun subscribeToPostUpdates( - courseId: Long, - clientId: Long - ): Flow> { - return flowOf() - } } \ No newline at end of file diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/conversation_module.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/conversation_module.kt index 38fb47cc8..2479b3b50 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/conversation_module.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/conversation_module.kt @@ -21,7 +21,7 @@ import org.koin.androidx.workmanager.dsl.workerOf import org.koin.dsl.module val conversationModule = module { - single { MetisServiceImpl(get(), get()) } + single { MetisServiceImpl(get()) } single { MetisModificationServiceImpl(get()) } single { EmojiServiceImpl(androidContext()) } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/network/MetisService.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/network/MetisService.kt index 9fd5aeb05..1b8cd69d7 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/network/MetisService.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/network/MetisService.kt @@ -1,14 +1,11 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.network import de.tum.informatics.www1.artemis.native_app.core.data.NetworkResponse -import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisFilter -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisPostDTO import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisSortingStrategy import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.CourseWideContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.StandalonePost -import kotlinx.coroutines.flow.Flow interface MetisService { @@ -33,11 +30,6 @@ interface MetisService { authToken: String ): NetworkResponse - fun subscribeToPostUpdates( - courseId: Long, - clientId: Long, - ): Flow> - /** * The metis context needed to query standalone posts. * @param query if not null the posts will be filtered to contain the given query. diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/network/impl/MetisServiceImpl.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/network/impl/MetisServiceImpl.kt index 14f77a9be..942753ea1 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/network/impl/MetisServiceImpl.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/network/impl/MetisServiceImpl.kt @@ -4,13 +4,10 @@ import de.tum.informatics.www1.artemis.native_app.core.data.NetworkResponse import de.tum.informatics.www1.artemis.native_app.core.data.cookieAuth import de.tum.informatics.www1.artemis.native_app.core.data.performNetworkCall import de.tum.informatics.www1.artemis.native_app.core.data.service.KtorProvider -import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider -import de.tum.informatics.www1.artemis.native_app.core.websocket.impl.WebsocketTopic import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.network.MetisService import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.network.RESOURCE_PATH_SEGMENTS import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisFilter -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisPostDTO import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisSortingStrategy import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.CourseWideContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.StandalonePost @@ -18,12 +15,9 @@ import io.ktor.client.call.body import io.ktor.client.request.get import io.ktor.client.request.parameter import io.ktor.http.appendPathSegments -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.merge internal class MetisServiceImpl( private val ktorProvider: KtorProvider, - private val websocketProvider: WebsocketProvider ) : MetisService { override suspend fun getPosts( @@ -149,17 +143,4 @@ internal class MetisServiceImpl( } } } - - override fun subscribeToPostUpdates( - courseId: Long, - clientId: Long - ): Flow> { - val courseWideTopic = WebsocketTopic.getCourseWideConversationUpdateTopic(courseId) - val normalTopic = WebsocketTopic.getNormalConversationUpdateTopic(clientId) - - val courseWideUpdates = websocketProvider.subscribe(courseWideTopic, MetisPostDTO.serializer()) - val normalUpdates = websocketProvider.subscribe(normalTopic, MetisPostDTO.serializer()) - - return merge(courseWideUpdates, normalUpdates) - } } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationViewModel.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationViewModel.kt index 979911df1..46701ca9e 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationViewModel.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationViewModel.kt @@ -143,7 +143,7 @@ internal open class ConversationViewModel( * Manages updating from the websocket. */ private val webSocketUpdateUseCase = ConversationWebSocketUpdateUseCase( - metisService = metisService, + websocketProvider = websocketProvider, metisStorageService = metisStorageService ) @@ -233,7 +233,7 @@ internal open class ConversationViewModel( clientId.filterSuccess() ) { conversationDataState, clientId -> websocketProvider.subscribeToConversationUpdates(clientId, metisContext.courseId) - .filter { it.crudAction == MetisCrudAction.UPDATE } + .filter { it.action == MetisCrudAction.UPDATE } .map> { DataState.Success(it.conversation) } .onStart { emit(conversationDataState) } } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationWebSocketUpdateUseCase.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationWebSocketUpdateUseCase.kt index 1afe25d8b..a0faa8e7c 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationWebSocketUpdateUseCase.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationWebSocketUpdateUseCase.kt @@ -1,17 +1,17 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider -import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.network.MetisService import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.storage.MetisStorageService import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisCrudAction import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisPostDTO +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.service.network.subscribeToPostUpdates /** * Manages updates to the conversation over the web socket. */ class ConversationWebSocketUpdateUseCase( - private val metisService: MetisService, + private val websocketProvider: WebsocketProvider, private val metisStorageService: MetisStorageService ) { @@ -23,17 +23,15 @@ class ConversationWebSocketUpdateUseCase( context: MetisContext, clientId: Long ) { - metisService.subscribeToPostUpdates( + websocketProvider.subscribeToPostUpdates( courseId = context.courseId, clientId = clientId - ).collect { websocketData -> - if (websocketData is WebsocketProvider.WebsocketData.Message) { - updateDatabaseWithDto( - dto = websocketData.message, - context = context, - host = host - ) - } + ).collect { postDto -> + updateDatabaseWithDto( + dto = postDto, + context = context, + host = host + ) } } @@ -57,10 +55,6 @@ class ConversationWebSocketUpdateUseCase( listOf(dto.post.id ?: return) ) } - - MetisCrudAction.NEW_MESSAGE -> { - // Nothing to do here. Only relevant for the conversation overview. - } } } } \ No newline at end of file diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt index 4863750f6..ad79ccadc 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt @@ -16,16 +16,16 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowRight +import androidx.compose.material.icons.automirrored.filled.InsertDriveFile +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.automirrored.filled.Message import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowRight import androidx.compose.material.icons.filled.ChatBubble import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.Forum import androidx.compose.material.icons.filled.Groups2 -import androidx.compose.material.icons.filled.InsertDriveFile -import androidx.compose.material.icons.filled.List -import androidx.compose.material.icons.filled.Message import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material.icons.filled.NotInterested import androidx.compose.material.icons.filled.NotificationsActive @@ -33,9 +33,9 @@ import androidx.compose.material.icons.filled.NotificationsOff import androidx.compose.material.icons.filled.School import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -96,19 +96,16 @@ internal fun ConversationList( onToggleMarkAsFavourite: (conversationId: Long, favorite: Boolean) -> Unit, onToggleHidden: (conversationId: Long, hidden: Boolean) -> Unit, onToggleMuted: (conversationId: Long, muted: Boolean) -> Unit, - onRequestCreatePersonalConversation: () -> Unit, - onRequestAddChannel: () -> Unit, trailingContent: LazyListScope.() -> Unit ) { - val listWithHeader: LazyListScope.(ConversationCollections.ConversationCollection<*>, String, String, Int, ConversationSectionHeaderAction, () -> Unit, @Composable () -> Unit) -> Unit = - { collection, key, suffix, textRes, action, toggleIsExpanded, icon -> + val listWithHeader: LazyListScope.(ConversationCollections.ConversationCollection<*>, String, String, Int, () -> Unit, @Composable () -> Unit) -> Unit = + { collection, key, suffix, textRes, onClick, icon -> conversationSectionHeader( key = key, text = textRes, - onClickAddAction = action, isExpanded = collection.isExpanded, - toggleIsExpanded = toggleIsExpanded, + onClick = onClick, icon = icon ) @@ -130,8 +127,7 @@ internal fun ConversationList( SECTION_FAVORITES_KEY, KEY_SUFFIX_FAVORITES, R.string.conversation_overview_section_favorites, - NoAction, - { viewModel.toggleFavoritesExpanded() }, + viewModel::toggleFavoritesExpanded, { Icon(imageVector = Icons.Default.Favorite, contentDescription = null) } ) } @@ -141,7 +137,6 @@ internal fun ConversationList( SECTION_CHANNELS_KEY, KEY_SUFFIX_CHANNELS, R.string.conversation_overview_section_general_channels, - OnClickAction(onRequestAddChannel), viewModel::toggleGeneralsExpanded ) { Icon(imageVector = Icons.Default.ChatBubble, contentDescription = null) } @@ -151,9 +146,8 @@ internal fun ConversationList( SECTION_EXERCISES_KEY, KEY_SUFFIX_EXERCISES, R.string.conversation_overview_section_exercise_channels, - NoAction, viewModel::toggleExercisesExpanded - ) { Icon(imageVector = Icons.Default.List, contentDescription = null) } + ) { Icon(imageVector = Icons.AutoMirrored.Filled.List, contentDescription = null) } } if (conversationCollections.lectureChannels.conversations.isNotEmpty()) { @@ -162,9 +156,8 @@ internal fun ConversationList( SECTION_LECTURES_KEY, KEY_SUFFIX_LECTURES, R.string.conversation_overview_section_lecture_channels, - NoAction, viewModel::toggleLecturesExpanded - ) { Icon(imageVector = Icons.Default.InsertDriveFile, contentDescription = null) } + ) { Icon(imageVector = Icons.AutoMirrored.Filled.InsertDriveFile, contentDescription = null) } } if (conversationCollections.examChannels.conversations.isNotEmpty()) { @@ -173,7 +166,6 @@ internal fun ConversationList( SECTION_EXAMS_KEY, KEY_SUFFIX_EXAMS, R.string.conversation_overview_section_exam_channels, - NoAction, viewModel::toggleExamsExpanded ) { Icon(imageVector = Icons.Default.School, contentDescription = null) } } @@ -184,7 +176,6 @@ internal fun ConversationList( SECTION_GROUPS_KEY, KEY_SUFFIX_GROUPS, R.string.conversation_overview_section_groups, - OnClickAction(onRequestCreatePersonalConversation), viewModel::toggleGroupChatsExpanded ) { Icon(imageVector = Icons.Default.Forum, contentDescription = null) } } @@ -195,9 +186,8 @@ internal fun ConversationList( SECTION_DIRECT_MESSAGES_KEY, KEY_SUFFIX_PERSONAL, R.string.conversation_overview_section_direct_messages, - OnClickAction(onRequestCreatePersonalConversation), viewModel::togglePersonalConversationsExpanded - ) { Icon(imageVector = Icons.Default.Message, contentDescription = null) } + ) { Icon(imageVector = Icons.AutoMirrored.Filled.Message, contentDescription = null) } } if (conversationCollections.hidden.conversations.isNotEmpty()) { @@ -206,7 +196,6 @@ internal fun ConversationList( SECTION_HIDDEN_KEY, KEY_SUFFIX_HIDDEN, R.string.conversation_overview_section_hidden, - NoAction, viewModel::toggleHiddenExpanded ) { Icon(imageVector = Icons.Default.NotInterested, contentDescription = null) } } @@ -219,8 +208,7 @@ private fun LazyListScope.conversationSectionHeader( key: String, @StringRes text: Int, isExpanded: Boolean, - onClickAddAction: ConversationSectionHeaderAction, - toggleIsExpanded: () -> Unit, + onClick: () -> Unit, icon: @Composable () -> Unit ) { item(key = key) { @@ -229,12 +217,12 @@ private fun LazyListScope.conversationSectionHeader( .fillMaxWidth() .testTag(key) ) { - Divider() + HorizontalDivider() Row( modifier = Modifier .fillMaxWidth() - .clickable { toggleIsExpanded() } + .clickable { onClick() } .padding(horizontal = 16.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween @@ -255,17 +243,17 @@ private fun LazyListScope.conversationSectionHeader( IconButton( modifier = Modifier.testTag(TEST_TAG_HEADER_EXPAND_ICON), - onClick = { toggleIsExpanded() } + onClick = { onClick() } ) { Icon( - imageVector = if (isExpanded) Icons.Default.ArrowDropDown else Icons.Default.ArrowRight, + imageVector = if (isExpanded) Icons.Default.ArrowDropDown else Icons.AutoMirrored.Filled.ArrowRight, contentDescription = null, modifier = Modifier.size(32.dp) ) } } - Divider() + HorizontalDivider() } } } @@ -366,10 +354,7 @@ private fun ConversationListItem( } }, trailingContent = { - UnreadMessages( - modifier = Modifier.padding(end = 24.dp), - unreadMessagesCount = unreadMessagesCount - ) + UnreadMessages(unreadMessagesCount = unreadMessagesCount) } ) } @@ -515,6 +500,7 @@ private fun UnreadMessages(modifier: Modifier = Modifier, unreadMessagesCount: L if (unreadMessagesCount > 0) { Box( modifier = modifier + .padding(end = 24.dp) .size(24.dp) .aspectRatio(1f) .background( @@ -531,12 +517,6 @@ private fun UnreadMessages(modifier: Modifier = Modifier, unreadMessagesCount: L } } -private sealed interface ConversationSectionHeaderAction - -private data class OnClickAction(val onClick: () -> Unit) : ConversationSectionHeaderAction - -private object NoAction : ConversationSectionHeaderAction - private fun String.removeSectionPrefix(): String { val prefixes = listOf("exercise-", "lecture-", "exam-") var result = this diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt index d27066657..dd5dc33f1 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt @@ -26,10 +26,10 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Tag import androidx.compose.material.icons.filled.WifiOff -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -151,10 +151,8 @@ fun ConversationOverviewBody( onToggleMarkAsFavourite = viewModel::markConversationAsFavorite, onToggleHidden = viewModel::markConversationAsHidden, onToggleMuted = viewModel::markConversationAsMuted, - onRequestCreatePersonalConversation = onRequestCreatePersonalConversation, - onRequestAddChannel = onRequestAddChannel, trailingContent = { - item { Divider() } + item { HorizontalDivider() } item(key = KEY_BUTTON_SHOW_COC) { Box( diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewViewModel.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewViewModel.kt index cdcf8b958..a22b688e0 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewViewModel.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewViewModel.kt @@ -18,10 +18,11 @@ import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigura import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvider import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider +import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections.ConversationCollection import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.service.storage.ConversationPreferenceService -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisCrudAction +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisPostDTO import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.ConversationWebsocketDto import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.Conversation @@ -29,6 +30,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.d import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.OneToOneChat import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.service.network.ConversationService import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.service.network.subscribeToConversationUpdates +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.service.network.subscribeToPostUpdates import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.MetisViewModel import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.VisibleMetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.VisibleMetisContextReporter @@ -134,6 +136,18 @@ class ConversationOverviewViewModel( replay = 0 ) + private val postUpdates: Flow = clientId + .filterSuccess() + .flatMapLatest { userId -> + websocketProvider.subscribeToPostUpdates(courseId, userId) + } + .flowOn(coroutineContext) + .shareIn( + viewModelScope, + SharingStarted.WhileSubscribed(stopTimeout = 5.seconds), + replay = 0 + ) + private val manualConversationUpdates = MutableSharedFlow() /** @@ -180,7 +194,7 @@ class ConversationOverviewViewModel( } .shareIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, replay = 1) - private val conversationsAsCollections: StateFlow> = + private val conversationsAsCollections: StateFlow> = combine( updatedConversations, currentPreferences, @@ -189,7 +203,7 @@ class ConversationOverviewViewModel( conversationsDataState.bind { conversations -> val isFiltering = query.isNotBlank() - de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections( + ConversationCollections( channels = conversations.filterNotHiddenNorFavourite() .filter { !it.filterPredicate("exercise") && !it.filterPredicate("lecture") && !it.filterPredicate("exam") } .asCollection(isFiltering || preferences.generalsExpanded), @@ -231,7 +245,7 @@ class ConversationOverviewViewModel( /** * Holds the latest conversations we could successfully load. */ - private val latestConversations: StateFlow> = + private val latestConversations: StateFlow> = conversationsAsCollections .transformWhile { conversationsAsCollections -> emit(conversationsAsCollections) @@ -245,7 +259,7 @@ class ConversationOverviewViewModel( } .stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly) - val conversations: StateFlow> = + val conversations: StateFlow> = combine(latestConversations, query) { latestConversationsDataState, query -> if (query.isBlank()) { latestConversationsDataState @@ -265,53 +279,46 @@ class ConversationOverviewViewModel( emit(loadedConversations) merge( + manualConversationUpdates, conversationUpdates.map(::ServerSentConversationUpdate), - manualConversationUpdates + postUpdates.map(::ServerSentPostUpdate) ) .collect { update -> when (update) { is MarkMessagesRead -> { - val affectedConversation = currentConversations[update.conversationId] - if (affectedConversation != null) { - currentConversations[update.conversationId] = - affectedConversation.withUnreadMessagesCount(0L) - } + val affectedConversation = currentConversations[update.conversationId] ?: return@collect + currentConversations[update.conversationId] = affectedConversation.withUnreadMessagesCount(0L) } is ServerSentConversationUpdate -> { - val serverSentUpdate = update.update - - // TODO: It seems like there are no updates received from the websocket -> investigate + val serverConversationUpdate = update.update - when (serverSentUpdate.crudAction) { + when (serverConversationUpdate.action) { MetisCrudAction.CREATE, MetisCrudAction.UPDATE -> { - currentConversations[serverSentUpdate.conversation.id] = - serverSentUpdate.conversation - } - - MetisCrudAction.NEW_MESSAGE -> { - val isMetisContextVisible = - visibleMetisContexts.value.any { visibleMetisContext -> - val metisContext = visibleMetisContext.metisContext - - metisContext is MetisContext.Conversation && metisContext.conversationId == serverSentUpdate.conversation.id - } - - val existingConversation = - currentConversations[serverSentUpdate.conversation.id] - if (existingConversation != null && !isMetisContextVisible) { - currentConversations[serverSentUpdate.conversation.id] = - existingConversation.withUnreadMessagesCount( - (existingConversation.unreadMessagesCount ?: 0) + 1 - ) - } + currentConversations[serverConversationUpdate.conversation.id] = + serverConversationUpdate.conversation } MetisCrudAction.DELETE -> { - currentConversations.remove(serverSentUpdate.conversation.id) + currentConversations.remove(serverConversationUpdate.conversation.id) } } } + + is ServerSentPostUpdate -> { + val postUpdate = update.update + if (postUpdate.action != MetisCrudAction.CREATE) return@collect + + val conversationId = postUpdate.post.conversation?.id ?: return@collect + val isConversationVisible = visibleMetisContexts.value.any { it.isInConversation(conversationId) } + if (isConversationVisible) return@collect // The user is currently looking at the conversation in the UI + + val existingConversation = currentConversations[conversationId] ?: return@collect + + currentConversations[conversationId] = existingConversation.withUnreadMessagesCount( + (existingConversation.unreadMessagesCount ?: 0) + 1 + ) + } } emit(currentConversations.values.toList()) diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationUpdate.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationUpdate.kt index 556430cea..9781131a8 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationUpdate.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationUpdate.kt @@ -1,9 +1,13 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.overview +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisPostDTO import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.ConversationWebsocketDto sealed interface ConversationUpdate +data class MarkMessagesRead(val conversationId: Long) : ConversationUpdate + data class ServerSentConversationUpdate(val update: ConversationWebsocketDto) : ConversationUpdate -data class MarkMessagesRead(val conversationId: Long) : ConversationUpdate \ No newline at end of file +data class ServerSentPostUpdate(val update: MetisPostDTO) : ConversationUpdate + diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/MetisCrudAction.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/MetisCrudAction.kt index 6a560cc6c..9d16d95b0 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/MetisCrudAction.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/MetisCrudAction.kt @@ -7,5 +7,4 @@ enum class MetisCrudAction(val value: String) { CREATE("CREATE"), UPDATE("UPDATE"), DELETE("DELETE"), - NEW_MESSAGE("NEW_MESSAGE") // Only used when for the first message in a new conversation. } \ No newline at end of file diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/ConversationWebsocketDto.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/ConversationWebsocketDto.kt index 5c45aef16..e0e7fcbeb 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/ConversationWebsocketDto.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/ConversationWebsocketDto.kt @@ -2,12 +2,10 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content. import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisCrudAction import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.Conversation -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class ConversationWebsocketDto( val conversation: Conversation, - @SerialName("metisCrudAction") - val crudAction: MetisCrudAction + val action: MetisCrudAction ) diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/ConversationWebsocketExtensions.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/ConversationWebsocketExtensions.kt index cb888168c..485b39894 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/ConversationWebsocketExtensions.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/ConversationWebsocketExtensions.kt @@ -2,11 +2,27 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.service. import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider import de.tum.informatics.www1.artemis.native_app.core.websocket.impl.WebsocketTopic +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.MetisPostDTO import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.ConversationWebsocketDto import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.merge fun WebsocketProvider.subscribeToConversationUpdates(userId: Long, courseId: Long): Flow { val topic = WebsocketTopic.getConversationMetaUpdateTopic(courseId, userId) return subscribeMessage(topic, ConversationWebsocketDto.serializer()) +} + + +fun WebsocketProvider.subscribeToPostUpdates( + courseId: Long, + clientId: Long +): Flow { + val courseWideTopic = WebsocketTopic.getCourseWideConversationUpdateTopic(courseId) + val normalTopic = WebsocketTopic.getNormalConversationUpdateTopic(clientId) + + val courseWideUpdates = subscribeMessage(courseWideTopic, MetisPostDTO.serializer()) + val normalUpdates = subscribeMessage(normalTopic, MetisPostDTO.serializer()) + + return merge(courseWideUpdates, normalUpdates) } \ No newline at end of file diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/visiblemetiscontextreporter/VisibleMetisContext.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/visiblemetiscontextreporter/VisibleMetisContext.kt index 40b94e5ee..4aea88e2e 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/visiblemetiscontextreporter/VisibleMetisContext.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/visiblemetiscontextreporter/VisibleMetisContext.kt @@ -5,6 +5,11 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.M sealed interface VisibleMetisContext { val metisContext: MetisContext + + fun isInConversation(conversationId: Long): Boolean { + return metisContext is MetisContext.Conversation && + (metisContext as MetisContext.Conversation).conversationId == conversationId + } } data class VisiblePostList(override val metisContext: MetisContext) : VisibleMetisContext