Merge branch 'develop' into renovate/telephoto

This commit is contained in:
ganfra 2024-11-15 12:02:57 +01:00 committed by GitHub
commit 12e186fbca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
114 changed files with 454 additions and 764 deletions

View file

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
variant: [debug, release, nightly, samples]
variant: [debug, release, nightly]
fail-fast: false
# Allow all jobs on develop. Just one per PR.
concurrency:
@ -82,6 +82,3 @@ jobs:
- name: Compile nightly sources
if: ${{ matrix.variant == 'nightly' }}
run: ./gradlew compileGplayNightlySources -PallWarningsAsErrors=true $CI_GRADLE_ARG_PROPERTIES
- name: Compile samples minimal
if: ${{ matrix.variant == 'samples' }}
run: ./gradlew :samples:minimal:assemble $CI_GRADLE_ARG_PROPERTIES

View file

@ -49,8 +49,6 @@ Please ensure that you're using the project formatting rules (which are in the p
This project should compile without any special action. Just clone it and open it with Android Studio, or compile from command line using `gradlew`.
Note: please make sure that the configuration is `app` and not `samples.minimal`.
## Strings
The strings of the project are managed externally using [https://localazy.com](https://localazy.com) and shared with Element X iOS.

View file

@ -40,7 +40,7 @@ We want:
The CI checks that:
1. The code is compiling, without any warnings, for all the app build types and variants and for the minimal app
1. The code is compiling, without any warnings, for all the app build types and variants
2. The tests are passing
3. The code quality is good (detekt, ktlint, lint)
4. The code is running and smoke tests are passing (maestro)

View file

@ -19,6 +19,4 @@ data class CreateRoomConfig(
val avatarUri: Uri? = null,
val invites: ImmutableList<MatrixUser> = persistentListOf(),
val roomVisibility: RoomVisibilityState = RoomVisibilityState.Private,
) {
val isValid = roomName.isNullOrEmpty().not() && roomVisibility.isValid()
}
)

View file

@ -11,13 +11,13 @@ import android.net.Uri
import io.element.android.features.createroom.impl.configureroom.RoomAccess
import io.element.android.features.createroom.impl.configureroom.RoomAccessItem
import io.element.android.features.createroom.impl.configureroom.RoomAddress
import io.element.android.features.createroom.impl.configureroom.RoomAddressErrorState
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityItem
import io.element.android.features.createroom.impl.configureroom.RoomVisibilityState
import io.element.android.features.createroom.impl.di.CreateRoomScope
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.androidutils.file.safeDelete
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -29,6 +29,7 @@ import javax.inject.Inject
@SingleIn(CreateRoomScope::class)
class CreateRoomDataStore @Inject constructor(
val selectedUserListDataStore: UserListDataStore,
private val roomAliasHelper: RoomAliasHelper,
) {
private val createRoomConfigFlow: MutableStateFlow<CreateRoomConfig> = MutableStateFlow(CreateRoomConfig())
private var cachedAvatarUri: Uri? = null
@ -46,13 +47,13 @@ class CreateRoomDataStore @Inject constructor(
fun setRoomName(roomName: String) {
createRoomConfigFlow.getAndUpdate { config ->
/*
val newVisibility = when (config.roomVisibility) {
is RoomVisibilityState.Public -> {
val roomAddress = config.roomVisibility.roomAddress
if (roomAddress is RoomAddress.AutoFilled || roomName.isEmpty()) {
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(roomName)
config.roomVisibility.copy(
roomAddress = RoomAddress.AutoFilled(roomName),
roomAddress = RoomAddress.AutoFilled(roomAliasName),
)
} else {
config.roomVisibility
@ -60,9 +61,9 @@ class CreateRoomDataStore @Inject constructor(
}
else -> config.roomVisibility
}
*/
config.copy(
roomName = roomName.takeIf { it.isNotEmpty() },
roomVisibility = newVisibility,
)
}
}
@ -85,11 +86,13 @@ class CreateRoomDataStore @Inject constructor(
config.copy(
roomVisibility = when (visibility) {
RoomVisibilityItem.Private -> RoomVisibilityState.Private
RoomVisibilityItem.Public -> RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(config.roomName.orEmpty()),
roomAddressErrorState = RoomAddressErrorState.None,
roomAccess = RoomAccess.Anyone,
)
RoomVisibilityItem.Public -> {
val roomAliasName = roomAliasHelper.roomAliasNameFromRoomDisplayName(config.roomName.orEmpty())
RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(roomAliasName),
roomAccess = RoomAccess.Anyone,
)
}
}
)
}

View file

@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.impl.CreateRoomConfig
import io.element.android.features.createroom.impl.CreateRoomDataStore
@ -31,6 +32,8 @@ import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.createroom.RoomPreset
import io.element.android.libraries.matrix.api.createroom.RoomVisibility
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.api.roomAliasFromName
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
import io.element.android.libraries.mediaupload.api.MediaPreProcessor
@ -39,9 +42,12 @@ import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.services.analytics.api.AnalyticsService
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import java.util.Optional
import javax.inject.Inject
import kotlin.jvm.optionals.getOrNull
class ConfigureRoomPresenter @Inject constructor(
private val dataStore: CreateRoomDataStore,
@ -51,6 +57,7 @@ class ConfigureRoomPresenter @Inject constructor(
private val analyticsService: AnalyticsService,
permissionsPresenterFactory: PermissionsPresenter.Factory,
private val featureFlagService: FeatureFlagService,
private val roomAliasHelper: RoomAliasHelper,
) : Presenter<ConfigureRoomState> {
private val cameraPermissionPresenter: PermissionsPresenter = permissionsPresenterFactory.create(android.Manifest.permission.CAMERA)
private var pendingPermissionRequest = false
@ -58,9 +65,12 @@ class ConfigureRoomPresenter @Inject constructor(
@Composable
override fun present(): ConfigureRoomState {
val cameraPermissionState = cameraPermissionPresenter.present()
val createRoomConfig = dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
val createRoomConfig by dataStore.createRoomConfigWithInvites.collectAsState(CreateRoomConfig())
val homeserverName = remember { matrixClient.userIdServerName() }
val isKnockFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(initial = false)
val roomAddressValidity = remember {
mutableStateOf<RoomAddressValidity>(RoomAddressValidity.Unknown)
}
val cameraPhotoPicker = mediaPickerProvider.registerCameraPhotoPicker(
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri, cached = true) },
@ -69,12 +79,12 @@ class ConfigureRoomPresenter @Inject constructor(
onResult = { uri -> if (uri != null) dataStore.setAvatarUri(uri = uri) }
)
val avatarActions by remember(createRoomConfig.value.avatarUri) {
val avatarActions by remember(createRoomConfig.avatarUri) {
derivedStateOf {
listOfNotNull(
AvatarAction.TakePhoto,
AvatarAction.ChoosePhoto,
AvatarAction.Remove.takeIf { createRoomConfig.value.avatarUri != null },
AvatarAction.Remove.takeIf { createRoomConfig.avatarUri != null },
).toImmutableList()
}
}
@ -86,6 +96,10 @@ class ConfigureRoomPresenter @Inject constructor(
}
}
RoomAddressValidityEffect(createRoomConfig.roomVisibility.roomAddress()) { newRoomAddressValidity ->
roomAddressValidity.value = newRoomAddressValidity
}
val localCoroutineScope = rememberCoroutineScope()
val createRoomAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
@ -102,7 +116,7 @@ class ConfigureRoomPresenter @Inject constructor(
is ConfigureRoomEvents.RemoveUserFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser)
is ConfigureRoomEvents.RoomAccessChanged -> dataStore.setRoomAccess(event.roomAccess)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig.value)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
is ConfigureRoomEvents.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
@ -122,15 +136,49 @@ class ConfigureRoomPresenter @Inject constructor(
return ConfigureRoomState(
isKnockFeatureEnabled = isKnockFeatureEnabled,
config = createRoomConfig.value,
config = createRoomConfig,
avatarActions = avatarActions,
createRoomAction = createRoomAction.value,
cameraPermissionState = cameraPermissionState,
homeserverName = homeserverName,
roomAddressValidity = roomAddressValidity.value,
eventSink = ::handleEvents,
)
}
@Composable
private fun RoomAddressValidityEffect(
roomAddress: Optional<String>,
onRoomAddressValidityChange: (RoomAddressValidity) -> Unit,
) {
val onChange by rememberUpdatedState(onRoomAddressValidityChange)
LaunchedEffect(roomAddress) {
val roomAliasName = roomAddress.getOrNull().orEmpty()
if (roomAliasName.isEmpty()) {
onChange(RoomAddressValidity.Unknown)
return@LaunchedEffect
}
// debounce the room address validation
delay(300)
val roomAlias = matrixClient.roomAliasFromName(roomAliasName).getOrNull()
if (roomAlias == null || !roomAliasHelper.isRoomAliasValid(roomAlias)) {
onChange(RoomAddressValidity.InvalidSymbols)
} else {
matrixClient.resolveRoomAlias(roomAlias)
.onSuccess { resolved ->
if (resolved.isPresent) {
onChange(RoomAddressValidity.NotAvailable)
} else {
onChange(RoomAddressValidity.Valid)
}
}
.onFailure {
onChange(RoomAddressValidity.Valid)
}
}
}
}
private fun CoroutineScope.createRoom(
config: CreateRoomConfig,
createRoomAction: MutableState<AsyncAction<RoomId>>
@ -148,7 +196,7 @@ class ConfigureRoomPresenter @Inject constructor(
preset = RoomPreset.PUBLIC_CHAT,
invite = config.invites.map { it.userId },
avatar = avatarUrl,
canonicalAlias = config.roomVisibility.roomAddress()
roomAliasName = config.roomVisibility.roomAddress()
)
} else {
CreateRoomParameters(

View file

@ -20,6 +20,10 @@ data class ConfigureRoomState(
val avatarActions: ImmutableList<AvatarAction>,
val createRoomAction: AsyncAction<RoomId>,
val cameraPermissionState: PermissionsState,
val roomAddressValidity: RoomAddressValidity,
val homeserverName: String,
val eventSink: (ConfigureRoomEvents) -> Unit
)
) {
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
(config.roomVisibility is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid)
}

View file

@ -28,9 +28,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
invites = aMatrixUserList().toImmutableList(),
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room 101"),
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
roomAddressErrorState = RoomAddressErrorState.None,
),
),
),
@ -40,12 +39,44 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider<ConfigureRoomSt
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
invites = aMatrixUserList().toImmutableList(),
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room 101"),
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
roomAddressErrorState = RoomAddressErrorState.None,
),
),
),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.NotAvailable,
),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.InvalidSymbols,
),
aConfigureRoomState(
config = CreateRoomConfig(
roomName = "Room 101",
topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldnt be more than 3 lines",
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled("Room-101"),
roomAccess = RoomAccess.Knocking,
),
),
roomAddressValidity = RoomAddressValidity.Valid,
),
)
}
@ -56,6 +87,7 @@ fun aConfigureRoomState(
createRoomAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
cameraPermissionState: PermissionsState = aPermissionsState(showDialog = false),
homeserverName: String = "matrix.org",
roomAddressValidity: RoomAddressValidity = RoomAddressValidity.Valid,
eventSink: (ConfigureRoomEvents) -> Unit = { },
) = ConfigureRoomState(
config = config,
@ -64,5 +96,6 @@ fun aConfigureRoomState(
createRoomAction = createRoomAction,
cameraPermissionState = cameraPermissionState,
homeserverName = homeserverName,
roomAddressValidity = roomAddressValidity,
eventSink = eventSink,
)

View file

@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
@ -79,7 +80,7 @@ fun ConfigureRoomView(
modifier = modifier.clearFocusOnTap(focusManager),
topBar = {
ConfigureRoomToolbar(
isNextActionEnabled = state.config.isValid,
isNextActionEnabled = state.isValid,
onBackClick = onBackClick,
onNextClick = {
focusManager.clearFocus()
@ -143,8 +144,10 @@ fun ConfigureRoomView(
modifier = Modifier.padding(horizontal = 16.dp),
address = state.config.roomVisibility.roomAddress,
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
)
Spacer(Modifier)
}
}
}
@ -319,6 +322,7 @@ private fun RoomAccessOptions(
private fun RoomAddressField(
address: RoomAddress,
homeserverName: String,
addressValidity: RoomAddressValidity,
onAddressChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
@ -340,7 +344,16 @@ private fun RoomAddressField(
color = ElementTheme.colors.textSecondary,
)
},
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
supportingText = when (addressValidity) {
RoomAddressValidity.InvalidSymbols -> {
stringResource(R.string.screen_create_room_room_address_invalid_symbols_error_description)
}
RoomAddressValidity.NotAvailable -> {
stringResource(R.string.screen_create_room_room_address_not_available_error_description)
}
else -> stringResource(R.string.screen_create_room_room_address_section_footer)
},
isError = addressValidity.isError(),
onValueChange = onAddressChange,
singleLine = true,
)

View file

@ -1,17 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
/**
* Represents the error state of a room address.
*/
sealed interface RoomAddressErrorState {
data object InvalidCharacters : RoomAddressErrorState
data object AlreadyExists : RoomAddressErrorState
data object None : RoomAddressErrorState
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.createroom.impl.configureroom
import androidx.compose.runtime.Immutable
/**
* Represents the validity state of a room address.
* ie. whether it contains invalid characters, is already taken, or is valid.
*/
@Immutable
sealed interface RoomAddressValidity {
data object Unknown : RoomAddressValidity
data object InvalidSymbols : RoomAddressValidity
data object NotAvailable : RoomAddressValidity
data object Valid : RoomAddressValidity
fun isError(): Boolean {
return this is InvalidSymbols || this is NotAvailable
}
}

View file

@ -14,7 +14,6 @@ sealed interface RoomVisibilityState {
data class Public(
val roomAddress: RoomAddress,
val roomAddressErrorState: RoomAddressErrorState,
val roomAccess: RoomAccess,
) : RoomVisibilityState
@ -24,11 +23,4 @@ sealed interface RoomVisibilityState {
is Public -> Optional.of(roomAddress.value)
}
}
fun isValid(): Boolean {
return when (this) {
is Private -> true
is Public -> roomAddressErrorState is RoomAddressErrorState.None && roomAddress.value.isNotEmpty()
}
}
}

View file

@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.createroom.impl.CreateRoomDataStore
import io.element.android.features.createroom.impl.userlist.FakeUserListPresenterFactory
import io.element.android.features.createroom.impl.userlist.UserListDataStore
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.usersearch.test.FakeUserRepository
import io.element.android.tests.testutils.WarmUpRule
import kotlinx.coroutines.test.runTest
@ -32,7 +33,7 @@ class AddPeoplePresenterTest {
presenter = AddPeoplePresenter(
FakeUserListPresenterFactory(),
FakeUserRepository(),
CreateRoomDataStore(UserListDataStore())
CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
)
}

View file

@ -19,11 +19,15 @@ import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_MESSAGE
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_THROWABLE
import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.alias.FakeRoomAliasHelper
import io.element.android.libraries.matrix.ui.components.aMatrixUser
import io.element.android.libraries.matrix.ui.media.AvatarAction
import io.element.android.libraries.mediapickers.api.PickerProvider
@ -44,6 +48,8 @@ import io.mockk.mockkStatic
import io.mockk.unmockkAll
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
@ -52,6 +58,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import java.io.File
import java.util.Optional
private const val AN_URI_FROM_CAMERA = "content://uri_from_camera"
private const val AN_URI_FROM_CAMERA_2 = "content://uri_from_camera_2"
@ -95,21 +102,21 @@ class ConfigureRoomPresenterTest {
presenter.test {
val initialState = initialState()
var config = initialState.config
assertThat(initialState.config.isValid).isFalse()
assertThat(initialState.isValid).isFalse()
// Room name not empty
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
var newState: ConfigureRoomState = awaitItem()
config = config.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(config)
assertThat(newState.config.isValid).isTrue()
assertThat(newState.isValid).isTrue()
// Clear room name
newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
newState = awaitItem()
config = config.copy(roomName = null)
assertThat(newState.config).isEqualTo(config)
assertThat(newState.config.isValid).isFalse()
assertThat(newState.isValid).isFalse()
}
}
@ -118,8 +125,9 @@ class ConfigureRoomPresenterTest {
val userListDataStore = UserListDataStore()
val pickerProvider = FakePickerProvider()
val permissionsPresenter = FakePermissionsPresenter()
val roomAliasHelper = FakeRoomAliasHelper()
val presenter = createConfigureRoomPresenter(
createRoomDataStore = CreateRoomDataStore(userListDataStore),
createRoomDataStore = CreateRoomDataStore(userListDataStore, roomAliasHelper),
pickerProvider = pickerProvider,
permissionsPresenter = permissionsPresenter,
)
@ -191,8 +199,7 @@ class ConfigureRoomPresenterTest {
newState = awaitItem()
expectedConfig = expectedConfig.copy(
roomVisibility = RoomVisibilityState.Public(
roomAddress = RoomAddress.AutoFilled(expectedConfig.roomName ?: ""),
roomAddressErrorState = RoomAddressErrorState.None,
roomAddress = RoomAddress.AutoFilled(roomAliasHelper.roomAliasNameFromRoomDisplayName(expectedConfig.roomName ?: "")),
roomAccess = RoomAccess.Anyone,
)
)
@ -254,7 +261,7 @@ class ConfigureRoomPresenterTest {
val matrixClient = createMatrixClient()
val analyticsService = FakeAnalyticsService()
val mediaPreProcessor = FakeMediaPreProcessor()
val createRoomDataStore = CreateRoomDataStore(UserListDataStore())
val createRoomDataStore = CreateRoomDataStore(UserListDataStore(), FakeRoomAliasHelper())
val presenter = createConfigureRoomPresenter(
createRoomDataStore = createRoomDataStore,
mediaPreProcessor = mediaPreProcessor,
@ -315,17 +322,88 @@ class ConfigureRoomPresenterTest {
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is invalid when format is invalid`() = runTest {
val aliasHelper = FakeRoomAliasHelper(
isRoomAliasValidLambda = { false }
)
val presenter = createConfigureRoomPresenter(
roomAliasHelper = aliasHelper
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.InvalidSymbols)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is not available when alias is not available`() = runTest {
val fakeMatrixClient = createMatrixClient(isAliasAvailable = false)
val presenter = createConfigureRoomPresenter(
matrixClient = fakeMatrixClient,
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.NotAvailable)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun `present - address is valid when alias is available and format is valid`() = runTest {
val fakeMatrixClient = createMatrixClient(isAliasAvailable = true)
val presenter = createConfigureRoomPresenter(
matrixClient = fakeMatrixClient,
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.RoomVisibilityChanged(RoomVisibilityItem.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
assertThat(state.roomAddressValidity).isEqualTo(RoomAddressValidity.Valid)
}
}
}
private suspend fun TurbineTestContext<ConfigureRoomState>.initialState(): ConfigureRoomState {
skipItems(1)
return awaitItem()
}
private fun createMatrixClient() = FakeMatrixClient(
private fun createMatrixClient(isAliasAvailable: Boolean = true) = FakeMatrixClient(
userIdServerNameLambda = { "matrix.org" },
resolveRoomAliasResult = {
val resolvedRoomAlias = if (isAliasAvailable) {
Optional.empty()
} else {
Optional.of(ResolvedRoomAlias(A_ROOM_ID, emptyList()))
}
Result.success(resolvedRoomAlias)
}
)
private fun createConfigureRoomPresenter(
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore()),
roomAliasHelper: RoomAliasHelper = FakeRoomAliasHelper(),
createRoomDataStore: CreateRoomDataStore = CreateRoomDataStore(UserListDataStore(), roomAliasHelper),
matrixClient: MatrixClient = createMatrixClient(),
pickerProvider: PickerProvider = FakePickerProvider(),
mediaPreProcessor: MediaPreProcessor = FakeMediaPreProcessor(),
@ -339,6 +417,7 @@ class ConfigureRoomPresenterTest {
mediaPreProcessor = mediaPreProcessor,
analyticsService = analyticsService,
permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter),
roomAliasHelper = roomAliasHelper,
featureFlagService = FakeFeatureFlagService(
mapOf(FeatureFlags.Knock.key to isKnockFeatureEnabled)
)

View file

@ -26,7 +26,7 @@ import org.junit.Rule
import org.junit.Test
import java.util.Optional
class RoomAliasResolverPresenterTest {
class RoomAliasHelperPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()

View file

@ -27,7 +27,7 @@ import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class RoomAliasResolverViewTest {
class RoomAliasHelperViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
@Test

View file

@ -145,7 +145,7 @@ private fun RoomDirectoryRoomList(
Text(
text = stringResource(id = CommonStrings.common_no_results),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textPlaceholder,
color = ElementTheme.colors.textSecondary,
modifier = Modifier.padding(16.dp)
)
}
@ -185,8 +185,8 @@ private fun SearchTextField(
colors: TextFieldColors = TextFieldDefaults.colors(
focusedContainerColor = Color.Transparent,
unfocusedContainerColor = Color.Transparent,
unfocusedPlaceholderColor = ElementTheme.colors.textPlaceholder,
focusedPlaceholderColor = ElementTheme.colors.textPlaceholder,
unfocusedPlaceholderColor = ElementTheme.colors.textSecondary,
focusedPlaceholderColor = ElementTheme.colors.textSecondary,
focusedTextColor = ElementTheme.colors.textPrimary,
unfocusedTextColor = ElementTheme.colors.textPrimary,
focusedIndicatorColor = ElementTheme.colors.borderInteractiveSecondary,

View file

@ -41,7 +41,7 @@ fun NumberedListMolecule(
private fun ItemNumber(
index: Int,
) {
val color = ElementTheme.colors.textPlaceholder
val color = ElementTheme.colors.textSecondary
Box(
modifier = Modifier
.border(1.dp, color, CircleShape)

View file

@ -200,7 +200,7 @@ private fun DecorationBox(
if (placeholder != null && isTextEmpty) {
Text(
text = placeholder,
color = ElementTheme.colors.textPlaceholder,
color = ElementTheme.colors.textSecondary,
style = ElementTheme.typography.fontBodyLgRegular,
)
}

View file

@ -168,3 +168,13 @@ fun MatrixClient.getRoomInfoFlow(roomIdOrAlias: RoomIdOrAlias): Flow<Optional<Ma
.map { roomSummary -> roomSummary.map { it.info } }
.distinctUntilChanged()
}
/**
* Returns a room alias from a room alias name.
* @param name the room alias name ie. the local part of the room alias.
*/
fun MatrixClient.roomAliasFromName(name: String): Result<RoomAlias> {
return runCatching {
RoomAlias("#$name:${userIdServerName()}")
}
}

View file

@ -20,5 +20,5 @@ data class CreateRoomParameters(
val invite: List<UserId>? = null,
val avatar: String? = null,
val joinRuleOverride: JoinRuleOverride = JoinRuleOverride.None,
val canonicalAlias: Optional<String> = Optional.empty(),
val roomAliasName: Optional<String> = Optional.empty(),
)

View file

@ -0,0 +1,15 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room.alias
import io.element.android.libraries.matrix.api.core.RoomAlias
interface RoomAliasHelper {
fun roomAliasNameFromRoomDisplayName(name: String): String
fun isRoomAliasValid(roomAlias: RoomAlias): Boolean
}

View file

@ -334,7 +334,7 @@ class RustMatrixClient(
JoinRuleOverride.Knock -> RustJoinRule.Knock
JoinRuleOverride.None -> null
},
canonicalAlias = createRoomParams.canonicalAlias.getOrNull(),
canonicalAlias = createRoomParams.roomAliasName.getOrNull(),
)
val roomId = RoomId(client.createRoom(rustParams))
// Wait to receive the room back from the sync but do not returns failure if it fails.

View file

@ -0,0 +1,25 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.room.alias
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultRoomAliasHelper @Inject constructor() : RoomAliasHelper {
override fun roomAliasNameFromRoomDisplayName(name: String): String {
return org.matrix.rustcomponents.sdk.roomAliasNameFromRoomDisplayName(name)
}
override fun isRoomAliasValid(roomAlias: RoomAlias): Boolean {
return org.matrix.rustcomponents.sdk.isRoomAliasFormatValid(roomAlias.value)
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.test.room.alias
import io.element.android.libraries.matrix.api.core.RoomAlias
import io.element.android.libraries.matrix.api.room.alias.RoomAliasHelper
class FakeRoomAliasHelper(
private val roomAliasNameFromRoomDisplayNameLambda: (String) -> String = { name ->
name.trimStart().trimEnd().replace(" ", "_")
},
private val isRoomAliasValidLambda: (RoomAlias) -> Boolean = { true }
) : RoomAliasHelper {
override fun roomAliasNameFromRoomDisplayName(name: String): String {
return roomAliasNameFromRoomDisplayNameLambda(name)
}
override fun isRoomAliasValid(roomAlias: RoomAlias): Boolean {
return isRoomAliasValidLambda(roomAlias)
}
}

View file

@ -32,10 +32,8 @@ val localAarProjects = listOf(
val excludedKoverSubProjects = listOf(
":app",
":samples",
":anvilannotations",
":anvilcodegen",
":samples:minimal",
":tests:testutils",
// Exclude `:libraries:matrix:impl` module, it contains only wrappers to access the Rust Matrix
// SDK api, so it is not really relevant to unit test it: there is no logic to test.

View file

@ -6,7 +6,7 @@
*/
/**
* This will generate the plugin "io.element.android-compose-application" to use by app and samples modules
* This will generate the plugin "io.element.android-compose-application" to use by app
*/
import extension.androidConfig
import extension.commonDependencies

View file

@ -1 +0,0 @@
/build

View file

@ -1,67 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
plugins {
id("io.element.android-compose-application")
alias(libs.plugins.kotlin.android)
}
android {
namespace = "io.element.android.samples.minimal"
defaultConfig {
applicationId = "io.element.android.samples.minimal"
targetSdk = Versions.TARGET_SDK
versionCode = Versions.VERSION_CODE
versionName = Versions.VERSION_NAME
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildFeatures {
buildConfig = true
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.preference)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrix.impl)
implementation(projects.libraries.permissions.noop)
implementation(projects.libraries.sessionStorage.implMemory)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)
implementation(projects.libraries.network)
implementation(projects.libraries.dateformatter.impl)
implementation(projects.libraries.eventformatter.impl)
implementation(projects.libraries.fullscreenintent.impl)
implementation(projects.libraries.preferences.impl)
implementation(projects.libraries.preferences.test)
implementation(projects.libraries.indicator.impl)
implementation(projects.features.invite.impl)
implementation(projects.features.roomlist.impl)
implementation(projects.features.leaveroom.impl)
implementation(projects.features.login.impl)
implementation(projects.features.logout.impl)
implementation(projects.features.networkmonitor.impl)
implementation(projects.services.toolbox.impl)
implementation(projects.libraries.featureflag.impl)
implementation(projects.services.analytics.noop)
implementation(libs.coroutines.core)
implementation(projects.libraries.push.test)
}

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only
~ Please see LICENSE in the repository root for full details.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.ElementX">
<activity
android:name=".MainActivity"
android:exported="true"
android:theme="@style/Theme.ElementX">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

View file

@ -1,23 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.featureflag.api.Feature
import io.element.android.libraries.featureflag.api.FeatureFlagService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class AlwaysEnabledFeatureFlagService : FeatureFlagService {
override fun isFeatureEnabledFlow(feature: Feature): Flow<Boolean> {
return flowOf(true)
}
override suspend fun setFeatureEnabled(feature: Feature, enabled: Boolean): Boolean {
return true
}
}

View file

@ -1,43 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import io.element.android.features.login.impl.DefaultLoginUserStory
import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenter
import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordView
import io.element.android.features.login.impl.util.defaultAccountProvider
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
class LoginScreen(private val authenticationService: MatrixAuthenticationService) {
@Composable
fun Content(modifier: Modifier = Modifier) {
val presenter = remember {
LoginPasswordPresenter(
authenticationService = authenticationService,
AccountProviderDataSource(),
DefaultLoginUserStory(),
)
}
LaunchedEffect(Unit) {
authenticationService.setHomeserver(defaultAccountProvider.url)
}
val state = presenter.present()
LoginPasswordView(
state = state,
modifier = modifier,
onBackClick = {},
)
}
}

View file

@ -1,95 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService
import io.element.android.libraries.matrix.impl.RustClientBuilderProvider
import io.element.android.libraries.matrix.impl.RustMatrixClientFactory
import io.element.android.libraries.matrix.impl.auth.OidcConfigurationProvider
import io.element.android.libraries.matrix.impl.auth.RustMatrixAuthenticationService
import io.element.android.libraries.matrix.impl.paths.SessionPathsFactory
import io.element.android.libraries.matrix.impl.room.RustTimelineEventTypeFilterFactory
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
import io.element.android.libraries.sessionstorage.api.LoggedInState
import io.element.android.libraries.sessionstorage.impl.memory.InMemorySessionStore
import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.toolbox.impl.systemclock.DefaultSystemClock
import kotlinx.coroutines.runBlocking
import java.io.File
class MainActivity : ComponentActivity() {
private val matrixAuthenticationService: MatrixAuthenticationService by lazy {
val baseDirectory = File(applicationContext.filesDir, "sessions")
val userAgentProvider = SimpleUserAgentProvider("MinimalSample")
val sessionStore = InMemorySessionStore()
val userCertificatesProvider = NoOpUserCertificatesProvider()
val proxyProvider = NoOpProxyProvider()
RustMatrixAuthenticationService(
sessionPathsFactory = SessionPathsFactory(baseDirectory, applicationContext.cacheDir),
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = sessionStore,
rustMatrixClientFactory = RustMatrixClientFactory(
baseDirectory = baseDirectory,
cacheDirectory = applicationContext.cacheDir,
appCoroutineScope = Singleton.appScope,
coroutineDispatchers = Singleton.coroutineDispatchers,
sessionStore = sessionStore,
userAgentProvider = userAgentProvider,
userCertificatesProvider = userCertificatesProvider,
proxyProvider = proxyProvider,
clock = DefaultSystemClock(),
analyticsService = NoopAnalyticsService(),
featureFlagService = AlwaysEnabledFeatureFlagService(),
timelineEventTypeFilterFactory = RustTimelineEventTypeFilterFactory(),
clientBuilderProvider = RustClientBuilderProvider(),
),
passphraseGenerator = NullPassphraseGenerator(),
oidcConfigurationProvider = OidcConfigurationProvider(baseDirectory),
appPreferencesStore = InMemoryAppPreferencesStore(),
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
ElementTheme {
val loggedInState by matrixAuthenticationService.loggedInStateFlow().collectAsState(initial = LoggedInState.NotLoggedIn)
Content(isLoggedIn = loggedInState is LoggedInState.LoggedIn, modifier = Modifier.fillMaxSize())
}
}
}
@Composable
fun Content(
isLoggedIn: Boolean,
modifier: Modifier = Modifier
) {
if (!isLoggedIn) {
LoginScreen(authenticationService = matrixAuthenticationService).Content(modifier)
} else {
val matrixClient = runBlocking {
val sessionId = matrixAuthenticationService.getLatestSessionId()!!
matrixAuthenticationService.restoreSession(sessionId).getOrNull()
}
RoomListScreen(LocalContext.current, matrixClient!!).Content(modifier)
}
}
}

View file

@ -1,14 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
class NoOpProxyProvider : ProxyProvider {
override fun provides(): String? = null
}

View file

@ -1,14 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
class NoOpUserCertificatesProvider : UserCertificatesProvider {
override fun provides(): List<ByteArray> = emptyList()
}

View file

@ -1,14 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
class NullPassphraseGenerator : PassphraseGenerator {
override fun generatePassphrase(): String? = null
}

View file

@ -1,18 +0,0 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import android.net.Uri
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.permalink.PermalinkParser
class OnlyFallbackPermalinkParser : PermalinkParser {
override fun parse(uriString: String): PermalinkData {
return PermalinkData.FallbackLink(Uri.parse(uriString))
}
}

View file

@ -1,174 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
import io.element.android.features.leaveroom.impl.LeaveRoomPresenter
import io.element.android.features.logout.impl.direct.DirectLogoutPresenter
import io.element.android.features.networkmonitor.impl.DefaultNetworkMonitor
import io.element.android.features.roomlist.impl.RoomListPresenter
import io.element.android.features.roomlist.impl.RoomListView
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
import io.element.android.features.roomlist.impl.filters.selection.DefaultFilterSelectionStrategy
import io.element.android.features.roomlist.impl.search.RoomListSearchDataSource
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter
import io.element.android.libraries.androidutils.system.DefaultDateTimeObserver
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.dateformatter.impl.DateFormatters
import io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.eventformatter.impl.DefaultRoomLastMessageFormatter
import io.element.android.libraries.eventformatter.impl.ProfileChangeContentFormatter
import io.element.android.libraries.eventformatter.impl.RoomMembershipContentFormatter
import io.element.android.libraries.eventformatter.impl.StateContentFormatter
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.indicator.impl.DefaultIndicatorService
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.impl.room.join.DefaultJoinRoom
import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
import io.element.android.services.analytics.noop.NoopAnalyticsService
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import timber.log.Timber
import java.util.Locale
class RoomListScreen(
context: Context,
private val matrixClient: MatrixClient,
private val coroutineDispatchers: CoroutineDispatchers = Singleton.coroutineDispatchers,
) {
private val clock = Clock.System
private val locale = Locale.getDefault()
private val dateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.currentSystemDefault() }
private val dateFormatters = DateFormatters(locale, clock) { TimeZone.currentSystemDefault() }
private val sessionVerificationService = matrixClient.sessionVerificationService()
private val encryptionService = matrixClient.encryptionService()
private val stringProvider = AndroidStringProvider(context.resources)
private val featureFlagService = AlwaysEnabledFeatureFlagService()
private val roomListRoomSummaryFactory = RoomListRoomSummaryFactory(
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(
localDateTimeProvider = dateTimeProvider,
dateFormatters = dateFormatters
),
roomLastMessageFormatter = DefaultRoomLastMessageFormatter(
sp = stringProvider,
roomMembershipContentFormatter = RoomMembershipContentFormatter(
matrixClient = matrixClient,
sp = stringProvider
),
profileChangeContentFormatter = ProfileChangeContentFormatter(stringProvider),
stateContentFormatter = StateContentFormatter(stringProvider),
permalinkParser = OnlyFallbackPermalinkParser(),
),
)
private val presenter = RoomListPresenter(
client = matrixClient,
networkMonitor = DefaultNetworkMonitor(context, Singleton.appScope),
snackbarDispatcher = SnackbarDispatcher(),
leaveRoomPresenter = LeaveRoomPresenter(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
roomListDataSource = RoomListDataSource(
roomListService = matrixClient.roomListService,
roomListRoomSummaryFactory = roomListRoomSummaryFactory,
coroutineDispatchers = coroutineDispatchers,
notificationSettingsService = matrixClient.notificationSettingsService(),
appScope = Singleton.appScope,
dateTimeObserver = DefaultDateTimeObserver(context),
),
indicatorService = DefaultIndicatorService(
sessionVerificationService = sessionVerificationService,
encryptionService = encryptionService,
),
featureFlagService = featureFlagService,
searchPresenter = RoomListSearchPresenter(
RoomListSearchDataSource(
roomListService = matrixClient.roomListService,
roomSummaryFactory = roomListRoomSummaryFactory,
coroutineDispatchers = coroutineDispatchers,
),
featureFlagService = featureFlagService,
),
sessionPreferencesStore = DefaultSessionPreferencesStore(
context = context,
sessionId = matrixClient.sessionId,
sessionCoroutineScope = Singleton.appScope
),
filtersPresenter = RoomListFiltersPresenter(
roomListService = matrixClient.roomListService,
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
),
acceptDeclineInvitePresenter = AcceptDeclineInvitePresenter(
client = matrixClient,
joinRoom = DefaultJoinRoom(matrixClient, NoopAnalyticsService()),
notificationCleaner = FakeNotificationCleaner(),
),
analyticsService = NoopAnalyticsService(),
fullScreenIntentPermissionsPresenter = { aFullScreenIntentPermissionsState() },
notificationCleaner = FakeNotificationCleaner(),
logoutPresenter = DirectLogoutPresenter(matrixClient, encryptionService),
)
@Composable
fun Content(modifier: Modifier = Modifier) {
fun onRoomClick(roomId: RoomId) {
Singleton.appScope.launch {
withContext(coroutineDispatchers.io) {
matrixClient.getRoom(roomId)!!.use { room ->
room.liveTimeline.paginate(Timeline.PaginationDirection.BACKWARDS)
}
}
}
}
val state = presenter.present()
RoomListView(
state = state,
onRoomClick = ::onRoomClick,
onSettingsClick = {},
onSetUpRecoveryClick = {},
onConfirmRecoveryKeyClick = {},
onCreateRoomClick = {},
onRoomSettingsClick = {},
onMenuActionClick = {},
onRoomDirectorySearchClick = {},
modifier = modifier,
acceptDeclineInviteView = {
AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onAcceptInvite = {}, onDeclineInvite = {})
},
onMigrateToNativeSlidingSyncClick = {},
)
DisposableEffect(Unit) {
Timber.w("Start sync!")
runBlocking {
matrixClient.syncService().startSync()
}
onDispose {
Timber.w("Stop sync!")
runBlocking {
matrixClient.syncService().stopSync()
}
}
}
}
}

View file

@ -1,55 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.samples.minimal
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.matrix.api.tracing.TracingConfiguration
import io.element.android.libraries.matrix.api.tracing.TracingFilterConfigurations
import io.element.android.libraries.matrix.api.tracing.WriteToFilesConfiguration
import io.element.android.libraries.matrix.impl.tracing.RustTracingService
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.plus
object Singleton {
val buildMeta = BuildMeta(
isDebuggable = true,
buildType = BuildType.DEBUG,
applicationName = "EAX-Minimal",
productionApplicationName = "EAX-Minimal",
desktopApplicationName = "EAX-Minimal-Desktop",
applicationId = "io.element.android.samples.minimal",
isEnterpriseBuild = false,
lowPrivacyLoggingEnabled = false,
versionName = "0.1.0",
versionCode = 1,
gitRevision = "",
gitBranchName = "",
flavorDescription = "NA",
flavorShortDescription = "NA",
)
init {
val tracingConfiguration = TracingConfiguration(
filterConfiguration = TracingFilterConfigurations.debug,
writesToLogcat = true,
writesToFilesConfiguration = WriteToFilesConfiguration.Disabled
)
RustTracingService(buildMeta).setupTracing(tracingConfiguration)
}
val appScope = MainScope() + CoroutineName("Minimal Scope")
val coroutineDispatchers = CoroutineDispatchers(
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main,
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only
~ Please see LICENSE in the repository root for full details.
-->
<resources>
<style name="Theme.ElementX" parent="android:Theme.Material.NoActionBar" />
</resources>

View file

@ -1,9 +0,0 @@
<!--
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only
~ Please see LICENSE in the repository root for full details.
-->
<resources>
<string name="app_name">EAX-Sample</string>
</resources>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2023 New Vector Ltd.
~
~ SPDX-License-Identifier: AGPL-3.0-only
~ Please see LICENSE in the repository root for full details.
-->
<resources>
<style name="Theme.ElementX" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View file

@ -70,8 +70,6 @@ include(":tests:testutils")
include(":anvilannotations")
include(":anvilcodegen")
include(":samples:minimal")
fun includeProjects(directory: File, path: String, maxDepth: Int = 1) {
directory.listFiles().orEmpty().also { it.sort() }.forEach { file ->
if (file.isDirectory) {

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c94e5e7c3b044017e6cb98985671875c7ab6103533e2a5e15aed17a01a52576b
size 30470
oid sha256:3b380df8a4e69e9e11337db374b682e01f96738948e69eb33886817894a049b0
size 30716

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e6270f3b7935c47da76ce4e81aa7e8eac13a9340e7ccb198f8cd6956d746dfa8
size 59452
oid sha256:ffbdcd28c32808fba8ffb7071b6440578716d3ae89149eb319c22d3f3b78b1ff
size 59499

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ccbb514ad9bd6a95ecff2e278574ac5bff9bd8a63a5fc91fa35302dbaa9e71f2
size 55276

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a2ea9839e3dd1288d99c2115c37bc7fe72716af9785c4de3e328a77a57d12d70
size 56633

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5b28da0e8ff5cbedf8777b5af8ff6bab2bd5c476df80878b2c28dd96a354d978
size 54383

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f6a98ba9f5edc21b8781e84ccaf6f93c3608a4aa64464494304962d8cb62a102
size 31477
oid sha256:9dc23b7e9533aa94245efa2089956259b00882809157cf32decaaa22827aeb59
size 31678

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c3b02b0bc0d557bac4d5aa945daf04d5d58dab44d9e4284d030804e6b5ed6a49
size 60703
oid sha256:9dc54d0a6c3cb3069482b1c27ed6d80fd63b0bcad1a550bf5c4edb09f69db3a7
size 60749

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:71c42c595070733ac8ac2c50da116e73ea6eea7dc120f6f0d28022b5a5f925bd
size 57099

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e10c1c3f2b4045d20c9513c70bc4ea041e091976df5385b93d45c2d03b8f6e5f
size 58513

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb54ae79de75af32e32642063d9dc9e0c24b0089f187a043f0fc330151c72c75
size 56170

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:91cdcd831bc82480a130093a2f561bde8f0c3f0f2b9e7251628763202fc7bc39
size 35249
oid sha256:ad1a15591ad4c27119d50e14869314371a87e3afe7bd6757bf245f27c99ef00c
size 35246

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:971e260530cfd5b25cce035272820cb067c7e4a4a34fa6992526e5176913ea2d
size 36617
oid sha256:4a63c918393297e53f1a54b8de150ae807662ac905d5112d43812665f5a8f0ef
size 36647

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:56fde0cdb5a5196c277c43a1b9909401f40a4e7ef8bfe2e4d80592f0768fe5d5
size 27306
oid sha256:59bf5b8ed10344e299e5a8b977f3bc21915c500c7b79cfd44bdcc2a8a134fbdc
size 27291

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2db3dad746069968a83693752e985c25b72ca31be11b5c65d7da234f96d377d3
size 34055
oid sha256:0a3339897e11dbdb7d1fa99cf208d467dc19db03599cde5e6ba21d7b19c522de
size 34193

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:61739bd834ee18df778b4737f38f656d4342397135d518791ff4518d8fdea96d
size 35173
oid sha256:116c5f660aca1866667814903df752ae522c4dcdee2478c7d6815bc5f427684e
size 35317

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae8a729e91657959dbac49ef1c0f41fe37c296923eef6c22bdc9692d389ca346
size 25207
oid sha256:30ac5683fe4cc16bd6e31745b9f479a1b1d5e2a1dcf3f18ec7bd5f7793b8fda9
size 25273

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c66e45979657915aa7010b4796bf80865e8cb96118888c78d86e9edf0fa9bb88
size 66346
oid sha256:b738607cad294fea5617f81e4d410896704c395ffcf4118c25228e1d4ab693ba
size 66436

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:341782379e27258de391ba5d9713172eda666a593ca3040c9cfff96aaff72825
size 64908
oid sha256:d35646ae638353674a985215cc521db18e8ae6df7de3d241767ad5c994985814
size 65036

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:788722dec315d910767e187eb6c868fb85918d0220c7b923fed82fae736c7c38
size 49947
oid sha256:b1b50c558703a1fb83dfd8b4dcc8484fc3aaf019e4ab466f0e515db401f0797d
size 49999

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b08155f87cc38f33eeaf8d1c1a71760ff3620e8db384159721c49648555a0beb
size 48509
oid sha256:c2227b4a58b2a42b08dd41b5955504e23738bc1c49318c4e39c994d550a70bf2
size 48524

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:958606fd01dc7126836329f57c04913561cdc667cb47bd86d4a1cde654abd7f4
size 48458
oid sha256:d664509d4d3b3f3124952fd477d1cf990ede82a47bba9ddd0d44644b2a67e4a9
size 48628

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3bf3c0ad50dd204809f7ef0aa95a38e9f18b52fe17943807a732aa4c74d962a7
size 46027
oid sha256:717c8d869250d8a8f253fee4340daf95397975cd9eb0795e134541e6c7015b58
size 46078

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8a8e7035c518edf069c9860e9fe4a894d144420541d6397066ec5ee7742df57
size 77584
oid sha256:168ccd4dd8afa84f6e08bd18b29a14a90474d3d05d14a13047ad766186ffafc6
size 77524

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8bcd7f9287d275ee4fe2d9c8d6ef39715644696c8ddeaf84db7eb5877cce4c7
size 75645
oid sha256:288f1451dddbb41d0a45b99f8d06f89fcdc87b6f29696af18a6873062675fede
size 75681

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:24d232b144ff5664fecbd3c83fc04533eef8b3e82e4984b93f4156d996ad33e2
size 43569
oid sha256:8fb068f011864a5045c7bd65ffba67bd4b9234f42f40960c1eba1408328f5b3e
size 43782

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:31e50bed6ff94d2e20bb8cb0b9c35154de4c148c56a37518b0721fe463d7078b
size 42242
oid sha256:0e8cdaa2d979c68923e554754a6b5e5d9b31040988c2d4e6fc8248a28a8aa8dc
size 42409

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:88fbd2415816962033bc3a5798c2e75d3e6fffe2e2886072b67f9acb8643d1e2
size 31586
oid sha256:b85dc08c15c268ae04b6a89d630587a600d1f32cd2fffe72902ac1ab3f7d398f
size 32057

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dd2845e9385c972347a1200ae3edb4d6a2bc866a4f4f34f07eaf9f96d0407405
size 32270
oid sha256:23ccb6cb1ea3bc23c062760488e7ee2d6bb46a6f706702d11f910428b02a1b71
size 32714

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:98a74b997bf3c1e1365abf1c944453b15f1f8eb4ee48e24d91dda05c0a602b2a
size 33245
oid sha256:05bc4899a147b59134494910e51c3ee884a15ea9c34a6d93f999cc4332852c7b
size 33487

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ed0cba1b78f970ab1ef5bfc277da6fcfbad8e07d3d3d796daab43948b0423f41
size 30639
oid sha256:3f12091a5467d727b48926ab2182ae7def5f1a7820044613d0a15fbd4bbd622c
size 30976

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b5b1e6dbf5d662f0e4255dd0328bbe969ae6d309e4c9a5dbb6977671b940d428
size 31310
oid sha256:6e66b8b86ecd2f3febc7121384357db22a0d0cd5ca5b6155bb2c02dc9fc43180
size 31627

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:31940f629ceaec427e44e1c4576d489550e40c652da5894bcca6da3f0642c5ff
size 30708
oid sha256:00e648be36e7ff1c8bed368491579ed0467b970d9863ce576b2d5e53f822c62e
size 30896

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c19217fdb11145af9a2e86bbdb9fa203ac717e8e94ca61798b4861392bb396e1
size 69416
oid sha256:cc804b1b40c10b55b33e33e1614de72cc5b8592762f2319cc2aaf2ac60583671
size 69614

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:905112d921f655c9a122261b4a1d1980ed16079611be0b20066d976780ea8c5c
size 66019
oid sha256:d964de0d0cc6b2210690c99f77bfd70d41173f71893a2c072565101a84982583
size 66203

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c19217fdb11145af9a2e86bbdb9fa203ac717e8e94ca61798b4861392bb396e1
size 69416
oid sha256:cc804b1b40c10b55b33e33e1614de72cc5b8592762f2319cc2aaf2ac60583671
size 69614

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c86dd4d1ababe8df2d373aa1b47c14bf71e4744de78bff3efdab2f9fcb5034da
size 49211
oid sha256:ebfd71c25bee58cc440f8e5537a2b981fc92552be3f6076b41f3d0642e70c60e
size 49385

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f7c6104e319a7246c04dcc4749c01e7147b412e6905e1fec70b7331632cf1a2
size 67441
oid sha256:e153740038de710dfd3b636fa60100f814c966ffc4cbb26b84f9f388ae1f0771
size 67551

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d84ab79382862503da8f18a54d05578a560f515f9062af5fd6c922fe830ca84a
size 63656
oid sha256:de1bc2159d3c9fe8ddfa2b1c078b66a40a839e45f9c33f8744aeaec58f5a238e
size 63800

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1f7c6104e319a7246c04dcc4749c01e7147b412e6905e1fec70b7331632cf1a2
size 67441
oid sha256:e153740038de710dfd3b636fa60100f814c966ffc4cbb26b84f9f388ae1f0771
size 67551

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3afd2de9146b6d29dec8e6249acbf771cc602d27f5783523da844f24f24c8daf
size 45984
oid sha256:d31b50c1c79d1172e773a3106adb01232eb89058a12ab89232c6bcf6fae577db
size 46143

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2633d18f133660a2daff4ba902abe0450cf233714d43f5de8897379784f082c5
size 21375
oid sha256:d546519848aa915e33bc8b609cc4feda8037e22048acd063b13c829c6339fa72
size 21584

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4dbe0286f64a876af3bc4570141e26881462d8ca17485632e2fc599ae9eed563
size 29037
oid sha256:fc00c11dc9c4dcac7f5f80d463959180480ceb0cc2de2ff4482eeb44f3d32c99
size 29198

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c667e75edc8dcafe685abc8027d24c7dc2ed8dff6c5f1b61e7416adaf1831956
size 21186
oid sha256:a3be82b15c29d320ed317c522247330c4ec517292311dd662828391ff47522f5
size 21306

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:37182714a6079cbf149e4029f3ceb9d95f9a4381663c4d45838a1748d3907a16
size 28580
oid sha256:c13b24c426826901e7685023c4340e6735e81ffb86f76b7c47baaf38551e5a71
size 28733

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9f0629e8c3e8ecba64ae0a0a535b14f3b1be9dd6be1e12890340d102d0e4e1a
size 12095
oid sha256:8fbab73e2fda7b60e4b9b7033a5fdc1bcff1dc1e921783add8b1fb2b12bfafeb
size 12062

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00afd0d99cc415ec5c66a45030c22067d2ef06208ab9085a3929c3f586579062
size 11776
oid sha256:74751abc4f122ce216ca5ded8dc76a040b6512572f0f89b9fa53b5d8cbd7bde3
size 11801

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d8271a686840ae72ab68309eeb42a051c38cec331399a63a375ceaf958591172
size 31006
oid sha256:a45c0e66b8d59b6de1098c045bbfb569f2b89cac7adcdeaf047f0e86bb59fc78
size 31000

Some files were not shown because too many files have changed in this diff Show more