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")