diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ArtemisApplication.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ArtemisApplication.kt index 66e56a3da..022cccecc 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ArtemisApplication.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ArtemisApplication.kt @@ -5,10 +5,12 @@ import android.app.Application import android.app.NotificationChannel import android.os.Bundle import androidx.core.app.NotificationManagerCompat -import coil.ImageLoader -import coil.ImageLoaderFactory -import coil.memory.MemoryCache +import coil3.ImageLoader +import coil3.PlatformContext +import coil3.SingletonImageLoader +import coil3.memory.MemoryCache import de.tum.informatics.www1.artemis.native_app.android.db.dbModule +import de.tum.informatics.www1.artemis.native_app.core.common.ArtemisNotificationChannel import de.tum.informatics.www1.artemis.native_app.core.common.CurrentActivityListener import de.tum.informatics.www1.artemis.native_app.core.data.dataModule import de.tum.informatics.www1.artemis.native_app.core.datastore.datastoreModule @@ -22,7 +24,6 @@ import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.exerciseM import de.tum.informatics.www1.artemis.native_app.feature.lectureview.lectureModule import de.tum.informatics.www1.artemis.native_app.feature.login.loginModule import de.tum.informatics.www1.artemis.native_app.feature.metis.communicationModule -import de.tum.informatics.www1.artemis.native_app.core.common.ArtemisNotificationChannel import de.tum.informatics.www1.artemis.native_app.feature.push.pushModule import de.tum.informatics.www1.artemis.native_app.feature.quiz.quizParticipationModule import de.tum.informatics.www1.artemis.native_app.feature.settings.settingsModule @@ -32,7 +33,7 @@ import org.koin.android.ext.koin.androidContext import org.koin.androidx.workmanager.koin.workManagerFactory import org.koin.core.context.startKoin -class ArtemisApplication : Application(), ImageLoaderFactory, CurrentActivityListener { +class ArtemisApplication : Application(), SingletonImageLoader.Factory, CurrentActivityListener { override val currentActivity = MutableStateFlow(null) @@ -84,11 +85,11 @@ class ArtemisApplication : Application(), ImageLoaderFactory, CurrentActivityLis registerActivityLifecycleCallbacks(this) } - override fun newImageLoader(): ImageLoader = + override fun newImageLoader(context: PlatformContext): ImageLoader = ImageLoader.Builder(this) .memoryCache { - MemoryCache.Builder(this) - .maxSizePercent(0.25) + MemoryCache.Builder() + .maxSizePercent(context, 0.25) .build() } .build() diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt index 8961e79ee..6edf96c55 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt @@ -29,7 +29,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.push.communication_not CommunicationMessageEntity::class ], exportSchema = true, - version = 11, + version = 12, ) @TypeConverters(RoomTypeConverters::class) abstract class AppDatabase : RoomDatabase() { diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt index 7954206ce..73fcfdb14 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/MainActivity.kt @@ -36,6 +36,7 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.LocalLinkOpener import de.tum.informatics.www1.artemis.native_app.core.ui.LocalWindowSizeClassProvider import de.tum.informatics.www1.artemis.native_app.core.ui.WindowSizeClassProvider import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.courseRegistration import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.navigateToCourseRegistration import de.tum.informatics.www1.artemis.native_app.feature.courseview.ui.course_overview.course @@ -53,7 +54,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.lectureview.navigateTo import de.tum.informatics.www1.artemis.native_app.feature.login.LoginScreen import de.tum.informatics.www1.artemis.native_app.feature.login.loginScreen import de.tum.informatics.www1.artemis.native_app.feature.login.navigateToLogin -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.ProvideLocalVisibleMetisContextManager +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.LocalVisibleMetisContextManager 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.VisibleMetisContextManager import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.VisibleMetisContextReporter @@ -73,6 +74,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import org.koin.android.ext.android.get +import org.koin.compose.koinInject /** * Main and only activity used in the android app. @@ -134,8 +136,8 @@ class MainActivity : AppCompatActivity(), setContent { AppTheme { - ProvideLocalVisibleMetisContextManager( - visibleMetisContextManager = visibleMetisContextManager + CompositionLocalProvider( + LocalVisibleMetisContextManager provides visibleMetisContextManager, ) { val navController = rememberNavController() @@ -251,7 +253,8 @@ class MainActivity : AppCompatActivity(), CompositionLocalProvider( LocalWindowSizeClassProvider provides windowSizeClassProvider, - LocalLinkOpener provides linkOpener + LocalLinkOpener provides linkOpener, + LocalArtemisImageProvider provides koinInject() ) { // Use jetpack compose navigation for the navigation logic. NavHost(navController = navController, startDestination = startDestination) { diff --git a/core/core-test/build.gradle.kts b/core/core-test/build.gradle.kts index 4925e98c6..93f3b2f28 100644 --- a/core/core-test/build.gradle.kts +++ b/core/core-test/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(project(":core:common")) api(project(":core:common-test")) implementation(project(":core:ui")) + api(project(":core:ui-test")) implementation(project(":core:datastore")) implementation(project(":core:device")) api(project(":core:device-test")) diff --git a/core/core-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/test/core_test_modules.kt b/core/core-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/test/core_test_modules.kt index 75ccce40a..02a095e79 100644 --- a/core/core-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/test/core_test_modules.kt +++ b/core/core-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/test/core_test_modules.kt @@ -5,7 +5,7 @@ import de.tum.informatics.www1.artemis.native_app.core.data.test.testDataModule import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService import de.tum.informatics.www1.artemis.native_app.core.datastore.datastoreModule import de.tum.informatics.www1.artemis.native_app.core.test.test_setup.TestServerConfigurationProvider -import de.tum.informatics.www1.artemis.native_app.core.ui.uiModule +import de.tum.informatics.www1.artemis.native_app.core.ui.test.uiTestModule import de.tum.informatics.www1.artemis.native_app.core.websocket.websocketModule import de.tum.informatics.www1.artemis.native_app.device.test.deviceTestModule import org.koin.dsl.module @@ -15,7 +15,7 @@ val coreTestModules = listOf( testDataModule, datastoreModule, deviceTestModule, - uiModule, + uiTestModule, websocketModule, module { single { TestServerConfigurationProvider() } diff --git a/core/ui-test/build.gradle.kts b/core/ui-test/build.gradle.kts new file mode 100644 index 000000000..a198e3728 --- /dev/null +++ b/core/ui-test/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("artemis.android.library") + id("artemis.android.library.compose") + id("artemis.android.flavor.library.instanceSelection") +} + +android { + namespace = "de.tum.informatics.www1.artemis.native_app.core.ui.test" +} + +dependencies { + implementation(project(":core:ui")) + + implementation(libs.koin.core) + implementation(libs.kotlinx.coroutines.core) + + implementation(libs.coil.test) +} diff --git a/core/ui-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/test/ArtemisImageProviderStub.kt b/core/ui-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/test/ArtemisImageProviderStub.kt new file mode 100644 index 000000000..07f5f0ae6 --- /dev/null +++ b/core/ui-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/test/ArtemisImageProviderStub.kt @@ -0,0 +1,42 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.test + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import coil3.ImageLoader +import coil3.SingletonImageLoader +import coil3.annotation.DelicateCoilApi +import coil3.request.ImageRequest +import coil3.test.FakeImage +import coil3.test.FakeImageLoaderEngine +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.ArtemisImageProvider + + +@OptIn(DelicateCoilApi::class) +class ArtemisImageProviderStub : ArtemisImageProvider { + + companion object { + fun setup(context: Context) { + // For more info on testing coil, see: https://coil-kt.github.io/coil/testing/ + val engine = FakeImageLoaderEngine.Builder() + .default(FakeImage(color = 0x00F)) + .build() + val imageLoader = ImageLoader.Builder(context) + .components { add(engine) } + .build() + SingletonImageLoader.setUnsafe(imageLoader) + } + } + + @Composable + override fun rememberArtemisImageRequest(imagePath: String): ImageRequest { + return ImageRequest.Builder(LocalContext.current) + .data(imagePath) + .build() + } + + @Composable + override fun rememberArtemisImageLoader(): coil.ImageLoader { + return coil.ImageLoader(LocalContext.current) + } +} \ No newline at end of file diff --git a/core/ui-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/test/ui_test_module.kt b/core/ui-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/test/ui_test_module.kt new file mode 100644 index 000000000..d3522bb07 --- /dev/null +++ b/core/ui-test/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/test/ui_test_module.kt @@ -0,0 +1,8 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.test + +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.ArtemisImageProvider +import org.koin.dsl.module + +val uiTestModule = module { + single { ArtemisImageProviderStub() } +} \ No newline at end of file diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 0fda6922a..6e3c5708a 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -26,7 +26,11 @@ dependencies { api(libs.androidx.compose.ui.util) api(libs.androidx.compose.runtime) api(libs.androidx.navigation.compose) + api(libs.coil2.base) api(libs.coil.compose) + api(libs.coil.network) + debugApi(libs.coil.compose.core) + debugApi(libs.coil.test) api(libs.accompanist.webview) implementation(libs.kotlinx.datetime) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/course/CourseListUi.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/course/CourseListUi.kt index 4bb14fe0c..40b70ce33 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/course/CourseListUi.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/course/CourseListUi.kt @@ -10,19 +10,12 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeContent -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.grid.GridCells @@ -56,11 +49,11 @@ import androidx.compose.ui.unit.sp import androidx.core.graphics.toColorInt import de.tum.informatics.www1.artemis.native_app.core.model.Course import de.tum.informatics.www1.artemis.native_app.core.model.CourseWithScore -import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalCourseImageProvider import de.tum.informatics.www1.artemis.native_app.core.ui.R import de.tum.informatics.www1.artemis.native_app.core.ui.common.AutoResizeText import de.tum.informatics.www1.artemis.native_app.core.ui.common.FontSizeRange import de.tum.informatics.www1.artemis.native_app.core.ui.getWindowSizeClass +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider private val headerHeight = 80.dp @@ -107,13 +100,11 @@ fun Modifier.computeCourseItemModifier(isCompact: Boolean): Modifier { fun CompactCourseItemHeader( modifier: Modifier, course: Course, - serverUrl: String, - authorizationToken: String, compactCourseHeaderViewMode: CompactCourseHeaderViewMode, onClick: () -> Unit = {}, content: @Composable ColumnScope.() -> Unit ) { - val painter = getCourseIconPainter(course, serverUrl, authorizationToken) + val painter = getCourseIconPainter(course) Card(modifier = modifier, onClick = onClick) { Column( @@ -180,14 +171,10 @@ fun CompactCourseItemHeader( @Composable private fun getCourseIconPainter( course: Course, - serverUrl: String, - authorizationToken: String ): Painter { return if (course.courseIconPath != null) { - LocalCourseImageProvider.current.rememberCourseImagePainter( - courseIconPath = course.courseIconPath.orEmpty(), - serverUrl = serverUrl, - authorizationToken = authorizationToken + LocalArtemisImageProvider.current.rememberArtemisAsyncImagePainter( + imagePath = course.courseIconPath.orEmpty() ) } else rememberVectorPainter(image = Icons.Default.QuestionMark) } @@ -196,13 +183,11 @@ private fun getCourseIconPainter( fun ExpandedCourseItemHeader( modifier: Modifier, course: Course, - serverUrl: String, - authorizationToken: String, onClick: () -> Unit = {}, content: @Composable ColumnScope.() -> Unit, rightHeaderContent: @Composable BoxScope.() -> Unit ) { - val courseIconPainter = getCourseIconPainter(course, serverUrl, authorizationToken) + val courseIconPainter = getCourseIconPainter(course) val courseColor: Color? = remember { try { diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/image/RetryableAsyncImage.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/image/RetryableAsyncImage.kt index e5ee58a01..4170746e0 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/image/RetryableAsyncImage.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/image/RetryableAsyncImage.kt @@ -1,6 +1,5 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.common.image -import android.graphics.drawable.Drawable import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf @@ -9,16 +8,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext -import coil.imageLoader -import coil.request.ErrorResult -import coil.request.ImageRequest -import coil.request.SuccessResult +import coil3.Image +import coil3.imageLoader +import coil3.request.ErrorResult +import coil3.request.ImageRequest +import coil3.request.SuccessResult import de.tum.informatics.www1.artemis.native_app.core.data.DataState @Composable fun loadAsyncImageDrawable(request: ImageRequest): AsyncImageDrawableResult { val context = LocalContext.current - var dataState: DataState by remember(request) { mutableStateOf(DataState.Loading()) } + var dataState: DataState by remember(request) { mutableStateOf(DataState.Loading()) } // We simply increase this counter to trigger a reload var reloadCounter by remember(request) { mutableStateOf(0) } @@ -31,7 +31,7 @@ fun loadAsyncImageDrawable(request: ImageRequest): AsyncImageDrawableResult { } is SuccessResult -> { - DataState.Success(result.drawable) + DataState.Success(result.image) } } } @@ -48,6 +48,6 @@ fun loadAsyncImageDrawable(request: ImageRequest): AsyncImageDrawableResult { } data class AsyncImageDrawableResult( - val dataState: DataState, + val dataState: DataState, val requestRetry: () -> Unit ) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/text_unit_util.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/text_unit_util.kt new file mode 100644 index 000000000..ed0e9c84a --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/text_unit_util.kt @@ -0,0 +1,10 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.common + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp + +val TextUnit.nonScaledSp + @Composable + get() = (this.value / LocalDensity.current.fontScale).sp \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/LocalMarkwon.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/LocalMarkwon.kt index ec5e2efd3..4888cacf0 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/LocalMarkwon.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/markdown/LocalMarkwon.kt @@ -6,14 +6,15 @@ import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import coil.ImageLoader +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider import io.noties.markwon.Markwon val LocalMarkwon: ProvidableCompositionLocal = compositionLocalOf { null } @Composable -fun ProvideMarkwon(imageLoader: ImageLoader? = null, content: @Composable () -> Unit) { +fun ProvideMarkwon(content: @Composable () -> Unit) { + val imageLoader = LocalArtemisImageProvider.current.rememberArtemisImageLoader() val context = LocalContext.current val imageWith = context.resources.displayMetrics.widthPixels diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/ArtemisImageProvider.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/ArtemisImageProvider.kt new file mode 100644 index 000000000..3068a7c3e --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/ArtemisImageProvider.kt @@ -0,0 +1,32 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.remote_images + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import coil.ImageLoader +import coil3.compose.AsyncImagePainter +import coil3.compose.rememberAsyncImagePainter +import coil3.request.ImageRequest + + +val LocalArtemisImageProvider = compositionLocalOf { error("No ArtemisImageProvider provided") } + +/** + * Provides a way to load images from the Artemis server. This interface implementation takes care + * of authentication and the applicable Artemis server URL. + */ +interface ArtemisImageProvider { + + @Composable + fun rememberArtemisImageRequest( + imagePath: String, + ): ImageRequest + + @Composable + fun rememberArtemisAsyncImagePainter( + imagePath: String, + ): AsyncImagePainter = rememberAsyncImagePainter(model = rememberArtemisImageRequest(imagePath)) + + @Composable + fun rememberArtemisImageLoader() : ImageLoader +} + diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/BaseImageProvider.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/BaseImageProvider.kt index 65ee05731..6f8d3fe97 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/BaseImageProvider.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/BaseImageProvider.kt @@ -2,13 +2,12 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.remote_images import android.content.Context import coil.ImageLoader -import coil.request.ImageRequest +import coil3.request.ImageRequest interface BaseImageProvider { fun createImageRequest( context: Context, - imagePath: String, - serverUrl: String, + imageUrl: String, authorizationToken: String, memoryCacheKey: String? = null ): ImageRequest diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/CourseImageProvider.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/CourseImageProvider.kt deleted file mode 100644 index 6f7951dc3..000000000 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/CourseImageProvider.kt +++ /dev/null @@ -1,36 +0,0 @@ -package de.tum.informatics.www1.artemis.native_app.core.ui.remote_images - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.LocalContext -import coil.compose.rememberAsyncImagePainter - -val LocalCourseImageProvider = compositionLocalOf { DefaultCourseImageProvider } - -interface CourseImageProvider { - @Composable - fun rememberCourseImagePainter( - courseIconPath: String, - serverUrl: String, - authorizationToken: String - ): Painter -} - -private object DefaultCourseImageProvider : CourseImageProvider { - private val imageProvider = DefaultImageProvider() - - @Composable - override fun rememberCourseImagePainter( - courseIconPath: String, - serverUrl: String, - authorizationToken: String - ): Painter { - val context = LocalContext.current - val imageRequest = remember { - imageProvider.createImageRequest(context, courseIconPath, serverUrl, authorizationToken) - } - return rememberAsyncImagePainter(model = imageRequest) - } -} diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/impl/ArtemisImageProviderImpl.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/impl/ArtemisImageProviderImpl.kt new file mode 100644 index 000000000..df5cf1a6d --- /dev/null +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/impl/ArtemisImageProviderImpl.kt @@ -0,0 +1,51 @@ +package de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import coil.ImageLoader +import coil3.request.ImageRequest +import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService +import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService +import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.ArtemisImageProvider +import io.ktor.http.URLBuilder +import io.ktor.http.appendPathSegments + +class ArtemisImageProviderImpl( + private val accountService: AccountService, + private val serverConfigurationService: ServerConfigurationService, +) : ArtemisImageProvider { + + private val imageProvider = BaseImageProviderImpl() + + @Composable + override fun rememberArtemisImageRequest(imagePath: String): ImageRequest { + val serverUrl by serverConfigurationService.serverUrl.collectAsState(initial = "") + val authToken by accountService.authToken.collectAsState(initial = "") + + val imageUrl = URLBuilder(serverUrl).appendPathSegments(imagePath).buildString() + val context = LocalContext.current + + return remember(imageUrl, authToken) { + imageProvider.createImageRequest( + context = context, + imageUrl = imageUrl, + authorizationToken = authToken, + memoryCacheKey = serverUrl + imagePath + ) + } + } + + @Composable + override fun rememberArtemisImageLoader(): ImageLoader { + val authorizationToken by accountService.authToken.collectAsState(initial = "") + val context = LocalContext.current + + return remember(authorizationToken) { + imageProvider.createImageLoader(context, authorizationToken) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/DefaultImageProvider.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/impl/BaseImageProviderImpl.kt similarity index 71% rename from core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/DefaultImageProvider.kt rename to core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/impl/BaseImageProviderImpl.kt index c581e33ae..dff8543cd 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/DefaultImageProvider.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/remote_images/impl/BaseImageProviderImpl.kt @@ -1,24 +1,26 @@ -package de.tum.informatics.www1.artemis.native_app.core.ui.remote_images +package de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.impl import android.content.Context import coil.ImageLoader -import coil.request.ImageRequest +import coil3.network.NetworkHeaders +import coil3.network.httpHeaders +import coil3.request.ImageRequest +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.BaseImageProvider import io.ktor.http.HttpHeaders -import io.ktor.http.URLBuilder -import io.ktor.http.appendPathSegments -class DefaultImageProvider : BaseImageProvider { +class BaseImageProviderImpl : BaseImageProvider { override fun createImageRequest( context: Context, - imagePath: String, - serverUrl: String, + imageUrl: String, authorizationToken: String, memoryCacheKey: String? ): ImageRequest { - val imageUrl = URLBuilder(serverUrl).appendPathSegments(imagePath).buildString() + val headers = NetworkHeaders.Builder() + .set(HttpHeaders.Cookie, "jwt=$authorizationToken") + .build() val builder = ImageRequest.Builder(context) - .addHeader(HttpHeaders.Cookie, "jwt=$authorizationToken") + .httpHeaders(headers) .data(imageUrl) memoryCacheKey?.let { diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/ui_module.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/ui_module.kt index 4a351597b..8ad2090fa 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/ui_module.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/ui_module.kt @@ -1,6 +1,9 @@ package de.tum.informatics.www1.artemis.native_app.core.ui +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.ArtemisImageProvider +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.impl.ArtemisImageProviderImpl import org.koin.dsl.module val uiModule = module { + single { ArtemisImageProviderImpl(get(), get()) } } \ No newline at end of file diff --git a/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt b/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt index d789f7f70..bc23f247f 100644 --- a/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt +++ b/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseUi.kt @@ -18,10 +18,10 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -104,12 +104,6 @@ internal fun RegisterForCourseScreen( var signUpCandidate: Course? by remember { mutableStateOf(null) } var displayRegistrationFailedDialog: Boolean by rememberSaveable { mutableStateOf(false) } - val authToken by viewModel.authToken.collectAsState() - val serverUrl by viewModel.serverUrl.collectAsState() - - // CourseHeader requires url without trailing / - val properServerUrl = remember(serverUrl) { serverUrl.dropLast(1) } - val topAppBarState = rememberTopAppBarState() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( @@ -137,7 +131,7 @@ internal fun RegisterForCourseScreen( scrollBehavior = scrollBehavior, navigationIcon = { IconButton(onClick = onNavigateUp) { - Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null) + Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } } ) @@ -150,8 +144,6 @@ internal fun RegisterForCourseScreen( .consumeWindowInsets(WindowInsets.systemBars) .padding(horizontal = 8.dp), courses = courses, - serverUrl = properServerUrl, - authToken = authToken, reloadCourses = viewModel::reloadRegistrableCourses, onClickSignup = { course -> signUpCandidate = course @@ -204,8 +196,6 @@ internal fun RegisterForCourseScreen( private fun RegisterForCourseContent( modifier: Modifier, courses: DataState>, - serverUrl: String, - authToken: String, reloadCourses: () -> Unit, onClickSignup: (Course) -> Unit ) { @@ -258,8 +248,6 @@ private fun RegisterForCourseContent( RegistrableCourse( modifier = courseItemModifier.testTag(testTagForRegistrableCourse(course.id ?: 0L)), course = course, - serverUrl = serverUrl, - authToken = authToken, onClickSignup = { onClickSignup(course) }, isCompact = isCompact ) @@ -273,13 +261,11 @@ private fun RegisterForCourseContent( private fun RegistrableCourse( modifier: Modifier, course: Course, - serverUrl: String, - authToken: String, isCompact: Boolean, onClickSignup: () -> Unit ) { val content: @Composable ColumnScope.() -> Unit = @Composable { - Divider() + HorizontalDivider() Row( modifier = Modifier @@ -301,8 +287,6 @@ private fun RegistrableCourse( CompactCourseItemHeader( modifier = modifier, course = course, - serverUrl = serverUrl, - authorizationToken = authToken, content = content, compactCourseHeaderViewMode = CompactCourseHeaderViewMode.DESCRIPTION ) @@ -310,8 +294,6 @@ private fun RegistrableCourse( ExpandedCourseItemHeader( modifier = modifier, course = course, - serverUrl = serverUrl, - authorizationToken = authToken, content = { Text( modifier = Modifier diff --git a/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseViewModel.kt b/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseViewModel.kt index 622bb0a17..d8b9b0908 100644 --- a/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseViewModel.kt +++ b/feature/course-registration/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseregistration/RegisterForCourseViewModel.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.viewModelScope import de.tum.informatics.www1.artemis.native_app.core.common.flatMapLatest import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.data.retryOnInternet -import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.service.CourseRegistrationService import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken @@ -13,6 +12,7 @@ import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvi import de.tum.informatics.www1.artemis.native_app.core.model.Course import de.tum.informatics.www1.artemis.native_app.core.ui.authTokenStateFlow import de.tum.informatics.www1.artemis.native_app.core.ui.serverUrlStateFlow +import de.tum.informatics.www1.artemis.native_app.feature.courseregistration.service.CourseRegistrationService import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow @@ -29,9 +29,9 @@ import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext class RegisterForCourseViewModel( - private val accountService: AccountService, + accountService: AccountService, private val courseRegistrationService: CourseRegistrationService, - private val serverConfigurationService: ServerConfigurationService, + serverConfigurationService: ServerConfigurationService, networkStatusProvider: NetworkStatusProvider, private val coroutineContext: CoroutineContext = EmptyCoroutineContext ) : ViewModel() { @@ -60,8 +60,8 @@ class RegisterForCourseViewModel( } .stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, initialValue = DataState.Loading()) - val authToken: StateFlow = authTokenStateFlow(accountService) - val serverUrl: StateFlow = serverUrlStateFlow(serverConfigurationService) + private val authToken: StateFlow = authTokenStateFlow(accountService) + private val serverUrl: StateFlow = serverUrlStateFlow(serverConfigurationService) fun reloadRegistrableCourses() { viewModelScope.launch(coroutineContext) { diff --git a/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/CourseViewScreenshots.kt b/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/CourseViewScreenshots.kt index 3e8f8fdb1..62a2fb206 100644 --- a/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/CourseViewScreenshots.kt +++ b/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/CourseViewScreenshots.kt @@ -3,6 +3,7 @@ package de.tum.informatics.www1.artemis.native_app.feature.courseview import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import de.tum.informatics.www1.artemis.native_app.core.data.CourseServiceFake import de.tum.informatics.www1.artemis.native_app.core.data.NetworkResponse import de.tum.informatics.www1.artemis.native_app.core.data.service.network.CourseExerciseService @@ -22,6 +23,7 @@ import org.koin.core.context.startKoin import org.koin.dsl.module @PlayStoreScreenshots +@Preview @Composable fun `Course View - Exercise List`() { startKoin { diff --git a/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/MessagingScreenshots.kt b/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/MessagingScreenshots.kt index 05ede95c2..a08f5fdde 100644 --- a/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/MessagingScreenshots.kt +++ b/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/MessagingScreenshots.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -37,7 +38,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.db.pojo.PostPojo import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.ProvideLocalVisibleMetisContextManager +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.LocalVisibleMetisContextManager 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.VisibleMetisContextManager import kotlinx.coroutines.CompletableDeferred @@ -195,6 +196,15 @@ fun `Metis - Conversation Channel`() { ), ).reversed() + val visibleMetisContextManagerStub = object : VisibleMetisContextManager { + override fun registerMetisContext(metisContext: VisibleMetisContext) = + Unit + + override fun unregisterMetisContext(metisContext: VisibleMetisContext) = + Unit + } + + // TODO: Provide artemis image provider ScreenshotFrame(title = "Send and receive messages directly from the app") { CourseUiScreen( modifier = Modifier.fillMaxSize(), @@ -204,14 +214,8 @@ fun `Metis - Conversation Channel`() { exerciseTabContent = { }, lectureTabContent = { }, communicationTabContent = { - ProvideLocalVisibleMetisContextManager( - visibleMetisContextManager = object : VisibleMetisContextManager { - override fun registerMetisContext(metisContext: VisibleMetisContext) = - Unit - - override fun unregisterMetisContext(metisContext: VisibleMetisContext) = - Unit - } + CompositionLocalProvider( + LocalVisibleMetisContextManager provides visibleMetisContextManagerStub, ) { ConversationChatListScreen( modifier = Modifier.fillMaxSize(), @@ -284,6 +288,7 @@ private fun generateMessage( authorName = name, authorRole = UserRole.USER, authorId = authorId, + authorImageUrl = null, creationDate = time, updatedDate = null, resolved = false, diff --git a/feature/dashboard/build.gradle.kts b/feature/dashboard/build.gradle.kts index 28440dc30..d40b32054 100644 --- a/feature/dashboard/build.gradle.kts +++ b/feature/dashboard/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(libs.androidx.dataStore.preferences) implementation(libs.accompanist.swiperefresh) + debugImplementation(project(":core:ui-test")) testImplementation(project(":feature:login")) testImplementation(project(":feature:login-test")) } \ No newline at end of file diff --git a/feature/dashboard/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardScreenshots.kt b/feature/dashboard/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardScreenshots.kt index 8897d1892..59a008d0b 100644 --- a/feature/dashboard/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardScreenshots.kt +++ b/feature/dashboard/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/DashboardScreenshots.kt @@ -5,8 +5,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.res.imageResource +import coil3.annotation.ExperimentalCoilApi +import coil3.asImage +import coil3.compose.AsyncImagePreviewHandler +import coil3.compose.LocalAsyncImagePreviewHandler import de.tum.informatics.www1.artemis.native_app.core.data.NetworkResponse import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountServiceStub import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationServiceStub @@ -16,10 +21,10 @@ import de.tum.informatics.www1.artemis.native_app.core.model.CourseWithScore import de.tum.informatics.www1.artemis.native_app.core.model.Dashboard import de.tum.informatics.www1.artemis.native_app.core.model.exercise.TextExercise import de.tum.informatics.www1.artemis.native_app.core.model.lecture.Lecture -import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.CourseImageProvider -import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalCourseImageProvider import de.tum.informatics.www1.artemis.native_app.core.ui.PlayStoreScreenshots import de.tum.informatics.www1.artemis.native_app.core.ui.ScreenshotFrame +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider +import de.tum.informatics.www1.artemis.native_app.core.ui.test.ArtemisImageProviderStub import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.DashboardService import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CourseOverviewViewModel import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CoursesOverview @@ -27,6 +32,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.dashboard.ui.CoursesOv private const val IMAGE_MARS = "mars" private const val IMAGE_SATURN_5 = "saturn5" +@OptIn(ExperimentalCoilApi::class) @PlayStoreScreenshots @Composable fun `Dashboard - Exercise List`() { @@ -87,26 +93,20 @@ fun `Dashboard - Exercise List`() { val betaHintService = remember { BetaHintServiceFake() } - val fakeCourseImageProvider = remember { - object : CourseImageProvider { - @Composable - override fun rememberCourseImagePainter( - courseIconPath: String, - serverUrl: String, - authorizationToken: String - ): Painter { - return painterResource( - id = when (courseIconPath) { - IMAGE_MARS -> R.drawable.mars - else -> R.drawable.saturn5 - } - ) - } - + val marsImage = ImageBitmap.imageResource(R.drawable.mars).asAndroidBitmap().asImage() + val saturnImage = ImageBitmap.imageResource(R.drawable.saturn5).asAndroidBitmap().asImage() + val previewHandler = AsyncImagePreviewHandler { request -> + when (request.data) { + IMAGE_MARS -> marsImage + IMAGE_SATURN_5 -> saturnImage + else -> null } } - CompositionLocalProvider(LocalCourseImageProvider provides fakeCourseImageProvider) { + CompositionLocalProvider( + LocalArtemisImageProvider provides ArtemisImageProviderStub(), + LocalAsyncImagePreviewHandler provides previewHandler + ) { ScreenshotFrame(title = "Manage all of your courses in one app") { CoursesOverview( modifier = Modifier.fillMaxSize(), diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseList.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseList.kt index 4314375fd..1c67dae76 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseList.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseList.kt @@ -10,7 +10,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -46,8 +46,6 @@ import java.text.DecimalFormat fun CourseList( modifier: Modifier, courses: List, - serverUrl: String, - authorizationToken: String, onClickOnCourse: (Course) -> Unit ) { CourseItemGrid( @@ -57,8 +55,6 @@ fun CourseList( CourseItem( modifier = courseItemModifier.testTag(testTagForCourse(dashboardCourse.course.id!!)), courseWithScore = dashboardCourse, - serverUrl = serverUrl, - authorizationToken = authorizationToken, onClick = { onClickOnCourse(dashboardCourse.course) }, isCompact = isCompact ) @@ -73,8 +69,6 @@ fun CourseItem( modifier: Modifier, isCompact: Boolean, courseWithScore: CourseWithScore, - serverUrl: String, - authorizationToken: String, onClick: () -> Unit ) { val currentPoints = courseWithScore.totalScores.studentScores.absoluteScore @@ -97,12 +91,10 @@ fun CourseItem( CompactCourseItemHeader( modifier = modifier, course = courseWithScore.course, - serverUrl = serverUrl, - authorizationToken = authorizationToken, onClick = onClick, compactCourseHeaderViewMode = CompactCourseHeaderViewMode.EXERCISE_AND_LECTURE_COUNT, content = { - Divider() + HorizontalDivider() Row( modifier = Modifier @@ -112,9 +104,9 @@ fun CourseItem( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { LinearProgressIndicator( + progress = { progress }, modifier = Modifier.weight(1f), - progress = progress, - trackColor = MaterialTheme.colorScheme.onPrimary + trackColor = MaterialTheme.colorScheme.onPrimary, ) CourseProgressText( @@ -130,8 +122,6 @@ fun CourseItem( ExpandedCourseItemHeader( modifier = modifier, course = courseWithScore.course, - serverUrl = serverUrl, - authorizationToken = authorizationToken, onClick = onClick, content = { Box( diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseOverviewViewModel.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseOverviewViewModel.kt index 9d0948f6e..cfca0def8 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseOverviewViewModel.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CourseOverviewViewModel.kt @@ -10,12 +10,10 @@ 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.Dashboard -import de.tum.informatics.www1.artemis.native_app.core.ui.authTokenStateFlow import de.tum.informatics.www1.artemis.native_app.feature.dashboard.service.DashboardService import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus @@ -27,7 +25,7 @@ import kotlin.coroutines.EmptyCoroutineContext */ internal class CourseOverviewViewModel( private val dashboardService: DashboardService, - private val accountService: AccountService, + accountService: AccountService, serverConfigurationService: ServerConfigurationService, networkStatusProvider: NetworkStatusProvider, coroutineContext: CoroutineContext = EmptyCoroutineContext @@ -60,18 +58,6 @@ internal class CourseOverviewViewModel( //Store the loaded dashboard, so it is not loaded again when somebody collects this flow. .stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, DataState.Loading()) - /** - * The client needs access to this url, to load the course icon. - * The serverUrl comes without a trailing / - */ - val serverUrl: StateFlow = serverConfigurationService.serverUrl - .map { it.dropLast(1) } //Remove the / - .stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, "") - - /** - * Emits the current authentication bearer in the form: "Bearer $token" - */ - val authToken: StateFlow = authTokenStateFlow(accountService) /** * Request a reload of the dashboard. diff --git a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt index 8bd4bfa86..4fa5adcae 100644 --- a/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt +++ b/feature/dashboard/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/dashboard/ui/CoursesOverview.kt @@ -99,11 +99,6 @@ internal fun CoursesOverview( ) { val coursesDataState by viewModel.dashboard.collectAsState() - //The course composable needs the serverUrl to build the correct url to fetch the course icon from. - val serverUrl by viewModel.serverUrl.collectAsState() - //The server wants an authorization token to send the course icon. - val authToken by viewModel.authToken.collectAsState() - val topAppBarState = rememberTopAppBarState() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( @@ -201,8 +196,6 @@ internal fun CoursesOverview( .padding(horizontal = 8.dp) .testTag(TEST_TAG_COURSE_LIST), courses = dashboard.courses, - serverUrl = serverUrl, - authorizationToken = authToken, onClickOnCourse = { course -> onViewCourse(course.id ?: 0L) } ) } diff --git a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/instance_selection/InstanceSelectionScreen.kt b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/instance_selection/InstanceSelectionScreen.kt index 0342ff367..21560bdbf 100644 --- a/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/instance_selection/InstanceSelectionScreen.kt +++ b/feature/login/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/login/instance_selection/InstanceSelectionScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -33,8 +32,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import coil.request.ImageRequest +import coil3.compose.AsyncImage +import coil3.request.ImageRequest import de.tum.informatics.www1.artemis.native_app.core.datastore.defaults.ArtemisInstances import de.tum.informatics.www1.artemis.native_app.core.ui.getWindowSizeClass import de.tum.informatics.www1.artemis.native_app.feature.login.ArtemisHeader diff --git a/feature/metis/conversation/build.gradle.kts b/feature/metis/conversation/build.gradle.kts index c83ed2e88..2a9b531a2 100644 --- a/feature/metis/conversation/build.gradle.kts +++ b/feature/metis/conversation/build.gradle.kts @@ -18,6 +18,7 @@ android { dependencies { implementation(project(":core:device")) + testImplementation(project(":core:ui-test")) implementation(project(":feature:metis:shared")) testImplementation(project(":feature:metis-test")) diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/storage/impl/MetisStorageServiceImpl.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/storage/impl/MetisStorageServiceImpl.kt index 85e6be33e..63883a971 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/storage/impl/MetisStorageServiceImpl.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/storage/impl/MetisStorageServiceImpl.kt @@ -117,7 +117,8 @@ internal class MetisStorageServiceImpl( val user = MetisUserEntity( serverId = serverId, id = authorId, - displayName = author?.name ?: "NULL" + displayName = author?.name ?: "NULL", + imageUrl = author?.imageUrl ) return Triple(basePost, answer, user) @@ -138,7 +139,8 @@ internal class MetisStorageServiceImpl( ) to MetisUserEntity( serverId = serverId, id = userId, - displayName = user?.name ?: return null + imageUrl = user?.imageUrl, + displayName = user?.name ?: return null, ) } @@ -443,7 +445,8 @@ internal class MetisStorageServiceImpl( val postingAuthor = MetisUserEntity( serverId = host, id = sp.author?.id ?: return null, - displayName = sp.author?.name ?: return null + displayName = sp.author?.name ?: return null, + imageUrl = sp.author?.imageUrl ) val (standaloneBasePosting, standalonePosting) = sp.asDb( @@ -466,7 +469,9 @@ internal class MetisStorageServiceImpl( } // First insert the users as they have no dependencies - metisDao.updateUsers(standalonePostReactionsUsers) + // TODO: Do not update existing users, as for the reactions we always get null as image_url + // Can be undone when https://github.com/ls1intum/Artemis/pull/9897 is merged. +// metisDao.updateUsers(standalonePostReactionsUsers) metisDao.insertUsers(standalonePostReactionsUsers) metisDao.insertOrUpdateUser(postingAuthor) @@ -571,7 +576,10 @@ internal class MetisStorageServiceImpl( val answerPostReactionUsers = answerPostReactionsWithUsers.map { it.second } metisDao.insertOrUpdateUser(metisUserEntity) - metisDao.updateUsers(answerPostReactionUsers) + + // TODO: Do not update existing users, as for the reactions we always get null as image_url + // Can be undone when https://github.com/ls1intum/Artemis/pull/9897 is merged. +// metisDao.updateUsers(answerPostReactionUsers) metisDao.insertUsers(answerPostReactionUsers) if (isNewPost) { 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 0cd0a62cd..979911df1 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 @@ -1,9 +1,7 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui -import android.content.Context import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.viewModelScope -import coil.ImageLoader import de.tum.informatics.www1.artemis.native_app.core.common.flatMapLatest import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.data.NetworkResponse @@ -27,7 +25,6 @@ import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Programmin import de.tum.informatics.www1.artemis.native_app.core.model.exercise.QuizExercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.TextExercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.UnknownExercise -import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.DefaultImageProvider import de.tum.informatics.www1.artemis.native_app.core.ui.serverUrlStateFlow import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R @@ -707,12 +704,4 @@ internal open class ConversationViewModel( fun updateOpenedThread(newPostId: StandalonePostId?) { _postId.value = newPostId } - - fun createMarkdownImageLoader(context: Context): Deferred { - return viewModelScope.async(coroutineContext) { - val imageProvider = DefaultImageProvider() - val authorizationToken = accountService.authToken.first() - imageProvider.createImageLoader(context, authorizationToken) - } - } } \ 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/ui/chatlist/MetisChatList.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/chatlist/MetisChatList.kt index 900010940..9ea43648f 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/chatlist/MetisChatList.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/chatlist/MetisChatList.kt @@ -1,5 +1,7 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.chatlist +import android.content.Intent +import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -16,14 +18,10 @@ import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -33,7 +31,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 coil.ImageLoader import de.tum.informatics.www1.artemis.native_app.core.ui.markdown.ProvideMarkwon import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.EmojiService @@ -90,19 +87,13 @@ internal fun MetisChatList( val conversationDataState by viewModel.latestUpdatedConversation.collectAsState() - val context = LocalContext.current - var imageLoader: ImageLoader? by remember { mutableStateOf(null) } - LaunchedEffect(true) { - imageLoader = viewModel.createMarkdownImageLoader(context).await() - } - val updatedTitle by remember(conversationDataState) { derivedStateOf { conversationDataState.bind { it.humanReadableName }.orElse("Conversation") } } - ProvideMarkwon(imageLoader) { + ProvideMarkwon { MetisChatList( modifier = modifier, initialReplyTextProvider = viewModel, @@ -150,6 +141,14 @@ fun MetisChatList( onRequestRetrySend: (StandalonePostId) -> Unit, title: String ) { + val context = LocalContext.current + + val navigateToChat = { userId: Long -> + val chatLink = "artemis://courses/$courseId/messages?userId=$userId" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(chatLink)) + context.startActivity(intent) + } + MetisReplyHandler( initialReplyTextProvider = initialReplyTextProvider, onCreatePost = onCreatePost, @@ -208,7 +207,8 @@ fun MetisChatList( onRequestEdit = onEditPostDelegate, onRequestDelete = onDeletePostDelegate, onRequestReactWithEmoji = onRequestReactWithEmojiDelegate, - onRequestRetrySend = onRequestRetrySend + onRequestRetrySend = onRequestRetrySend, + onSendMessageToUser = navigateToChat ) } } @@ -239,6 +239,7 @@ private fun ChatList( onRequestEdit: (IStandalonePost) -> Unit, onRequestDelete: (IStandalonePost) -> Unit, onRequestReactWithEmoji: (IStandalonePost, emojiId: String, create: Boolean) -> Unit, + onSendMessageToUser: (userId: Long) -> Unit, onRequestRetrySend: (StandalonePostId) -> Unit ) { LazyColumn( @@ -279,6 +280,9 @@ private fun ChatList( onClickViewPost(post?.standalonePostId ?: return@rememberPostActions) }, onResolvePost = null, + onSendMessageToAuthor = { + onSendMessageToUser(it) + }, onRequestRetrySend = { onRequestRetrySend( post?.standalonePostId ?: return@rememberPostActions diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostActions.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostActions.kt index 6a1643f64..bfc98c976 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostActions.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostActions.kt @@ -13,7 +13,8 @@ data class PostActions( val onCopyText: () -> Unit = {}, val onReplyInThread: (() -> Unit)? = null, val onResolvePost: (() -> Unit)? = null, - val onRequestRetrySend: () -> Unit = {} + val onRequestRetrySend: () -> Unit = {}, + val onSendMessageToAuthor: ((userId: Long) -> Unit)? = null ) { val canPerformAnyAction: Boolean get() = requestDeletePost != null || requestEditPost != null } @@ -29,7 +30,8 @@ fun rememberPostActions( onClickReaction: (emojiId: String, create: Boolean) -> Unit, onReplyInThread: (() -> Unit)?, onResolvePost: (() -> Unit)?, - onRequestRetrySend: () -> Unit + onRequestRetrySend: () -> Unit, + onSendMessageToAuthor: ((userId: Long) -> Unit)? ): PostActions { val clipboardManager = LocalClipboardManager.current @@ -43,6 +45,7 @@ fun rememberPostActions( onReplyInThread, onResolvePost, onRequestRetrySend, + onSendMessageToAuthor, clipboardManager ) { if (post == null) { @@ -62,7 +65,8 @@ fun rememberPostActions( }, onReplyInThread = if (doesPostExistOnServer) onReplyInThread else null, onResolvePost = if (hasResolvePostRights) onResolvePost else null, - onRequestRetrySend = onRequestRetrySend + onRequestRetrySend = onRequestRetrySend, + onSendMessageToAuthor = if (isPostAuthor) null else onSendMessageToAuthor ) } } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostItem.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostItem.kt index 5df625238..d4d041135 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostItem.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostItem.kt @@ -16,18 +16,17 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.School -import androidx.compose.material.icons.filled.SupervisorAccount import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -38,7 +37,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -55,6 +53,8 @@ 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.IReaction 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.content.dto.UserRole +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.UserRoleIcon +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePictureWithDialog import io.github.fornewid.placeholder.material3.placeholder import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -90,6 +90,7 @@ internal fun PostItem( clientId: Long, displayHeader: Boolean, onClickOnReaction: ((emojiId: String, create: Boolean) -> Unit)?, + onSendMessageToAuthor: ((userId: Long) -> Unit)?, onClick: () -> Unit, onLongClick: () -> Unit, onRequestRetrySend: () -> Unit @@ -133,9 +134,12 @@ internal fun PostItem( postStatus = postStatus, authorRole = post?.authorRole, authorName = post?.authorName, + authorId = post?.authorId ?: -1, + authorImageUrl = post?.authorImageUrl, + onSendMessageToAuthor = onSendMessageToAuthor, creationDate = post?.creationDate, expanded = isExpanded, - displayHeader = displayHeader + displayHeader = displayHeader, ) { Column( modifier = Modifier.fillMaxWidth(), @@ -207,6 +211,9 @@ private fun PostHeadline( modifier: Modifier, authorRole: UserRole?, authorName: String?, + authorId: Long, + authorImageUrl: String?, + onSendMessageToAuthor: ((userId: Long) -> Unit)?, creationDate: Instant?, postStatus: CreatePostService.Status, expanded: Boolean = false, @@ -219,11 +226,18 @@ private fun PostHeadline( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { - HeadlineAuthorIcon(authorRole) + HeadlineProfilePicture( + userId = authorId, + userName = authorName.orEmpty(), + imageUrl = authorImageUrl, + userRole = authorRole, + onSendMessageToAuthor = onSendMessageToAuthor, + ) HeadlineAuthorInfo( modifier = Modifier.fillMaxWidth(), authorName = authorName, + authorRole = authorRole, creationDate = creationDate, expanded = true ) @@ -238,7 +252,14 @@ private fun PostHeadline( ) { val doDisplayHeader = displayHeader || postStatus == CreatePostService.Status.FAILED - HeadlineAuthorIcon(authorRole, displayIcon = doDisplayHeader) + HeadlineProfilePicture( + userId = authorId, + userName = authorName.orEmpty(), + imageUrl = authorImageUrl, + userRole = authorRole, + onSendMessageToAuthor = onSendMessageToAuthor, + displayImage = doDisplayHeader + ) Column( modifier = Modifier.fillMaxWidth(), @@ -256,6 +277,7 @@ private fun PostHeadline( HeadlineAuthorInfo( modifier = Modifier.fillMaxWidth(), authorName = authorName, + authorRole = authorRole, creationDate = creationDate, expanded = false ) @@ -297,6 +319,7 @@ private fun ResolvedLabel( private fun HeadlineAuthorInfo( modifier: Modifier, authorName: String?, + authorRole: UserRole?, creationDate: Instant?, expanded: Boolean ) { @@ -304,16 +327,6 @@ private fun HeadlineAuthorInfo( creationDate ?: Clock.System.now() } - val authorNameContent: @Composable () -> Unit = { - Text( - modifier = Modifier, - text = remember(authorName) { authorName ?: "Placeholder" }, - maxLines = 1, - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Bold - ) - } - val creationDateContent: @Composable () -> Unit = { val relativeTime = getRelativeTime(to = relativeTimeTo, showDate = false) @@ -326,8 +339,7 @@ private fun HeadlineAuthorInfo( if (expanded) { Column(modifier) { - authorNameContent() - + AuthorRoleAndNameRow(authorRole, authorName) creationDateContent() } } else { @@ -336,37 +348,62 @@ private fun HeadlineAuthorInfo( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - authorNameContent() - + AuthorRoleAndNameRow(authorRole, authorName) creationDateContent() } } } @Composable -private fun HeadlineAuthorIcon( +private fun AuthorRoleAndNameRow( authorRole: UserRole?, - displayIcon: Boolean = true + authorName: String? ) { - if (displayIcon) { - val icon = when (authorRole) { - UserRole.INSTRUCTOR -> Icons.Default.School - UserRole.TUTOR -> Icons.Default.SupervisorAccount - UserRole.USER -> Icons.Default.Person - null -> Icons.Default.Person - } + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + UserRoleIcon(userRole = authorRole) - Icon( - modifier = Modifier.size(30.dp), - imageVector = icon, - contentDescription = null + Spacer(modifier = Modifier.width(4.dp)) + + Text( + modifier = Modifier, + text = remember(authorName) { authorName ?: "Placeholder" }, + maxLines = 1, + style = MaterialTheme.typography.titleSmall, + fontWeight = FontWeight.Bold ) - } else { - Box(modifier = Modifier.size(30.dp)) } +} + +@Composable +private fun HeadlineProfilePicture( + userId: Long, + userName: String, + imageUrl: String?, + userRole: UserRole?, + onSendMessageToAuthor: ((userId: Long) -> Unit)?, + displayImage: Boolean = true +) { + val size = 30.dp + Box(modifier = Modifier.size(size)) { + if (!displayImage) { + return + } + ProfilePictureWithDialog( + modifier = Modifier.size(size), + userId = userId, + userName = userName, + userRole = userRole, + imageUrl = imageUrl, + onSendMessage = onSendMessageToAuthor + ) + } } + + /** * Display the tags, the reactions and the action buttons like reply, view replies and react with emoji. */ diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostWithBottomSheet.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostWithBottomSheet.kt index 03811a46f..53593ec4e 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostWithBottomSheet.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostWithBottomSheet.kt @@ -34,7 +34,8 @@ internal fun PostWithBottomSheet( onLongClick = { displayBottomSheet = true }, - onRequestRetrySend = postActions.onRequestRetrySend + onRequestRetrySend = postActions.onRequestRetrySend, + onSendMessageToAuthor = postActions.onSendMessageToAuthor ) if (displayBottomSheet && post != null) { @@ -47,4 +48,6 @@ internal fun PostWithBottomSheet( } ) } + + } \ 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/ui/reply/ReplyTextField.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextField.kt index 8f75c0eda..b9f184c35 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextField.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextField.kt @@ -41,6 +41,7 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -53,6 +54,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.ui.AwaitDeferredCompletion +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.thread.ReplyState import kotlinx.coroutines.CompletableDeferred @@ -102,7 +104,7 @@ internal fun ReplyTextField( .testTag(TEST_TAG_CAN_CREATE_REPLY), replyMode = replyMode, onReply = { targetReplyState.onCreateReply() }, - title = "Message $title" + title = stringResource(R.string.create_reply_click_to_write, title) ) } @@ -122,7 +124,7 @@ internal fun ReplyTextField( .fillMaxWidth() .padding(horizontal = 8.dp), onCancel = targetReplyState.onCancelSendReply, - title = title + title = stringResource(R.string.create_reply_sending_reply) ) } } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/thread/MetisThreadUi.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/thread/MetisThreadUi.kt index 90cb8b9e4..d496d1506 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/thread/MetisThreadUi.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/thread/MetisThreadUi.kt @@ -16,20 +16,14 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Divider import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import coil.ImageLoader import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.data.isSuccess import de.tum.informatics.www1.artemis.native_app.core.data.orNull @@ -51,13 +45,13 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui. import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.reply.MetisReplyHandler import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.reply.ReplyTextField import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.shared.isReplyEnabled -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.ReportVisibleMetisContext -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.VisibleStandalonePostDetails 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.conversation.Conversation import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.AnswerPostPojo import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.PostPojo import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.ReportVisibleMetisContext +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.VisibleStandalonePostDetails import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Deferred import org.koin.compose.koinInject @@ -82,12 +76,6 @@ internal fun MetisThreadUi( val hasModerationRights by viewModel.hasModerationRights.collectAsState() val isAtLeastTutorInCourse by viewModel.isAtLeastTutorInCourse.collectAsState() - val context = LocalContext.current - var imageLoader: ImageLoader? by remember { mutableStateOf(null) } - LaunchedEffect(true) { - imageLoader = viewModel.createMarkdownImageLoader(context).await() - } - postDataState.bind { it.serverPostId }.orNull()?.let { serverSidePostId -> ReportVisibleMetisContext( remember( @@ -101,7 +89,7 @@ internal fun MetisThreadUi( val conversationDataState by viewModel.conversation.collectAsState() - ProvideMarkwon(imageLoader) { + ProvideMarkwon { MetisThreadUi( modifier = modifier, courseId = viewModel.courseId, @@ -276,6 +264,9 @@ private fun PostAndRepliesList( }, onReplyInThread = null, onResolvePost = { onRequestResolve(affectedPost) }, + onSendMessageToAuthor = { + // TODO: Implement + }, onRequestRetrySend = { onRequestRetrySend( affectedPost.clientPostId ?: return@rememberPostActions, diff --git a/feature/metis/conversation/src/main/res/values/create_answer_strings.xml b/feature/metis/conversation/src/main/res/values/create_answer_strings.xml deleted file mode 100644 index 4a5c37721..000000000 --- a/feature/metis/conversation/src/main/res/values/create_answer_strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - Click to write reply - Sending reply… - \ No newline at end of file diff --git a/feature/metis/conversation/src/main/res/values/create_reply_strings.xml b/feature/metis/conversation/src/main/res/values/create_reply_strings.xml new file mode 100644 index 000000000..89383a04c --- /dev/null +++ b/feature/metis/conversation/src/main/res/values/create_reply_strings.xml @@ -0,0 +1,5 @@ + + + Message %1$s + Sending reply… + \ No newline at end of file diff --git a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/BaseChatUItest.kt b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/BaseChatUItest.kt index f47eb705b..05932a000 100644 --- a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/BaseChatUItest.kt +++ b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/BaseChatUItest.kt @@ -44,7 +44,8 @@ abstract class BaseChatUItest : BaseComposeTest() { updatedDate = null, content = "Answer Post content $index", authorRole = UserRole.USER, - authorName = "author name" + authorName = "author name", + authorImageUrl = null, ), reactions = emptyList(), serverPostIdCache = AnswerPostPojo.ServerPostIdCache( @@ -65,6 +66,7 @@ abstract class BaseChatUItest : BaseComposeTest() { title = null, authorName = "author name", authorRole = UserRole.USER, + authorImageUrl = null, courseWideContext = null, tags = emptyList(), answers = if (index == 0) answers else emptyList(), diff --git a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/storage/impl/MetisStorageBaseTest.kt b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/storage/impl/MetisStorageBaseTest.kt index bef36786f..eab922e23 100644 --- a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/storage/impl/MetisStorageBaseTest.kt +++ b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/service/storage/impl/MetisStorageBaseTest.kt @@ -45,7 +45,8 @@ abstract class MetisStorageBaseTest { updatedDate = null, content = "Answer post content 0", authorRole = UserRole.USER, - authorName = author.name!! + authorName = author.name!!, + authorImageUrl = null ), reactions = emptyList(), serverPostIdCache = AnswerPostPojo.ServerPostIdCache( @@ -64,6 +65,7 @@ abstract class MetisStorageBaseTest { title = null, authorName = author.name!!, authorRole = UserRole.USER, + authorImageUrl = null, courseWideContext = null, tags = emptyList(), answers = emptyList(), diff --git a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationProfilePictureE2eTest.kt b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationProfilePictureE2eTest.kt new file mode 100644 index 000000000..b2279510f --- /dev/null +++ b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationProfilePictureE2eTest.kt @@ -0,0 +1,153 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Modifier +import androidx.compose.ui.test.junit4.ComposeTestRule +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.core.common.test.UnitTest +import de.tum.informatics.www1.artemis.native_app.core.model.account.User +import de.tum.informatics.www1.artemis.native_app.core.test.BaseComposeTest +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider +import de.tum.informatics.www1.artemis.native_app.core.ui.test.ArtemisImageProviderStub +import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.TestInitialReplyTextProvider +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.chatlist.ChatListItem +import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.chatlist.MetisChatList +import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.chatlist.PostsDataState +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.content.dto.StandalonePost +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.TEST_TAG_PROFILE_PICTURE_IMAGE +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.TEST_TAG_PROFILE_PICTURE_INITIALS +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.TEST_TAG_PROFILE_PICTURE_UNKNOWN +import kotlinx.coroutines.CompletableDeferred +import org.junit.Before +import org.junit.Test +import org.junit.experimental.categories.Category +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.shadows.ShadowLog + + +@Category(UnitTest::class) +@RunWith(RobolectricTestRunner::class) +class ConversationProfilePictureUiTest : BaseComposeTest() { + + private val clientId = 20L + private val courseId = 1L + + private val allTestTags = listOf( + TEST_TAG_PROFILE_PICTURE_IMAGE, + TEST_TAG_PROFILE_PICTURE_INITIALS, + TEST_TAG_PROFILE_PICTURE_UNKNOWN + ) + + @Before + fun setUp() { + ShadowLog.stream = System.out + ArtemisImageProviderStub.setup(context) + } + + @Test + fun `test GIVEN a post with an authorImageUrl WHEN displaying the post THEN the profile picture is shown`() { + val post = StandalonePost( + id = 1L, + author = User( + id = 1L, + name = "author", + imageUrl = "test.png" + ), + ) + setupUi(post) + + composeTestRule.assertTestTagExclusivelyExists( + exclusiveTag = TEST_TAG_PROFILE_PICTURE_IMAGE, + allTags = allTestTags + ) + } + + @Test + fun `test GIVEN a post without an authorImageUrl WHEN displaying the post THEN the users initials are shown`() { + val post = StandalonePost( + id = 1L, + author = User( + id = 1L, + name = "author", + imageUrl = null + ), + ) + setupUi(post) + + composeTestRule.assertTestTagExclusivelyExists( + exclusiveTag = TEST_TAG_PROFILE_PICTURE_INITIALS, + allTags = allTestTags + ) + } + + @Test + fun `test GIVEN a post with null for userName WHEN displaying the post THEN the unknown profile picture is shown`() { + val post = StandalonePost( + id = 1L, + author = User( + id = 1L, + name = null, + imageUrl = null + ), + ) + setupUi(post) + + composeTestRule.assertTestTagExclusivelyExists( + exclusiveTag = TEST_TAG_PROFILE_PICTURE_UNKNOWN, + allTags = allTestTags + ) + } + + private fun setupUi( + post: IStandalonePost + ) { + composeTestRule.setContent { + CompositionLocalProvider( + LocalArtemisImageProvider provides ArtemisImageProviderStub() + ) { + MetisChatList( + modifier = Modifier, + initialReplyTextProvider = TestInitialReplyTextProvider(), + posts = PostsDataState.Loaded.WithList( + posts = listOf(ChatListItem.PostChatListItem(post)), + appendState = PostsDataState.NotLoading + ), + bottomItem = null, + clientId = clientId, + hasModerationRights = false, + isAtLeastTutorInCourse = false, + listContentPadding = PaddingValues(0.dp), + serverUrl = "", + courseId = courseId, + state = LazyListState(), + isReplyEnabled = false, + emojiService = EmojiServiceStub, + onCreatePost = { CompletableDeferred() }, + onEditPost = { _, _ -> CompletableDeferred() }, + onDeletePost = { CompletableDeferred() }, + onRequestReactWithEmoji = { _, _, _ -> CompletableDeferred() }, + onClickViewPost = {}, + onRequestRetrySend = {}, + title = "title", + ) + } + } + } + + private fun ComposeTestRule.assertTestTagExclusivelyExists( + exclusiveTag: String, + allTags: List, + useUnmergedTree: Boolean = true + ) { + allTags.filter { it != exclusiveTag }.forEach { + onNodeWithTag(it, useUnmergedTree).assertDoesNotExist() + } + onNodeWithTag(exclusiveTag, useUnmergedTree).assertExists() + } +} \ 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/BasePost.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/BasePost.kt index 1ff0e9f05..623beef31 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/BasePost.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/BasePost.kt @@ -16,4 +16,7 @@ sealed class BasePost : IBasePost { override val authorName: String? get() = author?.name + + override val authorImageUrl: String? + get() = author?.imageUrl } \ 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/IBasePost.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/IBasePost.kt index e22c4da51..44e9bfe9b 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/IBasePost.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/IBasePost.kt @@ -6,6 +6,7 @@ sealed interface IBasePost { val authorName: String? val authorId: Long? val authorRole: UserRole? + val authorImageUrl: String? val creationDate: Instant? val updatedDate: Instant? val content: String? diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/StandalonePost.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/StandalonePost.kt index bf8dbd29f..8e2c57aa4 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/StandalonePost.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/StandalonePost.kt @@ -32,7 +32,8 @@ data class StandalonePost( constructor(post: PostPojo, conversation: Conversation) : this( id = post.serverPostId, author = User( - id = post.authorId + id = post.authorId, + imageUrl = post.authorImageUrl ), authorRole = post.authorRole, content = post.content, diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDao.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDao.kt index c82654043..5b3bb67a4 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDao.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDao.kt @@ -213,7 +213,8 @@ interface MetisDao { sp.resolved, u.name as author_name, p.author_role, - p.author_id as author_id + p.author_id as author_id, + u.image_url as author_image_url from metis_post_context mpc, postings p, standalone_postings sp, users u where @@ -239,7 +240,8 @@ interface MetisDao { sp.resolved, u.name as author_name, p.author_role, - p.author_id as author_id + p.author_id as author_id, + u.image_url as author_image_url from metis_post_context mpc, postings p, @@ -274,7 +276,8 @@ interface MetisDao { sp.resolved, u.name as author_name, p.author_role, - p.author_id as author_id + p.author_id as author_id, + u.image_url as author_image_url from metis_post_context mpc, postings p, diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/BasePostingEntity.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/BasePostingEntity.kt index 2736b6d2b..250600fc7 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/BasePostingEntity.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/BasePostingEntity.kt @@ -36,7 +36,7 @@ data class BasePostingEntity( @ColumnInfo(name = "content") val content: String?, @ColumnInfo(name = "author_role") - val authorRole: UserRole? + val authorRole: UserRole?, ) { enum class CourseWideContext { @ColumnInfo(name = "tech_support") diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/MetisUserEntity.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/MetisUserEntity.kt index 3dd57c817..fcdb11090 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/MetisUserEntity.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/entities/MetisUserEntity.kt @@ -13,5 +13,7 @@ class MetisUserEntity( @ColumnInfo(name = "id") val id: Long, @ColumnInfo(name = "name") - val displayName: String + val displayName: String, + @ColumnInfo(name = "image_url") + val imageUrl: String? ) \ 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/db/pojo/AnswerPostPojo.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/pojo/AnswerPostPojo.kt index 079904d83..34290039e 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/pojo/AnswerPostPojo.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/pojo/AnswerPostPojo.kt @@ -58,6 +58,9 @@ data class AnswerPostPojo( @Ignore override val authorId: Long = basePostingCache.authorId + @Ignore + override val authorImageUrl: String? = basePostingCache.authorImageUrl + @Ignore override val serverPostId: Long? = serverPostIdCache.serverPostId @@ -83,7 +86,14 @@ data class AnswerPostPojo( parentColumn = "author_id", projection = ["name"] ) - val authorName: String + val authorName: String, + @Relation( + entity = MetisUserEntity::class, + entityColumn = "id", + parentColumn = "author_id", + projection = ["image_url"] + ) + val authorImageUrl: String? ) data class ServerPostIdCache( diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/pojo/PostPojo.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/pojo/PostPojo.kt index a7626f246..153f2ae26 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/pojo/PostPojo.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/pojo/PostPojo.kt @@ -29,6 +29,8 @@ data class PostPojo( override val authorRole: UserRole, @ColumnInfo(name = "author_id") override val authorId: Long, + @ColumnInfo(name = "author_image_url") + override val authorImageUrl: String?, @ColumnInfo(name = "creation_date") override val creationDate: Instant, @ColumnInfo(name = "updated_date") diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/UserRoleIcon.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/UserRoleIcon.kt new file mode 100644 index 000000000..aa6f771f7 --- /dev/null +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/UserRoleIcon.kt @@ -0,0 +1,35 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.School +import androidx.compose.material.icons.filled.SupervisorAccount +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.UserRole + +@Composable +fun UserRoleIcon( + modifier: Modifier = Modifier, + userRole: UserRole?, +) { + val icon = when (userRole) { + UserRole.INSTRUCTOR -> Icons.Default.School + UserRole.TUTOR -> Icons.Default.SupervisorAccount + UserRole.USER -> Icons.Default.Person + null -> Icons.Default.Person + } + + Box { + Icon( + modifier = modifier + .size(16.dp), + imageVector = icon, + contentDescription = null + ) + } +} \ 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/ui/profile_picture/ProfilePicture.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/ProfilePicture.kt new file mode 100644 index 000000000..682ccd434 --- /dev/null +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/ProfilePicture.kt @@ -0,0 +1,174 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture + +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.AsyncImagePainter +import de.tum.informatics.www1.artemis.native_app.core.ui.common.nonScaledSp +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.UserRole + +const val TEST_TAG_PROFILE_PICTURE_IMAGE = "TEST_TAG_PROFILE_PICTURE_IMAGE" +const val TEST_TAG_PROFILE_PICTURE_INITIALS = "TEST_TAG_PROFILE_PICTURE_INITIALS" +const val TEST_TAG_PROFILE_PICTURE_UNKNOWN = "TEST_TAG_PROFILE_PICTURE_UNKNOWN" + +private const val BoxSizeToFontSizeMultiplier = 0.16f + +@Composable +fun ProfilePictureWithDialog( + modifier: Modifier = Modifier, + userId: Long, + userName: String, + userRole: UserRole?, + imageUrl: String?, + onSendMessage: ((userId: Long) -> Unit)?, +) { + var displayUserProfileDialog by remember{ mutableStateOf(false) } + + val profilePictureData = ProfilePictureData.create( + userId = userId, + username = userName, + imageUrl = imageUrl, + ) + + ProfilePicture( + modifier = modifier.clickable { + displayUserProfileDialog = true + }, + profilePictureData = profilePictureData, + ) + + if (displayUserProfileDialog) { + UserProfileDialog( + username = userName, + userRole = userRole, + profilePictureData = profilePictureData, + onDismiss = { + displayUserProfileDialog = false + }, + isAppUser = onSendMessage == null, + onSendMessageClick = { + if (onSendMessage != null) { + onSendMessage(userId) + } + displayUserProfileDialog = false + } + ) + } +} + +@Composable +fun ProfilePicture( + modifier: Modifier = Modifier, + profilePictureData: ProfilePictureData, +) { + val modifierWithDefault = modifier + .size(30.dp) + .clip(shape = RoundedCornerShape(percent = 15)) + + when(profilePictureData) { + is ProfilePictureData.Image -> { + ProfilePictureImage( + modifier = modifierWithDefault, + profilePictureData = profilePictureData, + ) + } + is ProfilePictureData.InitialsPlaceholder -> { + InitialsPlaceholder( + modifier = modifierWithDefault.testTag(TEST_TAG_PROFILE_PICTURE_INITIALS), + profilePictureData = profilePictureData, + ) + } + ProfilePictureData.Unknown -> { + InitialsPlaceholder( + modifier = modifierWithDefault.testTag(TEST_TAG_PROFILE_PICTURE_UNKNOWN), + profilePictureData = ProfilePictureData.InitialsPlaceholder(0, "?"), + ) + } + } +} + +@Composable +fun ProfilePictureImage( + modifier: Modifier, + profilePictureData: ProfilePictureData.Image, +) { + val imageUrl = profilePictureData.url + val artemisImageProvider = LocalArtemisImageProvider.current + val painter = artemisImageProvider.rememberArtemisAsyncImagePainter(imagePath = imageUrl) + val painterState by painter.state.collectAsState() + + if (painterState is AsyncImagePainter.State.Error) { + Log.e("ProfilePicture", "Error loading image: ${(painterState as AsyncImagePainter.State.Error).result.throwable.message}") + } + + when (painterState) { + is AsyncImagePainter.State.Success -> { + Image( + modifier = modifier + .fillMaxWidth() + .testTag(TEST_TAG_PROFILE_PICTURE_IMAGE), + painter = painter, + contentDescription = null, + contentScale = ContentScale.Fit + ) + } + else -> { + InitialsPlaceholder( + modifier = modifier, + profilePictureData = profilePictureData.fallBack, + ) + } + } +} + +@Composable +fun InitialsPlaceholder( + modifier: Modifier, + profilePictureData: ProfilePictureData.InitialsPlaceholder, +) { + val boxSize = remember { mutableIntStateOf(0) } + + Box( + modifier = modifier + .background(color = profilePictureData.backgroundColor) + .onGloballyPositioned { + boxSize.intValue = it.size.width + }, + contentAlignment = Alignment.Center, + ) { + Text( + text = profilePictureData.initials, + color = Color.White, + fontSize = boxSize.intValue.sp.nonScaledSp * BoxSizeToFontSizeMultiplier, + fontWeight = FontWeight.Bold + ) + } +} + + + + diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/ProfilePictureData.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/ProfilePictureData.kt new file mode 100644 index 000000000..f59728cfa --- /dev/null +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/ProfilePictureData.kt @@ -0,0 +1,91 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture + +import androidx.compose.ui.graphics.Color + +sealed class ProfilePictureData { + + companion object { + fun create(userId: Long?, username: String?, imageUrl: String?): ProfilePictureData { + if (userId == null || username.isNullOrEmpty()) { + return Unknown + } + + val fallBack = InitialsPlaceholder(userId, username) + return if (imageUrl != null) { + Image(imageUrl, fallBack) + } else { + fallBack + } + } + } + + data class Image(val url: String, val fallBack: InitialsPlaceholder) : ProfilePictureData() + + data class InitialsPlaceholder(val userId: Long, val username: String) : ProfilePictureData() { + val initials: String = getInitialsFromString(username) + val backgroundColor: Color = getBackgroundColorHue(userId.toString()) + } + + data object Unknown: ProfilePictureData() +} + +// The following util functions are copied from the Artemis webapp implementation. +// Sources: +// https://github.com/ls1intum/Artemis/blob/fa32c243b568c92aa5e075e8176abdc7c7452444/src/main/webapp/app/utils/text.utils.ts +// https://github.com/ls1intum/Artemis/blob/fa32c243b568c92aa5e075e8176abdc7c7452444/src/main/webapp/app/utils/color.utils.ts + +/** + * Returns 2 capitalized initials of a given string. + * If it has multiple names, it takes the first and last (Albert Berta Muster -> AM) + * If it has one name, it'll return a deterministic random other string (Albert -> AB) + * If it consists of a single letter it will return the single letter. + * @param username The string used to generate the initials. + */ +private fun getInitialsFromString(username: String): String { + val parts = username.trim().split("\\s+".toRegex()) + + var initials: String + + if (parts.size > 1) { + // Takes first and last word in string and returns their initials. + initials = parts[0][0].toString() + parts[parts.size - 1][0] + } else { + // If only one single word, it will take the first letter and a random second. + initials = parts[0][0].toString() + val remainder = parts[0].substring(1) + val secondInitial = remainder.find { it.isLetterOrDigit() } + if (secondInitial != null) { + initials += secondInitial + } + } + + return initials.uppercase() +} + +/** + * Returns a background color hue for a given string. + * @param seed The string used to determine the random value. + */ +private fun getBackgroundColorHue(seed: String?): Color { + val seedValue = seed ?: Math.random().toString() + val hue = deterministicRandomValueFromString(seedValue) * 360 + return Color.hsl(hue.toFloat(), 0.5f, 0.5f) +} + +/** + * Returns a pseudo-random numeric value for a given string using a simple hash function. + * @param str The string used for the hash function. + */ +private fun deterministicRandomValueFromString(str: String): Double { + var seed = 0L + for (i in str.indices) { + seed = str[i].code + ((seed shl 5) - seed) + } + val m = 0x80000000 + val a = 1103515245 + val c = 42718 + + seed = (a * seed + c) % m + + return seed.toDouble() / (m - 1) +} \ 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/ui/profile_picture/UserProfileDialog.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/UserProfileDialog.kt new file mode 100644 index 000000000..807067781 --- /dev/null +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/UserProfileDialog.kt @@ -0,0 +1,139 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.R +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.UserRole +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.UserRoleIcon + + +@Composable +fun UserProfileDialog( + username: String, + userRole: UserRole?, + profilePictureData: ProfilePictureData, + isAppUser: Boolean, + onDismiss: () -> Unit, + onSendMessageClick: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { + UserProfileDialogHeader( + username = username, + userRole = userRole, + profilePictureData = profilePictureData, + ) + }, + text = { + if (!isAppUser) { + FilledTonalButton( + onClick = onSendMessageClick, + ) { + Text(stringResource(R.string.user_profile_dialog_send_message_action)) + } + } else { + Text( + stringResource(R.string.user_profile_dialog_is_app_user_placeholder), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary + ) + } + }, + confirmButton = {}, + dismissButton = { + TextButton( + onClick = onDismiss, + ) { + Text(stringResource(R.string.user_profile_dialog_close)) + } + }, + ) + +} + +@Composable +private fun UserProfileDialogHeader( + username: String, + userRole: UserRole?, + profilePictureData: ProfilePictureData, +) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + ProfilePicture( + modifier = Modifier.size(80.dp), + profilePictureData = profilePictureData, + ) + + Column( + modifier = Modifier.padding(start = 16.dp), + ) { + Text( + text = username, + style = MaterialTheme.typography.titleMedium, + ) + + userRole?.let { + UserRoleRow(userRole = it) + } + } + } +} + +@Composable +private fun UserRoleRow(userRole: UserRole) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + UserRoleIcon( + modifier = Modifier + .size(30.dp) + .padding(end = 4.dp), + userRole = userRole, + ) + + Text( + text = userRole.toString(), + style = MaterialTheme.typography.labelMedium, + ) + } +} + + +@Preview +@Composable +fun UserProfileDialogPreview() { + Box( + modifier = Modifier.fillMaxSize(), + ) { + UserProfileDialog( + username = "Max Mustermann", + userRole = UserRole.TUTOR, + profilePictureData = ProfilePictureData.create( + userId = 1L, + username = "Max Mustermann", + imageUrl = null, + ), + onDismiss = {}, + isAppUser = true, + onSendMessageClick = {}, + ) + } +} \ 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/LocalVisibleMetisContextManager.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/visiblemetiscontextreporter/LocalVisibleMetisContextManager.kt index 8c88a68d1..c8e597bb0 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/visiblemetiscontextreporter/LocalVisibleMetisContextManager.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/visiblemetiscontextreporter/LocalVisibleMetisContextManager.kt @@ -1,11 +1,10 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.compositionLocalOf -private val localVisibleMetisContextManager = +val LocalVisibleMetisContextManager = compositionLocalOf { throw IllegalStateException("No VisibleMetisContextManager provided.") } interface VisibleMetisContextManager { @@ -14,20 +13,9 @@ interface VisibleMetisContextManager { fun unregisterMetisContext(metisContext: VisibleMetisContext) } -@Composable -fun ProvideLocalVisibleMetisContextManager( - visibleMetisContextManager: VisibleMetisContextManager, - content: @Composable () -> Unit -) { - CompositionLocalProvider( - localVisibleMetisContextManager provides visibleMetisContextManager, - content = content - ) -} - @Composable fun ReportVisibleMetisContext(visibleMetisContext: VisibleMetisContext) { - val visibleMetisContextManager = localVisibleMetisContextManager.current + val visibleMetisContextManager = LocalVisibleMetisContextManager.current DisposableEffect(visibleMetisContextManager, visibleMetisContext) { visibleMetisContextManager.registerMetisContext(visibleMetisContext) diff --git a/feature/metis/shared/src/main/res/values/user_profile_dialog_strings.xml b/feature/metis/shared/src/main/res/values/user_profile_dialog_strings.xml new file mode 100644 index 000000000..17dca05c3 --- /dev/null +++ b/feature/metis/shared/src/main/res/values/user_profile_dialog_strings.xml @@ -0,0 +1,6 @@ + + + Close + Send Message + Wow, such a cool user! + \ No newline at end of file diff --git a/feature/metis/shared/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDaoTest.kt b/feature/metis/shared/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDaoTest.kt index 216ec3504..acfd67645 100644 --- a/feature/metis/shared/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDaoTest.kt +++ b/feature/metis/shared/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/db/MetisDaoTest.kt @@ -33,7 +33,8 @@ class MetisDaoTest { private val user = MetisUserEntity( serverId = serverId, id = 4, - displayName = "User4" + displayName = "User4", + imageUrl = null, ) private val basePost = BasePostingEntity( postId = clientPostId, diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationUi.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationUi.kt index 3a4128e19..b292835d0 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationUi.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationUi.kt @@ -45,9 +45,6 @@ internal fun QuizParticipationUi( onNavigateToInspectResult: (QuizType.ViewableQuizType) -> Unit, onNavigateUp: () -> Unit ) { - val authToken by viewModel.authToken.collectAsState() - val serverUrl: String = viewModel.serverUrl.collectAsState().value.dropLast(1) - val exerciseDataState by viewModel.quizExerciseDataState.collectAsState() val isWaitingForQuizStart by viewModel.waitingForQuizStart.collectAsState(initial = false) val hasQuizEnded by viewModel.quizEndedStatus.collectAsState(initial = false) @@ -161,8 +158,6 @@ internal fun QuizParticipationUi( questionsWithData = questionsWithData, lastSubmissionTime = lastSubmission.submissionDate, endDate = endDate, - authToken = authToken, - serverUrl = serverUrl, isConnected = isConnected, overallPoints = overallPoints, latestWebsocketSubmission = latestWebsocketSubmission, diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationViewModel.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationViewModel.kt index 440c5d0c1..f670b6dd1 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationViewModel.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/participation/QuizParticipationViewModel.kt @@ -11,6 +11,7 @@ import de.tum.informatics.www1.artemis.native_app.core.data.NetworkResponse import de.tum.informatics.www1.artemis.native_app.core.data.filterSuccess import de.tum.informatics.www1.artemis.native_app.core.data.onSuccess import de.tum.informatics.www1.artemis.native_app.core.data.service.network.ParticipationService +import de.tum.informatics.www1.artemis.native_app.core.data.service.network.ServerTimeService import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken @@ -30,7 +31,6 @@ import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.quiz.SubmittedAnswer import de.tum.informatics.www1.artemis.native_app.core.ui.authTokenStateFlow import de.tum.informatics.www1.artemis.native_app.core.ui.serverUrlStateFlow -import de.tum.informatics.www1.artemis.native_app.core.data.service.network.ServerTimeService import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider import de.tum.informatics.www1.artemis.native_app.feature.quiz.AnswerOptionId import de.tum.informatics.www1.artemis.native_app.feature.quiz.BaseQuizViewModel @@ -121,8 +121,8 @@ internal class QuizParticipationViewModel( private val submissionChannel = "/topic/quizExercise/$exerciseId/submission" private val quizExerciseChannel = "/topic/courses/$courseId/quizExercises" - val serverUrl = serverUrlStateFlow(serverConfigurationService) - val authToken = authTokenStateFlow(accountService) + private val serverUrl = serverUrlStateFlow(serverConfigurationService) + private val authToken = authTokenStateFlow(accountService) /** * Use server time for best time approximation. diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/DragAndDropQuizQuestionUi.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/DragAndDropQuizQuestionUi.kt index 67c709b74..f9309beab 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/DragAndDropQuizQuestionUi.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/DragAndDropQuizQuestionUi.kt @@ -22,8 +22,6 @@ internal fun DragAndDropQuizQuestionUi( questionIndex: Int, question: DragAndDropQuizQuestion, data: QuizQuestionData.DragAndDropData, - serverUrl: String, - authToken: String, onRequestDisplayHint: () -> Unit, ) { Column(modifier = modifier) { @@ -48,8 +46,6 @@ internal fun DragAndDropQuizQuestionUi( DragAndDropQuizQuestionBody( question, data, - authToken, - serverUrl, ) } } diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragAndDropItemsRow.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragAndDropItemsRow.kt index 7e9b78080..ceaa361d4 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragAndDropItemsRow.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragAndDropItemsRow.kt @@ -27,8 +27,6 @@ internal sealed interface DragAndDropDragItemsRowType { internal fun DragAndDropAvailableDragItemsContainer( modifier: Modifier, dragItems: List, - serverUrl: String, - authToken: String, type: DragAndDropDragItemsRowType ) { Box( @@ -49,8 +47,7 @@ internal fun DragAndDropAvailableDragItemsContainer( DragItemUiElement( modifier = Modifier, text = dragItem.text, - pictureFilePath = dragItem.backgroundPictureServerUrl(serverUrl), - authToken = authToken + pictureFilePath = dragItem.pictureFilePath, ) } diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragAndDropQuizQuestionBody.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragAndDropQuizQuestionBody.kt index 17c842719..416cddd41 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragAndDropQuizQuestionBody.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragAndDropQuizQuestionBody.kt @@ -31,15 +31,11 @@ import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.dragandd import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.body.work_area.DragAndDropWorkArea import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.dragOffset import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.dragPosition -import io.ktor.http.URLBuilder -import io.ktor.http.appendPathSegments @Composable internal fun DragAndDropQuizQuestionBody( question: DragAndDropQuizQuestion, data: QuizQuestionData.DragAndDropData, - authToken: String, - serverUrl: String, ) { var isSampleSolutionDisplayed by rememberSaveable { mutableStateOf(false) } @@ -82,8 +78,6 @@ internal fun DragAndDropQuizQuestionBody( } }, dragItems = data.availableDragItems, - authToken = authToken, - serverUrl = serverUrl, type = when (data) { is QuizQuestionData.DragAndDropData.Editable -> DragAndDropDragItemsRowType.Editable( onDragRelease = { dragItem -> @@ -101,12 +95,6 @@ internal fun DragAndDropQuizQuestionBody( } ) - val imageUrl = remember(serverUrl, backgroundFilePath) { - URLBuilder(serverUrl) - .appendPathSegments(backgroundFilePath) - .buildString() - } - // Correct mappings as sent from the server. Not relevant for participation mode. val sampleSolutionMappings = remember(question.correctMappings) { question.correctMappings @@ -129,12 +117,9 @@ internal fun DragAndDropQuizQuestionBody( modifier = Modifier .fillMaxWidth() .zIndex(if (isDraggingFromArea) 20f else 1f), - questionId = question.id, dropLocationMapping = dropLocationMapping, - imageUrl = imageUrl, + imageUrl = backgroundFilePath, dropLocations = question.dropLocations, - serverUrl = serverUrl, - authToken = authToken, type = data.getDragAndDropAreaType( currentDropTarget = currentDropTarget, isSampleSolutionDisplayed = isSampleSolutionDisplayed, @@ -210,13 +195,3 @@ private fun QuizQuestionData.DragAndDropData.getDragAndDropAreaType( internal val dragItemOutlineColor: Color @Composable get() = Color.DarkGray - -@Composable -internal fun DragAndDropQuizQuestion.DragItem.backgroundPictureServerUrl(serverUrl: String): String? = - remember(pictureFilePath, serverUrl) { - pictureFilePath?.let { - URLBuilder(serverUrl) - .appendPathSegments(it) - .buildString() - } - } diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragItemUi.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragItemUi.kt index a538204e1..531729e41 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragItemUi.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/DragItemUi.kt @@ -1,5 +1,6 @@ package de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.body +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.gestures.detectDragGestures @@ -22,20 +23,17 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex -import coil.compose.AsyncImage -import coil.request.ImageRequest import de.tum.informatics.www1.artemis.native_app.core.model.exercise.quiz.DragAndDropQuizQuestion import de.tum.informatics.www1.artemis.native_app.core.ui.common.AutoResizeText import de.tum.informatics.www1.artemis.native_app.core.ui.common.FontSizeRange +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.DragTargetInfo import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.LocalDragTargetInfo -import io.ktor.http.HttpHeaders private val dragItemBackgroundColor: Color @Composable get() = if (isSystemInDarkTheme()) Color.Black else Color.White @@ -48,7 +46,6 @@ internal fun DragItemUiElement( modifier: Modifier, text: String?, pictureFilePath: String?, - authToken: String ) { Box( modifier = modifier @@ -60,7 +57,6 @@ internal fun DragItemUiElement( modifier = Modifier.padding(4.dp), text = text, pictureFilePath = pictureFilePath, - authToken = authToken ) } } @@ -70,7 +66,6 @@ internal fun DragItemUiElementContent( modifier: Modifier, text: String?, pictureFilePath: String?, - authToken: String, fontColor: Color = dragItemTextColor ) { Box( @@ -78,16 +73,16 @@ internal fun DragItemUiElementContent( contentAlignment = Alignment.Center ) { if (pictureFilePath != null) { - val request = ImageRequest.Builder(LocalContext.current) - .data(pictureFilePath) - .addHeader(HttpHeaders.Authorization, "Bearer $authToken") - .build() + val asyncImagePainter = LocalArtemisImageProvider.current.rememberArtemisAsyncImagePainter( + imagePath = pictureFilePath + ) - AsyncImage( - model = request, contentDescription = null, + Image( modifier = Modifier .widthIn(max = 60.dp) - .heightIn(max = 60.dp) + .heightIn(max = 60.dp), + painter = asyncImagePainter, + contentDescription = null ) } diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/work_area/DragAndDropWorkArea.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/work_area/DragAndDropWorkArea.kt index 92ea2967a..a3b131aac 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/work_area/DragAndDropWorkArea.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/work_area/DragAndDropWorkArea.kt @@ -1,33 +1,30 @@ package de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.body.work_area -import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.absoluteOffset import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.times -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toSize -import androidx.core.graphics.drawable.toBitmap -import androidx.core.graphics.scale +import coil3.compose.AsyncImagePainter +import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.model.exercise.quiz.DragAndDropQuizQuestion import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi -import de.tum.informatics.www1.artemis.native_app.core.ui.common.image.loadAsyncImageDrawable -import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.DefaultImageProvider +import de.tum.informatics.www1.artemis.native_app.core.ui.remote_images.LocalArtemisImageProvider import de.tum.informatics.www1.artemis.native_app.feature.quiz.R internal sealed interface DragAndDropAreaType { @@ -49,37 +46,28 @@ internal sealed interface DragAndDropAreaType { @Composable internal fun DragAndDropWorkArea( modifier: Modifier, - questionId: Long, dropLocationMapping: Map, imageUrl: String, dropLocations: List, - serverUrl: String, - authToken: String, type: DragAndDropAreaType ) { - val context = LocalContext.current - - val defaultImageProvider = DefaultImageProvider() - val request = remember(imageUrl, questionId) { - defaultImageProvider.createImageRequest( - context, - imageUrl, - serverUrl, - authToken, - "QQ_$questionId" - ) - } + val asyncImagePainter = LocalArtemisImageProvider.current.rememberArtemisAsyncImagePainter( + imagePath = imageUrl + ) + val asyncImagePainterState by asyncImagePainter.state.collectAsState() + asyncImagePainter.restart() - val resultData = loadAsyncImageDrawable(request = request) BasicDataStateUi( modifier = modifier, - dataState = resultData.dataState, + dataState = asyncImagePainterState.toDataState(), loadingText = stringResource(id = R.string.quiz_participation_load_dnd_image_loading), failureText = stringResource(id = R.string.quiz_participation_load_dnd_image_failure), retryButtonText = stringResource(id = R.string.quiz_participation_load_dnd_image_retry), - onClickRetry = resultData.requestRetry - ) { loadedDrawable -> + onClickRetry = { asyncImagePainter.restart() } + ) { painter -> + // TODO: verify that the image is loaded properly after re-enabling quizes: https://github.com/ls1intum/artemis-android/issues/107 + val localDensity = LocalDensity.current BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { @@ -94,25 +82,26 @@ internal fun DragAndDropWorkArea( }) } - val painter = remember(loadedDrawable, maxWidthInPx) { - val bitmap = if (loadedDrawable.intrinsicWidth < maxWidthInPx) { - val scale = maxWidthInPx / loadedDrawable.intrinsicWidth.toFloat() - val newWidth = (loadedDrawable.intrinsicWidth * scale).toInt() - val newHeight = (loadedDrawable.intrinsicHeight * scale).toInt() - - loadedDrawable - .toBitmap() - .copy(Bitmap.Config.ARGB_8888, true) - .scale(newWidth, newHeight) - .asImageBitmap() - } else { - loadedDrawable - .toBitmap() - .asImageBitmap() - } - - BitmapPainter(bitmap) - } + // TODO: check that the image is still scaled correctly: test with different image sizes +// val painter = remember(loadedDrawable, maxWidthInPx) { +// val bitmap = if (loadedDrawable.width < maxWidthInPx) { +// val scale = maxWidthInPx / loadedDrawable.width.toFloat() +// val newWidth = (loadedDrawable.width * scale).toInt() +// val newHeight = (loadedDrawable.height * scale).toInt() +// +// loadedDrawable +// .toBitmap() +// .copy(Bitmap.Config.ARGB_8888, true) +// .scale(newWidth, newHeight) +// .asImageBitmap() +// } else { +// loadedDrawable +// .toBitmap() +// .asImageBitmap() +// } +// +// BitmapPainter(bitmap) +// } Image( modifier = Modifier @@ -157,8 +146,6 @@ internal fun DragAndDropWorkArea( height = height.toDp() ), dragItem = dragItem, - serverUrl = serverUrl, - authToken = authToken, type = when (type) { is DragAndDropAreaType.Editable -> { WorkAreaDropLocationType.Editable( @@ -189,3 +176,12 @@ internal fun DragAndDropWorkArea( } } } + +fun AsyncImagePainter.State.toDataState(): DataState { + return when (this) { + is AsyncImagePainter.State.Success -> DataState.Success(this.painter) + is AsyncImagePainter.State.Loading -> DataState.Loading() + is AsyncImagePainter.State.Error -> DataState.Failure(this.result.throwable) + AsyncImagePainter.State.Empty -> DataState.Failure(IllegalStateException("The AsyncImagePainter state is Empty")) + } +} \ No newline at end of file diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/work_area/WorkAreaDropLocation.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/work_area/WorkAreaDropLocation.kt index 3eec400f6..c4d0e8ab2 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/work_area/WorkAreaDropLocation.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/question/draganddrop/body/work_area/WorkAreaDropLocation.kt @@ -25,7 +25,6 @@ import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.dragandd import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.body.DragItemDraggableContainer import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.body.DragItemUiElement import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.body.DragItemUiElementContent -import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.body.backgroundPictureServerUrl import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.body.dragItemOutlineColor import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.dragOffset import de.tum.informatics.www1.artemis.native_app.feature.quiz.question.draganddrop.dragPosition @@ -58,8 +57,6 @@ internal sealed interface WorkAreaDropLocationType { internal fun WorkAreaDropLocation( modifier: Modifier, dragItem: DragAndDropQuizQuestion.DragItem?, - serverUrl: String, - authToken: String, type: WorkAreaDropLocationType ) { val targetInfo = LocalDragTargetInfo.current.currentDragTargetInfo @@ -146,14 +143,11 @@ internal fun WorkAreaDropLocation( } if (dragItem != null) { - val pictureFilePath = dragItem.backgroundPictureServerUrl(serverUrl) - val nonDraggedElementContent = @Composable { DragItemUiElementContent( modifier = Modifier.fillMaxSize(), text = dragItem.text, - pictureFilePath = pictureFilePath, - authToken = authToken, + pictureFilePath = dragItem.pictureFilePath, fontColor = Color.Black ) } @@ -169,8 +163,7 @@ internal fun WorkAreaDropLocation( DragItemUiElement( modifier = Modifier.zIndex(20f), text = dragItem.text, - pictureFilePath = pictureFilePath, - authToken = authToken + pictureFilePath = dragItem.pictureFilePath, ) } else { nonDraggedElementContent() diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/screens/QuizQuestionBody.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/screens/QuizQuestionBody.kt index 58f862ac2..e72d68665 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/screens/QuizQuestionBody.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/screens/QuizQuestionBody.kt @@ -22,8 +22,6 @@ internal fun QuizQuestionBody( modifier: Modifier, questionIndex: Int, quizQuestionData: QuizQuestionData<*>, - serverUrl: String, - authToken: String ) { var displayQuestionHint by rememberSaveable { mutableStateOf(false) } // Store the hint text of the answer option @@ -39,8 +37,6 @@ internal fun QuizQuestionBody( questionIndex = questionIndex, question = quizQuestionData.question, data = quizQuestionData, - serverUrl = serverUrl, - authToken = authToken, onRequestDisplayHint = onRequestDisplayHint ) diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/screens/work/WorkOnQuizQuestionsScreen.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/screens/work/WorkOnQuizQuestionsScreen.kt index d60e35905..249f905ef 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/screens/work/WorkOnQuizQuestionsScreen.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/screens/work/WorkOnQuizQuestionsScreen.kt @@ -34,8 +34,6 @@ internal fun WorkOnQuizQuestionsScreen( overallPoints: Int, latestWebsocketSubmission: Result?, clock: Clock, - serverUrl: String, - authToken: String, onRequestRetrySave: () -> Unit ) { var selectedQuestionIndex by rememberSaveable(questionsWithData.size) { mutableStateOf(0) } @@ -60,8 +58,6 @@ internal fun WorkOnQuizQuestionsScreen( modifier = bodyModifier, quizQuestionData = currentQuestion, questionIndex = selectedQuestionIndex, - serverUrl = serverUrl, - authToken = authToken ) } else { Box(modifier = bodyModifier) diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/QuizResultUi.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/QuizResultUi.kt index 748ef10b9..dc0551e32 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/QuizResultUi.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/QuizResultUi.kt @@ -42,8 +42,6 @@ internal fun QuizResultUi( maxPoints: Int, quizQuestions: List, quizQuestionsWithData: List>, - serverUrl: String, - authToken: String ) { val scope = rememberCoroutineScope() val lazyListState = rememberLazyListState() @@ -108,8 +106,6 @@ internal fun QuizResultUi( .align(Alignment.Center), questionIndex = index, quizQuestionData = questionWithData, - serverUrl = serverUrl, - authToken = authToken ) } } diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/QuizResultViewModel.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/QuizResultViewModel.kt index 2d149c6e8..fc22c08aa 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/QuizResultViewModel.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/QuizResultViewModel.kt @@ -18,8 +18,6 @@ import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.quiz.DragAndDropSubmittedAnswer import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.quiz.MultipleChoiceSubmittedAnswer import de.tum.informatics.www1.artemis.native_app.core.model.exercise.submission.quiz.ShortAnswerSubmittedAnswer -import de.tum.informatics.www1.artemis.native_app.core.ui.authTokenStateFlow -import de.tum.informatics.www1.artemis.native_app.core.ui.serverUrlStateFlow import de.tum.informatics.www1.artemis.native_app.feature.quiz.AnswerOptionId import de.tum.informatics.www1.artemis.native_app.feature.quiz.BaseQuizViewModel import de.tum.informatics.www1.artemis.native_app.feature.quiz.DragAndDropStorageData @@ -208,9 +206,6 @@ internal class QuizResultViewModel( .filterSuccess() .shareIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, replay = 1) - val serverUrl: StateFlow = serverUrlStateFlow(serverConfigurationService) - val authToken: StateFlow = authTokenStateFlow(accountService) - override fun constructDragAndDropData( questionId: Long, question: DragAndDropQuizQuestion, diff --git a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt index c33d5b5d0..6362488a7 100644 --- a/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt +++ b/feature/quiz/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/quiz/view_result/ViewQuizResultScreen.kt @@ -70,9 +70,6 @@ internal fun ViewQuizResultScreen( val quizQuestions by viewModel.quizQuestionsWithData.collectAsState() val maxPoints by viewModel.maxPoints.collectAsState() - val serverUrl by viewModel.serverUrl.collectAsState() - val authToken by viewModel.authToken.collectAsState() - Scaffold( modifier = modifier, topBar = { @@ -118,8 +115,6 @@ internal fun ViewQuizResultScreen( modifier = Modifier.fillMaxSize(), quizQuestions = data.quizExercise.quizQuestions, quizQuestionsWithData = data.quizQuestions, - serverUrl = serverUrl, - authToken = authToken, achievedPoints = data.submission.scoreInPoints ?: 0.0, maxPoints = data.maxPoints, quizTitle = data.quizExercise.title.orEmpty() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69b2f109e..b6cca21df 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -15,7 +15,8 @@ androidxNavigation = "2.8.4" androidxPaging = "3.3.4" androidxDataStore = "1.1.1" androidxPagingCompose = "3.2.1" -coil = "2.7.0" +coil = "3.0.4" +coil2 = "2.7.0" # The markwon library still depends on coil2: https://github.com/ls1intum/artemis-android/issues/171 emoji2 = "1.5.0" kotlin = "1.9.25" kotlinxCoroutines = "1.9.0" @@ -81,7 +82,11 @@ androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = androidx-security-crypto = { group = "androidx.security", name = "security-crypto", version = "1.1.0-alpha06" } androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work" } auth0-java-jwt = { group = "com.auth0.android", name = "jwtdecode", version = "2.0.2" } -coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } +coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } +coil-compose-core = { group = "io.coil-kt.coil3", name = "coil-compose-core", version.ref = "coil" } +coil-test = { group = "io.coil-kt.coil3", name = "coil-test", version.ref = "coil" } +coil-network = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } +coil2-base = { group = "io.coil-kt", name = "coil-base", version.ref = "coil2" } google-firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version = "33.4.0" } google-firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging" } kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib-jdk8", version.ref = "kotlin" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 36b429ddb..5117ba42c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,7 @@ include(":core:data-test") include(":core:datastore") include(":core:model") include(":core:ui") +include(":core:ui-test") include(":core:websocket") include(":core:device") include(":core:device-test")