Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix: Fix websocket conversation overview #147

Merged
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<StandalonePost> = emptyList()
Expand All @@ -31,11 +27,4 @@ class MetisServiceStub(
): NetworkResponse<StandalonePost> {
return NetworkResponse.Response(posts.first())
}

override fun subscribeToPostUpdates(
courseId: Long,
clientId: Long
): Flow<WebsocketProvider.WebsocketData<MetisPostDTO>> {
return flowOf()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import org.koin.androidx.workmanager.dsl.workerOf
import org.koin.dsl.module

val conversationModule = module {
single<MetisService> { MetisServiceImpl(get(), get()) }
single<MetisService> { MetisServiceImpl(get()) }
single<MetisModificationService> { MetisModificationServiceImpl(get()) }
single<EmojiService> { EmojiServiceImpl(androidContext()) }

Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Expand All @@ -33,11 +30,6 @@ interface MetisService {
authToken: String
): NetworkResponse<StandalonePost>

fun subscribeToPostUpdates(
courseId: Long,
clientId: Long,
): Flow<WebsocketProvider.WebsocketData<MetisPostDTO>>

/**
* The metis context needed to query standalone posts.
* @param query if not null the posts will be filtered to contain the given query.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,20 @@ 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
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(
Expand Down Expand Up @@ -149,17 +143,4 @@ internal class MetisServiceImpl(
}
}
}

override fun subscribeToPostUpdates(
courseId: Long,
clientId: Long
): Flow<WebsocketProvider.WebsocketData<MetisPostDTO>> {
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ internal open class ConversationViewModel(
* Manages updating from the websocket.
*/
private val webSocketUpdateUseCase = ConversationWebSocketUpdateUseCase(
metisService = metisService,
websocketProvider = websocketProvider,
metisStorageService = metisStorageService
)

Expand Down Expand Up @@ -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<ConversationWebsocketDto, DataState<Conversation>> { DataState.Success(it.conversation) }
.onStart { emit(conversationDataState) }
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
) {

Expand All @@ -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
)
}
}

Expand All @@ -57,10 +55,6 @@ class ConversationWebSocketUpdateUseCase(
listOf(dto.post.id ?: return)
)
}

MetisCrudAction.NEW_MESSAGE -> {
// Nothing to do here. Only relevant for the conversation overview.
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,19 @@ 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
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.GroupChat
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
Expand Down Expand Up @@ -134,6 +136,18 @@ class ConversationOverviewViewModel(
replay = 0
)

private val postUpdates: Flow<MetisPostDTO> = clientId
.filterSuccess()
.flatMapLatest { userId ->
websocketProvider.subscribeToPostUpdates(courseId, userId)
}
.flowOn(coroutineContext)
.shareIn(
viewModelScope,
SharingStarted.WhileSubscribed(stopTimeout = 5.seconds),
replay = 0
)

private val manualConversationUpdates = MutableSharedFlow<MarkMessagesRead>()

/**
Expand Down Expand Up @@ -180,7 +194,7 @@ class ConversationOverviewViewModel(
}
.shareIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, replay = 1)

private val conversationsAsCollections: StateFlow<DataState<de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections>> =
private val conversationsAsCollections: StateFlow<DataState<ConversationCollections>> =
combine(
updatedConversations,
currentPreferences,
Expand All @@ -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<ChannelChat>()
.filter { !it.filterPredicate("exercise") && !it.filterPredicate("lecture") && !it.filterPredicate("exam") }
.asCollection(isFiltering || preferences.generalsExpanded),
Expand Down Expand Up @@ -231,7 +245,7 @@ class ConversationOverviewViewModel(
/**
* Holds the latest conversations we could successfully load.
*/
private val latestConversations: StateFlow<DataState<de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections>> =
private val latestConversations: StateFlow<DataState<ConversationCollections>> =
conversationsAsCollections
.transformWhile { conversationsAsCollections ->
emit(conversationsAsCollections)
Expand All @@ -245,7 +259,7 @@ class ConversationOverviewViewModel(
}
.stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly)

val conversations: StateFlow<DataState<de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections>> =
val conversations: StateFlow<DataState<ConversationCollections>> =
combine(latestConversations, query) { latestConversationsDataState, query ->
if (query.isBlank()) {
latestConversationsDataState
Expand All @@ -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())
Expand Down
Original file line number Diff line number Diff line change
@@ -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
data class ServerSentPostUpdate(val update: MetisPostDTO) : ConversationUpdate

Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Loading
Loading