Browse Source

Continue refinement of RoomList (and remove avatar library)

feature/bma/flipper
ganfra 2 years ago
parent
commit
f55bb16bfa
  1. 1
      features/roomlist/build.gradle.kts
  2. 159
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt
  3. 15
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt
  4. 157
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomItem.kt
  5. 48
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomListTopBar.kt
  6. 16
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt
  7. 25
      features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/stubbed.kt
  8. 1
      gradle/libs.versions.toml
  9. 1
      libraries/avatar/.gitignore
  10. 12
      libraries/avatar/build.gradle.kts
  11. 0
      libraries/avatar/consumer-rules.pro
  12. 21
      libraries/avatar/proguard-rules.pro
  13. 24
      libraries/avatar/src/androidTest/java/io/element/android/x/avatar/ExampleInstrumentedTest.kt
  14. 4
      libraries/avatar/src/main/AndroidManifest.xml
  15. 33
      libraries/avatar/src/main/java/io/element/android/x/avatar/Avatar.kt
  16. 10
      libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarData.kt
  17. 41
      libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarFetcher.kt
  18. 17
      libraries/avatar/src/test/java/io/element/android/x/avatar/ExampleUnitTest.kt
  19. 24
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt
  20. 4
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt
  21. 11
      libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarSize.kt
  22. 1
      settings.gradle.kts

1
features/roomlist/build.gradle.kts

@ -13,6 +13,7 @@ dependencies {
implementation(libs.mavericks.compose) implementation(libs.mavericks.compose)
implementation(libs.timber) implementation(libs.timber)
implementation(libs.datetime) implementation(libs.datetime)
implementation(libs.accompanist.placeholder)
testImplementation("junit:junit:4.13.2") testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.3") androidTestImplementation("androidx.test.ext:junit:1.1.3")
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")

159
features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt

@ -2,39 +2,30 @@
package io.element.android.x.features.roomlist package io.element.android.x.features.roomlist
import Avatar import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material.icons.Icons import androidx.compose.material3.Scaffold
import androidx.compose.material.icons.filled.ExitToApp import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.compose.collectAsState import com.airbnb.mvrx.compose.collectAsState
import com.airbnb.mvrx.compose.mavericksViewModel import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.data.LogCompositions import io.element.android.x.core.data.LogCompositions
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.RoomItem
import io.element.android.x.features.roomlist.components.RoomListTopBar
import io.element.android.x.features.roomlist.model.MatrixUser 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.matrix.core.RoomId import io.element.android.x.matrix.core.RoomId
@Composable @Composable
@ -87,125 +78,29 @@ fun RoomListContent(
} }
@OptIn(ExperimentalMaterial3Api::class) @Preview
@Composable
fun RoomListTopBar(
matrixUser: MatrixUser?,
onLogoutClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior
) {
LogCompositions(tag = "RoomListScreen", msg = "TopBar")
if (matrixUser == null) return
MediumTopAppBar(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
title = {
Text(
fontWeight = FontWeight.Bold,
text = "All Chats"
)
},
navigationIcon = {
IconButton(onClick = {}) {
Avatar(matrixUser.avatarData)
}
},
actions = {
IconButton(
onClick = onLogoutClicked
) {
Icon(Icons.Default.ExitToApp, contentDescription = "logout")
}
},
scrollBehavior = scrollBehavior,
)
}
@Composable @Composable
private fun RoomItem( private fun PreviewableRoomListContent() {
modifier: Modifier = Modifier, ElementXTheme(darkTheme = false) {
room: RoomListRoomSummary, RoomListContent(
onClick: (RoomId) -> Unit roomSummaries = stubbedRoomSummaries(),
) { matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
if (room.isPlaceholder) { onRoomClicked = {},
return onLogoutClicked = {}
} )
Column(
modifier = modifier
.fillMaxWidth()
.height(72.dp)
.clickable(
onClick = { onClick(room.roomId) },
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() }
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Avatar(room.avatarData)
Column(
modifier = Modifier
.padding(12.dp)
.weight(1f)
) {
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
text = room.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = room.lastMessage?.toString().orEmpty(),
color = MaterialTheme.colorScheme.secondary,
fontSize = 15.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Column(
) {
Text(
fontSize = 12.sp,
text = room.timestamp ?: "",
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier.size(4.dp))
val unreadIndicatorColor =
if (room.hasUnread) MaterialTheme.colorScheme.primary else Color.Transparent
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(unreadIndicatorColor)
.align(Alignment.End),
)
}
}
} }
} }
@Preview @Preview
@Composable @Composable
private fun PreviewableRoomListContent() { private fun PreviewableDarkRoomListContent() {
val roomSummaries = listOf( ElementXTheme(darkTheme = true) {
RoomListRoomSummary( RoomListContent(
name = "Room", roomSummaries = stubbedRoomSummaries(),
hasUnread = true, matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
timestamp = "14:18", onRoomClicked = {},
lastMessage = "A message", onLogoutClicked = {}
avatarData = AvatarData("R"),
id = "roomId"
) )
) }
RoomListContent(
roomSummaries = roomSummaries,
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
onRoomClicked = {},
onLogoutClicked = {}
)
} }

15
features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListViewModel.kt

@ -1,12 +1,12 @@
package io.element.android.x.features.roomlist package io.element.android.x.features.roomlist
import androidx.compose.ui.unit.dp
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MavericksViewModel import com.airbnb.mvrx.MavericksViewModel
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import io.element.android.x.core.data.parallelMap import io.element.android.x.core.data.parallelMap
import io.element.android.x.designsystem.components.avatar.AvatarData import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.features.roomlist.model.MatrixUser 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
@ -47,7 +47,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
val userDisplayName = client.loadUserDisplayName().getOrNull() val userDisplayName = client.loadUserDisplayName().getOrNull()
val avatarData = val avatarData =
loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl, 32) loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl, AvatarSize.SMALL)
MatrixUser( MatrixUser(
username = userDisplayName ?: client.userId().value, username = userDisplayName ?: client.userId().value,
avatarUrl = userAvatarUrl, avatarUrl = userAvatarUrl,
@ -73,10 +73,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
): List<RoomListRoomSummary> { ): List<RoomListRoomSummary> {
return roomSummaries.parallelMap { roomSummary -> return roomSummaries.parallelMap { roomSummary ->
when (roomSummary) { when (roomSummary) {
is RoomSummary.Empty -> RoomListRoomSummary( is RoomSummary.Empty -> RoomListRoomSummary.placeholder(roomSummary.identifier)
id = roomSummary.identifier,
isPlaceholder = true
)
is RoomSummary.Filled -> { is RoomSummary.Filled -> {
val avatarData = loadAvatarData( val avatarData = loadAvatarData(
client, client,
@ -100,17 +97,17 @@ class RoomListViewModel(initialState: RoomListViewState) :
client: MatrixClient, client: MatrixClient,
name: String, name: String,
url: String?, url: String?,
size: Long = 48 size: AvatarSize = AvatarSize.MEDIUM
): AvatarData { ): AvatarData {
val mediaContent = url?.let { val mediaContent = url?.let {
val mediaSource = mediaSourceFromUrl(it) val mediaSource = mediaSourceFromUrl(it)
client.loadMediaThumbnailForSource(mediaSource, size, size) client.loadMediaThumbnailForSource(mediaSource, size.value.toLong(), size.value.toLong())
} }
return mediaContent?.fold( return mediaContent?.fold(
{ it }, { it },
{ null } { null }
).let { model -> ).let { model ->
AvatarData(name.first().toString(), model, size.toInt()) AvatarData(name.first().uppercase(), model, size)
} }
} }

157
features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomItem.kt

@ -0,0 +1,157 @@
package io.element.android.x.features.roomlist.components
import Avatar
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.placeholder.material.placeholder
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
import io.element.android.x.matrix.core.RoomId
@Composable
internal fun RoomItem(
modifier: Modifier = Modifier,
room: RoomListRoomSummary,
onClick: (RoomId) -> Unit
) {
if (room.isPlaceholder) {
return PlaceholderRoomItem(modifier = modifier, room = room)
}
Column(
modifier = modifier
.fillMaxWidth()
.clickable(
onClick = { onClick(room.roomId) },
indication = rememberRipple(),
interactionSource = remember { MutableInteractionSource() }
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(IntrinsicSize.Min),
verticalAlignment = CenterVertically
) {
Avatar(room.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 = room.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Last Message
Text(
text = room.lastMessage?.toString().orEmpty(),
color = MaterialTheme.colorScheme.secondary,
lineHeight = 20.sp,
fontSize = 15.sp,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
// Timestamp and Unread
Column(
modifier = Modifier
.alignByBaseline(),
) {
Text(
fontSize = 12.sp,
text = room.timestamp ?: "",
color = MaterialTheme.colorScheme.secondary,
)
Spacer(modifier.size(4.dp))
val unreadIndicatorColor =
if (room.hasUnread) MaterialTheme.colorScheme.primary else Color.Transparent
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(unreadIndicatorColor)
.align(Alignment.End),
)
}
}
}
}
@Composable
internal fun PlaceholderRoomItem(
modifier: Modifier = Modifier,
room: RoomListRoomSummary,
) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = CenterVertically,
) {
Text(
modifier = Modifier
.size(room.avatarData.size.dp)
.clip(CircleShape)
.placeholder(true),
text = ""
)
Column(
modifier = Modifier
.padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp)
.weight(1f)
) {
Text(
modifier = Modifier
.size(width = 80.dp, height = 12.dp)
.placeholder(visible = true),
text = "",
)
Spacer(modifier = Modifier.size(4.dp))
Text(
modifier = Modifier
.size(width = 160.dp, height = 12.dp)
.placeholder(visible = true),
text = "",
)
}
Column {
Text(
modifier = Modifier
.size(width = 24.dp, height = 12.dp)
.placeholder(visible = true),
text = "",
color = MaterialTheme.colorScheme.secondary,
)
Spacer(Modifier.size(4.dp))
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(Color.Transparent)
.align(Alignment.End),
)
}
}
}

48
features/roomlist/src/main/java/io/element/android/x/features/roomlist/components/RoomListTopBar.kt

@ -0,0 +1,48 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package io.element.android.x.features.roomlist.components
import Avatar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.text.font.FontWeight
import io.element.android.x.core.data.LogCompositions
import io.element.android.x.features.roomlist.model.MatrixUser
@Composable
fun RoomListTopBar(
matrixUser: MatrixUser?,
onLogoutClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior
) {
LogCompositions(tag = "RoomListScreen", msg = "TopBar")
MediumTopAppBar(
modifier = Modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
title = {
Text(
fontWeight = FontWeight.Bold,
text = "All Chats"
)
},
navigationIcon = {
if (matrixUser != null) {
IconButton(onClick = {}) {
Avatar(matrixUser.avatarData)
}
}
},
actions = {
IconButton(
onClick = onLogoutClicked
) {
Icon(Icons.Default.ExitToApp, contentDescription = "logout")
}
},
scrollBehavior = scrollBehavior,
)
}

16
features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/RoomListRoomSummary.kt

@ -12,4 +12,18 @@ data class RoomListRoomSummary(
val lastMessage: CharSequence? = null, val lastMessage: CharSequence? = null,
val avatarData: AvatarData = AvatarData(), val avatarData: AvatarData = AvatarData(),
val isPlaceholder: Boolean = false, val isPlaceholder: Boolean = false,
) ) {
companion object {
fun placeholder(id: String): RoomListRoomSummary {
return RoomListRoomSummary(
id = id,
isPlaceholder = true,
name = "Short name",
timestamp = "hh:mm",
lastMessage = "Last message for placeholder",
avatarData = AvatarData("S")
)
}
}
}

25
features/roomlist/src/main/java/io/element/android/x/features/roomlist/model/stubbed.kt

@ -0,0 +1,25 @@
package io.element.android.x.features.roomlist.model
import io.element.android.x.designsystem.components.avatar.AvatarData
internal fun stubbedRoomSummaries(): List<RoomListRoomSummary> {
return listOf(
RoomListRoomSummary(
name = "Room",
hasUnread = true,
timestamp = "14:18",
lastMessage = "A very very very very long message which suites on two lines",
avatarData = AvatarData("R"),
id = "roomId"
),
RoomListRoomSummary(
name = "Room#2",
hasUnread = false,
timestamp = "14:16",
lastMessage = "A short message",
avatarData = AvatarData("Z"),
id = "roomId2"
),
RoomListRoomSummary.placeholder("roomId2")
)
}

1
gradle/libs.versions.toml

@ -57,6 +57,7 @@ accompanist_animation = { module = "com.google.accompanist:accompanist-navigatio
accompanist_permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" } accompanist_permission = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
accompanist_material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" } accompanist_material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" }
accompanist_systemui = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanist_systemui = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" }
accompanist_placeholder = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanist" }
# Test # Test
test_junit = { module = "junit:junit", version.ref = "test_junit" } test_junit = { module = "junit:junit", version.ref = "test_junit" }

1
libraries/avatar/.gitignore vendored

@ -1 +0,0 @@
/build

12
libraries/avatar/build.gradle.kts

@ -1,12 +0,0 @@
plugins {
id("io.element.android-compose")
}
android {
namespace = "io.element.android.x.libraries.avatar"
}
dependencies {
implementation(project(":libraries:matrix"))
implementation(libs.coil.compose)
}

0
libraries/avatar/consumer-rules.pro

21
libraries/avatar/proguard-rules.pro vendored

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

24
libraries/avatar/src/androidTest/java/io/element/android/x/avatar/ExampleInstrumentedTest.kt

@ -1,24 +0,0 @@
package io.element.android.x.avatar
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.avatar.test", appContext.packageName)
}
}

4
libraries/avatar/src/main/AndroidManifest.xml

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

33
libraries/avatar/src/main/java/io/element/android/x/avatar/Avatar.kt

@ -1,33 +0,0 @@
package io.element.android.x.avatar
import android.util.Log
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import coil.compose.rememberAsyncImagePainter
/**
* TODO fallback Avatar
*/
@Composable
fun Avatar(avatarData: AvatarData) {
Image(
painter = rememberAsyncImagePainter(
model = avatarData.url,
onError = {
Log.e("TAG", "Error $it\n${it.result}", it.result.throwable)
}),
contentDescription = null,
modifier = Modifier
.size(avatarData.size)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
)
}

10
libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarData.kt

@ -1,10 +0,0 @@
package io.element.android.x.avatar
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
data class AvatarData(
val url: String,
val size: Dp = 48.dp
)

41
libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarFetcher.kt

@ -1,41 +0,0 @@
package io.element.android.x.avatar
import coil.ImageLoader
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.request.Options
import io.element.android.x.matrix.MatrixClient
import org.matrix.rustcomponents.sdk.mediaSourceFromUrl
class AvatarFetcher(
private val matrixClient: MatrixClient,
private val avatarData: AvatarData,
private val options: Options,
private val imageLoader: ImageLoader
) :
Fetcher {
override suspend fun fetch(): FetchResult? {
val mediaSource = mediaSourceFromUrl(avatarData.url)
val mediaContent = matrixClient.loadMediaContentForSource(mediaSource)
return mediaContent.fold(
{ mediaContent ->
val byteArray = mediaContent.toUByteArray().toByteArray()
val fetcher = imageLoader.components.newFetcher(byteArray, options, imageLoader)
fetcher?.first?.fetch()
},
{null}
)
}
class Factory(private val matrixClient: MatrixClient) : Fetcher.Factory<AvatarData> {
override fun create(
data: AvatarData,
options: Options,
imageLoader: ImageLoader
): Fetcher? {
return AvatarFetcher(matrixClient, data, options, imageLoader)
}
}
}

17
libraries/avatar/src/test/java/io/element/android/x/avatar/ExampleUnitTest.kt

@ -1,17 +0,0 @@
package io.element.android.x.avatar
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

24
libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/Avatar.kt

@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
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
@ -20,18 +21,17 @@ import io.element.android.x.designsystem.components.avatar.AvatarData
@Composable @Composable
fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) { fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
val commonModifier = modifier
.size(avatarData.size.dp)
.clip(CircleShape)
if (avatarData.model == null) { if (avatarData.model == null) {
InitialsAvatar( InitialsAvatar(
modifier = modifier modifier = commonModifier,
.size(avatarData.size.dp)
.clip(CircleShape),
initials = avatarData.initials initials = avatarData.initials
) )
} else { } else {
ImageAvatar( ImageAvatar(
modifier = modifier modifier = commonModifier,
.size(avatarData.size.dp)
.clip(CircleShape),
avatarData = avatarData avatarData = avatarData
) )
} }
@ -50,8 +50,6 @@ private fun ImageAvatar(
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = modifier modifier = modifier
.size(avatarData.size.dp)
.clip(CircleShape)
) )
} }
@ -65,15 +63,15 @@ private fun InitialsAvatar(
listOf( listOf(
AvatarGradientStart, AvatarGradientStart,
AvatarGradientEnd, AvatarGradientEnd,
) ),
start = Offset(0.0f, 100f),
end = Offset(100f, 0f)
) )
Box( Box(
modifier modifier.background(brush = initialsGradient)
.background(brush = initialsGradient)
) { ) {
Text( Text(
modifier = Modifier modifier = Modifier.align(Alignment.Center),
.align(Alignment.Center),
text = initials, text = initials,
fontSize = 24.sp, fontSize = 24.sp,
color = Color.White, color = Color.White,

4
libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarData.kt

@ -6,7 +6,7 @@ import androidx.compose.runtime.Stable
data class AvatarData( data class AvatarData(
val initials: String = "", val initials: String = "",
val model: ByteArray? = null, val model: ByteArray? = null,
val size: Int = 0 val size: AvatarSize = AvatarSize.MEDIUM
) { ) {
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -27,7 +27,7 @@ data class AvatarData(
override fun hashCode(): Int { override fun hashCode(): Int {
var result = initials.hashCode() var result = initials.hashCode()
result = 31 * result + (model?.contentHashCode() ?: 0) result = 31 * result + (model?.contentHashCode() ?: 0)
result = 31 * result + size result = 31 * result + size.value
return result return result
} }

11
libraries/designsystem/src/main/java/io/element/android/x/designsystem/components/avatar/AvatarSize.kt

@ -0,0 +1,11 @@
package io.element.android.x.designsystem.components.avatar
import androidx.compose.ui.unit.dp
enum class AvatarSize(val value: Int) {
SMALL(32),
MEDIUM(40),
BIG(48);
val dp = value.dp
}

1
settings.gradle.kts

@ -24,4 +24,3 @@ include(":features:login")
include(":features:roomlist") include(":features:roomlist")
include(":features:messages") include(":features:messages")
include(":libraries:designsystem") include(":libraries:designsystem")
include(":libraries:avatar")

Loading…
Cancel
Save