ganfra
2 years ago
22 changed files with 305 additions and 322 deletions
@ -0,0 +1,157 @@
@@ -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 @@
@@ -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, |
||||
) |
||||
} |
@ -0,0 +1,25 @@
@@ -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,12 +0,0 @@
@@ -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) |
||||
} |
@ -1,21 +0,0 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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) |
||||
} |
||||
} |
@ -0,0 +1,11 @@
@@ -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 |
||||
} |
Loading…
Reference in new issue