diff --git a/app/src/main/java/io/element/android/x/MainActivity.kt b/app/src/main/java/io/element/android/x/MainActivity.kt
index f85e435ce5..23b4728d47 100644
--- a/app/src/main/java/io/element/android/x/MainActivity.kt
+++ b/app/src/main/java/io/element/android/x/MainActivity.kt
@@ -23,7 +23,7 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
- ElementXTheme {
+ ElementXTheme(darkTheme = false) {
MainScreen(viewModel = viewModel)
}
}
diff --git a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt
index e7db4c3549..9fad891834 100644
--- a/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt
+++ b/features/roomlist/src/main/java/io/element/android/x/features/roomlist/RoomListScreen.kt
@@ -1,22 +1,30 @@
package io.element.android.x.features.roomlist
+import android.widget.Space
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.items
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.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.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.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.LightGrey
import io.element.android.x.designsystem.components.Avatar
import io.element.android.x.features.roomlist.model.MatrixUser
import io.element.android.x.matrix.core.RoomId
@@ -107,17 +115,59 @@ private fun RoomItem(
return
}
val details = room.details
- Row(verticalAlignment = Alignment.CenterVertically,
+ Column(
modifier = modifier
- .clickable {
- onClick(room.details.roomId)
- }
.fillMaxWidth()
- .padding(horizontal = 8.dp)
+ .clickable(
+ onClick = { onClick(room.details.roomId) },
+ indication = rememberRipple(),
+ interactionSource = remember { MutableInteractionSource() }
+ ),
) {
- Column(modifier = modifier.padding(8.dp)) {
- Text(fontSize = 18.sp, text = details.name.orEmpty())
- Text(text = details.lastMessage?.toString().orEmpty(), maxLines = 2)
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
+ Box(modifier = Modifier
+ .align(Alignment.CenterVertically)
+ ) {
+ Avatar(data = null)
+ }
+ Column(
+ modifier = Modifier
+ .padding(12.dp)
+ .weight(1f)
+ ) {
+ Text(
+ fontSize = 16.sp,
+ fontWeight = FontWeight.Bold,
+ color = Color.Black,
+ text = details.name,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ Text(
+ text = details.lastMessage?.toString().orEmpty(),
+ color = LightGrey,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ }
+ Column(
+ Modifier
+ .padding(horizontal = 8.dp)
+ .align(Alignment.CenterVertically)
+ ) {
+ Text(
+ fontSize = 12.sp,
+ text = "14:18",
+ color = LightGrey
+ )
+ Spacer(Modifier.size(20.dp))
+ }
}
}
+
+
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0256e0b19e..88fa3a94f2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -32,6 +32,7 @@ test_orchestrator = "1.4.1"
#other
mavericks = "3.0.1"
timber = "5.0.1"
+coil = "2.2.1"
[libraries]
# Project
@@ -69,5 +70,6 @@ test_orchestrator = { module = "androidx.test:orchestrator", version.ref = "test
mavericks_compose = { module = "com.airbnb.android:mavericks-compose", version.ref = "mavericks" }
timber = { module = "com.jakewharton.timber:timber", version.ref = "timber" }
+coil_compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
[bundles]
diff --git a/libraries/avatar/.gitignore b/libraries/avatar/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/libraries/avatar/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/libraries/avatar/build.gradle.kts b/libraries/avatar/build.gradle.kts
new file mode 100644
index 0000000000..9808ef7ba6
--- /dev/null
+++ b/libraries/avatar/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ id("io.element.android-compose")
+}
+
+android {
+ namespace = "io.element.android.x.libraries.avatar"
+}
+
+dependencies {
+ implementation(project(":libraries:matrix"))
+ implementation(libs.coil.compose)
+}
\ No newline at end of file
diff --git a/libraries/avatar/consumer-rules.pro b/libraries/avatar/consumer-rules.pro
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/libraries/avatar/proguard-rules.pro b/libraries/avatar/proguard-rules.pro
new file mode 100644
index 0000000000..481bb43481
--- /dev/null
+++ b/libraries/avatar/proguard-rules.pro
@@ -0,0 +1,21 @@
+# 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
\ No newline at end of file
diff --git a/libraries/avatar/src/androidTest/java/io/element/android/x/avatar/ExampleInstrumentedTest.kt b/libraries/avatar/src/androidTest/java/io/element/android/x/avatar/ExampleInstrumentedTest.kt
new file mode 100644
index 0000000000..2ab7adcb23
--- /dev/null
+++ b/libraries/avatar/src/androidTest/java/io/element/android/x/avatar/ExampleInstrumentedTest.kt
@@ -0,0 +1,24 @@
+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)
+ }
+}
\ No newline at end of file
diff --git a/libraries/avatar/src/main/AndroidManifest.xml b/libraries/avatar/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..a5918e68ab
--- /dev/null
+++ b/libraries/avatar/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/libraries/avatar/src/main/java/io/element/android/x/avatar/Avatar.kt b/libraries/avatar/src/main/java/io/element/android/x/avatar/Avatar.kt
new file mode 100644
index 0000000000..ec71819247
--- /dev/null
+++ b/libraries/avatar/src/main/java/io/element/android/x/avatar/Avatar.kt
@@ -0,0 +1,33 @@
+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)
+ )
+}
+
diff --git a/libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarData.kt b/libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarData.kt
new file mode 100644
index 0000000000..c5004e6666
--- /dev/null
+++ b/libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarData.kt
@@ -0,0 +1,10 @@
+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
+)
+
diff --git a/libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarFetcher.kt b/libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarFetcher.kt
new file mode 100644
index 0000000000..bf88e161b6
--- /dev/null
+++ b/libraries/avatar/src/main/java/io/element/android/x/avatar/AvatarFetcher.kt
@@ -0,0 +1,41 @@
+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 {
+
+ override fun create(
+ data: AvatarData,
+ options: Options,
+ imageLoader: ImageLoader
+ ): Fetcher? {
+ return AvatarFetcher(matrixClient, data, options, imageLoader)
+ }
+ }
+}
diff --git a/libraries/avatar/src/test/java/io/element/android/x/avatar/ExampleUnitTest.kt b/libraries/avatar/src/test/java/io/element/android/x/avatar/ExampleUnitTest.kt
new file mode 100644
index 0000000000..79be6a0b85
--- /dev/null
+++ b/libraries/avatar/src/test/java/io/element/android/x/avatar/ExampleUnitTest.kt
@@ -0,0 +1,17 @@
+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)
+ }
+}
\ No newline at end of file
diff --git a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt
index 087195d9e4..c24648789f 100644
--- a/libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt
+++ b/libraries/designsystem/src/main/java/io/element/android/x/designsystem/Color.kt
@@ -8,4 +8,6 @@ val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
-val Pink40 = Color(0xFF7D5260)
\ No newline at end of file
+val Pink40 = Color(0xFF7D5260)
+
+val LightGrey = Color(0x993C3C43)
diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummary.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummary.kt
index 54aad3bdd1..1a831d4ef6 100644
--- a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummary.kt
+++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummary.kt
@@ -18,7 +18,7 @@ sealed interface RoomSummary {
data class RoomSummaryDetails(
val roomId: RoomId,
- val name: String?,
+ val name: String,
val isDirect: Boolean,
val avatarURLString: String?,
val lastMessage: CharSequence?,
diff --git a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDataSource.kt b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDataSource.kt
index 48eaccd807..6c5f93183f 100644
--- a/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDataSource.kt
+++ b/libraries/matrix/src/main/java/io/element/android/x/matrix/room/RoomSummaryDataSource.kt
@@ -123,7 +123,7 @@ internal class RustRoomSummaryDataSource(
return RoomSummary.Filled(
details = RoomSummaryDetails(
roomId = RoomId(identifier),
- name = room.name(),
+ name = room.name() ?: identifier,
isDirect = room.isDm() ?: false,
avatarURLString = room.fullRoom()?.avatarUrl(),
unreadNotificationCount = room.unreadNotifications().notificationCount(),
diff --git a/settings.gradle.kts b/settings.gradle.kts
index e64417e0aa..865db142fc 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -24,3 +24,4 @@ include(":features:login")
include(":features:roomlist")
include(":features:messages")
include(":libraries:designsystem")
+include(":libraries:avatar")