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 46364c562..631033c27 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 @@ -38,7 +38,6 @@ 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.GroupChat import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.OneToOneChat import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.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.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 @@ -222,8 +221,8 @@ fun `Metis - Conversation Channel`() { modifier = Modifier.fillMaxSize(), courseId = ScreenshotCourse.id!!, conversationId = sharedConversation.id, - conversationTitle = sharedConversation.humanReadableName, - isConversationLoaded = true, + conversationDataState = DataState.Success(sharedConversation), + clientId = 1L, query = "", onUpdateQuery = {}, onNavigateBack = {}, diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationChatListScreen.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationChatListScreen.kt index 5b2472fed..dccd0f32c 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationChatListScreen.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationChatListScreen.kt @@ -2,16 +2,19 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon @@ -24,28 +27,35 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider 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.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems +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.ui.common.BasicHintTextField +import de.tum.informatics.www1.artemis.native_app.core.ui.common.EmptyDataStateUi 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.reply.LocalReplyAutoCompleteHintProvider import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.shared.ConversationDataStatusButton 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.content.StandalonePostId +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.ui.ConversationIcon import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName import io.github.fornewid.placeholder.material3.placeholder + @Composable internal fun ConversationChatListScreen( modifier: Modifier, @@ -78,12 +88,7 @@ internal fun ConversationChatListScreen( val query by viewModel.chatListUseCase.query.collectAsState() val conversationDataState by viewModel.latestUpdatedConversation.collectAsState() val conversationDataStatus by viewModel.conversationDataStatus.collectAsState() - - val title by remember(conversationDataState) { - derivedStateOf { - conversationDataState.bind { it.humanReadableName }.orElse("Conversation") - } - } + val clientId by viewModel.clientIdOrDefault.collectAsState() val chatListState: LazyListState = rememberLazyListState() @@ -94,10 +99,10 @@ internal fun ConversationChatListScreen( modifier = modifier, courseId = courseId, conversationId = conversationId, - isConversationLoaded = conversationDataState.isSuccess, + clientId = clientId, + conversationDataState = conversationDataState, conversationDataStatus = conversationDataStatus, query = query, - conversationTitle = title, onNavigateBack = onNavigateBack, onNavigateToSettings = onNavigateToSettings, onUpdateQuery = viewModel.chatListUseCase::updateQuery, @@ -111,7 +116,11 @@ internal fun ConversationChatListScreen( modifier = Modifier .fillMaxSize() .imePadding() - .padding(bottom = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()), + .padding( + bottom = WindowInsets.systemBars + .asPaddingValues() + .calculateBottomPadding() + ), viewModel = viewModel, listContentPadding = PaddingValues(top = padding.calculateTopPadding()), onClickViewPost = onClickViewPost, @@ -129,10 +138,10 @@ fun ConversationChatListScreen( modifier: Modifier, courseId: Long, conversationId: Long, - conversationTitle: String, query: String, conversationDataStatus: DataStatus, - isConversationLoaded: Boolean, + conversationDataState: DataState, + clientId: Long, onNavigateBack: (() -> Unit)?, onNavigateToSettings: () -> Unit, onUpdateQuery: (String) -> Unit, @@ -169,11 +178,14 @@ fun ConversationChatListScreen( hintStyle = MaterialTheme.typography.bodyMedium ) } else { - Text( - modifier = Modifier.placeholder(!isConversationLoaded), - text = conversationTitle, - maxLines = 1 - ) + EmptyDataStateUi( + dataState = conversationDataState, + otherwise = { + Text("placeholder", Modifier.placeholder(true)) + } + ) { + ConversationTitle(conversation = it, clientId = clientId) + } } }, navigationIcon = { @@ -187,7 +199,7 @@ fun ConversationChatListScreen( } } ) { - Icon(imageVector = Icons.Default.ArrowBack, contentDescription = null) + Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } } }, @@ -212,3 +224,30 @@ fun ConversationChatListScreen( content = content ) } + + +@Composable +private fun ConversationTitle( + modifier: Modifier = Modifier, + conversation: Conversation, + clientId: Long +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + ConversationIcon( + conversation = conversation, + clientId = clientId, + showDialogOnOneToOneChatClick = true + ) + + Spacer(Modifier.width(8.dp)) + + Text( + text = conversation.humanReadableName, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} \ 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 bb2fc6123..ce4a6d43e 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 @@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState -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 @@ -73,7 +73,6 @@ internal fun MetisChatList( state: LazyListState = rememberLazyListState(), isReplyEnabled: Boolean = true, onClickViewPost: (StandalonePostId) -> Unit, - title: String? = "Replying..." ) { ReportVisibleMetisContext(remember(viewModel.metisContext) { VisiblePostList(viewModel.metisContext) }) @@ -146,6 +145,15 @@ fun MetisChatList( title: String, onFileSelected: (Uri) -> Unit ) { + val context = LocalContext.current + + // TODO: https://github.com/ls1intum/artemis-android/issues/213 +// 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, @@ -205,7 +213,7 @@ fun MetisChatList( onRequestDelete = onDeletePostDelegate, onRequestPin = onPinPostDelegate, onRequestReactWithEmoji = onRequestReactWithEmojiDelegate, - onRequestRetrySend = onRequestRetrySend + onRequestRetrySend = onRequestRetrySend, ) } } @@ -397,7 +405,7 @@ private fun DateDivider(modifier: Modifier, date: LocalDate) { horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically ) { - Divider(modifier = Modifier.weight(1f)) + HorizontalDivider(modifier = Modifier.weight(1f)) Text( text = dateAsString, @@ -405,6 +413,6 @@ private fun DateDivider(modifier: Modifier, date: LocalDate) { fontWeight = FontWeight.Bold ) - Divider(modifier = Modifier.weight(1f)) + HorizontalDivider(modifier = Modifier.weight(1f)) } } \ 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/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 d48b70f41..247cb0284 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 @@ -27,9 +27,6 @@ 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.material.icons.outlined.PushPin import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -60,8 +57,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.profile_picture.ProfilePicture -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePictureData +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 @@ -157,14 +154,11 @@ internal fun PostItem( postStatus = postStatus, authorRole = post?.authorRole, authorName = post?.authorName, - profilePictureData = ProfilePictureData.create( - userId = post?.authorId, - username = post?.authorName, - imageUrl = post?.authorImageUrl - ), + authorId = post?.authorId ?: -1, + authorImageUrl = post?.authorImageUrl, creationDate = post?.creationDate, expanded = isExpanded, - displayHeader = displayHeader + displayHeader = displayHeader, ) { Column( modifier = Modifier.fillMaxWidth(), @@ -238,7 +232,8 @@ private fun PostHeadline( modifier: Modifier, authorRole: UserRole?, authorName: String?, - profilePictureData: ProfilePictureData, + authorId: Long, + authorImageUrl: String?, creationDate: Instant?, postStatus: CreatePostService.Status, expanded: Boolean = false, @@ -252,7 +247,10 @@ private fun PostHeadline( horizontalArrangement = Arrangement.spacedBy(16.dp) ) { HeadlineProfilePicture( - profilePictureData = profilePictureData, + userId = authorId, + userName = authorName.orEmpty(), + imageUrl = authorImageUrl, + userRole = authorRole, ) HeadlineAuthorInfo( @@ -274,7 +272,10 @@ private fun PostHeadline( val doDisplayHeader = displayHeader || postStatus == CreatePostService.Status.FAILED HeadlineProfilePicture( - profilePictureData = profilePictureData, + userId = authorId, + userName = authorName.orEmpty(), + imageUrl = authorImageUrl, + userRole = authorRole, displayImage = doDisplayHeader ) @@ -380,7 +381,7 @@ private fun AuthorRoleAndNameRow( Row( verticalAlignment = Alignment.CenterVertically, ) { - HeadlineAuthorIcon(authorRole) + UserRoleIcon(userRole = authorRole) Spacer(modifier = Modifier.width(4.dp)) @@ -396,7 +397,10 @@ private fun AuthorRoleAndNameRow( @Composable private fun HeadlineProfilePicture( - profilePictureData: ProfilePictureData, + userId: Long, + userName: String, + imageUrl: String?, + userRole: UserRole?, displayImage: Boolean = true ) { val size = 30.dp @@ -405,32 +409,17 @@ private fun HeadlineProfilePicture( return } - ProfilePicture( - modifier = Modifier - .size(size) - .clip(MaterialTheme.shapes.extraSmall), - profilePictureData = profilePictureData, + ProfilePictureWithDialog( + modifier = Modifier.size(size), + userId = userId, + userName = userName, + userRole = userRole, + imageUrl = imageUrl, ) } } -@Composable -private fun HeadlineAuthorIcon( - authorRole: UserRole?, -) { - val icon = when (authorRole) { - UserRole.INSTRUCTOR -> Icons.Default.School - UserRole.TUTOR -> Icons.Default.SupervisorAccount - UserRole.USER -> Icons.Default.Person - null -> Icons.Default.Person - } - Icon( - modifier = Modifier.size(16.dp), - imageVector = icon, - contentDescription = null - ) -} /** * 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 aa7f1b3e5..866d5b093 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 @@ -36,7 +36,7 @@ internal fun PostWithBottomSheet( onLongClick = { displayBottomSheet = true }, - onRequestRetrySend = postActions.onRequestRetrySend + onRequestRetrySend = postActions.onRequestRetrySend, ) if (displayBottomSheet && post != null) { @@ -49,4 +49,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/post/post_actions/PostActions.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/post_actions/PostActions.kt index 06b8b5776..d417e280a 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/post_actions/PostActions.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/post_actions/PostActions.kt @@ -67,7 +67,7 @@ fun rememberPostActions( onReplyInThread = if (doesPostExistOnServer) onReplyInThread else null, onResolvePost = if (hasResolvePostRights) onResolvePost else null, onPinPost = if (hasPinPostRights) onPinPost else null, - onRequestRetrySend = onRequestRetrySend + onRequestRetrySend = onRequestRetrySend, ) } } 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/ConversationProfilePictureUiTest.kt similarity index 88% rename from feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationProfilePictureE2eTest.kt rename to feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationProfilePictureUiTest.kt index 48bc443d4..326b85443 100644 --- 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/ConversationProfilePictureUiTest.kt @@ -6,6 +6,7 @@ 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.test.performClick 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 @@ -23,6 +24,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.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 de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.TEST_TAG_USER_PROFILE_DIALOG import kotlinx.coroutines.CompletableDeferred import org.junit.Before import org.junit.Test @@ -105,6 +107,30 @@ class ConversationProfilePictureUiTest : BaseComposeTest() { ) } + @Test + fun `test GIVEN a post with a profile picture WHEN clicking on it THEN the user profile dialog is 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 + ) + + composeTestRule.onNodeWithTag(TEST_TAG_PROFILE_PICTURE_INITIALS) + .performClick() + + composeTestRule.onNodeWithTag(TEST_TAG_USER_PROFILE_DIALOG) + .assertExists() + } + private fun setupUi( post: IStandalonePost ) { diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/common/ChannelIcons.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/common/ChannelIcons.kt deleted file mode 100644 index 0bc05f25a..000000000 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/common/ChannelIcons.kt +++ /dev/null @@ -1,49 +0,0 @@ -package de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.common - -import androidx.compose.foundation.layout.Row -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Archive -import androidx.compose.material.icons.filled.Campaign -import androidx.compose.material.icons.filled.Lock -import androidx.compose.material.icons.filled.Numbers -import androidx.compose.material3.Icon -import androidx.compose.runtime.Composable -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat - -@Composable -internal fun ChannelIcons(channelChat: ChannelChat) { - Row { - PrimaryChannelIcon(channelChat) - - ExtraChannelIcons(channelChat) - } -} - -@Composable -internal fun ExtraChannelIcons(channelChat: ChannelChat) { - Row { - if (channelChat.isArchived) { - Icon( - imageVector = Icons.Default.Archive, - contentDescription = null - ) - } - - if (channelChat.isAnnouncementChannel) { - Icon( - imageVector = Icons.Default.Campaign, - contentDescription = null - ) - } - } - -} - -@Composable -internal fun PrimaryChannelIcon(channelChat: ChannelChat) { - Icon( - imageVector = if (channelChat.isPublic) Icons.Default.Numbers else Icons.Default.Lock, - contentDescription = null - ) -} - diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsScreen.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsScreen.kt index 1ff3fa51a..c3f34ca89 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsScreen.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsScreen.kt @@ -39,8 +39,8 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi import de.tum.informatics.www1.artemis.native_app.core.ui.compose.NavigationBackButton import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.R -import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.common.ChannelIcons import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.ChannelChatIcon import kotlinx.coroutines.Deferred import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -156,7 +156,7 @@ private fun ChannelChatItem(channelChat: ChannelChat, onClick: () -> Unit) { ListItem( modifier = Modifier.fillMaxWidth(), leadingContent = { - ChannelIcons(channelChat) + ChannelChatIcon(channelChat = channelChat) }, headlineContent = { Text(channelChat.name) }, supportingContent = { diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_personal_conversation/PotentialRecipientsUi.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_personal_conversation/PotentialRecipientsUi.kt index 779486e8e..37c05d9bf 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_personal_conversation/PotentialRecipientsUi.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_personal_conversation/PotentialRecipientsUi.kt @@ -34,6 +34,8 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateU import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.R import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.member_selection.MemberSelectionBaseViewModel import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePicture +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePictureData internal fun testTagForPotentialRecipient(username: String) = "potentialRecipient$username" @@ -123,6 +125,11 @@ private fun PotentialRecipientsList( modifier = Modifier .fillMaxWidth() .testTag(testTagForPotentialRecipient(user.username.orEmpty())), + leadingContent = { + ProfilePicture( + profilePictureData = ProfilePictureData.fromAccount(user) + ) + }, headlineContent = { val username = remember(user) { user.humanReadableName } diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/member_selection/MemberSelection.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/member_selection/MemberSelection.kt index f6bb5412b..12fcaf77d 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/member_selection/MemberSelection.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/member_selection/MemberSelection.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Divider +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme @@ -65,7 +65,7 @@ internal fun MemberSelection( onRemoveRecipient = viewModel::removeRecipient ) - Divider() + HorizontalDivider() PotentialRecipientsUi( modifier = Modifier diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt index 65b0ff1f5..4e01323d1 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt @@ -25,7 +25,6 @@ import androidx.compose.material.icons.filled.ChatBubble import androidx.compose.material.icons.filled.Favorite import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.Forum -import androidx.compose.material.icons.filled.Groups2 import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material.icons.filled.NotInterested import androidx.compose.material.icons.filled.NotificationsActive @@ -43,6 +42,7 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text 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 @@ -55,12 +55,11 @@ import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.R -import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.common.ExtraChannelIcons -import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.common.PrimaryChannelIcon import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.Conversation import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.GroupChat import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.OneToOneChat +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.ConversationIcon import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName internal const val TEST_TAG_CONVERSATION_LIST = "conversation list" @@ -98,6 +97,7 @@ internal fun ConversationList( onToggleMuted: (conversationId: Long, muted: Boolean) -> Unit, trailingContent: LazyListScope.() -> Unit ) { + val clientId by viewModel.clientIdOrDefault.collectAsState() val listWithHeader: LazyListScope.(ConversationCollections.ConversationCollection<*>, String, String, Int, () -> Unit, @Composable () -> Unit) -> Unit = { collection, key, suffix, textRes, onClick, icon -> @@ -112,6 +112,7 @@ internal fun ConversationList( conversationList( keySuffix = suffix, conversations = collection, + clientId = clientId, showPrefix = collection.showPrefix, onNavigateToConversation = onNavigateToConversation, onToggleMarkAsFavourite = onToggleMarkAsFavourite, @@ -261,6 +262,7 @@ private fun LazyListScope.conversationSectionHeader( private fun LazyListScope.conversationList( keySuffix: String, conversations: ConversationCollections.ConversationCollection, + clientId: Long, showPrefix: Boolean, onNavigateToConversation: (conversationId: Long) -> Unit, onToggleMarkAsFavourite: (conversationId: Long, favorite: Boolean) -> Unit, @@ -279,6 +281,7 @@ private fun LazyListScope.conversationList( .testTag(itemTag), itemBaseTag = itemTag, conversation = conversation, + clientId = clientId, showPrefix = showPrefix, onNavigateToConversation = { onNavigateToConversation(conversation.id) }, onToggleMarkAsFavourite = { @@ -298,6 +301,7 @@ private fun ConversationListItem( modifier: Modifier = Modifier, itemBaseTag: String, conversation: Conversation, + clientId: Long, showPrefix: Boolean, onNavigateToConversation: () -> Unit, onToggleMarkAsFavourite: () -> Unit, @@ -339,59 +343,25 @@ private fun ConversationListItem( } Box(modifier = modifier) { - when (conversation) { - is ChannelChat -> { - ListItem( - modifier = Modifier.clickable(onClick = onNavigateToConversation), - leadingContent = { - PrimaryChannelIcon(channelChat = conversation) - }, - headlineContent = { - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - Text(text = displayName, maxLines = 1, color = headlineColor) - - ExtraChannelIcons(channelChat = conversation) - } - }, - trailingContent = { - UnreadMessages(unreadMessagesCount = unreadMessagesCount) - } + ListItem( + modifier = Modifier.clickable(onClick = onNavigateToConversation), + leadingContent = { + ConversationIcon( + conversation = conversation, + clientId = clientId ) - } - - is GroupChat -> { - ListItem( - modifier = Modifier.clickable(onClick = onNavigateToConversation), - headlineContent = { - Text( - displayName, - color = headlineColor - ) - }, - leadingContent = { - Icon(imageVector = Icons.Default.Groups2, contentDescription = null) - }, - trailingContent = { - UnreadMessages(unreadMessagesCount = unreadMessagesCount) - } - ) - } - - is OneToOneChat -> { - ListItem( - modifier = Modifier.clickable(onClick = onNavigateToConversation), - headlineContent = { - Text( - displayName, - color = headlineColor - ) - }, - trailingContent = { - UnreadMessages(unreadMessagesCount = unreadMessagesCount) - } + }, + headlineContent = { + Text( + text = displayName, + maxLines = 1, + color = headlineColor ) + }, + trailingContent = { + UnreadMessages(unreadMessagesCount = unreadMessagesCount) } - } + ) IconButton( modifier = Modifier @@ -403,7 +373,7 @@ private fun ConversationListItem( } ConversationListItemDropdownMenu( - modifier = Modifier.Companion.align(Alignment.TopEnd), + modifier = Modifier.align(Alignment.TopEnd), isContextDialogShown = isContextDialogShown, onDismissRequest = onDismissRequest, conversation = conversation, diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/settings/ConversationMemberListItem.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/settings/ConversationMemberListItem.kt index 895d34dbb..f3133d906 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/settings/ConversationMemberListItem.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/settings/ConversationMemberListItem.kt @@ -1,27 +1,29 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.settings +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AddModerator import androidx.compose.material.icons.filled.GroupRemove -import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.RemoveModerator -import androidx.compose.material.icons.filled.School -import androidx.compose.material.icons.filled.Shield -import androidx.compose.material.icons.filled.SupervisorAccount import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.Text 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.unit.dp import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.R import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.Conversation import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ConversationUser import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.hasModerationRights +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.ConversationUserRoleIndicators import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePictureWithDialog @Composable internal fun ConversationMemberListItem( @@ -36,7 +38,17 @@ internal fun ConversationMemberListItem( ListItem( modifier = modifier, headlineContent = { - Text(text = member.humanReadableName) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + ConversationUserRoleIndicators( + modifier = Modifier.size(20.dp), + user = member + ) + + Text(text = member.humanReadableName) + } }, supportingContent = member.username?.let { username -> { @@ -44,32 +56,9 @@ internal fun ConversationMemberListItem( } }, leadingContent = { - Row { - val personIcon = when { - member.isInstructor -> Icons.Default.School - member.isEditor || member.isTeachingAssistant -> Icons.Default.SupervisorAccount - else -> Icons.Default.Person - } - - val contentDescription = when { - member.isInstructor -> R.string.conversation_members_content_description_instructor - member.isEditor -> R.string.conversation_members_content_description_editor - member.isTeachingAssistant -> R.string.conversation_members_content_description_teaching_assistant - else -> R.string.conversation_members_content_description_student - } - - Icon( - imageVector = personIcon, - contentDescription = stringResource(id = contentDescription) - ) - - if (member.isChannelModerator) { - Icon( - imageVector = Icons.Default.Shield, - contentDescription = stringResource(id = R.string.conversation_members_content_description_moderator) - ) - } - } + ProfilePictureWithDialog( + conversationUser = member + ) }, trailingContent = { if (member.username != clientUsername && conversation.hasModerationRights) { diff --git a/feature/metis/manage-conversations/src/main/res/values/conversation_members_strings.xml b/feature/metis/manage-conversations/src/main/res/values/conversation_members_strings.xml index fdfc1e8b2..74c7d3174 100644 --- a/feature/metis/manage-conversations/src/main/res/values/conversation_members_strings.xml +++ b/feature/metis/manage-conversations/src/main/res/values/conversation_members_strings.xml @@ -7,13 +7,6 @@ Something went wrong while loading conversation members. Try again - Instructor - Editor - Teaching assistant - Student - - Moderator - Remove user Add moderator Remove moderator diff --git a/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/members/ConversationMemberSettingsE2eTest.kt b/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/members/ConversationMemberSettingsE2eTest.kt index 67f15900d..18a695de0 100644 --- a/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/members/ConversationMemberSettingsE2eTest.kt +++ b/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/members/ConversationMemberSettingsE2eTest.kt @@ -38,6 +38,7 @@ import org.koin.mp.KoinPlatformTools import org.koin.test.get import org.robolectric.RobolectricTestRunner import kotlin.time.Duration.Companion.seconds +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.R as R_shared @OptIn(ExperimentalTestApi::class) @Category(EndToEndTest::class) @@ -81,7 +82,7 @@ class ConversationMemberSettingsE2eTest : ConversationBaseTest() { .onNodeWithTag(testTagForMember(user1Username)) .performScrollTo() .assert(hasText(user1Username)) - .assert(hasContentDescription(context.getString(R.string.conversation_members_content_description_moderator)) + .assert(hasContentDescription(context.getString(R_shared.string.user_role_icon_content_description_moderator)) ) composeTestRule @@ -89,7 +90,7 @@ class ConversationMemberSettingsE2eTest : ConversationBaseTest() { .performScrollTo() .assert( hasText(user2Username) and ! - hasContentDescription(context.getString(R.string.conversation_members_content_description_moderator)) + hasContentDescription(context.getString(R_shared.string.user_role_icon_content_description_moderator)) ) composeTestRule @@ -97,7 +98,7 @@ class ConversationMemberSettingsE2eTest : ConversationBaseTest() { .performScrollTo() .assert( hasText(user2Username) and ! - hasContentDescription(context.getString(R.string.conversation_members_content_description_moderator)) + hasContentDescription(context.getString(R_shared.string.user_role_icon_content_description_moderator)) ) } @@ -178,7 +179,7 @@ class ConversationMemberSettingsE2eTest : ConversationBaseTest() { .performClick() val isModeratorCheck = hasContentDescription( - context.getString(R.string.conversation_members_content_description_moderator) + context.getString(R_shared.string.user_role_icon_content_description_moderator) ) composeTestRule.waitUntilExactlyOneExists( diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/ConversationUser.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/ConversationUser.kt index 3729aca9b..db9df5a47 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/ConversationUser.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/ConversationUser.kt @@ -1,6 +1,7 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation import de.tum.informatics.www1.artemis.native_app.core.model.account.BaseAccount +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.UserRole import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -24,4 +25,13 @@ data class ConversationUser( val isEditor: Boolean = false, val isTeachingAssistant: Boolean = false, val isStudent: Boolean = false -) : BaseAccount \ No newline at end of file +) : BaseAccount { + + fun getUserRole(): UserRole { + return when { + isInstructor -> UserRole.INSTRUCTOR + isTeachingAssistant || isEditor -> UserRole.TUTOR + else -> UserRole.USER + } + } +} \ 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/shared_conversation_module.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/shared_conversation_module.kt index 0242818c8..75cc1a3ab 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/shared_conversation_module.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/shared_conversation_module.kt @@ -2,6 +2,8 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.shared import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.service.network.ConversationService import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.service.network.impl.ConversationServiceImpl +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.UserProfileDialogViewModel +import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.dsl.module val sharedConversationModule = module { @@ -10,4 +12,8 @@ val sharedConversationModule = module { get() ) } + + viewModel { params -> + UserProfileDialogViewModel(params[0], params[1], get(), get(), get(), get(), get(), get()) + } } \ 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/ConversationIcon.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/ConversationIcon.kt new file mode 100644 index 000000000..a4396300b --- /dev/null +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/ConversationIcon.kt @@ -0,0 +1,89 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Archive +import androidx.compose.material.icons.filled.Campaign +import androidx.compose.material.icons.filled.Groups2 +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Numbers +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.Conversation +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.GroupChat +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.OneToOneChat +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePicture +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePictureData +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePictureWithDialog + +@Composable +fun ConversationIcon( + modifier: Modifier = Modifier, + conversation: Conversation, + clientId: Long, + showDialogOnOneToOneChatClick: Boolean = false +) { + when(conversation) { + is ChannelChat -> ChannelChatIcon(modifier, conversation) + is GroupChat -> GroupChatIcon(modifier) + is OneToOneChat -> OneToOneChatIcon(modifier, conversation, clientId, showDialogOnOneToOneChatClick) + } +} + + +@Composable +fun ChannelChatIcon( + modifier: Modifier = Modifier, + channelChat: ChannelChat +) { + Icon( + modifier = modifier, + imageVector = getChannelIconImageVector(channelChat), + contentDescription = null + ) +} + +private fun getChannelIconImageVector(channelChat: ChannelChat): ImageVector { + if (channelChat.isArchived) { + return Icons.Default.Archive + } + if (channelChat.isAnnouncementChannel) { + return Icons.Default.Campaign + } + if (channelChat.isPublic) { + return Icons.Default.Numbers + } + return Icons.Default.Lock +} + +@Composable +fun GroupChatIcon(modifier: Modifier) { + Icon( + modifier = modifier, + imageVector = Icons.Default.Groups2, + contentDescription = null + ) +} + +@Composable +fun OneToOneChatIcon( + modifier: Modifier, + oneToOneChat: OneToOneChat, + clientId: Long, + showDialogOnClick: Boolean = false +) { + val conversationPartner = oneToOneChat.members.first { it.id != clientId } + if (showDialogOnClick) { + ProfilePictureWithDialog( + modifier = modifier, + conversationUser = conversationPartner + ) + } else { + ProfilePicture( + modifier = modifier, + profilePictureData = ProfilePictureData.fromAccount(conversationPartner) + ) + } +} 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..60fc8d938 --- /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,72 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +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.Shield +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.res.stringResource +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.content.dto.conversation.ConversationUser + + +@Composable +fun ConversationUserRoleIndicators( + modifier: Modifier = Modifier, + user: ConversationUser, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + UserRoleIcon( + modifier = modifier, + userRole = user.getUserRole() + ) + + if (user.isChannelModerator) { + Icon( + modifier = modifier, + imageVector = Icons.Default.Shield, + contentDescription = stringResource(id = R.string.user_role_icon_content_description_moderator) + ) + } + } +} + +@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 + } + + val contentDescription = when(userRole) { + UserRole.INSTRUCTOR -> R.string.user_role_icon_content_description_instructor + UserRole.TUTOR -> R.string.user_role_icon_content_description_teaching_assistant + UserRole.USER -> R.string.user_role_icon_content_description_student + null -> R.string.user_role_icon_content_description_student + } + + Box { + Icon( + modifier = modifier + .size(16.dp), + imageVector = icon, + contentDescription = stringResource(id = contentDescription) + ) + } +} \ 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 index 5e9de3361..9ec44a4cf 100644 --- 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 @@ -3,25 +3,35 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profi 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 +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ConversationUser +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName const val TEST_TAG_PROFILE_PICTURE_IMAGE = "TEST_TAG_PROFILE_PICTURE_IMAGE" const val TEST_TAG_PROFILE_PICTURE_INITIALS = "TEST_TAG_PROFILE_PICTURE_INITIALS" @@ -30,30 +40,79 @@ const val TEST_TAG_PROFILE_PICTURE_UNKNOWN = "TEST_TAG_PROFILE_PICTURE_UNKNOWN" private const val BoxSizeToFontSizeMultiplier = 0.16f +@Composable +fun ProfilePictureWithDialog( + modifier: Modifier = Modifier, + conversationUser: ConversationUser, +) = ProfilePictureWithDialog( + modifier = modifier, + userId = conversationUser.id, + userName = conversationUser.humanReadableName, + userRole = conversationUser.getUserRole(), + imageUrl = conversationUser.imageUrl +) + + +@Composable +fun ProfilePictureWithDialog( + modifier: Modifier = Modifier, + userId: Long, + userName: String, + userRole: UserRole?, + imageUrl: String?, +) { + 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 + }, + ) + } +} + @Composable fun ProfilePicture( - modifier: Modifier, + modifier: Modifier = Modifier, profilePictureData: ProfilePictureData, ) { - // TODO: Add onClick that opens a dialog with info about the user, see iOS - // https://github.com/ls1intum/artemis-android/issues/154 + val modifierWithDefault = modifier + .size(30.dp) + .clip(shape = RoundedCornerShape(percent = 15)) when(profilePictureData) { is ProfilePictureData.Image -> { ProfilePictureImage( - modifier = modifier, + modifier = modifierWithDefault, profilePictureData = profilePictureData, ) } is ProfilePictureData.InitialsPlaceholder -> { InitialsPlaceholder( - modifier = modifier.testTag(TEST_TAG_PROFILE_PICTURE_INITIALS), + modifier = modifierWithDefault.testTag(TEST_TAG_PROFILE_PICTURE_INITIALS), profilePictureData = profilePictureData, ) } ProfilePictureData.Unknown -> { InitialsPlaceholder( - modifier = modifier.testTag(TEST_TAG_PROFILE_PICTURE_UNKNOWN), + modifier = modifierWithDefault.testTag(TEST_TAG_PROFILE_PICTURE_UNKNOWN), profilePictureData = ProfilePictureData.InitialsPlaceholder(0, "?"), ) } 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 index f59728cfa..8fbc7c07a 100644 --- 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 @@ -1,10 +1,18 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture import androidx.compose.ui.graphics.Color +import de.tum.informatics.www1.artemis.native_app.core.model.account.BaseAccount +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName sealed class ProfilePictureData { companion object { + fun fromAccount(user: BaseAccount): ProfilePictureData = create( + userId = user.id, + username = user.humanReadableName, + imageUrl = user.imageUrl + ) + fun create(userId: Long?, username: String?, imageUrl: String?): ProfilePictureData { if (userId == null || username.isNullOrEmpty()) { return Unknown 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..3f8bb8c99 --- /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,165 @@ +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.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.platform.testTag +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.content.dto.conversation.ConversationUser +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.humanReadableName +import org.koin.androidx.compose.koinViewModel +import org.koin.core.parameter.parametersOf + + +const val TEST_TAG_USER_PROFILE_DIALOG = "TEST_TAG_USER_PROFILE_DIALOG" + + +@Composable +// TODO: currently unused, due to https://github.com/ls1intum/artemis-android/issues/213 +fun UserProfileDialog( + courseId: Long, + user: ConversationUser, + onDismiss: () -> Unit, +) { + val viewModel = koinViewModel() { + parametersOf(courseId, user.id) + } + + UserProfileDialog( + username = user.humanReadableName, + userRole = user.getUserRole(), + profilePictureData = ProfilePictureData.fromAccount(user), + onDismiss = onDismiss + ) +} + + +@Composable +fun UserProfileDialog( + username: String, + userRole: UserRole?, + profilePictureData: ProfilePictureData, + onDismiss: () -> Unit, +) { + AlertDialog( + modifier = Modifier.testTag(TEST_TAG_USER_PROFILE_DIALOG), + onDismissRequest = onDismiss, + title = { + UserProfileDialogHeader( + username = username, + userRole = userRole, + profilePictureData = profilePictureData, + ) + }, + text = { + // TODO: Due to API changes on the backend, this feature has been postponed. + // See https://github.com/ls1intum/artemis-android/issues/213 +// 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 = {}, + ) + } +} \ 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/UserProfileDialogViewModel.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/UserProfileDialogViewModel.kt new file mode 100644 index 000000000..e98375106 --- /dev/null +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ui/profile_picture/UserProfileDialogViewModel.kt @@ -0,0 +1,53 @@ +package de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.lifecycle.viewModelScope +import de.tum.informatics.www1.artemis.native_app.core.data.DataState +import de.tum.informatics.www1.artemis.native_app.core.data.service.network.AccountDataService +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.device.NetworkStatusProvider +import de.tum.informatics.www1.artemis.native_app.core.websocket.WebsocketProvider +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.MetisViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.plus +import kotlin.coroutines.CoroutineContext + +class UserProfileDialogViewModel( + private val courseId: Long, + private val userId: Long, + serverConfigurationService: ServerConfigurationService, + accountService: AccountService, + private val accountDataService: AccountDataService, + private val networkStatusProvider: NetworkStatusProvider, + websocketProvider: WebsocketProvider, + private val coroutineContext: CoroutineContext +) : MetisViewModel( + serverConfigurationService, + accountService, + accountDataService, + networkStatusProvider, + websocketProvider, + coroutineContext +) { + + val isShownUserTheAppUser: StateFlow> = clientId + .map { dataState -> + dataState.bind { + it == userId + } + } + .stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, DataState.Loading()) + + fun navigateToOneToOneChat(context: Context) { + // TODO: This does currently not work yet, see: https://github.com/ls1intum/artemis-android/issues/213 + val chatLink = "artemis://courses/$courseId/messages?userId=$userId" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(chatLink)) + context.startActivity(intent) + } +} \ No newline at end of file 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/main/res/values/user_role_icons_strings.xml b/feature/metis/shared/src/main/res/values/user_role_icons_strings.xml new file mode 100644 index 000000000..d488c1135 --- /dev/null +++ b/feature/metis/shared/src/main/res/values/user_role_icons_strings.xml @@ -0,0 +1,9 @@ + + + Instructor + Editor + Teaching assistant + Student + + Moderator + \ No newline at end of file diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index dd05f2bb5..4e76737dd 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(libs.placeholder.material) implementation(libs.androidx.browser) + implementation(project(":feature:metis:shared")) testImplementation(project(":feature:login")) testImplementation(project(":feature:login-test")) diff --git a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt index 15cc9e7a6..e7887c7aa 100644 --- a/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/settings/SettingsScreen.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.asPaddingValues 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.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -19,7 +20,6 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.AlternateEmail import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material.icons.filled.Mail -import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.Icon @@ -34,6 +34,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector @@ -54,6 +55,8 @@ import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvi import de.tum.informatics.www1.artemis.native_app.core.model.account.Account import de.tum.informatics.www1.artemis.native_app.core.ui.LocalLinkOpener import de.tum.informatics.www1.artemis.native_app.core.ui.common.EmptyDataStateUi +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePicture +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.profile_picture.ProfilePictureData import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationConfigurationService import de.tum.informatics.www1.artemis.native_app.feature.push.service.PushNotificationJobService import de.tum.informatics.www1.artemis.native_app.feature.push.unsubscribeFromNotifications @@ -129,26 +132,15 @@ private fun SettingsScreen( val pushNotificationConfigurationService: PushNotificationConfigurationService = koinInject() val accountService: AccountService = koinInject() - val authenticationData: AccountService.AuthenticationData? by accountService.authenticationData.collectAsState( - initial = null - ) val serverConfigurationService: ServerConfigurationService = koinInject() val serverUrl by serverConfigurationService.serverUrl.collectAsState(initial = "") val scope = rememberCoroutineScope() - // The auth token if logged in or null otherwise - val authToken: String? = - (authenticationData as? AccountService.AuthenticationData.LoggedIn)?.authToken val hasUserSelectedInstance by serverConfigurationService.hasUserSelectedInstance.collectAsState( initial = false ) - val username = when (val authData = authenticationData) { - is AccountService.AuthenticationData.LoggedIn -> authData.username - else -> null - } - val accountDataService: AccountDataService = koinInject() val networkStatusProvider: NetworkStatusProvider = koinInject() @@ -199,11 +191,10 @@ private fun SettingsScreen( ), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - if (authToken != null) { + accountData?.let { UserInformationSection( modifier = Modifier.fillMaxWidth(), - authData = accountData, - username = username, + accountDataState = it, onRequestLogout = { scope.launch { // the user manually logs out. Therefore we need to tell the server asap. @@ -258,8 +249,7 @@ private fun SettingsScreen( @Composable private fun UserInformationSection( modifier: Modifier, - username: String?, - authData: DataState?, + accountDataState: DataState, onRequestLogout: () -> Unit ) { PreferenceSection( @@ -268,47 +258,50 @@ private fun UserInformationSection( ) { val childModifier = Modifier.fillMaxWidth() - if (authData != null && username != null) { - EmptyDataStateUi( - dataState = authData, - otherwise = { - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - ) - } - ) { account -> - PreferenceEntry( - modifier = childModifier, - icon = Icons.Default.Person, - text = stringResource( - id = R.string.settings_account_information_full_name, - ), - valueText = account.name.orEmpty(), - onClick = {} + EmptyDataStateUi( + dataState = accountDataState, + otherwise = { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) ) + } + ) { account -> + PreferenceEntry( + modifier = childModifier, + leadingContent = { + ProfilePicture( + modifier = Modifier.size(24.dp), + profilePictureData = ProfilePictureData.fromAccount(account) + ) + }, + text = stringResource( + id = R.string.settings_account_information_full_name, + ), + valueText = account.name, + onClick = {} + ) - PreferenceEntry( - modifier = childModifier, - icon = Icons.Filled.AlternateEmail, - text = stringResource( - id = R.string.settings_account_information_login, - ), - valueText = username, - onClick = {} - ) + PreferenceEntry( + modifier = childModifier, + icon = Icons.Filled.AlternateEmail, + text = stringResource( + id = R.string.settings_account_information_login, + ), + valueText = account.username, + onClick = {} + ) - PreferenceEntry( - modifier = childModifier, - icon = Icons.Default.Mail, - text = stringResource( - id = R.string.settings_account_information_email - ), - valueText = account.email.orEmpty(), - onClick = {} - ) - } + PreferenceEntry( + modifier = childModifier, + icon = Icons.Default.Mail, + text = stringResource( + id = R.string.settings_account_information_email + ), + valueText = account.email, + onClick = {} + ) } ButtonEntry( @@ -435,24 +428,38 @@ private fun BuildInformationSection( fun PreferenceEntry( modifier: Modifier = Modifier, text: String, - icon: ImageVector? = null, + icon: ImageVector, + valueText: String? = null, + onClick: () -> Unit, +) = PreferenceEntry( + modifier = modifier, + text = text, + leadingContent = { + Icon(icon, contentDescription = null) + }, + valueText = valueText, + onClick = onClick +) + +@Composable +fun PreferenceEntry( + modifier: Modifier = Modifier, + text: String, + leadingContent: @Composable (() -> Unit)? = null, valueText: String? = null, onClick: () -> Unit, ) { Row( modifier = modifier .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 10.dp) + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, ) { Row( - horizontalArrangement = Arrangement.spacedBy(10.dp) + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), ) { - icon?.let { - Icon( - imageVector = it, - contentDescription = null - ) - } + leadingContent?.invoke() Text( text = text,