Continue refinement of RoomList (and remove avatar library)
This commit is contained in:
@@ -13,6 +13,7 @@ dependencies {
|
||||
implementation(libs.mavericks.compose)
|
||||
implementation(libs.timber)
|
||||
implementation(libs.datetime)
|
||||
implementation(libs.accompanist.placeholder)
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.3")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0")
|
||||
|
||||
@@ -2,39 +2,30 @@
|
||||
|
||||
package io.element.android.x.features.roomlist
|
||||
|
||||
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.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ExitToApp
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberTopAppBarState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.airbnb.mvrx.Success
|
||||
import com.airbnb.mvrx.compose.collectAsState
|
||||
import com.airbnb.mvrx.compose.mavericksViewModel
|
||||
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.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.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
|
||||
|
||||
@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
|
||||
private fun RoomItem(
|
||||
modifier: Modifier = Modifier,
|
||||
room: RoomListRoomSummary,
|
||||
onClick: (RoomId) -> Unit
|
||||
) {
|
||||
if (room.isPlaceholder) {
|
||||
return
|
||||
}
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun PreviewableRoomListContent() {
|
||||
ElementXTheme(darkTheme = false) {
|
||||
RoomListContent(
|
||||
roomSummaries = stubbedRoomSummaries(),
|
||||
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
|
||||
onRoomClicked = {},
|
||||
onLogoutClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewableRoomListContent() {
|
||||
val roomSummaries = listOf(
|
||||
RoomListRoomSummary(
|
||||
name = "Room",
|
||||
hasUnread = true,
|
||||
timestamp = "14:18",
|
||||
lastMessage = "A message",
|
||||
avatarData = AvatarData("R"),
|
||||
id = "roomId"
|
||||
private fun PreviewableDarkRoomListContent() {
|
||||
ElementXTheme(darkTheme = true) {
|
||||
RoomListContent(
|
||||
roomSummaries = stubbedRoomSummaries(),
|
||||
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
|
||||
onRoomClicked = {},
|
||||
onLogoutClicked = {}
|
||||
)
|
||||
)
|
||||
RoomListContent(
|
||||
roomSummaries = roomSummaries,
|
||||
matrixUser = MatrixUser("User#1", avatarData = AvatarData("U")),
|
||||
onRoomClicked = {},
|
||||
onLogoutClicked = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package io.element.android.x.features.roomlist
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.Loading
|
||||
import com.airbnb.mvrx.MavericksViewModel
|
||||
import com.airbnb.mvrx.Success
|
||||
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.AvatarSize
|
||||
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
|
||||
@@ -47,7 +47,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
||||
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
||||
val avatarData =
|
||||
loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl, 32)
|
||||
loadAvatarData(client, userDisplayName ?: client.userId().value, userAvatarUrl, AvatarSize.SMALL)
|
||||
MatrixUser(
|
||||
username = userDisplayName ?: client.userId().value,
|
||||
avatarUrl = userAvatarUrl,
|
||||
@@ -73,10 +73,7 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||
): List<RoomListRoomSummary> {
|
||||
return roomSummaries.parallelMap { roomSummary ->
|
||||
when (roomSummary) {
|
||||
is RoomSummary.Empty -> RoomListRoomSummary(
|
||||
id = roomSummary.identifier,
|
||||
isPlaceholder = true
|
||||
)
|
||||
is RoomSummary.Empty -> RoomListRoomSummary.placeholder(roomSummary.identifier)
|
||||
is RoomSummary.Filled -> {
|
||||
val avatarData = loadAvatarData(
|
||||
client,
|
||||
@@ -100,17 +97,17 @@ class RoomListViewModel(initialState: RoomListViewState) :
|
||||
client: MatrixClient,
|
||||
name: String,
|
||||
url: String?,
|
||||
size: Long = 48
|
||||
size: AvatarSize = AvatarSize.MEDIUM
|
||||
): AvatarData {
|
||||
val mediaContent = url?.let {
|
||||
val mediaSource = mediaSourceFromUrl(it)
|
||||
client.loadMediaThumbnailForSource(mediaSource, size, size)
|
||||
client.loadMediaThumbnailForSource(mediaSource, size.value.toLong(), size.value.toLong())
|
||||
}
|
||||
return mediaContent?.fold(
|
||||
{ it },
|
||||
{ null }
|
||||
).let { model ->
|
||||
AvatarData(name.first().toString(), model, size.toInt())
|
||||
AvatarData(name.first().uppercase(), model, size)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -12,4 +12,18 @@ data class RoomListRoomSummary(
|
||||
val lastMessage: CharSequence? = null,
|
||||
val avatarData: AvatarData = AvatarData(),
|
||||
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")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
@@ -57,6 +57,7 @@ accompanist_animation = { module = "com.google.accompanist:accompanist-navigatio
|
||||
accompanist_permission = { module = "com.google.accompanist:accompanist-permissions", 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_placeholder = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanist" }
|
||||
|
||||
# Test
|
||||
test_junit = { module = "junit:junit", version.ref = "test_junit" }
|
||||
|
||||
1
libraries/avatar/.gitignore
vendored
1
libraries/avatar/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -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)
|
||||
}
|
||||
21
libraries/avatar/proguard-rules.pro
vendored
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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@@ -20,18 +21,17 @@ import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||
|
||||
@Composable
|
||||
fun Avatar(avatarData: AvatarData, modifier: Modifier = Modifier) {
|
||||
val commonModifier = modifier
|
||||
.size(avatarData.size.dp)
|
||||
.clip(CircleShape)
|
||||
if (avatarData.model == null) {
|
||||
InitialsAvatar(
|
||||
modifier = modifier
|
||||
.size(avatarData.size.dp)
|
||||
.clip(CircleShape),
|
||||
modifier = commonModifier,
|
||||
initials = avatarData.initials
|
||||
)
|
||||
} else {
|
||||
ImageAvatar(
|
||||
modifier = modifier
|
||||
.size(avatarData.size.dp)
|
||||
.clip(CircleShape),
|
||||
modifier = commonModifier,
|
||||
avatarData = avatarData
|
||||
)
|
||||
}
|
||||
@@ -50,8 +50,6 @@ private fun ImageAvatar(
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = modifier
|
||||
.size(avatarData.size.dp)
|
||||
.clip(CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -65,15 +63,15 @@ private fun InitialsAvatar(
|
||||
listOf(
|
||||
AvatarGradientStart,
|
||||
AvatarGradientEnd,
|
||||
)
|
||||
),
|
||||
start = Offset(0.0f, 100f),
|
||||
end = Offset(100f, 0f)
|
||||
)
|
||||
Box(
|
||||
modifier
|
||||
.background(brush = initialsGradient)
|
||||
modifier.background(brush = initialsGradient)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center),
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = initials,
|
||||
fontSize = 24.sp,
|
||||
color = Color.White,
|
||||
|
||||
@@ -6,7 +6,7 @@ import androidx.compose.runtime.Stable
|
||||
data class AvatarData(
|
||||
val initials: String = "",
|
||||
val model: ByteArray? = null,
|
||||
val size: Int = 0
|
||||
val size: AvatarSize = AvatarSize.MEDIUM
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
@@ -27,7 +27,7 @@ data class AvatarData(
|
||||
override fun hashCode(): Int {
|
||||
var result = initials.hashCode()
|
||||
result = 31 * result + (model?.contentHashCode() ?: 0)
|
||||
result = 31 * result + size
|
||||
result = 31 * result + size.value
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -24,4 +24,3 @@ include(":features:login")
|
||||
include(":features:roomlist")
|
||||
include(":features:messages")
|
||||
include(":libraries:designsystem")
|
||||
include(":libraries:avatar")
|
||||
|
||||
Reference in New Issue
Block a user