Merge develop into feature/fga/dagger_setup

This commit is contained in:
ganfra 2022-12-19 16:14:14 +01:00
commit 4c88d8e3c2
214 changed files with 2662 additions and 1833 deletions

View file

@ -23,8 +23,6 @@ dependencies {
implementation(project(":libraries:elementresources"))
implementation(libs.mavericks.compose)
ksp(libs.showkase.processor)
implementation(libs.timber)
testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junitext)
}
}

View file

@ -1,13 +1,11 @@
package io.element.android.x.features.login
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.login.test", appContext.packageName)
}
}
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest>
</manifest>
</manifest>

View file

@ -2,7 +2,13 @@
package io.element.android.x.features.login
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
@ -10,8 +16,21 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
@ -56,19 +75,22 @@ fun LoginScreen(
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LoginContent(
state: LoginViewState,
formState: LoginFormState,
modifier: Modifier = Modifier,
onChangeServer: () -> Unit = {},
onLoginChanged: (String) -> Unit = {},
onPasswordChanged: (String) -> Unit = {},
onSubmitClicked: () -> Unit = {},
onLoginWithSuccess: (MatrixClient) -> Unit = {},
) {
Surface(color = MaterialTheme.colorScheme.background) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background,
) {
Box(
modifier = Modifier
.fillMaxSize()
@ -96,7 +118,7 @@ fun LoginContent(
)
// Form
Column(
//modifier = Modifier.weight(1f),
// modifier = Modifier.weight(1f),
) {
Box(
modifier = Modifier.fillMaxWidth()

View file

@ -66,4 +66,4 @@ class LoginViewModel @AssistedInject constructor(
formState.value = formState.value.copy(login = name)
setState { copy(loggedInClient = Uninitialized) }
}
}
}

View file

@ -23,5 +23,4 @@ data class LoginFormState(
companion object {
val Default = LoginFormState("", "")
}
}
}

View file

@ -3,13 +3,27 @@
package io.element.android.x.features.login.changeserver
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
@ -45,15 +59,18 @@ fun ChangeServerScreen(
)
}
@Composable
fun ChangeServerContent(
state: ChangeServerViewState,
modifier: Modifier = Modifier,
onChangeServer: (String) -> Unit = {},
onChangeServerSubmit: () -> Unit = {},
onChangeServerSuccess: () -> Unit = {},
) {
Surface(color = MaterialTheme.colorScheme.background) {
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background,
) {
val scrollState = rememberScrollState()
Box(
modifier = Modifier
@ -67,8 +84,7 @@ fun ChangeServerContent(
state = scrollState,
)
.padding(horizontal = 16.dp)
)
{
) {
val isError = state.changeServerAction is Fail
Box(
modifier = Modifier
@ -101,7 +117,7 @@ fun ChangeServerContent(
)
Text(
text = "A server is a home for all your data.\n" +
"You choose your server and its easy to make one.", // TODO "Learn more.",
"You choose your server and its easy to make one.", // TODO "Learn more.",
modifier = Modifier
.fillMaxWidth()
.align(Alignment.CenterHorizontally)

View file

@ -48,4 +48,4 @@ class ChangeServerViewModel @AssistedInject constructor(
}
}
}
}
}

View file

@ -10,4 +10,4 @@ data class ChangeServerViewState(
val changeServerAction: Async<Unit> = Uninitialized,
) : MavericksState {
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Loading
}
}

View file

@ -3,8 +3,8 @@ package io.element.android.x.features.login.error
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import io.element.android.x.core.uri.isValidUrl
import io.element.android.x.features.login.LoginFormState
import io.element.android.x.element.resources.R as ElementR
import io.element.android.x.features.login.LoginFormState
@Composable
fun loginError(

View file

@ -1,22 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="64"
android:viewportHeight="64">
<path
android:pathData="M23.04,3.84C23.04,1.7192 24.7593,0 26.88,0C41.0185,0 52.48,11.4615 52.48,25.6C52.48,27.7208 50.7608,29.44 48.64,29.44C46.5193,29.44 44.8,27.7208 44.8,25.6C44.8,15.7031 36.777,7.68 26.88,7.68C24.7593,7.68 23.04,5.9608 23.04,3.84Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M40.96,60.16C40.96,62.2808 39.2407,64 37.12,64C22.9815,64 11.52,52.5385 11.52,38.4C11.52,36.2792 13.2392,34.56 15.36,34.56C17.4807,34.56 19.2,36.2792 19.2,38.4C19.2,48.2969 27.223,56.32 37.12,56.32C39.2407,56.32 40.96,58.0392 40.96,60.16Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M3.84,40.96C1.7192,40.96 -0,39.2407 -0,37.12C-0,22.9815 11.4615,11.52 25.6,11.52C27.7208,11.52 29.44,13.2392 29.44,15.36C29.44,17.4807 27.7208,19.2 25.6,19.2C15.7031,19.2 7.68,27.223 7.68,37.12C7.68,39.2407 5.9608,40.96 3.84,40.96Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
<path
android:pathData="M60.16,23.04C62.2808,23.04 64,24.7593 64,26.88C64,41.0185 52.5385,52.48 38.4,52.48C36.2792,52.48 34.56,50.7608 34.56,48.64C34.56,46.5193 36.2792,44.8 38.4,44.8C48.2969,44.8 56.32,36.777 56.32,26.88C56.32,24.7593 58.0392,23.04 60.16,23.04Z"
android:fillColor="#0DBD8B"
android:fillType="evenOdd"/>
</vector>

View file

@ -1,5 +1,10 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM11,17H7v-4h4V17zM11,11H7V7h4V11zM17,17h-4v-4h4V17zM17,11h-4V7h4V11z"/>
<vector android:height="24dp"
android:tint="#000000"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp"
xmlns:android="http://schemas.android.com/apk/res/android">
<path
android:fillColor="@android:color/white"
android:pathData="M19,3H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM11,17H7v-4h4V17zM11,11H7V7h4V11zM17,17h-4v-4h4V17zM17,11h-4V7h4V11z" />
</vector>

View file

@ -1,9 +1,8 @@
package io.element.android.x.features.login
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
@ -14,4 +13,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
}

View file

@ -23,7 +23,6 @@ dependencies {
implementation(project(":libraries:textcomposer"))
implementation(libs.mavericks.compose)
implementation(libs.coil.compose)
implementation(libs.timber)
implementation(libs.datetime)
implementation(libs.accompanist.flowlayout)
implementation(libs.androidx.recyclerview)

View file

@ -1,13 +1,11 @@
package io.element.android.x.features.messages
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.messages.test", appContext.packageName)
}
}
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest>
</manifest>
</manifest>

View file

@ -9,12 +9,20 @@ import io.element.android.x.features.messages.model.AggregatedReaction
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.model.MessagesItemReactionState
import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.content.*
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEmoteContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemNoticeContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent
import io.element.android.x.features.messages.util.invalidateLast
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.MatrixRoom
import io.element.android.x.matrix.timeline.MatrixTimelineItem
import kotlin.system.measureTimeMillis
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -28,7 +36,6 @@ import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat
import org.matrix.rustcomponents.sdk.MessageType
import timber.log.Timber
import kotlin.system.measureTimeMillis
class MessageTimelineItemStateFactory(
private val client: MatrixClient,
@ -83,7 +90,6 @@ class MessageTimelineItemStateFactory(
timelineItemStates.emit(newTimelineItemStates)
}
private fun calculateAndApplyDiff(newTimelineItems: List<MatrixTimelineItem>) {
val timeToDiff = measureTimeMillis {
val diffCallback =
@ -192,7 +198,6 @@ class MessageTimelineItemStateFactory(
htmlDocument = messageType.content.formatted?.toHtmlDocument()
)
else -> MessagesTimelineItemUnknownContent
}
}
@ -232,5 +237,4 @@ class MessageTimelineItemStateFactory(
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
}

View file

@ -6,9 +6,26 @@
package io.element.android.x.features.messages
import Avatar
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
@ -20,8 +37,24 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.End
import androidx.compose.ui.Alignment.Companion.Start
@ -41,18 +74,40 @@ import com.airbnb.mvrx.compose.mavericksViewModel
import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.core.compose.PairCombinedPreviewParameter
import io.element.android.x.core.data.StableCharSequence
import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.features.messages.components.*
import io.element.android.x.features.messages.model.*
import io.element.android.x.features.messages.model.content.*
import io.element.android.x.features.messages.components.MessageEventBubble
import io.element.android.x.features.messages.components.MessagesReactionsView
import io.element.android.x.features.messages.components.MessagesTimelineItemEncryptedView
import io.element.android.x.features.messages.components.MessagesTimelineItemImageView
import io.element.android.x.features.messages.components.MessagesTimelineItemRedactedView
import io.element.android.x.features.messages.components.MessagesTimelineItemTextView
import io.element.android.x.features.messages.components.MessagesTimelineItemUnknownView
import io.element.android.x.features.messages.components.TimelineItemActionsScreen
import io.element.android.x.features.messages.model.AggregatedReaction
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
import io.element.android.x.features.messages.model.MessagesItemGroupPositionProvider
import io.element.android.x.features.messages.model.MessagesItemReactionState
import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemContentProvider
import io.element.android.x.features.messages.model.content.MessagesTimelineItemEncryptedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemRedactedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
import io.element.android.x.features.messages.model.content.MessagesTimelineItemUnknownContent
import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel
import io.element.android.x.features.messages.textcomposer.MessageComposerViewState
import io.element.android.x.textcomposer.MessageComposerMode
import io.element.android.x.textcomposer.TextComposer
import java.lang.Math.random
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import timber.log.Timber
import java.lang.Math.random
@Composable
fun MessagesScreen(
@ -61,7 +116,6 @@ fun MessagesScreen(
viewModel: MessagesViewModel = mavericksViewModel(argsFactory = { roomId }),
composerViewModel: MessageComposerViewModel = mavericksViewModel(argsFactory = { roomId })
) {
fun onSendMessage(textMessage: String) {
viewModel.sendMessage(textMessage)
composerViewModel.updateText("")
@ -88,7 +142,7 @@ fun MessagesScreen(
MessagesScreenContent(
roomTitle = roomTitle,
roomAvatar = roomAvatar,
timelineItems = timelineItems().orEmpty(),
timelineItems = timelineItems().orEmpty().toImmutableList(),
hasMoreToLoad = hasMoreToLoad,
onReachedLoadMore = viewModel::loadMore,
onBackPressed = onBackPressed,
@ -130,7 +184,7 @@ fun MessagesScreen(
fun MessagesScreenContent(
roomTitle: String?,
roomAvatar: AvatarData?,
timelineItems: List<MessagesTimelineItemState>,
timelineItems: ImmutableList<MessagesTimelineItemState>,
hasMoreToLoad: Boolean,
onReachedLoadMore: () -> Unit,
onBackPressed: () -> Unit,
@ -146,9 +200,11 @@ fun MessagesScreenContent(
composerCanSendMessage: Boolean,
composerText: StableCharSequence?,
snackbarHostState: SnackbarHostState,
modifier: Modifier = Modifier,
) {
LogCompositions(tag = "MessagesScreen", msg = "Content")
Scaffold(
modifier = modifier,
contentWindowInsets = WindowInsets.statusBars,
topBar = {
MessagesTopAppBar(
@ -187,7 +243,7 @@ fun MessagesScreenContent(
@Composable
fun MessagesContent(
timelineItems: List<MessagesTimelineItemState>,
timelineItems: ImmutableList<MessagesTimelineItemState>,
hasMoreToLoad: Boolean,
onReachedLoadMore: () -> Unit,
onSendMessage: (String) -> Unit,
@ -203,7 +259,6 @@ fun MessagesContent(
composerText: StableCharSequence?,
modifier: Modifier = Modifier
) {
val lazyListState = rememberLazyListState()
Column(
modifier = modifier
@ -249,9 +304,11 @@ fun MessagesContent(
fun MessagesTopAppBar(
roomTitle: String?,
roomAvatar: AvatarData?,
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
onBackPressed: () -> Unit = {},
) {
TopAppBar(
modifier = modifier,
navigationIcon = {
IconButton(onClick = onBackPressed) {
Icon(
@ -282,7 +339,7 @@ fun MessagesTopAppBar(
@Composable
fun TimelineItems(
lazyListState: LazyListState,
timelineItems: List<MessagesTimelineItemState>,
timelineItems: ImmutableList<MessagesTimelineItemState>,
highlightedEventId: String?,
modifier: Modifier = Modifier,
hasMoreToLoad: Boolean = false,
@ -322,7 +379,6 @@ fun TimelineItems(
onLoadMore = onReachedLoadMore
)
}
}
private fun MessagesTimelineItemState.key(): String {
@ -339,7 +395,6 @@ private fun MessagesTimelineItemState.contentType(): Int {
}
}
@Composable
fun TimelineItemRow(
timelineItem: MessagesTimelineItemState,
@ -426,7 +481,6 @@ fun MessageEventRow(
content = messageEvent.content,
modifier = contentModifier
)
else -> TODO() /* compiler issue ? */
}
}
MessagesReactionsView(
@ -471,8 +525,8 @@ private fun MessageSenderInformation(
@Composable
internal fun BoxScope.MessagesScrollHelper(
lazyListState: LazyListState,
timelineItems: List<MessagesTimelineItemState>,
onLoadMore: () -> Unit,
timelineItems: ImmutableList<MessagesTimelineItemState>,
onLoadMore: () -> Unit = {},
) {
val coroutineScope = rememberCoroutineScope()
val firstVisibleItemIndex by remember { derivedStateOf { lazyListState.firstVisibleItemIndex } }
@ -526,7 +580,6 @@ internal fun BoxScope.MessagesScrollHelper(
Icon(Icons.Default.ArrowDownward, "")
}
}
}
@Composable
@ -543,7 +596,6 @@ internal fun MessagesLoadingMoreIndicator() {
color = MaterialTheme.colorScheme.primary
)
}
}
class MessagesItemGroupPositionToMessagesTimelineItemContentProvider :
@ -551,6 +603,7 @@ class MessagesItemGroupPositionToMessagesTimelineItemContentProvider :
MessagesItemGroupPositionProvider() to MessagesTimelineItemContentProvider()
)
@Suppress("PreviewPublic")
@Preview(showBackground = true)
@Composable
fun TimelineItemsPreview(
@ -559,7 +612,7 @@ fun TimelineItemsPreview(
) {
TimelineItems(
lazyListState = LazyListState(),
timelineItems = listOf(
timelineItems = persistentListOf(
// 3 items (First Middle Last) with isMine = false
createMessageEvent(
isMine = false,

View file

@ -218,4 +218,4 @@ class MessagesViewModel @AssistedInject constructor(
timeline.callback = null
timeline.dispose()
}
}
}

View file

@ -13,7 +13,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import io.element.android.x.designsystem.*
import io.element.android.x.designsystem.LocalIsDarkTheme
import io.element.android.x.designsystem.MessageHighlightDark
import io.element.android.x.designsystem.MessageHighlightLight
import io.element.android.x.designsystem.SystemGrey5Dark
import io.element.android.x.designsystem.SystemGrey5Light
import io.element.android.x.designsystem.SystemGrey6Dark
import io.element.android.x.designsystem.SystemGrey6Light
import io.element.android.x.features.messages.model.MessagesItemGroupPosition
private val BUBBLE_RADIUS = 16.dp
@ -26,9 +32,9 @@ fun MessageEventBubble(
interactionSource: MutableInteractionSource,
isHighlighted: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit,
onLongClick: () -> Unit,
content: @Composable () -> Unit,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
content: @Composable () -> Unit = {},
) {
fun bubbleShape(): Shape {
return when (groupPosition) {
@ -102,4 +108,4 @@ fun MessageEventBubble(
shape = bubbleShape,
content = content
)
}
}

View file

@ -24,7 +24,7 @@ fun MessagesReactionsView(
reactionsState: MessagesItemReactionState,
modifier: Modifier = Modifier,
) {
if(reactionsState.reactions.isEmpty()) return
if (reactionsState.reactions.isEmpty()) return
FlowRow(
modifier = modifier,
mainAxisSpacing = 2.dp,
@ -53,4 +53,4 @@ fun MessagesReactionButton(reaction: AggregatedReaction, modifier: Modifier = Mo
Text(text = reaction.count, color = MaterialTheme.colorScheme.secondary, fontSize = 12.sp)
}
}
}
}

View file

@ -3,14 +3,28 @@
package io.element.android.x.features.messages.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ListItem
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.unit.dp
import com.airbnb.mvrx.compose.collectAsState
import io.element.android.x.designsystem.components.VectorIcon
@ -21,7 +35,6 @@ import io.element.android.x.features.messages.model.MessagesTimelineItemState
import io.element.android.x.features.messages.model.MessagesViewState
import io.element.android.x.features.messages.model.content.MessagesTimelineItemTextBasedContent
import io.element.android.x.features.messages.textcomposer.MessageComposerViewModel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
@ -63,7 +76,6 @@ fun TimelineItemActionsScreen(
}
}
ModalBottomSheetLayout(
modifier = modifier,
sheetState = modalBottomSheetState,
@ -71,46 +83,47 @@ fun TimelineItemActionsScreen(
SheetContent(
actionsSheetState = itemActionsSheetState(),
onActionClicked = ::onItemActionClicked,
modifier = Modifier.navigationBarsPadding().imePadding()
modifier = Modifier
.navigationBarsPadding()
.imePadding()
)
}
) {}
}
@Composable
private fun SheetContent(
actionsSheetState: MessagesItemActionsSheetState?,
onActionClicked: (MessagesItemAction, MessagesTimelineItemState.MessageEvent) -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
onActionClicked: (MessagesItemAction, MessagesTimelineItemState.MessageEvent) -> Unit = { _, _ -> },
) {
if (actionsSheetState == null || actionsSheetState.actions.isEmpty()) {
// Crashes if sheetContent size is zero
Box(modifier = modifier.size(1.dp))
return
}
LazyColumn(
modifier = modifier
.fillMaxWidth()
) {
items(actionsSheetState.actions) {
ListItem(
modifier = Modifier.clickable {
onActionClicked(it, actionsSheetState.targetItem)
},
text = {
Text(
text = it.title,
color = if (it.destructive) MaterialTheme.colors.error else Color.Unspecified,
)
},
icon = {
VectorIcon(
resourceId = it.icon,
tint = if (it.destructive) MaterialTheme.colors.error else LocalContentColor.current,
)
}
)
} else {
LazyColumn(
modifier = modifier
.fillMaxWidth()
) {
items(actionsSheetState.actions) {
ListItem(
modifier = Modifier.clickable {
onActionClicked(it, actionsSheetState.targetItem)
},
text = {
Text(
text = it.title,
color = if (it.destructive) MaterialTheme.colors.error else Color.Unspecified,
)
},
icon = {
VectorIcon(
resourceId = it.icon,
tint = if (it.destructive) MaterialTheme.colors.error else LocalContentColor.current,
)
}
)
}
}
}
}
}

View file

@ -17,4 +17,4 @@ fun MessagesTimelineItemEncryptedView(
icon = Icons.Default.Warning,
modifier = modifier
)
}
}

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@ -16,7 +15,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.element.android.x.features.messages.model.content.MessagesTimelineItemImageContent
@ -26,9 +24,9 @@ fun MessagesTimelineItemImageView(
content: MessagesTimelineItemImageContent,
modifier: Modifier = Modifier
) {
val widthPercent = if(content.aspectRatio > 1f){
val widthPercent = if (content.aspectRatio > 1f) {
1f
}else {
} else {
0.7f
}
Box(
@ -37,7 +35,6 @@ fun MessagesTimelineItemImageView(
.aspectRatio(content.aspectRatio),
contentAlignment = Alignment.Center,
) {
var isLoading = rememberSaveable(content.imageMeta) { mutableStateOf(true) }
val context = LocalContext.current
val model = ImageRequest.Builder(context)
@ -52,4 +49,4 @@ fun MessagesTimelineItemImageView(
onSuccess = { isLoading.value = false },
)
}
}
}

View file

@ -40,4 +40,4 @@ fun MessagesTimelineItemInformativeView(
text = text
)
}
}
}

View file

@ -17,4 +17,4 @@ fun MessagesTimelineItemRedactedView(
icon = Icons.Default.Delete,
modifier = modifier
)
}
}

View file

@ -22,8 +22,8 @@ fun MessagesTimelineItemTextView(
content: MessagesTimelineItemTextBasedContent,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit,
onTextLongClicked: () -> Unit,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
val htmlDocument = content.htmlDocument
if (htmlDocument != null) {
@ -74,4 +74,4 @@ private fun String.linkify(
end = end
)
}
}
}

View file

@ -17,4 +17,4 @@ fun MessagesTimelineItemUnknownView(
icon = Icons.Default.Info,
modifier = modifier
)
}
}

View file

@ -9,17 +9,25 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material3.*
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.*
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.flowlayout.FlowRow
@ -27,6 +35,7 @@ import io.element.android.x.designsystem.LinkColor
import io.element.android.x.designsystem.components.ClickableLinkText
import io.element.android.x.matrix.permalink.PermalinkData
import io.element.android.x.matrix.permalink.PermalinkParser
import kotlinx.collections.immutable.persistentMapOf
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.Node
@ -38,34 +47,33 @@ private const val chipId = "chip"
fun HtmlDocument(
document: Document,
interactionSource: MutableInteractionSource,
onTextClicked: () -> Unit,
onTextLongClicked: () -> Unit,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
HtmlBody(
body = document.body(),
interactionSource = interactionSource,
modifier = modifier,
onTextClicked = onTextClicked,
onTextLongClicked = onTextLongClicked,
interactionSource = interactionSource
)
}
@Composable
private fun HtmlBody(
body: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit,
onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
@Composable
fun NodesFlowRode(
nodes: Iterator<Node>,
onTextClicked: () -> Unit,
onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) = FlowRow(
mainAxisSpacing = 2.dp,
crossAxisSpacing = 8.dp,
@ -106,9 +114,9 @@ private fun HtmlBody(
while (nodesIterator.hasNext()) {
NodesFlowRode(
nodes = nodesIterator,
interactionSource = interactionSource,
onTextClicked = onTextClicked,
onTextLongClicked = onTextLongClicked,
interactionSource = interactionSource
)
}
}
@ -125,10 +133,10 @@ private fun Element.isInline(): Boolean {
@Composable
private fun HtmlBlock(
element: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit,
onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
val blockModifier = modifier
.padding(top = 4.dp)
@ -183,10 +191,10 @@ private fun HtmlBlock(
@Composable
private fun HtmlInline(
element: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit,
onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
Box(modifier) {
val styledText = buildAnnotatedString {
@ -202,7 +210,10 @@ private fun HtmlInline(
}
@Composable
private fun HtmlPreformatted(pre: Element, modifier: Modifier = Modifier) {
private fun HtmlPreformatted(
pre: Element,
modifier: Modifier = Modifier
) {
val isCode = pre.firstElementChild()?.normalName() == "code"
val backgroundColor =
if (isCode) MaterialTheme.colorScheme.codeBackground() else Color.Unspecified
@ -221,9 +232,10 @@ private fun HtmlPreformatted(pre: Element, modifier: Modifier = Modifier) {
@Composable
private fun HtmlParagraph(
paragraph: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit, onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
Box(modifier) {
val styledText = buildAnnotatedString {
@ -239,9 +251,10 @@ private fun HtmlParagraph(
@Composable
private fun HtmlBlockquote(
blockquote: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit, onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
val color = MaterialTheme.colorScheme.onBackground
Box(
@ -268,13 +281,13 @@ private fun HtmlBlockquote(
}
}
@Composable
private fun HtmlHeading(
heading: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit, onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
val style = when (heading.normalName()) {
"h1" -> MaterialTheme.typography.headlineLarge.copy(fontSize = 30.sp)
@ -304,9 +317,10 @@ private fun HtmlHeading(
@Composable
private fun HtmlMxReply(
mxReply: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit, onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
val blockquote = mxReply.childNodes().firstOrNull() ?: return
val shape = RoundedCornerShape(12.dp)
@ -356,9 +370,10 @@ private fun HtmlMxReply(
@Composable
private fun HtmlOrderedList(
orderedList: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit, onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
var number = 1
val delimiter = "."
@ -381,9 +396,10 @@ private fun HtmlOrderedList(
@Composable
private fun HtmlUnorderedList(
unorderedList: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit, onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
) {
val marker = ""
HtmlListItems(
@ -402,14 +418,14 @@ private fun HtmlUnorderedList(
}
}
@Composable
private fun HtmlListItems(
list: Element,
modifier: Modifier = Modifier,
onTextClicked: () -> Unit, onTextLongClicked: () -> Unit,
interactionSource: MutableInteractionSource,
content: @Composable (node: TextNode) -> Unit
modifier: Modifier = Modifier,
onTextClicked: () -> Unit = {},
onTextLongClicked: () -> Unit = {},
content: @Composable (node: TextNode) -> Unit = {}
) {
Column(modifier = modifier) {
for (node in list.children()) {
@ -420,13 +436,12 @@ private fun HtmlListItems(
}
is Element -> HtmlBlock(
element = innerNode,
modifier = modifier.padding(start = 4.dp),
modifier = Modifier.padding(start = 4.dp),
onTextClicked = onTextClicked, onTextLongClicked = onTextLongClicked,
interactionSource = interactionSource
)
}
}
}
}
}
@ -439,7 +454,6 @@ private fun AnnotatedString.Builder.appendInlineChildrenElements(
childNodes: List<Node>,
colors: ColorScheme
) {
for (node in childNodes) {
when (node) {
is TextNode -> {
@ -452,7 +466,6 @@ private fun AnnotatedString.Builder.appendInlineChildrenElements(
}
}
private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors: ColorScheme) {
when (element.normalName()) {
"br" -> {
@ -490,7 +503,6 @@ private fun AnnotatedString.Builder.appendInlineElement(element: Element, colors
appendInlineChildrenElements(element.childNodes(), colors)
}
}
}
private fun AnnotatedString.Builder.appendLink(link: Element) {
@ -521,13 +533,13 @@ private fun AnnotatedString.Builder.appendLink(link: Element) {
@Composable
private fun HtmlText(
text: AnnotatedString,
interactionSource: MutableInteractionSource,
modifier: Modifier = Modifier,
style: TextStyle = LocalTextStyle.current,
onClick: () -> Unit,
onLongClick: () -> Unit,
interactionSource: MutableInteractionSource,
onClick: () -> Unit = {},
onLongClick: () -> Unit = {},
) {
val inlineContentMap = emptyMap<String, InlineTextContent>()
val inlineContentMap = persistentMapOf<String, InlineTextContent>()
ClickableLinkText(
text = text,
linkAnnotationTag = "URL",
@ -539,4 +551,3 @@ private fun HtmlText(
onLongClick = onLongClick
)
}

View file

@ -37,5 +37,4 @@ internal class CacheInvalidator(private val itemStatesCache: MutableList<Message
itemStatesCache.removeAt(position)
}
}
}

View file

@ -31,5 +31,4 @@ internal class MatrixTimelineItemsDiffCallback(
val newItem = newList.getOrNull(newItemPosition)
return oldItem == newItem
}
}
}

View file

@ -15,4 +15,4 @@ sealed class MessagesItemAction(
object Redact : MessagesItemAction("Redact", VectorIcons.Delete, destructive = true)
object Reply : MessagesItemAction("Reply", VectorIcons.Reply)
object Edit : MessagesItemAction("Edit", VectorIcons.Edit)
}
}

View file

@ -12,7 +12,6 @@ sealed interface MessagesItemGroupPosition {
First, None -> true
else -> false
}
}
internal class MessagesItemGroupPositionProvider : PreviewParameterProvider<MessagesItemGroupPosition> {
@ -22,4 +21,4 @@ internal class MessagesItemGroupPositionProvider : PreviewParameterProvider<Mess
MessagesItemGroupPosition.Last,
MessagesItemGroupPosition.None,
)
}
}

View file

@ -6,9 +6,10 @@ import androidx.compose.runtime.Stable
data class MessagesItemReactionState(
val reactions: List<AggregatedReaction>
)
@Stable
data class AggregatedReaction(
val key: String,
val count: String,
val isHighlighted: Boolean = false
)
)

View file

@ -23,11 +23,5 @@ sealed interface MessagesTimelineItemState {
val showSenderInformation = groupPosition.isNew() && !isMine
val safeSenderName: String = senderDisplayName ?: senderId
}
}

View file

@ -2,8 +2,6 @@ package io.element.android.x.features.messages.model.content
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import org.matrix.rustcomponents.sdk.EncryptedMessage
import org.matrix.rustcomponents.sdk.FormattedBody
import org.matrix.rustcomponents.sdk.MessageFormat
sealed interface MessagesTimelineItemContent
@ -28,4 +26,4 @@ class MessagesTimelineItemContentProvider : PreviewParameterProvider<MessagesTim
),
MessagesTimelineItemUnknownContent,
)
}
}

View file

@ -5,4 +5,4 @@ import org.jsoup.nodes.Document
data class MessagesTimelineItemEmoteContent(
override val body: String,
override val htmlDocument: Document?
) : MessagesTimelineItemTextBasedContent
) : MessagesTimelineItemTextBasedContent

View file

@ -4,4 +4,4 @@ import org.matrix.rustcomponents.sdk.EncryptedMessage
data class MessagesTimelineItemEncryptedContent(
val encryptedMessage: EncryptedMessage
) : MessagesTimelineItemContent
) : MessagesTimelineItemContent

View file

@ -7,4 +7,4 @@ data class MessagesTimelineItemImageContent(
val imageMeta: MediaResolver.Meta,
val blurhash: String?,
val aspectRatio: Float
) : MessagesTimelineItemContent
) : MessagesTimelineItemContent

View file

@ -5,4 +5,4 @@ import org.jsoup.nodes.Document
data class MessagesTimelineItemNoticeContent(
override val body: String,
override val htmlDocument: Document?
) : MessagesTimelineItemTextBasedContent
) : MessagesTimelineItemTextBasedContent

View file

@ -1,3 +1,3 @@
package io.element.android.x.features.messages.model.content
object MessagesTimelineItemRedactedContent : MessagesTimelineItemContent
object MessagesTimelineItemRedactedContent : MessagesTimelineItemContent

View file

@ -5,4 +5,4 @@ import org.jsoup.nodes.Document
sealed interface MessagesTimelineItemTextBasedContent : MessagesTimelineItemContent {
val body: String
val htmlDocument: Document?
}
}

View file

@ -5,4 +5,4 @@ import org.jsoup.nodes.Document
data class MessagesTimelineItemTextContent(
override val body: String,
override val htmlDocument: Document?
) : MessagesTimelineItemTextBasedContent
) : MessagesTimelineItemTextBasedContent

View file

@ -1,3 +1,3 @@
package io.element.android.x.features.messages.model.content
object MessagesTimelineItemUnknownContent : MessagesTimelineItemContent
object MessagesTimelineItemUnknownContent : MessagesTimelineItemContent

View file

@ -35,4 +35,4 @@ class MessageComposerViewModel @AssistedInject constructor(
)
}
}
}
}

View file

@ -16,4 +16,4 @@ data class MessageComposerViewState(
// val voiceBroadcastState: VoiceBroadcastState? = null,
val text: StableCharSequence? = null,
val isFullScreen: Boolean = false,
) : MavericksState
) : MavericksState

View file

@ -5,4 +5,4 @@ internal inline fun <reified T> MutableList<T?>.invalidateLast() {
if (indexOfLast > 0) {
set(indexOfLast - 1, null)
}
}
}

View file

@ -1,9 +1,8 @@
package io.element.android.x.features.messages
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
@ -14,4 +13,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
}

View file

@ -12,10 +12,9 @@ dependencies {
implementation(project(":libraries:elementresources"))
implementation(project(":libraries:designsystem"))
implementation(libs.mavericks.compose)
implementation(libs.timber)
implementation(libs.accompanist.pager)
implementation(libs.accompanist.pagerindicator)
testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor)
}
}

View file

@ -1,13 +1,11 @@
package io.element.android.x.features.login
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.login.test", appContext.packageName)
}
}
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest>
</manifest>
</manifest>

View file

@ -3,12 +3,24 @@
package io.element.android.x.features.onboarding
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
@ -42,19 +54,20 @@ fun OnBoardingScreen(
)
}
@OptIn(ExperimentalPagerApi::class)
@Composable
fun OnBoardingContent(
state: OnBoardingViewState,
onPageChanged: (Int) -> Unit,
onSignUp: () -> Unit,
onSignIn: () -> Unit,
modifier: Modifier = Modifier,
onPageChanged: (Int) -> Unit = {},
onSignUp: () -> Unit = {},
onSignIn: () -> Unit = {},
) {
val carrouselState = remember { SplashCarouselStateFactory().create() }
val nbOfPages = carrouselState.items.size
var key by remember { mutableStateOf(false) }
Surface(
modifier = modifier,
color = MaterialTheme.colorScheme.background,
) {
Box(
@ -125,8 +138,11 @@ fun OnBoardingContent(
@Composable
fun OnBoardingPage(
item: SplashCarouselState.Item,
modifier: Modifier = Modifier,
) {
Box {
Box(
modifier = modifier,
) {
/*
Image(
painterResource(id = item.pageBackground),
@ -164,4 +180,4 @@ fun OnBoardingPage(
)
}
}
}
}

View file

@ -12,4 +12,4 @@ class OnBoardingViewModel(initialState: OnBoardingViewState) :
)
}
}
}
}

View file

@ -20,12 +20,12 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
data class SplashCarouselState(
val items: List<Item>
val items: List<Item>
) {
data class Item(
@StringRes val title: Int,
@StringRes val body: Int,
@DrawableRes val image: Int,
@DrawableRes val pageBackground: Int
@StringRes val title: Int,
@StringRes val body: Int,
@DrawableRes val image: Int,
@DrawableRes val pageBackground: Int
)
}

View file

@ -3,7 +3,7 @@ package io.element.android.x.features.onboarding
import androidx.annotation.DrawableRes
import io.element.android.x.element.resources.R as ElementR
class SplashCarouselStateFactory() {
class SplashCarouselStateFactory {
fun create(): SplashCarouselState {
val lightTheme = true
fun background(@DrawableRes lightDrawable: Int) =

View file

@ -1,9 +1,8 @@
package io.element.android.x.features.login
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
@ -14,4 +13,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
}

View file

@ -20,10 +20,9 @@ dependencies {
implementation(project(":libraries:matrix"))
implementation(project(":libraries:designsystem"))
implementation(libs.mavericks.compose)
implementation(libs.timber)
implementation(libs.datetime)
implementation(libs.accompanist.placeholder)
testImplementation(libs.test.junit)
androidTestImplementation(libs.test.junitext)
ksp(libs.showkase.processor)
}
}

View file

@ -1,13 +1,11 @@
package io.element.android.x.features.roomlist
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
@ -21,4 +19,4 @@ class ExampleInstrumentedTest {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("io.element.android.x.features.roomlist.test", appContext.packageName)
}
}
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest>
</manifest>
</manifest>

View file

@ -2,12 +2,18 @@ package io.element.android.x.features.roomlist
import android.text.format.DateFormat
import android.text.format.DateUtils
import kotlinx.datetime.*
import kotlinx.datetime.TimeZone
import java.time.Period
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.Locale
import kotlin.math.absoluteValue
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaLocalDateTime
import kotlinx.datetime.toLocalDateTime
class LastMessageFormatter(
private val clock: Clock = Clock.System,
@ -29,7 +35,6 @@ class LastMessageFormatter(
DateTimeFormatter.ofPattern(pattern)
}
fun format(timestamp: Long?): String {
if (timestamp == null) return ""
val now: Instant = clock.now()
@ -77,6 +82,4 @@ class LastMessageFormatter(
DateUtils.FORMAT_SHOW_WEEKDAY
).toString()
}
}
}

View file

@ -36,6 +36,8 @@ 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
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun RoomListScreen(
@ -53,7 +55,7 @@ fun RoomListScreen(
val roomSummaries by viewModel.collectAsState(RoomListViewState::rooms)
val matrixUser by viewModel.collectAsState(RoomListViewState::user)
RoomListContent(
roomSummaries = roomSummaries().orEmpty(),
roomSummaries = roomSummaries().orEmpty().toImmutableList(),
matrixUser = matrixUser(),
onRoomClicked = onRoomClicked,
onLogoutClicked = viewModel::logout,
@ -66,16 +68,16 @@ fun RoomListScreen(
@Composable
fun RoomListContent(
roomSummaries: List<RoomListRoomSummary>,
roomSummaries: ImmutableList<RoomListRoomSummary>,
matrixUser: MatrixUser?,
onRoomClicked: (RoomId) -> Unit,
filter: String,
onFilterChanged: (String) -> Unit,
onLogoutClicked: () -> Unit,
onScrollOver: (IntRange) -> Unit,
isLoginOut: Boolean,
modifier: Modifier = Modifier,
onRoomClicked: (RoomId) -> Unit = {},
onFilterChanged: (String) -> Unit = {},
onLogoutClicked: () -> Unit = {},
onScrollOver: (IntRange) -> Unit = {},
) {
fun onRoomClicked(room: RoomListRoomSummary) {
onRoomClicked(room.roomId)
}
@ -104,7 +106,7 @@ fun RoomListContent(
}
Scaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
RoomListTopBar(
matrixUser = matrixUser,
@ -133,7 +135,7 @@ fun RoomListContent(
}
)
if (isLoginOut) {
ProgressDialog("Login out...")
ProgressDialog(text = "Login out...")
}
}
@ -176,4 +178,3 @@ fun PreviewableDarkRoomListContent() {
)
}
}

View file

@ -4,17 +4,15 @@ import com.airbnb.mvrx.*
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.x.anvilannotations.ContributesViewModel
import io.element.android.x.core.data.parallelMap
import io.element.android.x.core.coroutine.parallelMap
import io.element.android.x.core.di.daggerMavericksViewModelFactory
import io.element.android.x.designsystem.components.avatar.AvatarData
import io.element.android.x.designsystem.components.avatar.AvatarSize
import io.element.android.x.di.AppScope
import io.element.android.x.di.SessionScope
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.RoomListRoomSummaryPlaceholders
import io.element.android.x.features.roomlist.model.RoomListViewState
import io.element.android.x.matrix.Matrix
import io.element.android.x.matrix.MatrixClient
import io.element.android.x.matrix.media.MediaResolver
import io.element.android.x.matrix.room.RoomSummary
@ -112,8 +110,8 @@ class RoomListViewModel @AssistedInject constructor(
copy(
rooms = when {
it is Loading ||
// Note: this second case will prevent to handle correctly the empty case
(it is Success && it().isEmpty() && filter.isEmpty()) -> {
// Note: this second case will prevent to handle correctly the empty case
(it is Success && it().isEmpty() && filter.isEmpty()) -> {
// Show fake placeholders to avoid having empty screen
Loading(RoomListRoomSummaryPlaceholders.createFakeList(size = 16))
}
@ -158,5 +156,4 @@ class RoomListViewModel @AssistedInject constructor(
.resolve(url, kind = MediaResolver.Kind.Thumbnail(size.value))
return AvatarData(name, model, size)
}
}
}

View file

@ -2,7 +2,6 @@
package io.element.android.x.features.roomlist.components
import Avatar
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ContentAlpha
@ -11,9 +10,23 @@ import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Logout
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -24,6 +37,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import io.element.android.x.core.compose.LogCompositions
import io.element.android.x.core.compose.textFieldState
import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.x.features.roomlist.model.MatrixUser
@ -35,7 +49,6 @@ fun RoomListTopBar(
onLogoutClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior
) {
LogCompositions(tag = "RoomListScreen", msg = "TopBar")
var searchWidgetStateIsOpened by rememberSaveable { mutableStateOf(false) }
@ -65,20 +78,20 @@ fun RoomListTopBar(
scrollBehavior = scrollBehavior,
)
}
}
@Composable
fun SearchRoomListTopBar(
text: String,
onFilterChanged: (String) -> Unit,
onCloseClicked: () -> Unit,
scrollBehavior: TopAppBarScrollBehavior
scrollBehavior: TopAppBarScrollBehavior,
modifier: Modifier = Modifier,
onFilterChanged: (String) -> Unit = {},
onCloseClicked: () -> Unit = {},
) {
var filterState by textFieldState(stateValue = text)
val focusRequester = remember { FocusRequester() }
TopAppBar(
modifier = Modifier
modifier = modifier
.nestedScroll(scrollBehavior.nestedScrollConnection),
title = {
TextField(
@ -184,10 +197,13 @@ private fun DefaultRoomListTopBar(
)
// Log out confirmation dialog
ConfirmationDialog(
openDialog,
isDisplayed = openDialog.value,
title = "Log out",
content = "Do you confirm you want to log out?",
submitText = "Log out",
onSubmitClicked = onLogoutClicked,
onDismiss = {
openDialog.value = false
}
)
}

View file

@ -1,16 +1,23 @@
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.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.Immutable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
@ -29,17 +36,17 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.google.accompanist.placeholder.material.placeholder
import io.element.android.x.designsystem.components.avatar.Avatar
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
private val minHeight = 72.dp
@Composable
internal fun RoomSummaryRow(
modifier: Modifier = Modifier,
room: RoomListRoomSummary,
onClick: (RoomListRoomSummary) -> Unit
modifier: Modifier = Modifier,
onClick: (RoomListRoomSummary) -> Unit = {},
) {
val clickModifier = if (room.isPlaceholder) {
modifier
} else {
@ -57,7 +64,6 @@ internal fun RoomSummaryRow(
) {
DefaultRoomSummaryRow(room = room)
}
}
@Composable

View file

@ -13,4 +13,5 @@ data class RoomListRoomSummary(
val timestamp: String? = null,
val lastMessage: CharSequence? = null,
val avatarData: AvatarData = AvatarData(),
val isPlaceholder: Boolean = false,)
val isPlaceholder: Boolean = false,
)

View file

@ -2,7 +2,6 @@ package io.element.android.x.features.roomlist.model
import io.element.android.x.designsystem.components.avatar.AvatarData
object RoomListRoomSummaryPlaceholders {
fun create(id: String): RoomListRoomSummary {
@ -23,6 +22,4 @@ object RoomListRoomSummaryPlaceholders {
}
}
}
}

View file

@ -1,9 +1,11 @@
package io.element.android.x.features.roomlist.model
import io.element.android.x.designsystem.components.avatar.AvatarData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal fun stubbedRoomSummaries(): List<RoomListRoomSummary> {
return listOf(
internal fun stubbedRoomSummaries(): ImmutableList<RoomListRoomSummary> {
return persistentListOf(
RoomListRoomSummary(
name = "Room",
hasUnread = true,
@ -22,4 +24,4 @@ internal fun stubbedRoomSummaries(): List<RoomListRoomSummary> {
),
RoomListRoomSummaryPlaceholders.create("roomId2")
)
}
}

View file

@ -1,9 +1,8 @@
package io.element.android.x.features.roomlist
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
@ -14,4 +13,4 @@ class ExampleUnitTest {
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
}