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

Communication: Resolve Post functionality for threads #59

Merged
merged 32 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
327cc9c
added post action for resolving posts
julian-wls Oct 16, 2024
a51ba7c
Merge branch 'main' into feature/communication/resolve-threads
julian-wls Oct 17, 2024
58fdc85
increased allowed lines for posts and fixed text not getting set into…
julian-wls Oct 17, 2024
0e60e3a
changed PostItem to show resolved label in chat list
julian-wls Oct 17, 2024
77f7a18
added resolved label for AnswerPosts
julian-wls Oct 17, 2024
14ea798
removes a typo in the key for resolvesPost in answer post model
julian-wls Oct 18, 2024
21bd4e7
Merge branch 'main' into feature/communication/resolve-threads
julian-wls Oct 19, 2024
85cead6
added reload requests to post modification functions in ConversationV…
julian-wls Oct 19, 2024
5df4ac8
changes to implement change requests
julian-wls Oct 19, 2024
047dbc4
removed manual request reload calls and temporary fix to show replies…
julian-wls Oct 19, 2024
ee4ed0f
adjustments
julian-wls Oct 20, 2024
9e87379
minor fixes
julian-wls Oct 20, 2024
4ef1a84
adjustments
julian-wls Oct 21, 2024
6870fda
minor fixes
julian-wls Oct 21, 2024
d687fa6
added TODO
julian-wls Oct 21, 2024
7bf5a2e
Merge branch 'main' into feature/communication/resolve-threads
julian-wls Oct 23, 2024
74c0fc1
added tests
julian-wls Oct 23, 2024
3cf0b90
Provide basis for UI test.
TimOrtel Oct 24, 2024
5fa626d
Provide basis for UI test 2.
TimOrtel Oct 24, 2024
37084cc
Merge branch 'main' into feature/communication/resolve-threads
julian-wls Oct 24, 2024
882fa53
added test functions to ConversationAnswerMessagesUITest
julian-wls Oct 24, 2024
03bd3e5
added more answer posts to test and use random resolving post for unr…
julian-wls Oct 24, 2024
4db150d
removed random answer post selection in UI test
julian-wls Oct 24, 2024
7507ca2
adjustments
julian-wls Oct 26, 2024
471bf65
fixed UI tests for resolving posts
julian-wls Oct 28, 2024
83e74d3
fixed UI Tests for resolving posts
julian-wls Oct 29, 2024
2619f75
code cleanup
julian-wls Oct 29, 2024
146894f
adjustments to resolve comments
julian-wls Nov 4, 2024
79f52b1
minor changes
julian-wls Nov 7, 2024
8ff5468
fixed scroll in UI test
julian-wls Nov 9, 2024
9dbbf2a
adjustments
julian-wls Nov 10, 2024
8aa833f
add for loop to test to avoid code duplication
julian-wls Nov 11, 2024
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 @@ -14,7 +14,7 @@ val testWebsocketModule = module {
single<WebsocketProvider> { TestWebsocketProvider() }
}

private class TestWebsocketProvider : WebsocketProvider {
class TestWebsocketProvider : WebsocketProvider {

override val connectionState: Flow<WebsocketProvider.WebsocketConnectionState> =
flowOf(WebsocketProvider.WebsocketConnectionState.WithSession(true))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ fun `Metis - Conversation Channel`() {
),
clientId = 0L,
hasModerationRights = true,
isAtLeastTutorInCourse = true,
listContentPadding = PaddingValues(),
serverUrl = "",
courseId = 0,
Expand Down
3 changes: 3 additions & 0 deletions feature/metis/conversation/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ dependencies {

testImplementation(project(":feature:metis-test"))
implementation("androidx.paging:paging-common:3.2.1")

testImplementation(libs.mockk.android)
testImplementation(libs.mockk.agent)
}

tasks.register("fetchAndPrepareEmojis", emoji.FetchAndPrepareEmojisTask::class) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ 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.model.Course
import de.tum.informatics.www1.artemis.native_app.core.model.account.isAtLeastTutorInCourse
import de.tum.informatics.www1.artemis.native_app.core.model.exercise.FileUploadExercise
import de.tum.informatics.www1.artemis.native_app.core.model.exercise.ModelingExercise
import de.tum.informatics.www1.artemis.native_app.core.model.exercise.ProgrammingExercise
Expand Down Expand Up @@ -146,6 +147,21 @@ internal open class ConversationViewModel(
metisStorageService = metisStorageService
)

private val course: StateFlow<DataState<Course>> = flatMapLatest(
serverConfigurationService.serverUrl,
accountService.authToken,
onRequestReload.onStart { emit(Unit) }
) { serverUrl, authToken, _ ->
retryOnInternet(networkStatusProvider.currentNetworkStatus) {
courseService.getCourse(
metisContext.courseId,
serverUrl,
authToken
).bind { it.course }
}
}
.stateIn(viewModelScope + coroutineContext, SharingStarted.Lazily)

val hasModerationRights: StateFlow<Boolean> = flatMapLatest(
serverConfigurationService.serverUrl,
accountService.authToken,
Expand All @@ -165,6 +181,22 @@ internal open class ConversationViewModel(
}
.stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, false)

val isAtLeastTutorInCourse: StateFlow<Boolean> = flatMapLatest(
serverConfigurationService.serverUrl,
accountService.authToken,
course,
onRequestReload.onStart { emit(Unit) }
) { serverUrl, authToken, course, _ ->
retryOnInternet(networkStatusProvider.currentNetworkStatus) {
accountDataService.getAccountData(
serverUrl = serverUrl,
bearerToken = authToken
)
.bind { it.isAtLeastTutorInCourse(course = course.orThrow()) }
}
.map { it.orElse(false) }
}
.stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, false)

val conversationDataStatus: StateFlow<DataStatus> = combine(
websocketProvider.isConnected,
Expand Down Expand Up @@ -207,21 +239,6 @@ internal open class ConversationViewModel(
}
.stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly)

private val course: StateFlow<DataState<Course>> = flatMapLatest(
serverConfigurationService.serverUrl,
accountService.authToken,
onRequestReload.onStart { emit(Unit) }
) { serverUrl, authToken, _ ->
retryOnInternet(networkStatusProvider.currentNetworkStatus) {
courseService.getCourse(
metisContext.courseId,
serverUrl,
authToken
).bind { it.course }
}
}
.stateIn(viewModelScope + coroutineContext, SharingStarted.Lazily)

private val conversations: StateFlow<DataState<List<Conversation>>> = flatMapLatest(
serverConfigurationService.serverUrl,
accountService.authToken,
Expand Down Expand Up @@ -322,6 +339,32 @@ internal open class ConversationViewModel(
return if (success) null else MetisModificationFailure.DELETE_REACTION
}

/**
* Handles a click on resolve or does not resolve post.
* It updates the post accordingly.
*/
fun toggleResolvePost(
parentPost: PostPojo,
post: AnswerPostPojo
): Deferred<MetisModificationFailure?> {
return viewModelScope.async(coroutineContext) {
val conversation =
loadConversation() ?: return@async MetisModificationFailure.UPDATE_POST

val resolved = !post.resolvesPost
val serializedParentPost = StandalonePost(parentPost, conversation)
val newPost = AnswerPost(post, serializedParentPost).copy(resolvesPost = resolved)

metisModificationService.updateAnswerPost(
context = metisContext,
post = newPost,
serverUrl = serverConfigurationService.serverUrl.first(),
authToken = accountService.authToken.first()
)
.asMetisModificationFailure(MetisModificationFailure.UPDATE_POST)
}
}

fun deletePost(post: IBasePost): Deferred<MetisModificationFailure?> {
return viewModelScope.async(coroutineContext) {
metisModificationService.deletePost(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.chatlist

import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IStandalonePost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.PostPojo
import kotlinx.datetime.LocalDate

sealed class ChatListItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import de.tum.informatics.www1.artemis.native_app.core.data.DataState
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.MetisModificationFailure
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.ConversationViewModel
Expand All @@ -52,6 +51,7 @@ import kotlinx.datetime.LocalDate
import kotlinx.datetime.TimeZone
import kotlinx.datetime.atStartOfDayIn
import kotlinx.datetime.toJavaInstant
import org.koin.compose.koinInject
import java.text.SimpleDateFormat
import java.util.Date

Expand All @@ -74,6 +74,7 @@ internal fun MetisChatList(

val clientId: Long by viewModel.clientIdOrDefault.collectAsState()
val hasModerationRights by viewModel.hasModerationRights.collectAsState()
val isAtLeastTutorInCourse by viewModel.isAtLeastTutorInCourse.collectAsState()

val serverUrl by viewModel.serverUrl.collectAsState()

Expand All @@ -93,6 +94,7 @@ internal fun MetisChatList(
posts = posts.asPostsDataState(),
clientId = clientId,
hasModerationRights = hasModerationRights,
isAtLeastTutorInCourse = isAtLeastTutorInCourse,
listContentPadding = listContentPadding,
serverUrl = serverUrl,
courseId = viewModel.courseId,
Expand All @@ -117,6 +119,7 @@ fun MetisChatList(
bottomItem: PostPojo?,
clientId: Long,
hasModerationRights: Boolean,
isAtLeastTutorInCourse: Boolean,
listContentPadding: PaddingValues,
serverUrl: String,
courseId: Long,
Expand All @@ -134,9 +137,10 @@ fun MetisChatList(
initialReplyTextProvider = initialReplyTextProvider,
onCreatePost = onCreatePost,
onEditPost = onEditPost,
onResolvePost = null,
onDeletePost = onDeletePost,
onRequestReactWithEmoji = onRequestReactWithEmoji
) { replyMode, onEditPostDelegate, onRequestReactWithEmojiDelegate, onDeletePostDelegate, updateFailureStateDelegate ->
onRequestReactWithEmoji = onRequestReactWithEmoji,
) { replyMode, onEditPostDelegate, _, onRequestReactWithEmojiDelegate, onDeletePostDelegate, updateFailureStateDelegate ->
Column(modifier = modifier) {
val informationModifier = Modifier
.fillMaxSize()
Expand All @@ -151,6 +155,7 @@ fun MetisChatList(
state = state,
itemCount = posts.itemCount,
order = DisplayPostOrder.REVERSED,
emojiService = koinInject(),
bottomItem = bottomItem
) {
when (posts) {
Expand Down Expand Up @@ -182,6 +187,7 @@ fun MetisChatList(
clientId = clientId,
onClickViewPost = onClickViewPost,
hasModerationRights = hasModerationRights,
isAtLeastTutorInCourse = isAtLeastTutorInCourse,
onRequestEdit = onEditPostDelegate,
onRequestDelete = onDeletePostDelegate,
onRequestReactWithEmoji = onRequestReactWithEmojiDelegate,
Expand Down Expand Up @@ -210,6 +216,7 @@ private fun ChatList(
state: LazyListState,
posts: PostsDataState.Loaded,
hasModerationRights: Boolean,
isAtLeastTutorInCourse: Boolean,
clientId: Long,
onClickViewPost: (StandalonePostId) -> Unit,
onRequestEdit: (IStandalonePost) -> Unit,
Expand Down Expand Up @@ -242,6 +249,7 @@ private fun ChatList(
val postActions = rememberPostActions(
post = post,
hasModerationRights = hasModerationRights,
isAtLeastTutorInCourse = isAtLeastTutorInCourse,
clientId = clientId,
onRequestEdit = { onRequestEdit(post ?: return@rememberPostActions) },
onRequestDelete = {
Expand All @@ -253,6 +261,7 @@ private fun ChatList(
onReplyInThread = {
onClickViewPost(post?.standalonePostId ?: return@rememberPostActions)
},
onResolvePost = null,
onRequestRetrySend = {
onRequestRetrySend(
post?.standalonePostId ?: return@rememberPostActions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ import androidx.compose.ui.Modifier
import de.tum.informatics.www1.artemis.native_app.core.common.markdown.PostArtemisMarkdownTransformer
import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.LocalMarkdownTransformer
import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.ProvideMarkwon
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.EmojiService
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.impl.EmojiServiceStub
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.ProvideEmojis
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.post.DisplayPostOrder
import kotlinx.coroutines.launch
import org.koin.compose.koinInject

/**
* Handles scrolling down to new items if the list was scrolled down before the new items came in.
Expand All @@ -40,6 +43,7 @@ internal fun <T : Any> MetisPostListHandler(
itemCount: Int,
bottomItem: T?,
order: DisplayPostOrder,
emojiService: EmojiService,
content: @Composable BoxScope.() -> Unit
) {
val scope = rememberCoroutineScope()
Expand Down Expand Up @@ -116,7 +120,7 @@ internal fun <T : Any> MetisPostListHandler(
}

ProvideMarkwon {
ProvideEmojis {
ProvideEmojis(emojiService) {
CompositionLocalProvider(LocalMarkdownTransformer provides markdownTransformer) {
content()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IAnswerPost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IBasePost

data class PostActions(
Expand All @@ -12,6 +13,7 @@ data class PostActions(
val onClickReaction: ((emojiId: String, create: Boolean) -> Unit)? = null,
val onCopyText: () -> Unit = {},
val onReplyInThread: (() -> Unit)? = null,
val onResolvePost: (() -> Unit)? = null,
val onRequestRetrySend: () -> Unit = {}
) {
val canPerformAnyAction: Boolean get() = requestDeletePost != null || requestEditPost != null
Expand All @@ -21,11 +23,13 @@ data class PostActions(
fun rememberPostActions(
post: IBasePost?,
hasModerationRights: Boolean,
isAtLeastTutorInCourse: Boolean,
clientId: Long,
onRequestEdit: () -> Unit,
onRequestDelete: () -> Unit,
onClickReaction: (emojiId: String, create: Boolean) -> Unit,
onReplyInThread: (() -> Unit)?,
onResolvePost: (() -> Unit)?,
onRequestRetrySend: () -> Unit
): PostActions {
val clipboardManager = LocalClipboardManager.current
Expand All @@ -38,12 +42,14 @@ fun rememberPostActions(
onRequestDelete,
onClickReaction,
onReplyInThread,
onResolvePost,
onRequestRetrySend,
clipboardManager
) {
if (post != null) {
val doesPostExistOnServer = post.serverPostId != null
val hasEditPostRights = hasModerationRights || post.authorId == clientId
val hasResolvePostRights = isAtLeastTutorInCourse || post.authorId == clientId

PostActions(
requestEditPost = if (doesPostExistOnServer && hasEditPostRights) onRequestEdit else null,
Expand All @@ -53,6 +59,7 @@ fun rememberPostActions(
clipboardManager.setText(AnnotatedString(post.content.orEmpty()))
},
onReplyInThread = if (doesPostExistOnServer) onReplyInThread else null,
onResolvePost = if (hasResolvePostRights) onResolvePost else null,
onRequestRetrySend = onRequestRetrySend
)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddReaction
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
Expand Down Expand Up @@ -52,6 +54,8 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.Spacings
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.LocalEmojiProvider
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.getUnicodeForEmojiId
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.AnswerPost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IAnswerPost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IBasePost
import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IReaction

Expand Down Expand Up @@ -136,6 +140,18 @@ internal fun PostContextBottomSheet(
}
)

if (postActions.onResolvePost != null && post is IAnswerPost) {
ActionButton(
modifier = actionButtonModifier,
icon = if (post.resolvesPost) Icons.Default.Clear else Icons.Default.Check,
text = if (post.resolvesPost) stringResource(id = R.string.post_does_not_resolve) else stringResource(id = R.string.post_resolves),
onClick = {
onDismissRequest()
postActions.onResolvePost.invoke()
}
)
}

postActions.onReplyInThread?.let {
ActionButton(
modifier = actionButtonModifier,
Expand Down
Loading
Loading