ganfra
2 years ago
22 changed files with 305 additions and 322 deletions
@ -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, |
||||||
|
) |
||||||
|
} |
@ -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 @@ |
|||||||
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 @@ |
|||||||
# 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) |
|
||||||
} |
|
||||||
} |
|
@ -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