diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dfd3d79759..2ad27323b1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -151,6 +151,7 @@ knit { dependencies { implementation(project(":libraries:designsystem")) implementation(project(":libraries:matrix")) + implementation(project(":libraries:matrixui")) implementation(project(":libraries:core")) implementation(project(":features:onboarding")) implementation(project(":features:login")) diff --git a/app/src/main/java/io/element/android/x/di/AppBindings.kt b/app/src/main/java/io/element/android/x/di/AppBindings.kt index 2db4459c9e..f330346e80 100644 --- a/app/src/main/java/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/java/io/element/android/x/di/AppBindings.kt @@ -18,11 +18,13 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.x.matrix.Matrix +import io.element.android.x.matrix.ui.MatrixUi import kotlinx.coroutines.CoroutineScope @ContributesTo(AppScope::class) interface AppBindings { fun coroutineScope(): CoroutineScope fun matrix(): Matrix + fun matrixUi(): MatrixUi fun sessionComponentsOwner(): SessionComponentsOwner } diff --git a/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt b/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt index f9bc07dce5..686e656e56 100644 --- a/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt +++ b/app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt @@ -35,19 +35,18 @@ class CoilInitializer : Initializer { private class ElementImageLoaderFactory( private val context: Context -) : - ImageLoaderFactory { +) : ImageLoaderFactory { override fun newImageLoader(): ImageLoader { return ImageLoader .Builder(context) .components { val appBindings = context.bindings() - val matrix = appBindings.matrix() + val matrixUi = appBindings.matrixUi() val matrixClientProvider = { appBindings .sessionComponentsOwner().activeSessionComponent?.matrixClient() } - matrix.registerCoilComponents(this, matrixClientProvider) + matrixUi.registerCoilComponents(this, matrixClientProvider) } .build() } diff --git a/app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt b/app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt index ada89705da..fdccf51078 100644 --- a/app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt +++ b/app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt @@ -18,9 +18,9 @@ package io.element.android.x.initializer import android.content.Context import androidx.startup.Initializer +import io.element.android.x.BuildConfig import io.element.android.x.matrix.tracing.TracingConfigurations import io.element.android.x.matrix.tracing.setupTracing -import io.element.android.x.sdk.matrix.BuildConfig class MatrixInitializer : Initializer { diff --git a/build.gradle.kts b/build.gradle.kts index b20b83477b..d55faf4842 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -95,6 +95,8 @@ allprojects { "experimental:kdoc-wrapping", // Ignore error "Redundant curly braces", since we use it to fix false positives, for instance in "elementLogs.${i}.txt" "string-template", + // Not the same order than Android Studio formatter... + "import-ordering", ) ) } diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index d0c44cfea5..bf488500a3 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(project(":libraries:di")) implementation(project(":libraries:core")) implementation(project(":libraries:matrix")) + implementation(project(":libraries:matrixui")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:textcomposer")) implementation(libs.mavericks.compose) diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt index f2779dd8aa..68bf073de7 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt @@ -17,7 +17,6 @@ package io.element.android.x.features.messages import androidx.recyclerview.widget.DiffUtil -import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.features.messages.diff.CacheInvalidator import io.element.android.x.features.messages.diff.MatrixTimelineItemsDiffCallback @@ -34,11 +33,10 @@ import io.element.android.x.features.messages.model.content.MessagesTimelineItem import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextContent import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent import io.element.android.x.features.messages.util.invalidateLast -import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.timeline.MatrixTimelineItem -import kotlin.system.measureTimeMillis +import io.element.android.x.matrix.ui.MatrixItemHelper import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -52,9 +50,10 @@ import org.matrix.rustcomponents.sdk.FormattedBody import org.matrix.rustcomponents.sdk.MessageFormat import org.matrix.rustcomponents.sdk.MessageType import timber.log.Timber +import kotlin.system.measureTimeMillis class MessageTimelineItemStateFactory( - private val client: MatrixClient, + private val matrixItemHelper: MatrixItemHelper, private val room: MatrixRoom, private val dispatcher: CoroutineDispatcher, ) { @@ -153,7 +152,11 @@ class MessageTimelineItemStateFactory( val senderDisplayName = room.userDisplayName(currentSender).getOrNull() val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull() val senderAvatarData = - loadAvatarData(senderDisplayName ?: currentSender, senderAvatarUrl) + matrixItemHelper.loadAvatarData( + name = senderDisplayName ?: currentSender, + url = senderAvatarUrl, + size = AvatarSize.SMALL + ) return MessagesTimelineItemState.MessageEvent( id = currentTimelineItem.uniqueId, senderId = currentSender, @@ -243,14 +246,4 @@ class MessageTimelineItemStateFactory( else -> MessagesItemGroupPosition.None } } - - private suspend fun loadAvatarData( - name: String, - url: String?, - size: AvatarSize = AvatarSize.SMALL - ): AvatarData { - val model = client.mediaResolver() - .resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value)) - return AvatarData(name, model, size) - } } diff --git a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt index 507af9d706..5083a9fd7b 100644 --- a/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt +++ b/features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt @@ -23,7 +23,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesViewModel import io.element.android.x.core.di.daggerMavericksViewModelFactory -import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.di.SessionScope import io.element.android.x.features.messages.model.MessagesItemAction @@ -33,9 +32,9 @@ import io.element.android.x.features.messages.model.MessagesViewState import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.timeline.MatrixTimeline import io.element.android.x.matrix.timeline.MatrixTimelineItem +import io.element.android.x.matrix.ui.MatrixItemHelper import io.element.android.x.textcomposer.MessageComposerMode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.launchIn @@ -53,9 +52,10 @@ class MessagesViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() + private val matrixItemHelper = MatrixItemHelper(client) private val room = client.getRoom(initialState.roomId)!! private val messageTimelineItemStateFactory = - MessageTimelineItemStateFactory(client, room, Dispatchers.Default) + MessageTimelineItemStateFactory(matrixItemHelper, room, Dispatchers.Default) private val timeline = room.timeline() private val timelineCallback = object : MatrixTimeline.Callback { @@ -161,7 +161,10 @@ class MessagesViewModel @AssistedInject constructor( room.syncUpdateFlow() .onEach { val avatarData = - loadAvatarData(room.name ?: room.roomId.value, room.avatarUrl, AvatarSize.SMALL) + matrixItemHelper.loadAvatarData( + room = room, + size = AvatarSize.SMALL + ) setState { copy( roomName = room.name, roomAvatar = avatarData, @@ -217,16 +220,6 @@ class MessagesViewModel @AssistedInject constructor( setSnackbarContent("Not implemented yet!") } - private suspend fun loadAvatarData( - name: String, - url: String?, - size: AvatarSize = AvatarSize.MEDIUM - ): AvatarData { - val model = client.mediaResolver() - .resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value)) - return AvatarData(name, model, size) - } - override fun onCleared() { super.onCleared() timeline.callback = null diff --git a/features/preferences/build.gradle.kts b/features/preferences/build.gradle.kts index e31375be26..e3c7f064e5 100644 --- a/features/preferences/build.gradle.kts +++ b/features/preferences/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { anvil(project(":anvilcodegen")) implementation(project(":libraries:di")) implementation(project(":libraries:core")) + implementation(project(":libraries:matrixui")) implementation(project(":features:rageshake")) implementation(project(":features:logout")) implementation(project(":libraries:designsystem")) diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt index 98a7158eea..61f38f4a14 100644 --- a/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import io.element.android.x.designsystem.components.preferences.PreferenceScreen import io.element.android.x.element.resources.R as ElementR import io.element.android.x.features.logout.LogoutPreference +import io.element.android.x.features.preferences.user.UserPreferences import io.element.android.x.features.rageshake.preferences.RageshakePreferences @Composable @@ -52,8 +53,9 @@ fun PreferencesContent( onBackPressed = onBackPressed, title = stringResource(id = ElementR.string.settings) ) { - LogoutPreference(onSuccessLogout = onSuccessLogout) + UserPreferences() RageshakePreferences(onOpenRageShake = onOpenRageShake) + LogoutPreference(onSuccessLogout = onSuccessLogout) } } diff --git a/features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt b/features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt new file mode 100644 index 0000000000..e3a58c8c46 --- /dev/null +++ b/features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.features.preferences.user + +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel +import io.element.android.x.matrix.ui.components.MatrixUserHeader +import io.element.android.x.matrix.ui.viewmodels.user.UserViewModel +import io.element.android.x.matrix.ui.viewmodels.user.UserViewState + +@Composable +fun UserPreferences( + modifier: Modifier = Modifier, + viewModel: UserViewModel = mavericksViewModel(), +) { + val user by viewModel.collectAsState(UserViewState::user) + when (user()) { + null -> Spacer(modifier = modifier.height(1.dp)) + else -> MatrixUserHeader( + modifier = modifier, + matrixUser = user.invoke()!! + ) + } +} diff --git a/features/roomlist/build.gradle.kts b/features/roomlist/build.gradle.kts index 31bee1dbb7..6dc8cfd7cc 100644 --- a/features/roomlist/build.gradle.kts +++ b/features/roomlist/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(project(":libraries:di")) implementation(project(":libraries:core")) implementation(project(":libraries:matrix")) + implementation(project(":libraries:matrixui")) implementation(project(":libraries:designsystem")) implementation(project(":libraries:elementresources")) implementation(libs.mavericks.compose) diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt index ba399e05dc..3695a80c48 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt @@ -44,24 +44,28 @@ import io.element.android.x.designsystem.ElementXTheme import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.features.roomlist.components.RoomListTopBar import io.element.android.x.features.roomlist.components.RoomSummaryRow -import io.element.android.x.features.roomlist.model.MatrixUser import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListViewState import io.element.android.x.features.roomlist.model.stubbedRoomSummaries import io.element.android.x.matrix.core.RoomId +import io.element.android.x.matrix.core.UserId +import io.element.android.x.matrix.ui.model.MatrixUser +import io.element.android.x.matrix.ui.viewmodels.user.UserViewModel +import io.element.android.x.matrix.ui.viewmodels.user.UserViewState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @Composable fun RoomListScreen( viewModel: RoomListViewModel = mavericksViewModel(), + userViewModel: UserViewModel = mavericksViewModel(), onRoomClicked: (RoomId) -> Unit = { }, onOpenSettings: () -> Unit = { }, ) { val filter by viewModel.collectAsState(RoomListViewState::filter) LogCompositions(tag = "RoomListScreen", msg = "Root") val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms) - val matrixUser by viewModel.collectAsState(RoomListViewState::user) + val matrixUser by userViewModel.collectAsState(UserViewState::user) RoomListContent( roomSummaries = roomSummaries().orEmpty().toImmutableList(), matrixUser = matrixUser(), @@ -154,7 +158,7 @@ fun PreviewableRoomListContent() { ElementXTheme(darkTheme = false) { RoomListContent( roomSummaries = stubbedRoomSummaries(), - matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), + matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")), onRoomClicked = {}, filter = "filter", onFilterChanged = {}, @@ -169,7 +173,7 @@ fun PreviewableDarkRoomListContent() { ElementXTheme(darkTheme = true) { RoomListContent( roomSummaries = stubbedRoomSummaries(), - matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), + matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")), onRoomClicked = {}, filter = "filter", onFilterChanged = {}, diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt index d9c1f72813..e1fdb5cfa6 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt @@ -25,16 +25,14 @@ import dagger.assisted.AssistedInject import io.element.android.x.anvilannotations.ContributesViewModel import io.element.android.x.core.coroutine.parallelMap import io.element.android.x.core.di.daggerMavericksViewModelFactory -import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarSize import io.element.android.x.di.SessionScope -import io.element.android.x.features.roomlist.model.MatrixUser import io.element.android.x.features.roomlist.model.RoomListRoomSummary import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders import io.element.android.x.features.roomlist.model.RoomListViewState import io.element.android.x.matrix.MatrixClient -import io.element.android.x.matrix.media.MediaResolver import io.element.android.x.matrix.room.RoomSummary +import io.element.android.x.matrix.ui.MatrixItemHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -53,6 +51,7 @@ class RoomListViewModel @AssistedInject constructor( companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() private val lastMessageFormatter = LastMessageFormatter() + private val matrixUserHelper = MatrixItemHelper(client) init { handleInit() @@ -79,24 +78,6 @@ class RoomListViewModel @AssistedInject constructor( } private fun handleInit() { - suspend { - val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() - val userDisplayName = client.loadUserDisplayName().getOrNull() - val avatarData = - loadAvatarData( - userDisplayName ?: client.userId().value, - userAvatarUrl, - AvatarSize.SMALL - ) - MatrixUser( - username = userDisplayName ?: client.userId().value, - avatarUrl = userAvatarUrl, - avatarData = avatarData, - ) - }.execute { - copy(user = it) - } - // Observe the room list and the filter combine( client.roomSummaryDataSource().roomSummaries() @@ -136,9 +117,9 @@ class RoomListViewModel @AssistedInject constructor( when (roomSummary) { is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) is RoomSummary.Filled -> { - val avatarData = loadAvatarData( - roomSummary.details.name, - roomSummary.details.avatarURLString + val avatarData = matrixUserHelper.loadAvatarData( + roomSummary = roomSummary, + size = AvatarSize.MEDIUM ) RoomListRoomSummary( id = roomSummary.identifier(), @@ -152,14 +133,4 @@ class RoomListViewModel @AssistedInject constructor( } } } - - private suspend fun loadAvatarData( - name: String, - url: String?, - size: AvatarSize = AvatarSize.MEDIUM - ): AvatarData { - val model = client.mediaResolver() - .resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value)) - return AvatarData(name, model, size) - } } diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomListTopBar.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomListTopBar.kt index 344ad99b64..a6db83f09d 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomListTopBar.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomListTopBar.kt @@ -54,7 +54,7 @@ import androidx.compose.ui.unit.sp import io.element.android.x.core.compose.LogCompositions import io.element.android.x.core.compose.textFieldState import io.element.android.x.designsystem.components.avatar.Avatar -import io.element.android.x.features.roomlist.model.MatrixUser +import io.element.android.x.matrix.ui.model.MatrixUser @Composable fun RoomListTopBar( diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt index 42fc6ab913..4fb02b054c 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt +++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt @@ -22,7 +22,6 @@ import com.airbnb.mvrx.Uninitialized import io.element.android.x.matrix.core.RoomId data class RoomListViewState( - val user: Async = Uninitialized, // Will contain the filtered rooms, using ::filter (if filter is not empty) val rooms: Async> = Uninitialized, val filter: String = "", diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt index 692a67a082..73451441e6 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import io.element.android.x.designsystem.AvatarGradientEnd @@ -42,13 +43,13 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) { .clip(CircleShape) if (avatarData.model == null) { InitialsAvatar( + avatarData = avatarData, modifier = commonModifier, - initials = avatarData.name.first().uppercase() ) } else { ImageAvatar( + avatarData = avatarData, modifier = commonModifier, - avatarData = avatarData ) } } @@ -71,7 +72,7 @@ private fun ImageAvatar( @Composable private fun InitialsAvatar( - initials: String, + avatarData: AvatarData, modifier: Modifier = Modifier, ) { val initialsGradient = Brush.linearGradient( @@ -87,9 +88,15 @@ private fun InitialsAvatar( ) { Text( modifier = Modifier.align(Alignment.Center), - text = initials, - fontSize = 24.sp, + text = avatarData.name.first().uppercase(), + fontSize = (avatarData.size.value / 2).sp, color = Color.White, ) } } + +@Preview +@Composable +fun InitialsAvatar() { + InitialsAvatar(AvatarData("A")) +} diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarSize.kt index 14b25feb3d..a0b4643191 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarSize.kt @@ -21,7 +21,8 @@ import androidx.compose.ui.unit.dp enum class AvatarSize(val value: Int) { SMALL(32), MEDIUM(40), - BIG(48); + BIG(48), + HUGE(96); val dp = value.dp } diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/Config.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/Config.kt index 43cd94af73..ffce99028d 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/Config.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/Config.kt @@ -19,3 +19,4 @@ package io.element.android.x.designsystem.components.preferences import androidx.compose.ui.unit.dp internal val preferenceMinHeight = 80.dp +internal val preferencePaddingEnd = 16.dp diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt index 2aba8b1f0e..6fca5f5504 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt @@ -20,8 +20,13 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api @@ -47,7 +52,10 @@ fun PreferenceScreen( content: @Composable ColumnScope.() -> Unit, ) { Scaffold( - modifier = modifier, + modifier = modifier + .fillMaxSize() + .systemBarsPadding() + .imePadding(), contentWindowInsets = WindowInsets.statusBars, topBar = { PreferenceTopAppBar( @@ -56,8 +64,13 @@ fun PreferenceScreen( ) }, content = { + val scrollState = rememberScrollState() Column( - modifier = Modifier.padding(it) + modifier = Modifier + .padding(it) + .verticalScroll( + state = scrollState, + ) ) { content() } diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSlide.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSlide.kt index 56acaa1958..672f94fbcb 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSlide.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSlide.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider import androidx.compose.material3.Text @@ -54,7 +55,9 @@ fun PreferenceSlide( Row(modifier = Modifier.fillMaxWidth()) { PreferenceIcon(icon = icon) Column( - modifier = Modifier.weight(1f), + modifier = Modifier + .weight(1f) + .padding(end = preferencePaddingEnd), ) { Text( modifier = Modifier.fillMaxWidth(), diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSwitch.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSwitch.kt index d9cfc12451..ab2757936d 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSwitch.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSwitch.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Announcement import androidx.compose.material3.Checkbox @@ -65,6 +66,7 @@ fun PreferenceSwitch( text = title ) Checkbox( + modifier = Modifier.padding(end = preferencePaddingEnd), checked = isChecked, enabled = enabled, onCheckedChange = onCheckedChange diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceText.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceText.kt index 2d698932ad..70c3419bc0 100644 --- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceText.kt +++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceText.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.BugReport import androidx.compose.material3.MaterialTheme @@ -53,7 +54,8 @@ fun PreferenceText( PreferenceIcon(icon = icon) Text( modifier = Modifier - .weight(1f), + .weight(1f) + .padding(end = preferencePaddingEnd), style = MaterialTheme.typography.bodyLarge, text = title ) diff --git a/libraries/matrix/build.gradle.kts b/libraries/matrix/build.gradle.kts index a4ceca79d3..9450134d78 100644 --- a/libraries/matrix/build.gradle.kts +++ b/libraries/matrix/build.gradle.kts @@ -21,7 +21,7 @@ plugins { } android { - namespace = "io.element.android.x.sdk.matrix" + namespace = "io.element.android.x.matrix" } anvil { @@ -33,7 +33,6 @@ dependencies { implementation(project(":libraries:di")) implementation(project(":libraries:core")) implementation("net.java.dev.jna:jna:5.12.1@aar") - implementation(libs.coil.compose) implementation(libs.androidx.datastore.preferences) implementation(libs.serialization.json) } diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt index aaffd43ea2..e2771051d0 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt @@ -17,13 +17,10 @@ package io.element.android.x.matrix import android.content.Context -import coil.ComponentRegistry import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.di.AppScope import io.element.android.x.di.ApplicationContext import io.element.android.x.di.SingleIn -import io.element.android.x.matrix.media.MediaFetcher -import io.element.android.x.matrix.media.MediaKeyer import io.element.android.x.matrix.session.SessionStore import io.element.android.x.matrix.util.logError import java.io.File @@ -58,14 +55,6 @@ class Matrix @Inject constructor( return sessionStore.isLoggedIn() } - fun registerCoilComponents( - builder: ComponentRegistry.Builder, - activeClientProvider: () -> MatrixClient? - ) { - builder.add(MediaKeyer()) - builder.add(MediaFetcher.Factory(activeClientProvider)) - } - suspend fun restoreSession() = withContext(coroutineDispatchers.io) { sessionStore.getLatestSession() ?.let { session -> diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt index bda1814a14..55dd355678 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt @@ -25,9 +25,6 @@ import io.element.android.x.matrix.room.RoomSummaryDataSource import io.element.android.x.matrix.room.RustRoomSummaryDataSource import io.element.android.x.matrix.session.SessionStore import io.element.android.x.matrix.sync.SlidingSyncObserverProxy -import java.io.Closeable -import java.io.File -import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client @@ -38,6 +35,9 @@ import org.matrix.rustcomponents.sdk.SlidingSyncMode import org.matrix.rustcomponents.sdk.SlidingSyncViewBuilder import org.matrix.rustcomponents.sdk.StoppableSpawn import timber.log.Timber +import java.io.Closeable +import java.io.File +import java.util.concurrent.atomic.AtomicBoolean class MatrixClient internal constructor( private val client: Client, @@ -157,6 +157,7 @@ class MatrixClient internal constructor( } fun userId(): UserId = UserId(client.userId()) + suspend fun loadUserDisplayName(): Result = withContext(dispatchers.io) { runCatching { client.displayName() diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/core/MatrixPatterns.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/MatrixPatterns.kt index 5f60c0aea6..c6a4ae4dcd 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/core/MatrixPatterns.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/core/MatrixPatterns.kt @@ -16,7 +16,7 @@ package io.element.android.x.matrix.core -import io.element.android.x.sdk.matrix.BuildConfig +import io.element.android.x.matrix.BuildConfig import timber.log.Timber /** diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt index d6c3f718d8..df86749a15 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt +++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt @@ -65,6 +65,11 @@ class MatrixRoom( return slidingSyncRoom.name() } + val bestName: String + get() { + return name?.takeIf { it.isNotEmpty() } ?: room.id() + } + val displayName: String get() { return room.displayName() diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts new file mode 100644 index 0000000000..c33aa13795 --- /dev/null +++ b/libraries/matrixui/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.x.matrix.ui" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(project(":anvilannotations")) + anvil(project(":anvilcodegen")) + implementation(project(":libraries:di")) + implementation(project(":libraries:matrix")) + implementation(project(":libraries:designsystem")) + implementation(project(":libraries:core")) + implementation(libs.coil.compose) +} diff --git a/libraries/matrixui/src/main/AndroidManifest.xml b/libraries/matrixui/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..122869829c --- /dev/null +++ b/libraries/matrixui/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixItemHelper.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixItemHelper.kt new file mode 100644 index 0000000000..a040e18585 --- /dev/null +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixItemHelper.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.ui + +import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.designsystem.components.avatar.AvatarSize +import io.element.android.x.matrix.MatrixClient +import io.element.android.x.matrix.media.MediaResolver +import io.element.android.x.matrix.room.MatrixRoom +import io.element.android.x.matrix.room.RoomSummary +import io.element.android.x.matrix.ui.model.MatrixUser +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow + +class MatrixItemHelper( + private val client: MatrixClient +) { + /** + * TODO Make username and avatar live... + */ + @OptIn(FlowPreview::class) + fun getCurrentUserData(avatarSize: AvatarSize): Flow { + return suspend { + val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() + val userDisplayName = client.loadUserDisplayName().getOrNull() + val avatarData = + loadAvatarData( + userDisplayName ?: client.userId().value, + userAvatarUrl, + avatarSize + ) + MatrixUser( + id = client.userId(), + username = userDisplayName, + avatarUrl = userAvatarUrl, + avatarData = avatarData, + ) + }.asFlow() + } + + suspend fun loadAvatarData(room: MatrixRoom, size: AvatarSize): AvatarData { + return loadAvatarData( + name = room.bestName, + url = room.avatarUrl, + size = size + ) + } + + suspend fun loadAvatarData(roomSummary: RoomSummary.Filled, size: AvatarSize): AvatarData { + return loadAvatarData( + name = roomSummary.details.name, + url = roomSummary.details.avatarURLString, + size = size + ) + } + + suspend fun loadAvatarData( + name: String, + url: String?, + size: AvatarSize + ): AvatarData { + val model = client.mediaResolver() + .resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value)) + return AvatarData(name, model, size) + } +} diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixUi.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixUi.kt new file mode 100644 index 0000000000..db4de72d7d --- /dev/null +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixUi.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.ui + +import coil.ComponentRegistry +import io.element.android.x.matrix.MatrixClient +import io.element.android.x.matrix.ui.media.MediaFetcher +import io.element.android.x.matrix.ui.media.MediaKeyer +import javax.inject.Inject + +class MatrixUi @Inject constructor() { + + fun registerCoilComponents( + builder: ComponentRegistry.Builder, + activeClientProvider: () -> MatrixClient? + ) { + builder.add(MediaKeyer()) + builder.add(MediaFetcher.Factory(activeClientProvider)) + } +} diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/components/MatrixUserHeader.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/components/MatrixUserHeader.kt new file mode 100644 index 0000000000..928c0f032b --- /dev/null +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/components/MatrixUserHeader.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.x.designsystem.ElementXTheme +import io.element.android.x.designsystem.components.avatar.Avatar +import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.designsystem.components.avatar.AvatarSize +import io.element.android.x.matrix.core.UserId +import io.element.android.x.matrix.ui.model.MatrixUser +import io.element.android.x.matrix.ui.model.getBestName + +@Composable +fun MatrixUserHeader( + matrixUser: MatrixUser, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Column( + modifier = modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .padding(all = 16.dp) + .height(IntrinsicSize.Min), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Avatar( + matrixUser.avatarData.copy(size = AvatarSize.HUGE), + ) + Spacer(modifier = Modifier.height(16.dp)) + // Name + Text( + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + text = matrixUser.getBestName(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + // Id + if (matrixUser.username.isNullOrEmpty().not()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = matrixUser.id.value, + color = MaterialTheme.colorScheme.secondary, + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Preview +@Composable +fun MatrixUserHeaderPreview() { + ElementXTheme { + MatrixUserHeader( + MatrixUser( + id = UserId("@alice:server.org"), + username = "Alice", + avatarUrl = null, + avatarData = AvatarData("Alice") + ) + ) + } +} + +@Preview +@Composable +fun MatrixUserHeaderNoUsernamePreview() { + ElementXTheme { + MatrixUserHeader( + MatrixUser( + id = UserId("@alice:server.org"), + username = null, + avatarUrl = null, + avatarData = AvatarData("Alice") + ) + ) + } +} diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/components/MatrixUserRow.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/components/MatrixUserRow.kt new file mode 100644 index 0000000000..6fbde8bd64 --- /dev/null +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/components/MatrixUserRow.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.x.designsystem.ElementXTheme +import io.element.android.x.designsystem.components.avatar.Avatar +import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.matrix.core.UserId +import io.element.android.x.matrix.ui.model.MatrixUser +import io.element.android.x.matrix.ui.model.getBestName + +@Composable +fun MatrixUserRow( + matrixUser: MatrixUser, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Row( + modifier = modifier + .clickable(onClick = onClick) + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(IntrinsicSize.Min), + verticalAlignment = Alignment.CenterVertically + ) { + Avatar( + matrixUser.avatarData, + ) + Column( + modifier = Modifier + .padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp) + .alignByBaseline() + .weight(1f) + ) { + // Name + Text( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + text = matrixUser.getBestName(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + // Id + if (matrixUser.username.isNullOrEmpty().not()) { + Text( + text = matrixUser.id.value, + color = MaterialTheme.colorScheme.secondary, + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} + +@Preview +@Composable +fun MatrixUserRowPreview() { + ElementXTheme { + MatrixUserRow( + MatrixUser( + id = UserId("@alice:server.org"), + username = "Alice", + avatarUrl = null, + avatarData = AvatarData("Alice") + ) + ) + } +} diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaFetcher.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt similarity index 94% rename from libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaFetcher.kt rename to libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt index 9e4eb7d268..d345f26984 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaFetcher.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt @@ -14,13 +14,14 @@ * limitations under the License. */ -package io.element.android.x.matrix.media +package io.element.android.x.matrix.ui.media import coil.ImageLoader import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.request.Options import io.element.android.x.matrix.MatrixClient +import io.element.android.x.matrix.media.MediaResolver import java.nio.ByteBuffer internal class MediaFetcher( diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaKeyer.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaKeyer.kt similarity index 89% rename from libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaKeyer.kt rename to libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaKeyer.kt index c43b07cb7c..0831f6815e 100644 --- a/libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaKeyer.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaKeyer.kt @@ -14,10 +14,11 @@ * limitations under the License. */ -package io.element.android.x.matrix.media +package io.element.android.x.matrix.ui.media import coil.key.Keyer import coil.request.Options +import io.element.android.x.matrix.media.MediaResolver internal class MediaKeyer : Keyer { override fun key(data: MediaResolver.Meta, options: Options): String? { diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt similarity index 80% rename from features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt rename to libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt index cb2957d918..5aaa69e6e6 100644 --- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt @@ -14,14 +14,20 @@ * limitations under the License. */ -package io.element.android.x.features.roomlist.model +package io.element.android.x.matrix.ui.model import androidx.compose.runtime.Stable import io.element.android.x.designsystem.components.avatar.AvatarData +import io.element.android.x.matrix.core.UserId @Stable data class MatrixUser( + val id: UserId, val username: String? = null, val avatarUrl: String? = null, val avatarData: AvatarData = AvatarData(), ) + +fun MatrixUser.getBestName(): String { + return username?.takeIf { it.isNotEmpty() } ?: id.value +} diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewModel.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewModel.kt new file mode 100644 index 0000000000..9ec53ee912 --- /dev/null +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewModel.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.ui.viewmodels.user + +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.x.anvilannotations.ContributesViewModel +import io.element.android.x.core.di.daggerMavericksViewModelFactory +import io.element.android.x.designsystem.components.avatar.AvatarSize +import io.element.android.x.di.SessionScope +import io.element.android.x.matrix.MatrixClient +import io.element.android.x.matrix.ui.MatrixItemHelper + +@ContributesViewModel(SessionScope::class) +class UserViewModel @AssistedInject constructor( + client: MatrixClient, + @Assisted initialState: UserViewState +) : MavericksViewModel(initialState) { + + companion object : MavericksViewModelFactory by daggerMavericksViewModelFactory() + + private val matrixUserHelper = MatrixItemHelper(client) + + init { + handleInit() + } + + private fun handleInit() { + matrixUserHelper.getCurrentUserData(avatarSize = AvatarSize.SMALL).execute { + copy(user = it) + } + } +} diff --git a/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewState.kt b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewState.kt new file mode 100644 index 0000000000..60d7c5bd48 --- /dev/null +++ b/libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewState.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.x.matrix.ui.viewmodels.user + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MavericksState +import com.airbnb.mvrx.Uninitialized +import io.element.android.x.matrix.ui.model.MatrixUser + +data class UserViewState( + val user: Async = Uninitialized, +) : MavericksState diff --git a/settings.gradle.kts b/settings.gradle.kts index 7102431768..b6ac63d837 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -37,6 +37,7 @@ include(":app") include(":libraries:core") include(":libraries:rustsdk") include(":libraries:matrix") +include(":libraries:matrixui") include(":libraries:textcomposer") include(":libraries:elementresources") include(":features:onboarding")