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

Preferences
This commit is contained in:
Benoit Marty
2022-12-23 10:23:13 +01:00
committed by GitHub
40 changed files with 609 additions and 105 deletions

View File

@@ -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"))

View File

@@ -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
}

View File

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

View File

@@ -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<Unit> {

View File

@@ -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",
)
)
}

View File

@@ -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)

View File

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

View File

@@ -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<MessagesViewModel, MessagesViewState> 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

View File

@@ -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"))

View File

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

View File

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

View File

@@ -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)

View File

@@ -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 = {},

View File

@@ -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<RoomListViewModel, RoomListViewState> 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)
}
}

View File

@@ -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(

View File

@@ -22,7 +22,6 @@ import com.airbnb.mvrx.Uninitialized
import io.element.android.x.matrix.core.RoomId
data class RoomListViewState(
val user: Async<MatrixUser> = Uninitialized,
// Will contain the filtered rooms, using ::filter (if filter is not empty)
val rooms: Async<List<RoomListRoomSummary>> = Uninitialized,
val filter: String = "",

View File

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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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()
}

View File

@@ -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(),

View File

@@ -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

View File

@@ -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
)

View File

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

View File

@@ -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 ->

View File

@@ -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<String> = withContext(dispatchers.io) {
runCatching {
client.displayName()

View File

@@ -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
/**

View File

@@ -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()

View File

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

View File

@@ -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/>

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(

View File

@@ -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<MediaResolver.Meta> {
override fun key(data: MediaResolver.Meta, options: Options): String? {

View File

@@ -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
}

View File

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

View File

@@ -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

View File

@@ -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")