Browse Source

Merge pull request #24 from vector-im/feature/bma/preferences

Preferences
feature/bma/flipper
Benoit Marty 2 years ago committed by GitHub
parent
commit
218113e0ca
  1. 1
      app/build.gradle.kts
  2. 2
      app/src/main/java/io/element/android/x/di/AppBindings.kt
  3. 7
      app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt
  4. 2
      app/src/main/java/io/element/android/x/initializer/MatrixInitializer.kt
  5. 2
      build.gradle.kts
  6. 1
      features/messages/build.gradle.kts
  7. 23
      features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt
  8. 21
      features/messages/src/main/java/io/element/android/x/features/messages/MessagesViewModel.kt
  9. 1
      features/preferences/build.gradle.kts
  10. 4
      features/preferences/src/main/java/io/element/android/x/features/preferences/PreferencesScreen.kt
  11. 44
      features/preferences/src/main/java/io/element/android/x/features/preferences/user/UserPreferences.kt
  12. 1
      features/roomlist/build.gradle.kts
  13. 12
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt
  14. 39
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt
  15. 2
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomListTopBar.kt
  16. 1
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListViewState.kt
  17. 17
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt
  18. 3
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarSize.kt
  19. 1
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/Config.kt
  20. 17
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceScreen.kt
  21. 5
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSlide.kt
  22. 2
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceSwitch.kt
  23. 4
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/preferences/PreferenceText.kt
  24. 3
      libraries/matrix/build.gradle.kts
  25. 11
      libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt
  26. 7
      libraries/matrix/src/main/java/io/element/android/x/matrix/MatrixClient.kt
  27. 2
      libraries/matrix/src/main/java/io/element/android/x/matrix/core/MatrixPatterns.kt
  28. 5
      libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt
  29. 38
      libraries/matrixui/build.gradle.kts
  30. 18
      libraries/matrixui/src/main/AndroidManifest.xml
  31. 81
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixItemHelper.kt
  32. 34
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/MatrixUi.kt
  33. 112
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/components/MatrixUserHeader.kt
  34. 101
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/components/MatrixUserRow.kt
  35. 3
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt
  36. 3
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaKeyer.kt
  37. 8
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt
  38. 49
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewModel.kt
  39. 26
      libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/viewmodels/user/UserViewState.kt
  40. 1
      settings.gradle.kts

1
app/build.gradle.kts

@ -151,6 +151,7 @@ knit {
dependencies { dependencies {
implementation(project(":libraries:designsystem")) implementation(project(":libraries:designsystem"))
implementation(project(":libraries:matrix")) implementation(project(":libraries:matrix"))
implementation(project(":libraries:matrixui"))
implementation(project(":libraries:core")) implementation(project(":libraries:core"))
implementation(project(":features:onboarding")) implementation(project(":features:onboarding"))
implementation(project(":features:login")) implementation(project(":features:login"))

2
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 com.squareup.anvil.annotations.ContributesTo
import io.element.android.x.matrix.Matrix import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.ui.MatrixUi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
interface AppBindings { interface AppBindings {
fun coroutineScope(): CoroutineScope fun coroutineScope(): CoroutineScope
fun matrix(): Matrix fun matrix(): Matrix
fun matrixUi(): MatrixUi
fun sessionComponentsOwner(): SessionComponentsOwner fun sessionComponentsOwner(): SessionComponentsOwner
} }

7
app/src/main/java/io/element/android/x/initializer/CoilInitializer.kt

@ -35,19 +35,18 @@ class CoilInitializer : Initializer<Unit> {
private class ElementImageLoaderFactory( private class ElementImageLoaderFactory(
private val context: Context private val context: Context
) : ) : ImageLoaderFactory {
ImageLoaderFactory {
override fun newImageLoader(): ImageLoader { override fun newImageLoader(): ImageLoader {
return ImageLoader return ImageLoader
.Builder(context) .Builder(context)
.components { .components {
val appBindings = context.bindings<AppBindings>() val appBindings = context.bindings<AppBindings>()
val matrix = appBindings.matrix() val matrixUi = appBindings.matrixUi()
val matrixClientProvider = { val matrixClientProvider = {
appBindings appBindings
.sessionComponentsOwner().activeSessionComponent?.matrixClient() .sessionComponentsOwner().activeSessionComponent?.matrixClient()
} }
matrix.registerCoilComponents(this, matrixClientProvider) matrixUi.registerCoilComponents(this, matrixClientProvider)
} }
.build() .build()
} }

2
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 android.content.Context
import androidx.startup.Initializer 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.TracingConfigurations
import io.element.android.x.matrix.tracing.setupTracing import io.element.android.x.matrix.tracing.setupTracing
import io.element.android.x.sdk.matrix.BuildConfig
class MatrixInitializer : Initializer<Unit> { class MatrixInitializer : Initializer<Unit> {

2
build.gradle.kts

@ -95,6 +95,8 @@ allprojects {
"experimental:kdoc-wrapping", "experimental:kdoc-wrapping",
// Ignore error "Redundant curly braces", since we use it to fix false positives, for instance in "elementLogs.${i}.txt" // Ignore error "Redundant curly braces", since we use it to fix false positives, for instance in "elementLogs.${i}.txt"
"string-template", "string-template",
// Not the same order than Android Studio formatter...
"import-ordering",
) )
) )
} }

1
features/messages/build.gradle.kts

@ -34,6 +34,7 @@ dependencies {
implementation(project(":libraries:di")) implementation(project(":libraries:di"))
implementation(project(":libraries:core")) implementation(project(":libraries:core"))
implementation(project(":libraries:matrix")) implementation(project(":libraries:matrix"))
implementation(project(":libraries:matrixui"))
implementation(project(":libraries:designsystem")) implementation(project(":libraries:designsystem"))
implementation(project(":libraries:textcomposer")) implementation(project(":libraries:textcomposer"))
implementation(libs.mavericks.compose) implementation(libs.mavericks.compose)

23
features/messages/src/main/java/io/element/android/x/features/messages/MessageTimelineItemStateFactory.kt

@ -17,7 +17,6 @@
package io.element.android.x.features.messages package io.element.android.x.features.messages
import androidx.recyclerview.widget.DiffUtil 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.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.messages.diff.CacheInvalidator import io.element.android.x.features.messages.diff.CacheInvalidator
import io.element.android.x.features.messages.diff.MatrixTimelineItemsDiffCallback 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.MessagesTimelineItemTextContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent
import io.element.android.x.features.messages.util.invalidateLast 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.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimelineItem 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.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow 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.MessageFormat
import org.matrix.rustcomponents.sdk.MessageType import org.matrix.rustcomponents.sdk.MessageType
import timber.log.Timber import timber.log.Timber
import kotlin.system.measureTimeMillis
class MessageTimelineItemStateFactory( class MessageTimelineItemStateFactory(
private val client: MatrixClient, private val matrixItemHelper: MatrixItemHelper,
private val room: MatrixRoom, private val room: MatrixRoom,
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
) { ) {
@ -153,7 +152,11 @@ class MessageTimelineItemStateFactory(
val senderDisplayName = room.userDisplayName(currentSender).getOrNull() val senderDisplayName = room.userDisplayName(currentSender).getOrNull()
val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull() val senderAvatarUrl = room.userAvatarUrl(currentSender).getOrNull()
val senderAvatarData = val senderAvatarData =
loadAvatarData(senderDisplayName ?: currentSender, senderAvatarUrl) matrixItemHelper.loadAvatarData(
name = senderDisplayName ?: currentSender,
url = senderAvatarUrl,
size = AvatarSize.SMALL
)
return MessagesTimelineItemState.MessageEvent( return MessagesTimelineItemState.MessageEvent(
id = currentTimelineItem.uniqueId, id = currentTimelineItem.uniqueId,
senderId = currentSender, senderId = currentSender,
@ -243,14 +246,4 @@ class MessageTimelineItemStateFactory(
else -> MessagesItemGroupPosition.None 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)
}
} }

21
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 dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.di.daggerMavericksViewModelFactory 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.designsystem.components.avatar.AvatarSize
import io.element.android.x.di.SessionScope import io.element.android.x.di.SessionScope
import io.element.android.x.features.messages.model.MessagesItemAction 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.MessagesTimelineItemRedactedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
import io.element.android.x.matrix.MatrixClient 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.MatrixTimeline
import io.element.android.x.matrix.timeline.MatrixTimelineItem import io.element.android.x.matrix.timeline.MatrixTimelineItem
import io.element.android.x.matrix.ui.MatrixItemHelper
import io.element.android.x.textcomposer.MessageComposerMode import io.element.android.x.textcomposer.MessageComposerMode
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -53,9 +52,10 @@ class MessagesViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<MessagesViewModel, MessagesViewState> by daggerMavericksViewModelFactory() companion object : MavericksViewModelFactory<MessagesViewModel, MessagesViewState> by daggerMavericksViewModelFactory()
private val matrixItemHelper = MatrixItemHelper(client)
private val room = client.getRoom(initialState.roomId)!! private val room = client.getRoom(initialState.roomId)!!
private val messageTimelineItemStateFactory = private val messageTimelineItemStateFactory =
MessageTimelineItemStateFactory(client, room, Dispatchers.Default) MessageTimelineItemStateFactory(matrixItemHelper, room, Dispatchers.Default)
private val timeline = room.timeline() private val timeline = room.timeline()
private val timelineCallback = object : MatrixTimeline.Callback { private val timelineCallback = object : MatrixTimeline.Callback {
@ -161,7 +161,10 @@ class MessagesViewModel @AssistedInject constructor(
room.syncUpdateFlow() room.syncUpdateFlow()
.onEach { .onEach {
val avatarData = val avatarData =
loadAvatarData(room.name ?: room.roomId.value, room.avatarUrl, AvatarSize.SMALL) matrixItemHelper.loadAvatarData(
room = room,
size = AvatarSize.SMALL
)
setState { setState {
copy( copy(
roomName = room.name, roomAvatar = avatarData, roomName = room.name, roomAvatar = avatarData,
@ -217,16 +220,6 @@ class MessagesViewModel @AssistedInject constructor(
setSnackbarContent("Not implemented yet!") 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() { override fun onCleared() {
super.onCleared() super.onCleared()
timeline.callback = null timeline.callback = null

1
features/preferences/build.gradle.kts

@ -33,6 +33,7 @@ dependencies {
anvil(project(":anvilcodegen")) anvil(project(":anvilcodegen"))
implementation(project(":libraries:di")) implementation(project(":libraries:di"))
implementation(project(":libraries:core")) implementation(project(":libraries:core"))
implementation(project(":libraries:matrixui"))
implementation(project(":features:rageshake")) implementation(project(":features:rageshake"))
implementation(project(":features:logout")) implementation(project(":features:logout"))
implementation(project(":libraries:designsystem")) implementation(project(":libraries:designsystem"))

4
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.designsystem.components.preferences.PreferenceScreen
import io.element.android.x.element.resources.R as ElementR import io.element.android.x.element.resources.R as ElementR
import io.element.android.x.features.logout.LogoutPreference 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 import io.element.android.x.features.rageshake.preferences.RageshakePreferences
@Composable @Composable
@ -52,8 +53,9 @@ fun PreferencesContent(
onBackPressed = onBackPressed, onBackPressed = onBackPressed,
title = stringResource(id = ElementR.string.settings) title = stringResource(id = ElementR.string.settings)
) { ) {
LogoutPreference(onSuccessLogout = onSuccessLogout) UserPreferences()
RageshakePreferences(onOpenRageShake = onOpenRageShake) RageshakePreferences(onOpenRageShake = onOpenRageShake)
LogoutPreference(onSuccessLogout = onSuccessLogout)
} }
} }

44
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()!!
)
}
}

1
features/roomlist/build.gradle.kts

@ -34,6 +34,7 @@ dependencies {
implementation(project(":libraries:di")) implementation(project(":libraries:di"))
implementation(project(":libraries:core")) implementation(project(":libraries:core"))
implementation(project(":libraries:matrix")) implementation(project(":libraries:matrix"))
implementation(project(":libraries:matrixui"))
implementation(project(":libraries:designsystem")) implementation(project(":libraries:designsystem"))
implementation(project(":libraries:elementresources")) implementation(project(":libraries:elementresources"))
implementation(libs.mavericks.compose) implementation(libs.mavericks.compose)

12
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.designsystem.components.avatar.AvatarData
import io.element.android.x.features.roomlist.components.RoomListTopBar 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.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.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListViewState import io.element.android.x.features.roomlist.model.RoomListViewState
import io.element.android.x.features.roomlist.model.stubbedRoomSummaries import io.element.android.x.features.roomlist.model.stubbedRoomSummaries
import io.element.android.x.matrix.core.RoomId 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.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@Composable @Composable
fun RoomListScreen( fun RoomListScreen(
viewModel: RoomListViewModel = mavericksViewModel(), viewModel: RoomListViewModel = mavericksViewModel(),
userViewModel: UserViewModel = mavericksViewModel(),
onRoomClicked: (RoomId) -> Unit = { }, onRoomClicked: (RoomId) -> Unit = { },
onOpenSettings: () -> Unit = { }, onOpenSettings: () -> Unit = { },
) { ) {
val filter by viewModel.collectAsState(RoomListViewState::filter) val filter by viewModel.collectAsState(RoomListViewState::filter)
LogCompositions(tag = "RoomListScreen", msg = "Root") LogCompositions(tag = "RoomListScreen", msg = "Root")
val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms) val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms)
val matrixUser by viewModel.collectAsState(RoomListViewState::user) val matrixUser by userViewModel.collectAsState(UserViewState::user)
RoomListContent( RoomListContent(
roomSummaries = roomSummaries().orEmpty().toImmutableList(), roomSummaries = roomSummaries().orEmpty().toImmutableList(),
matrixUser = matrixUser(), matrixUser = matrixUser(),
@ -154,7 +158,7 @@ fun PreviewableRoomListContent() {
ElementXTheme(darkTheme = false) { ElementXTheme(darkTheme = false) {
RoomListContent( RoomListContent(
roomSummaries = stubbedRoomSummaries(), roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")),
onRoomClicked = {}, onRoomClicked = {},
filter = "filter", filter = "filter",
onFilterChanged = {}, onFilterChanged = {},
@ -169,7 +173,7 @@ fun PreviewableDarkRoomListContent() {
ElementXTheme(darkTheme = true) { ElementXTheme(darkTheme = true) {
RoomListContent( RoomListContent(
roomSummaries = stubbedRoomSummaries(), roomSummaries = stubbedRoomSummaries(),
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")), matrixUser = MatrixUser(id = UserId("@id"), username = "User#1", avatarData = AvatarData("U")),
onRoomClicked = {}, onRoomClicked = {},
filter = "filter", filter = "filter",
onFilterChanged = {}, onFilterChanged = {},

39
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.anvilannotations.ContributesViewModel
import io.element.android.x.core.coroutine.parallelMap import io.element.android.x.core.coroutine.parallelMap
import io.element.android.x.core.di.daggerMavericksViewModelFactory 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.designsystem.components.avatar.AvatarSize
import io.element.android.x.di.SessionScope 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.RoomListRoomSummary
import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders import io.element.android.x.features.roomlist.model.RoomListRoomSummaryPlaceholders
import io.element.android.x.features.roomlist.model.RoomListViewState import io.element.android.x.features.roomlist.model.RoomListViewState
import io.element.android.x.matrix.MatrixClient 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.room.RoomSummary
import io.element.android.x.matrix.ui.MatrixItemHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@ -53,6 +51,7 @@ class RoomListViewModel @AssistedInject constructor(
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by daggerMavericksViewModelFactory() companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by daggerMavericksViewModelFactory()
private val lastMessageFormatter = LastMessageFormatter() private val lastMessageFormatter = LastMessageFormatter()
private val matrixUserHelper = MatrixItemHelper(client)
init { init {
handleInit() handleInit()
@ -79,24 +78,6 @@ class RoomListViewModel @AssistedInject constructor(
} }
private fun handleInit() { 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 // Observe the room list and the filter
combine( combine(
client.roomSummaryDataSource().roomSummaries() client.roomSummaryDataSource().roomSummaries()
@ -136,9 +117,9 @@ class RoomListViewModel @AssistedInject constructor(
when (roomSummary) { when (roomSummary) {
is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier) is RoomSummary.Empty -> RoomListRoomSummaryPlaceholders.create(roomSummary.identifier)
is RoomSummary.Filled -> { is RoomSummary.Filled -> {
val avatarData = loadAvatarData( val avatarData = matrixUserHelper.loadAvatarData(
roomSummary.details.name, roomSummary = roomSummary,
roomSummary.details.avatarURLString size = AvatarSize.MEDIUM
) )
RoomListRoomSummary( RoomListRoomSummary(
id = roomSummary.identifier(), 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)
}
} }

2
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.LogCompositions
import io.element.android.x.core.compose.textFieldState import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.components.avatar.Avatar 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 @Composable
fun RoomListTopBar( fun RoomListTopBar(

1
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 import io.element.android.x.matrix.core.RoomId
data class RoomListViewState( data class RoomListViewState(
val user: Async<MatrixUser> = Uninitialized,
// Will contain the filtered rooms, using ::filter (if filter is not empty) // Will contain the filtered rooms, using ::filter (if filter is not empty)
val rooms: Async<List<RoomListRoomSummary>> = Uninitialized, val rooms: Async<List<RoomListRoomSummary>> = Uninitialized,
val filter: String = "", val filter: String = "",

17
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.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import io.element.android.x.designsystem.AvatarGradientEnd import io.element.android.x.designsystem.AvatarGradientEnd
@ -42,13 +43,13 @@ fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
.clip(CircleShape) .clip(CircleShape)
if (avatarData.model == null) { if (avatarData.model == null) {
InitialsAvatar( InitialsAvatar(
avatarData = avatarData,
modifier = commonModifier, modifier = commonModifier,
initials = avatarData.name.first().uppercase()
) )
} else { } else {
ImageAvatar( ImageAvatar(
avatarData = avatarData,
modifier = commonModifier, modifier = commonModifier,
avatarData = avatarData
) )
} }
} }
@ -71,7 +72,7 @@ private fun ImageAvatar(
@Composable @Composable
private fun InitialsAvatar( private fun InitialsAvatar(
initials: String, avatarData: AvatarData,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val initialsGradient = Brush.linearGradient( val initialsGradient = Brush.linearGradient(
@ -87,9 +88,15 @@ private fun InitialsAvatar(
) { ) {
Text( Text(
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
text = initials, text = avatarData.name.first().uppercase(),
fontSize = 24.sp, fontSize = (avatarData.size.value / 2).sp,
color = Color.White, color = Color.White,
) )
} }
} }
@Preview
@Composable
fun InitialsAvatar() {
InitialsAvatar(AvatarData("A"))
}

3
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) { enum class AvatarSize(val value: Int) {
SMALL(32), SMALL(32),
MEDIUM(40), MEDIUM(40),
BIG(48); BIG(48),
HUGE(96);
val dp = value.dp val dp = value.dp
} }

1
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 import androidx.compose.ui.unit.dp
internal val preferenceMinHeight = 80.dp internal val preferenceMinHeight = 80.dp
internal val preferencePaddingEnd = 16.dp

17
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.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets 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.padding
import androidx.compose.foundation.layout.statusBars 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.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -47,7 +52,10 @@ fun PreferenceScreen(
content: @Composable ColumnScope.() -> Unit, content: @Composable ColumnScope.() -> Unit,
) { ) {
Scaffold( Scaffold(
modifier = modifier, modifier = modifier
.fillMaxSize()
.systemBarsPadding()
.imePadding(),
contentWindowInsets = WindowInsets.statusBars, contentWindowInsets = WindowInsets.statusBars,
topBar = { topBar = {
PreferenceTopAppBar( PreferenceTopAppBar(
@ -56,8 +64,13 @@ fun PreferenceScreen(
) )
}, },
content = { content = {
val scrollState = rememberScrollState()
Column( Column(
modifier = Modifier.padding(it) modifier = Modifier
.padding(it)
.verticalScroll(
state = scrollState,
)
) { ) {
content() content()
} }

5
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.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -54,7 +55,9 @@ fun PreferenceSlide(
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
PreferenceIcon(icon = icon) PreferenceIcon(icon = icon)
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier
.weight(1f)
.padding(end = preferencePaddingEnd),
) { ) {
Text( Text(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

2
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.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Announcement import androidx.compose.material.icons.filled.Announcement
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
@ -65,6 +66,7 @@ fun PreferenceSwitch(
text = title text = title
) )
Checkbox( Checkbox(
modifier = Modifier.padding(end = preferencePaddingEnd),
checked = isChecked, checked = isChecked,
enabled = enabled, enabled = enabled,
onCheckedChange = onCheckedChange onCheckedChange = onCheckedChange

4
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.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -53,7 +54,8 @@ fun PreferenceText(
PreferenceIcon(icon = icon) PreferenceIcon(icon = icon)
Text( Text(
modifier = Modifier modifier = Modifier
.weight(1f), .weight(1f)
.padding(end = preferencePaddingEnd),
style = MaterialTheme.typography.bodyLarge, style = MaterialTheme.typography.bodyLarge,
text = title text = title
) )

3
libraries/matrix/build.gradle.kts

@ -21,7 +21,7 @@ plugins {
} }
android { android {
namespace = "io.element.android.x.sdk.matrix" namespace = "io.element.android.x.matrix"
} }
anvil { anvil {
@ -33,7 +33,6 @@ dependencies {
implementation(project(":libraries:di")) implementation(project(":libraries:di"))
implementation(project(":libraries:core")) implementation(project(":libraries:core"))
implementation("net.java.dev.jna:jna:5.12.1@aar") implementation("net.java.dev.jna:jna:5.12.1@aar")
implementation(libs.coil.compose)
implementation(libs.androidx.datastore.preferences) implementation(libs.androidx.datastore.preferences)
implementation(libs.serialization.json) implementation(libs.serialization.json)
} }

11
libraries/matrix/src/main/java/io/element/android/x/matrix/Matrix.kt

@ -17,13 +17,10 @@
package io.element.android.x.matrix package io.element.android.x.matrix
import android.content.Context import android.content.Context
import coil.ComponentRegistry
import io.element.android.x.core.coroutine.CoroutineDispatchers import io.element.android.x.core.coroutine.CoroutineDispatchers
import io.element.android.x.di.AppScope import io.element.android.x.di.AppScope
import io.element.android.x.di.ApplicationContext import io.element.android.x.di.ApplicationContext
import io.element.android.x.di.SingleIn 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.session.SessionStore
import io.element.android.x.matrix.util.logError import io.element.android.x.matrix.util.logError
import java.io.File import java.io.File
@ -58,14 +55,6 @@ class Matrix @Inject constructor(
return sessionStore.isLoggedIn() return sessionStore.isLoggedIn()
} }
fun registerCoilComponents(
builder: ComponentRegistry.Builder,
activeClientProvider: () -> MatrixClient?
) {
builder.add(MediaKeyer())
builder.add(MediaFetcher.Factory(activeClientProvider))
}
suspend fun restoreSession() = withContext(coroutineDispatchers.io) { suspend fun restoreSession() = withContext(coroutineDispatchers.io) {
sessionStore.getLatestSession() sessionStore.getLatestSession()
?.let { session -> ?.let { session ->

7
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.room.RustRoomSummaryDataSource
import io.element.android.x.matrix.session.SessionStore import io.element.android.x.matrix.session.SessionStore
import io.element.android.x.matrix.sync.SlidingSyncObserverProxy 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.CoroutineScope
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.Client 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.SlidingSyncViewBuilder
import org.matrix.rustcomponents.sdk.StoppableSpawn import org.matrix.rustcomponents.sdk.StoppableSpawn
import timber.log.Timber import timber.log.Timber
import java.io.Closeable
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
class MatrixClient internal constructor( class MatrixClient internal constructor(
private val client: Client, private val client: Client,
@ -157,6 +157,7 @@ class MatrixClient internal constructor(
} }
fun userId(): UserId = UserId(client.userId()) fun userId(): UserId = UserId(client.userId())
suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) { suspend fun loadUserDisplayName(): Result<String> = withContext(dispatchers.io) {
runCatching { runCatching {
client.displayName() client.displayName()

2
libraries/matrix/src/main/java/io/element/android/x/matrix/core/MatrixPatterns.kt

@ -16,7 +16,7 @@
package io.element.android.x.matrix.core 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 import timber.log.Timber
/** /**

5
libraries/matrix/src/main/java/io/element/android/x/matrix/room/MatrixRoom.kt

@ -65,6 +65,11 @@ class MatrixRoom(
return slidingSyncRoom.name() return slidingSyncRoom.name()
} }
val bestName: String
get() {
return name?.takeIf { it.isNotEmpty() } ?: room.id()
}
val displayName: String val displayName: String
get() { get() {
return room.displayName() return room.displayName()

38
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)
}

18
libraries/matrixui/src/main/AndroidManifest.xml

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest/>

81
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<MatrixUser> {
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)
}
}

34
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))
}
}

112
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")
)
)
}
}

101
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")
)
)
}
}

3
libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaFetcher.kt → libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaFetcher.kt

@ -14,13 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.x.matrix.media package io.element.android.x.matrix.ui.media
import coil.ImageLoader import coil.ImageLoader
import coil.fetch.FetchResult import coil.fetch.FetchResult
import coil.fetch.Fetcher import coil.fetch.Fetcher
import coil.request.Options import coil.request.Options
import io.element.android.x.matrix.MatrixClient import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver
import java.nio.ByteBuffer import java.nio.ByteBuffer
internal class MediaFetcher( internal class MediaFetcher(

3
libraries/matrix/src/main/java/io/element/android/x/matrix/media/MediaKeyer.kt → libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/media/MediaKeyer.kt

@ -14,10 +14,11 @@
* limitations under the License. * 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.key.Keyer
import coil.request.Options import coil.request.Options
import io.element.android.x.matrix.media.MediaResolver
internal class MediaKeyer : Keyer<MediaResolver.Meta> { internal class MediaKeyer : Keyer<MediaResolver.Meta> {
override fun key(data: MediaResolver.Meta, options: Options): String? { override fun key(data: MediaResolver.Meta, options: Options): String? {

8
features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/MatrixUser.kt → libraries/matrixui/src/main/java/io/element/android/x/matrix/ui/model/MatrixUser.kt

@ -14,14 +14,20 @@
* limitations under the License. * 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 androidx.compose.runtime.Stable
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.matrix.core.UserId
@Stable @Stable
data class MatrixUser( data class MatrixUser(
val id: UserId,
val username: String? = null, val username: String? = null,
val avatarUrl: String? = null, val avatarUrl: String? = null,
val avatarData: AvatarData = AvatarData(), val avatarData: AvatarData = AvatarData(),
) )
fun MatrixUser.getBestName(): String {
return username?.takeIf { it.isNotEmpty() } ?: id.value
}

49
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<UserViewState>(initialState) {
companion object : MavericksViewModelFactory<UserViewModel, UserViewState> by daggerMavericksViewModelFactory()
private val matrixUserHelper = MatrixItemHelper(client)
init {
handleInit()
}
private fun handleInit() {
matrixUserHelper.getCurrentUserData(avatarSize = AvatarSize.SMALL).execute {
copy(user = it)
}
}
}

26
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<MatrixUser> = Uninitialized,
) : MavericksState

1
settings.gradle.kts

@ -37,6 +37,7 @@ include(":app")
include(":libraries:core") include(":libraries:core")
include(":libraries:rustsdk") include(":libraries:rustsdk")
include(":libraries:matrix") include(":libraries:matrix")
include(":libraries:matrixui")
include(":libraries:textcomposer") include(":libraries:textcomposer")
include(":libraries:elementresources") include(":libraries:elementresources")
include(":features:onboarding") include(":features:onboarding")

Loading…
Cancel
Save