Skip to content

Commit

Permalink
Feature: Chat reference link support (#22)
Browse files Browse the repository at this point in the history
* Support exercise and lecture mentioning.

* Support channel mentioning. Closes #9

* Filter exercise lecture and channel references by query. Update popup ui.

* Support user clicks and navigate to conversation.

* fix.

* Support channel mentions.

* resolve merge conflicts.

* Fix compilation error.

* fix compilation errors.
  • Loading branch information
TimOrtel authored Dec 1, 2023
1 parent 540158a commit bf16020
Show file tree
Hide file tree
Showing 18 changed files with 350 additions and 62 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,28 @@ package de.tum.informatics.www1.artemis.native_app.core.common.markdown

abstract class ArtemisMarkdownTransformer {

/**
* Empty markdown transformer.
*/
companion object : ArtemisMarkdownTransformer() {
override fun transformExerciseMarkdown(title: String, url: String): String = ""

override fun transformUserMentionMarkdown(
text: String,
fullName: String,
userName: String
): String = ""

override fun transformChannelMentionMarkdown(
channelName: String,
conversationId: Long
): String = ""
}

private val exerciseMarkdownPattern =
"\\[(text|quiz|lecture|modeling|file-upload|programing)](.*)\\(((?:/|\\w|\\d)+)\\)\\[/\\1]".toRegex()
private val userMarkdownPattern = "\\[user](.*?)\\((.*?)\\)\\[/user]".toRegex()
private val channelMarkdownPattern = "\\[channel](.*?)\\((\\d+?)\\)\\[/channel]".toRegex()

fun transformMarkdown(markdown: String): String {
return exerciseMarkdownPattern.replace(markdown) { matchResult ->
Expand All @@ -21,6 +40,15 @@ abstract class ArtemisMarkdownTransformer {
userName = userName
)
}
}.let {
channelMarkdownPattern.replace(it) { matchResult ->
val channelName = matchResult.groups[1]?.value.orEmpty()
val conversationId = matchResult.groups[2]?.value.orEmpty().toLong()
transformChannelMentionMarkdown(
channelName = channelName,
conversationId = conversationId
)
}
}
}

Expand All @@ -31,4 +59,9 @@ abstract class ArtemisMarkdownTransformer {
fullName: String,
userName: String
): String

protected abstract fun transformChannelMentionMarkdown(
channelName: String,
conversationId: Long
): String
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package de.tum.informatics.www1.artemis.native_app.core.common.markdown

class PostArtemisMarkdownTransformer(val serverUrl: String) : ArtemisMarkdownTransformer() {
class PostArtemisMarkdownTransformer(val serverUrl: String, val courseId: Long) : ArtemisMarkdownTransformer() {
override fun transformExerciseMarkdown(title: String, url: String): String {
return "[$title]($serverUrl$url)"
}

override fun transformUserMentionMarkdown(text: String, fullName: String, userName: String): String = "|||@$fullName|||"
override fun transformUserMentionMarkdown(text: String, fullName: String, userName: String): String = "[@$fullName](artemis://courses/$courseId/messages?username=$userName)"

override fun transformChannelMentionMarkdown(
channelName: String,
conversationId: Long
): String = "[#$channelName](artemis://courses/$courseId/messages?conversationId=$conversationId)"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ object PushNotificationArtemisMarkdownTransformer : ArtemisMarkdownTransformer()
override fun transformExerciseMarkdown(title: String, url: String): String = title

override fun transformUserMentionMarkdown(text: String, fullName: String, userName: String): String = "@$fullName"

override fun transformChannelMentionMarkdown(
channelName: String,
conversationId: Long
): String = "#$channelName"
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import de.tum.informatics.www1.artemis.native_app.core.datastore.defaults.Artemi

private const val LegacyArtemisInstanceHost = "artemis.ase.in.tum.de"

private val supportedUrls = ArtemisInstances.instances.map { it.host } + LegacyArtemisInstanceHost
private val supportedUrls = ArtemisInstances.instances.map { it.host } + LegacyArtemisInstanceHost + "artemis:/"

fun generateLinks(
path: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import androidx.annotation.IdRes
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
Expand All @@ -20,7 +20,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.semantics
Expand All @@ -32,16 +31,13 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.res.ResourcesCompat
import coil.ImageLoader
import de.tum.informatics.www1.artemis.native_app.core.common.markdown.PostArtemisMarkdownTransformer
import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService
import de.tum.informatics.www1.artemis.native_app.core.common.markdown.ArtemisMarkdownTransformer
import io.noties.markwon.Markwon
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.coil.CoilImagesPlugin
import io.noties.markwon.linkify.LinkifyPlugin
import io.noties.markwon.simple.ext.SimpleExtPlugin
import org.koin.compose.koinInject

// Copy from: https://github.com/jeziellago/compose-markdown
/*
Expand Down Expand Up @@ -69,7 +65,7 @@ import org.koin.compose.koinInject
* SOFTWARE.
*/

private const val PreviewModeServerUrl = "http://example.com"
val LocalMarkdownTransformer = compositionLocalOf<ArtemisMarkdownTransformer> { ArtemisMarkdownTransformer }

@Composable
fun MarkdownText(
Expand All @@ -94,26 +90,11 @@ fun MarkdownText(
createMarkdownRender(context, imageLoader)
}

val transformedMarkdown by if (LocalInspectionMode.current) {
remember { derivedStateOf { markdown } }
} else {
val serverConfigurationService: ServerConfigurationService = koinInject()
val serverUrl by serverConfigurationService.serverUrl.collectAsState(initial = "")
val markdownTransformer = LocalMarkdownTransformer.current

val markdownTransformer = remember(serverUrl) {
val strippedServerUrl =
if (serverUrl.endsWith("/")) serverUrl.substring(
0,
serverUrl.length - 1
) else serverUrl

PostArtemisMarkdownTransformer(strippedServerUrl)
}

remember(markdown, markdownTransformer) {
derivedStateOf {
markdownTransformer.transformMarkdown(markdown)
}
val transformedMarkdown by remember(markdown, markdownTransformer) {
derivedStateOf {
markdownTransformer.transformMarkdown(markdown)
}
}

Expand Down Expand Up @@ -205,12 +186,6 @@ fun createMarkdownRender(context: Context, imageLoader: ImageLoader?): Markwon {
.usePlugin(StrikethroughPlugin.create())
.usePlugin(TablePlugin.create(context))
.usePlugin(LinkifyPlugin.create())
// User mentions are transformed to |||@full name|||
.usePlugin(SimpleExtPlugin.create { p ->
p.addExtension(3, '|') { _, _ ->
arrayOf(ForegroundColorSpan(0xff3e8acc.toInt()))
}
})
.apply {
if (imageLoader != null) {
usePlugin(CoilImagesPlugin.create(context, imageLoader))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ fun `Course View - Exercise List`() {
CourseUiScreen(
modifier = Modifier.fillMaxSize(),
viewModel = courseViewModel,
username = "",
courseId = 0L,
conversationId = DEFAULT_CONVERSATION_ID,
postId = DEFAULT_POST_ID,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ fun `Metis - Conversation Channel`() {
posts,
PostsDataState.NotLoading
),
serverUrl = "",
courseId = 0,
isDataOutdated = false,
clientId = 0L,
hasModerationRights = true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
Expand Down Expand Up @@ -49,6 +50,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.LectureL
import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.exercise_list.ExerciseListUi
import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.thread.StandalonePostId
import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.ConversationFacadeUi
import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.NavigateToUserConversation
import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.NothingOpened
import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.OpenedConversation
import de.tum.informatics.www1.artemis.native_app.feature.metis.ui.OpenedThread
Expand Down Expand Up @@ -82,15 +84,17 @@ fun NavGraphBuilder.course(
) +
generateLinks("courses/{courseId}") +
generateLinks("courses/{courseId}/exercises") +
generateLinks("courses/{courseId}/messages?conversationId={conversationId}")
generateLinks("courses/{courseId}/messages?conversationId={conversationId}") +
generateLinks("courses/{courseId}/messages?username={username}")
composable(
route = "course/{courseId}",
arguments = listOf(
navArgument("courseId") { type = NavType.LongType; nullable = false },
navArgument("conversationId") {
type = NavType.LongType; defaultValue = DEFAULT_CONVERSATION_ID
},
navArgument("postId") { type = NavType.LongType; defaultValue = DEFAULT_POST_ID }
navArgument("postId") { type = NavType.LongType; defaultValue = DEFAULT_POST_ID },
navArgument("username") { type = NavType.StringType; defaultValue = "" }
),
deepLinks = deepLinks
) { backStackEntry ->
Expand All @@ -100,13 +104,15 @@ fun NavGraphBuilder.course(
val conversationId =
backStackEntry.arguments?.getLong("conversationId") ?: DEFAULT_CONVERSATION_ID
val postId = backStackEntry.arguments?.getLong("postId") ?: DEFAULT_POST_ID
val username = backStackEntry.arguments?.getString("username").orEmpty()

CourseUiScreen(
modifier = Modifier.fillMaxSize(),
viewModel = koinViewModel { parametersOf(courseId) },
courseId = courseId,
conversationId = conversationId,
postId = postId,
username = username,
onNavigateToExercise = onNavigateToExercise,
onNavigateToTextExerciseParticipation = onNavigateToTextExerciseParticipation,
onNavigateToExerciseResultView = onNavigateToExerciseResultView,
Expand All @@ -131,6 +137,7 @@ fun CourseUiScreen(
courseId: Long,
conversationId: Long,
postId: Long,
username: String,
onNavigateToExercise: (exerciseId: Long) -> Unit,
onNavigateToTextExerciseParticipation: (exerciseId: Long, participationId: Long) -> Unit,
onNavigateToExerciseResultView: (exerciseId: Long) -> Unit,
Expand All @@ -147,6 +154,7 @@ fun CourseUiScreen(
modifier = modifier,
conversationId = conversationId,
courseDataState = courseDataState,
username = username,
onNavigateBack = onNavigateBack,
weeklyExercisesDataState = weeklyExercisesDataState,
onNavigateToExercise = onNavigateToExercise,
Expand Down Expand Up @@ -176,6 +184,7 @@ internal fun CourseUiScreen(
courseId: Long,
conversationId: Long,
postId: Long,
username: String,
courseDataState: DataState<Course>,
weeklyExercisesDataState: DataState<List<GroupedByWeek<Exercise>>>,
weeklyLecturesDataState: DataState<List<GroupedByWeek<Lecture>>>,
Expand All @@ -191,7 +200,7 @@ internal fun CourseUiScreen(
) {
var selectedTabIndex by rememberSaveable(conversationId) {
val initialTab = when {
conversationId != DEFAULT_CONVERSATION_ID -> TAB_COMMUNICATION
conversationId != DEFAULT_CONVERSATION_ID || username.isNotBlank() -> TAB_COMMUNICATION
else -> TAB_EXERCISES
}

Expand Down Expand Up @@ -262,6 +271,8 @@ internal fun CourseUiScreen(
null
)

username.isNotBlank() -> NavigateToUserConversation(username)

else -> NothingOpened
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ abstract class BaseCourseTest : KoinTest {
courseId = course.id!!,
conversationId = DEFAULT_CONVERSATION_ID,
postId = DEFAULT_POST_ID,
username = "",
onNavigateToExercise = {},
onNavigateToExerciseResultView = {},
onNavigateToTextExerciseParticipation = { _, _ -> },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ internal fun <T> CodeOfConductDataStateUi(
BasicDataStateUi(
modifier = modifier,
dataState = dataState,
loadingText = stringResource(id = R.string.code_of_conduct_failure),
loadingText = stringResource(id = R.string.code_of_conduct_loading),
failureText = stringResource(id = R.string.code_of_conduct_failure),
retryButtonText = stringResource(id = R.string.code_of_conduct_try_again),
onClickRetry = onClickRetry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ internal fun MetisChatList(
val clientId: Long by viewModel.clientIdOrDefault.collectAsState()
val hasModerationRights by viewModel.hasModerationRights.collectAsState()

val serverUrl by viewModel.serverUrl.collectAsState()

MetisChatList(
modifier = modifier,
initialReplyTextProvider = viewModel,
Expand All @@ -86,6 +88,8 @@ internal fun MetisChatList(
clientId = clientId,
hasModerationRights = hasModerationRights,
listContentPadding = listContentPadding,
serverUrl = serverUrl,
courseId = metisContext.courseId,
onCreatePost = viewModel::createPost,
onEditPost = viewModel::editPost,
onDeletePost = viewModel::deletePost,
Expand All @@ -104,6 +108,8 @@ fun MetisChatList(
clientId: Long,
hasModerationRights: Boolean,
listContentPadding: PaddingValues,
serverUrl: String,
courseId: Long,
state: LazyListState,
isReplyEnabled: Boolean,
onCreatePost: () -> Deferred<MetisModificationFailure?>,
Expand Down Expand Up @@ -135,6 +141,8 @@ fun MetisChatList(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
serverUrl = serverUrl,
courseId = courseId,
state = state,
itemCount = posts.itemCount,
order = DisplayPostOrder.REVERSED,
Expand All @@ -159,24 +167,20 @@ fun MetisChatList(
}

is PostsDataState.Loaded -> {
ProvideMarkwon {
ProvideEmojis {
ChatList(
modifier = Modifier
.fillMaxSize()
.testTag(TEST_TAG_METIS_POST_LIST),
listContentPadding = listContentPadding,
state = state,
posts = posts,
clientId = clientId,
onClickViewPost = onClickViewPost,
hasModerationRights = hasModerationRights,
onRequestEdit = onEditPostDelegate,
onRequestDelete = onDeletePostDelegate,
onRequestReactWithEmoji = onRequestReactWithEmojiDelegate
)
}
}
ChatList(
modifier = Modifier
.fillMaxSize()
.testTag(TEST_TAG_METIS_POST_LIST),
listContentPadding = listContentPadding,
state = state,
posts = posts,
clientId = clientId,
onClickViewPost = onClickViewPost,
hasModerationRights = hasModerationRights,
onRequestEdit = onEditPostDelegate,
onRequestDelete = onDeletePostDelegate,
onRequestReactWithEmoji = onRequestReactWithEmojiDelegate
)
}
}
}
Expand Down
Loading

0 comments on commit bf16020

Please sign in to comment.