Merge branch 'develop' into feature/bma/metro070
This commit is contained in:
commit
52dffa5418
46 changed files with 306 additions and 233 deletions
|
|
@ -1 +1 @@
|
||||||
Subproject commit dac93821a6f9f9ad1494d3c69c115ef0696eb7ce
|
Subproject commit 867d1118e157ba89a4f5462f8d9c13e206f10026
|
||||||
|
|
@ -39,7 +39,7 @@ class DefaultAccountProviderAccessControl(
|
||||||
// Ensure that Element Pro is not required for this account provider
|
// Ensure that Element Pro is not required for this account provider
|
||||||
val wellKnown = wellknownRetriever.getElementWellKnown(
|
val wellKnown = wellknownRetriever.getElementWellKnown(
|
||||||
baseUrl = accountProviderUrl.ensureProtocol(),
|
baseUrl = accountProviderUrl.ensureProtocol(),
|
||||||
)
|
).dataOrNull()
|
||||||
if (wellKnown?.enforceElementPro == true) {
|
if (wellKnown?.enforceElementPro == true) {
|
||||||
throw AccountProviderAccessException.NeedElementProException(
|
throw AccountProviderAccessException.NeedElementProException(
|
||||||
unauthorisedAccountProviderTitle = title,
|
unauthorisedAccountProviderTitle = title,
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class HomeserverResolver(
|
||||||
wellknownRetriever.getWellKnown(url)
|
wellknownRetriever.getWellKnown(url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val isValid = wellKnown?.isValid().orFalse()
|
val isValid = wellKnown?.dataOrNull()?.isValid().orFalse()
|
||||||
if (isValid) {
|
if (isValid) {
|
||||||
// Emit the list as soon as possible
|
// Emit the list as soon as possible
|
||||||
currentList.add(
|
currentList.add(
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class DefaultWebClientUrlForAuthenticationRetriever(
|
||||||
Timber.w("Temporary account creation flow is only supported on matrix.org")
|
Timber.w("Temporary account creation flow is only supported on matrix.org")
|
||||||
throw AccountCreationNotSupported()
|
throw AccountCreationNotSupported()
|
||||||
}
|
}
|
||||||
val wellknown = wellknownRetriever.getElementWellKnown(homeServerUrl)
|
val wellknown = wellknownRetriever.getElementWellKnown(homeServerUrl).dataOrNull()
|
||||||
?: throw AccountCreationNotSupported()
|
?: throw AccountCreationNotSupported()
|
||||||
val registrationHelperUrl = wellknown.registrationHelperUrl
|
val registrationHelperUrl = wellknown.registrationHelperUrl
|
||||||
return if (registrationHelperUrl != null) {
|
return if (registrationHelperUrl != null) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2
|
||||||
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_URL
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Assert.assertThrows
|
import org.junit.Assert.assertThrows
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -155,7 +156,13 @@ class DefaultAccountProviderAccessControlTest {
|
||||||
defaultHomeserverListResult = { allowedAccountProviders },
|
defaultHomeserverListResult = { allowedAccountProviders },
|
||||||
),
|
),
|
||||||
wellknownRetriever = FakeWellknownRetriever(
|
wellknownRetriever = FakeWellknownRetriever(
|
||||||
getElementWellKnownResult = { elementWellKnown },
|
getElementWellKnownResult = {
|
||||||
|
if (elementWellKnown == null) {
|
||||||
|
WellknownRetrieverResult.NotFound
|
||||||
|
} else {
|
||||||
|
WellknownRetrieverResult.Success(elementWellKnown)
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
|
@ -114,9 +115,11 @@ class ChangeServerPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - change server element pro required error`() = runTest {
|
fun `present - change server element pro required error`() = runTest {
|
||||||
val getElementWellKnownResult = lambdaRecorder<String, ElementWellKnown> {
|
val getElementWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<ElementWellKnown>> {
|
||||||
anElementWellKnown(
|
WellknownRetrieverResult.Success(
|
||||||
enforceElementPro = true,
|
anElementWellKnown(
|
||||||
|
enforceElementPro = true,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
createPresenter(
|
createPresenter(
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
|
||||||
import io.element.android.libraries.wellknown.api.WellKnown
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
|
@ -94,12 +95,12 @@ class SearchAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - enter text one result with wellknown`() = runTest {
|
fun `present - enter text one result with wellknown`() = runTest {
|
||||||
val getWellKnownResult = lambdaRecorder<String, WellKnown> {
|
val getWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<WellKnown>> {
|
||||||
when (it) {
|
when (it) {
|
||||||
"https://test.org" -> error("not found")
|
"https://test.org" -> WellknownRetrieverResult.NotFound
|
||||||
"https://test.com" -> error("not found")
|
"https://test.com" -> WellknownRetrieverResult.NotFound
|
||||||
"https://test.io" -> aWellKnown()
|
"https://test.io" -> WellknownRetrieverResult.Success(aWellKnown())
|
||||||
"https://test" -> error("not found")
|
"https://test" -> WellknownRetrieverResult.NotFound
|
||||||
else -> error("should not happen")
|
else -> error("should not happen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,12 +139,12 @@ class SearchAccountProviderPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - enter text two results with wellknown`() = runTest {
|
fun `present - enter text two results with wellknown`() = runTest {
|
||||||
val getWellKnownResult = lambdaRecorder<String, WellKnown> {
|
val getWellKnownResult = lambdaRecorder<String, WellknownRetrieverResult<WellKnown>> {
|
||||||
when (it) {
|
when (it) {
|
||||||
"https://test.org" -> aWellKnown()
|
"https://test.org" -> WellknownRetrieverResult.Success(aWellKnown())
|
||||||
"https://test.com" -> error("not found")
|
"https://test.com" -> WellknownRetrieverResult.NotFound
|
||||||
"https://test.io" -> aWellKnown()
|
"https://test.io" -> WellknownRetrieverResult.Success(aWellKnown())
|
||||||
"https://test" -> error("not found")
|
"https://test" -> WellknownRetrieverResult.NotFound
|
||||||
else -> error("should not happen")
|
else -> error("should not happen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,7 +164,7 @@ class RoomDetailsPresenter(
|
||||||
val securityAndPrivacyPermissions = room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value)
|
val securityAndPrivacyPermissions = room.securityAndPrivacyPermissionsAsState(syncUpdateFlow.value)
|
||||||
val canShowSecurityAndPrivacy by remember {
|
val canShowSecurityAndPrivacy by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
isKnockRequestsEnabled && roomType is RoomDetailsType.Room && securityAndPrivacyPermissions.value.hasAny
|
roomType is RoomDetailsType.Room && securityAndPrivacyPermissions.value.hasAny
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
import io.element.android.libraries.matrix.api.core.RoomAlias
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
|
|
@ -45,6 +47,7 @@ class SecurityAndPrivacyPresenter(
|
||||||
@Assisted private val navigator: SecurityAndPrivacyNavigator,
|
@Assisted private val navigator: SecurityAndPrivacyNavigator,
|
||||||
private val matrixClient: MatrixClient,
|
private val matrixClient: MatrixClient,
|
||||||
private val room: JoinedRoom,
|
private val room: JoinedRoom,
|
||||||
|
private val featureFlagService: FeatureFlagService,
|
||||||
) : Presenter<SecurityAndPrivacyState> {
|
) : Presenter<SecurityAndPrivacyState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
|
|
@ -55,6 +58,9 @@ class SecurityAndPrivacyPresenter(
|
||||||
override fun present(): SecurityAndPrivacyState {
|
override fun present(): SecurityAndPrivacyState {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val isKnockEnabled by remember {
|
||||||
|
featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock)
|
||||||
|
}.collectAsState(false)
|
||||||
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
val saveAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
val homeserverName = remember { matrixClient.userIdServerName() }
|
val homeserverName = remember { matrixClient.userIdServerName() }
|
||||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||||
|
|
@ -149,6 +155,7 @@ class SecurityAndPrivacyPresenter(
|
||||||
editedSettings = editedSettings,
|
editedSettings = editedSettings,
|
||||||
homeserverName = homeserverName,
|
homeserverName = homeserverName,
|
||||||
showEnableEncryptionConfirmation = showEnableEncryptionConfirmation,
|
showEnableEncryptionConfirmation = showEnableEncryptionConfirmation,
|
||||||
|
isKnockEnabled = isKnockEnabled,
|
||||||
saveAction = saveAction.value,
|
saveAction = saveAction.value,
|
||||||
permissions = permissions,
|
permissions = permissions,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ data class SecurityAndPrivacyState(
|
||||||
val editedSettings: SecurityAndPrivacySettings,
|
val editedSettings: SecurityAndPrivacySettings,
|
||||||
val homeserverName: String,
|
val homeserverName: String,
|
||||||
val showEnableEncryptionConfirmation: Boolean,
|
val showEnableEncryptionConfirmation: Boolean,
|
||||||
|
val isKnockEnabled: Boolean,
|
||||||
val saveAction: AsyncAction<Unit>,
|
val saveAction: AsyncAction<Unit>,
|
||||||
private val permissions: SecurityAndPrivacyPermissions,
|
private val permissions: SecurityAndPrivacyPermissions,
|
||||||
val eventSink: (SecurityAndPrivacyEvents) -> Unit
|
val eventSink: (SecurityAndPrivacyEvents) -> Unit
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,8 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAn
|
||||||
aSecurityAndPrivacyState(
|
aSecurityAndPrivacyState(
|
||||||
savedSettings = aSecurityAndPrivacySettings(
|
savedSettings = aSecurityAndPrivacySettings(
|
||||||
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember
|
roomAccess = SecurityAndPrivacyRoomAccess.SpaceMember
|
||||||
)
|
),
|
||||||
|
isKnockEnabled = false,
|
||||||
),
|
),
|
||||||
aSecurityAndPrivacyState(
|
aSecurityAndPrivacyState(
|
||||||
editedSettings = aSecurityAndPrivacySettings(
|
editedSettings = aSecurityAndPrivacySettings(
|
||||||
|
|
@ -54,6 +55,12 @@ open class SecurityAndPrivacyStateProvider : PreviewParameterProvider<SecurityAn
|
||||||
aSecurityAndPrivacyState(
|
aSecurityAndPrivacyState(
|
||||||
saveAction = AsyncAction.Loading
|
saveAction = AsyncAction.Loading
|
||||||
),
|
),
|
||||||
|
aSecurityAndPrivacyState(
|
||||||
|
savedSettings = aSecurityAndPrivacySettings(
|
||||||
|
roomAccess = SecurityAndPrivacyRoomAccess.AskToJoin
|
||||||
|
),
|
||||||
|
isKnockEnabled = false,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,6 +90,7 @@ fun aSecurityAndPrivacyState(
|
||||||
canChangeEncryption = true,
|
canChangeEncryption = true,
|
||||||
canChangeRoomVisibility = true
|
canChangeRoomVisibility = true
|
||||||
),
|
),
|
||||||
|
isKnockEnabled: Boolean = true,
|
||||||
eventSink: (SecurityAndPrivacyEvents) -> Unit = {}
|
eventSink: (SecurityAndPrivacyEvents) -> Unit = {}
|
||||||
) = SecurityAndPrivacyState(
|
) = SecurityAndPrivacyState(
|
||||||
editedSettings = editedSettings,
|
editedSettings = editedSettings,
|
||||||
|
|
@ -90,6 +98,7 @@ fun aSecurityAndPrivacyState(
|
||||||
homeserverName = homeserverName,
|
homeserverName = homeserverName,
|
||||||
showEnableEncryptionConfirmation = showEncryptionConfirmation,
|
showEnableEncryptionConfirmation = showEncryptionConfirmation,
|
||||||
saveAction = saveAction,
|
saveAction = saveAction,
|
||||||
|
isKnockEnabled = isKnockEnabled,
|
||||||
permissions = permissions,
|
permissions = permissions,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,7 @@ fun SecurityAndPrivacyView(
|
||||||
modifier = Modifier.padding(top = 24.dp),
|
modifier = Modifier.padding(top = 24.dp),
|
||||||
edited = state.editedSettings.roomAccess,
|
edited = state.editedSettings.roomAccess,
|
||||||
saved = state.savedSettings.roomAccess,
|
saved = state.savedSettings.roomAccess,
|
||||||
|
isKnockEnabled = state.isKnockEnabled,
|
||||||
onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) },
|
onSelectOption = { state.eventSink(SecurityAndPrivacyEvents.ChangeRoomAccess(it)) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -176,6 +177,7 @@ private fun SecurityAndPrivacySection(
|
||||||
private fun RoomAccessSection(
|
private fun RoomAccessSection(
|
||||||
edited: SecurityAndPrivacyRoomAccess,
|
edited: SecurityAndPrivacyRoomAccess,
|
||||||
saved: SecurityAndPrivacyRoomAccess,
|
saved: SecurityAndPrivacyRoomAccess,
|
||||||
|
isKnockEnabled: Boolean,
|
||||||
onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit,
|
onSelectOption: (SecurityAndPrivacyRoomAccess) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -189,12 +191,18 @@ private fun RoomAccessSection(
|
||||||
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.InviteOnly),
|
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.InviteOnly),
|
||||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) },
|
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.InviteOnly) },
|
||||||
)
|
)
|
||||||
ListItem(
|
// Show Ask to join option in two cases:
|
||||||
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) },
|
// - the Knock FF is enabled
|
||||||
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description)) },
|
// - AskToJoin is the current saved value
|
||||||
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin),
|
if (saved == SecurityAndPrivacyRoomAccess.AskToJoin || isKnockEnabled) {
|
||||||
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) },
|
ListItem(
|
||||||
)
|
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_title)) },
|
||||||
|
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_ask_to_join_option_description)) },
|
||||||
|
trailingContent = ListItemContent.RadioButton(selected = edited == SecurityAndPrivacyRoomAccess.AskToJoin),
|
||||||
|
onClick = { onSelectOption(SecurityAndPrivacyRoomAccess.AskToJoin) },
|
||||||
|
enabled = isKnockEnabled,
|
||||||
|
)
|
||||||
|
}
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_title)) },
|
headlineContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_title)) },
|
||||||
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_description)) },
|
supportingContent = { Text(text = stringResource(R.string.screen_security_and_privacy_room_access_anyone_option_description)) },
|
||||||
|
|
|
||||||
|
|
@ -719,10 +719,6 @@ class RoomDetailsPresenterTest {
|
||||||
val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService)
|
val presenter = createRoomDetailsPresenter(room = room, featureFlagService = featureFlagService)
|
||||||
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
|
presenter.testWithLifecycleOwner(lifecycleOwner = fakeLifecycleOwner) {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
with(awaitItem()) {
|
|
||||||
assertThat(canShowSecurityAndPrivacy).isFalse()
|
|
||||||
}
|
|
||||||
featureFlagService.setFeatureEnabled(FeatureFlags.Knock, true)
|
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
assertThat(canShowSecurityAndPrivacy).isTrue()
|
assertThat(canShowSecurityAndPrivacy).isTrue()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ package io.element.android.features.roomdetails.impl.securityandprivacy
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
import io.element.android.libraries.matrix.api.room.history.RoomHistoryVisibility
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
import io.element.android.libraries.matrix.api.room.join.JoinRule
|
||||||
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
import io.element.android.libraries.matrix.api.roomdirectory.RoomVisibility
|
||||||
|
|
@ -38,6 +41,7 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
assertThat(showRoomVisibilitySections).isFalse()
|
assertThat(showRoomVisibilitySections).isFalse()
|
||||||
assertThat(showHistoryVisibilitySection).isFalse()
|
assertThat(showHistoryVisibilitySection).isFalse()
|
||||||
assertThat(showEncryptionSection).isFalse()
|
assertThat(showEncryptionSection).isFalse()
|
||||||
|
assertThat(isKnockEnabled).isFalse()
|
||||||
}
|
}
|
||||||
with(awaitItem()) {
|
with(awaitItem()) {
|
||||||
assertThat(editedSettings).isEqualTo(savedSettings)
|
assertThat(editedSettings).isEqualTo(savedSettings)
|
||||||
|
|
@ -48,6 +52,7 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
assertThat(showRoomVisibilitySections).isFalse()
|
assertThat(showRoomVisibilitySections).isFalse()
|
||||||
assertThat(showHistoryVisibilitySection).isTrue()
|
assertThat(showHistoryVisibilitySection).isTrue()
|
||||||
assertThat(showEncryptionSection).isTrue()
|
assertThat(showEncryptionSection).isTrue()
|
||||||
|
assertThat(isKnockEnabled).isFalse()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,14 +61,14 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
fun `present - room info change updates saved and edited settings`() = runTest {
|
fun `present - room info change updates saved and edited settings`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canSendStateResult = { _, _ -> Result.success(true) },
|
canSendStateResult = { _, _ -> Result.success(true) },
|
||||||
initialRoomInfo = aRoomInfo(
|
initialRoomInfo = aRoomInfo(
|
||||||
joinRule = JoinRule.Public,
|
joinRule = JoinRule.Public,
|
||||||
historyVisibility = RoomHistoryVisibility.WorldReadable,
|
historyVisibility = RoomHistoryVisibility.WorldReadable,
|
||||||
canonicalAlias = A_ROOM_ALIAS,
|
canonicalAlias = A_ROOM_ALIAS,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
@ -163,10 +168,10 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
fun `present - room visibility loading and change`() = runTest {
|
fun `present - room visibility loading and change`() = runTest {
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canSendStateResult = { _, _ -> Result.success(true) },
|
canSendStateResult = { _, _ -> Result.success(true) },
|
||||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||||
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared)
|
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
val presenter = createSecurityAndPrivacyPresenter(room = room)
|
||||||
presenter.test {
|
presenter.test {
|
||||||
|
|
@ -212,10 +217,10 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
val updateRoomHistoryVisibilityLambda = lambdaRecorder<RoomHistoryVisibility, Result<Unit>> { Result.success(Unit) }
|
val updateRoomHistoryVisibilityLambda = lambdaRecorder<RoomHistoryVisibility, Result<Unit>> { Result.success(Unit) }
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canSendStateResult = { _, _ -> Result.success(true) },
|
canSendStateResult = { _, _ -> Result.success(true) },
|
||||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||||
initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared)
|
initialRoomInfo = aRoomInfo(joinRule = JoinRule.Invite, historyVisibility = RoomHistoryVisibility.Shared)
|
||||||
),
|
),
|
||||||
enableEncryptionResult = enableEncryptionLambda,
|
enableEncryptionResult = enableEncryptionLambda,
|
||||||
updateJoinRuleResult = updateJoinRuleLambda,
|
updateJoinRuleResult = updateJoinRuleLambda,
|
||||||
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
||||||
|
|
@ -279,10 +284,10 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
val updateRoomHistoryVisibilityLambda = lambdaRecorder<RoomHistoryVisibility, Result<Unit>> { Result.success(Unit) }
|
val updateRoomHistoryVisibilityLambda = lambdaRecorder<RoomHistoryVisibility, Result<Unit>> { Result.success(Unit) }
|
||||||
val room = FakeJoinedRoom(
|
val room = FakeJoinedRoom(
|
||||||
baseRoom = FakeBaseRoom(
|
baseRoom = FakeBaseRoom(
|
||||||
canSendStateResult = { _, _ -> Result.success(true) },
|
canSendStateResult = { _, _ -> Result.success(true) },
|
||||||
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
getRoomVisibilityResult = { Result.success(RoomVisibility.Private) },
|
||||||
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private)
|
initialRoomInfo = aRoomInfo(historyVisibility = RoomHistoryVisibility.Shared, joinRule = JoinRule.Private)
|
||||||
),
|
),
|
||||||
enableEncryptionResult = enableEncryptionLambda,
|
enableEncryptionResult = enableEncryptionLambda,
|
||||||
updateJoinRuleResult = updateJoinRuleLambda,
|
updateJoinRuleResult = updateJoinRuleLambda,
|
||||||
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
updateRoomVisibilityResult = updateRoomVisibilityLambda,
|
||||||
|
|
@ -323,7 +328,8 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
)
|
)
|
||||||
// Saved settings are updated 2 times to match the edited settings
|
// Saved settings are updated 2 times to match the edited settings
|
||||||
skipItems(3)
|
skipItems(3)
|
||||||
with(awaitItem()) {
|
val state = awaitItem()
|
||||||
|
with(state) {
|
||||||
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
assertThat(saveAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory)
|
assertThat(savedSettings.isVisibleInRoomDirectory).isNotEqualTo(editedSettings.isVisibleInRoomDirectory)
|
||||||
assertThat(canBeSaved).isTrue()
|
assertThat(canBeSaved).isTrue()
|
||||||
|
|
@ -332,6 +338,26 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
assert(updateJoinRuleLambda).isCalledOnce()
|
assert(updateJoinRuleLambda).isCalledOnce()
|
||||||
assert(updateRoomVisibilityLambda).isCalledOnce()
|
assert(updateRoomVisibilityLambda).isCalledOnce()
|
||||||
assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
|
assert(updateRoomHistoryVisibilityLambda).isCalledOnce()
|
||||||
|
// Clear error
|
||||||
|
state.eventSink(SecurityAndPrivacyEvents.DismissSaveError)
|
||||||
|
with(awaitItem()) {
|
||||||
|
assertThat(saveAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - isKnockEnabled is true if the Knock feature flag is enabled`() = runTest {
|
||||||
|
val presenter = createSecurityAndPrivacyPresenter(
|
||||||
|
featureFlagService = FakeFeatureFlagService(
|
||||||
|
initialState = mapOf(
|
||||||
|
FeatureFlags.Knock.key to true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
assertThat(awaitItem().isKnockEnabled).isFalse()
|
||||||
|
assertThat(awaitItem().isKnockEnabled).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -345,13 +371,15 @@ class SecurityAndPrivacyPresenterTest {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(),
|
navigator: SecurityAndPrivacyNavigator = FakeSecurityAndPrivacyNavigator(),
|
||||||
|
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
|
||||||
): SecurityAndPrivacyPresenter {
|
): SecurityAndPrivacyPresenter {
|
||||||
return SecurityAndPrivacyPresenter(
|
return SecurityAndPrivacyPresenter(
|
||||||
room = room,
|
room = room,
|
||||||
matrixClient = FakeMatrixClient(
|
matrixClient = FakeMatrixClient(
|
||||||
userIdServerNameLambda = { serverName },
|
userIdServerNameLambda = { serverName },
|
||||||
),
|
),
|
||||||
navigator = navigator
|
navigator = navigator,
|
||||||
|
featureFlagService = featureFlagService,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ lifecycle = "2.9.2"
|
||||||
activity = "1.11.0"
|
activity = "1.11.0"
|
||||||
media3 = "1.8.0"
|
media3 = "1.8.0"
|
||||||
camera = "1.5.1"
|
camera = "1.5.1"
|
||||||
work = "2.10.5"
|
work = "2.11.0"
|
||||||
|
|
||||||
# Compose
|
# Compose
|
||||||
compose_bom = "2025.07.00"
|
compose_bom = "2025.07.00"
|
||||||
|
|
@ -210,7 +210,7 @@ haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref =
|
||||||
color_picker = "io.mhssn:colorpicker:1.0.0"
|
color_picker = "io.mhssn:colorpicker:1.0.0"
|
||||||
|
|
||||||
# Analytics
|
# Analytics
|
||||||
posthog = "com.posthog:posthog-android:3.24.0"
|
posthog = "com.posthog:posthog-android:3.25.0"
|
||||||
sentry = "io.sentry:sentry-android:8.24.0"
|
sentry = "io.sentry:sentry-android:8.24.0"
|
||||||
# main branch can be tested replacing the version with main-SNAPSHOT
|
# main branch can be tested replacing the version with main-SNAPSHOT
|
||||||
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.28.0"
|
||||||
|
|
|
||||||
|
|
@ -41,13 +41,13 @@ import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
@Preview(widthDp = 730, heightDp = 1800)
|
@Preview(widthDp = 730, heightDp = 1920)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IconsCompoundPreviewLight() = ElementTheme {
|
internal fun IconsCompoundPreviewLight() = ElementTheme {
|
||||||
IconsCompoundPreview()
|
IconsCompoundPreview()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(widthDp = 730, heightDp = 1800)
|
@Preview(widthDp = 730, heightDp = 1920)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IconsCompoundPreviewRtl() = ElementTheme {
|
internal fun IconsCompoundPreviewRtl() = ElementTheme {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
|
@ -59,7 +59,7 @@ internal fun IconsCompoundPreviewRtl() = ElementTheme {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(widthDp = 730, heightDp = 1800)
|
@Preview(widthDp = 730, heightDp = 1920)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IconsCompoundPreviewDark() = ElementTheme(darkTheme = true) {
|
internal fun IconsCompoundPreviewDark() = ElementTheme(darkTheme = true) {
|
||||||
IconsCompoundPreview()
|
IconsCompoundPreview()
|
||||||
|
|
|
||||||
|
|
@ -15,4 +15,5 @@ internal val iconsOther = listOf(
|
||||||
R.drawable.ic_notification,
|
R.drawable.ic_notification,
|
||||||
R.drawable.ic_stop,
|
R.drawable.ic_stop,
|
||||||
R.drawable.pin,
|
R.drawable.pin,
|
||||||
|
R.drawable.ic_winner,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,8 @@ import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
|
@ -30,53 +27,12 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
internal class CompoundIconChunkProvider : PreviewParameterProvider<IconChunk> {
|
|
||||||
override val values: Sequence<IconChunk>
|
|
||||||
get() {
|
|
||||||
val chunks = CompoundIcons.allResIds.chunked(36)
|
|
||||||
return chunks.mapIndexed { index, chunk ->
|
|
||||||
IconChunk(index = index + 1, total = chunks.size, icons = chunk.toImmutableList())
|
|
||||||
}
|
|
||||||
.asSequence()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class OtherIconChunkProvider : PreviewParameterProvider<IconChunk> {
|
|
||||||
override val values: Sequence<IconChunk>
|
|
||||||
get() {
|
|
||||||
val chunks = iconsOther.chunked(36)
|
|
||||||
return chunks.mapIndexed { index, chunk ->
|
|
||||||
IconChunk(index = index + 1, total = chunks.size, icons = chunk.toImmutableList())
|
|
||||||
}
|
|
||||||
.asSequence()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal data class IconChunk(
|
|
||||||
val index: Int,
|
|
||||||
val total: Int,
|
|
||||||
val icons: ImmutableList<Int>,
|
|
||||||
)
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun IconsCompoundPreview(@PreviewParameter(CompoundIconChunkProvider::class) chunk: IconChunk) = ElementPreview {
|
internal fun IconsOtherPreview() = ElementPreview {
|
||||||
IconsPreview(
|
IconsPreview(
|
||||||
title = "R.drawable.ic_compound_* ${chunk.index}/${chunk.total}",
|
title = "Other icons",
|
||||||
iconsList = chunk.icons,
|
iconsList = iconsOther.toImmutableList(),
|
||||||
iconNameTransform = { name ->
|
|
||||||
name.removePrefix("ic_compound_")
|
|
||||||
.replace("_", " ")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@PreviewsDayNight
|
|
||||||
@Composable
|
|
||||||
internal fun IconsOtherPreview(@PreviewParameter(OtherIconChunkProvider::class) iconChunk: IconChunk) = ElementPreview {
|
|
||||||
IconsPreview(
|
|
||||||
title = "R.drawable.ic_* ${iconChunk.index}/${iconChunk.total}",
|
|
||||||
iconsList = iconChunk.icons,
|
|
||||||
iconNameTransform = { name ->
|
iconNameTransform = { name ->
|
||||||
name.removePrefix("ic_")
|
name.removePrefix("ic_")
|
||||||
.replace("_", " ")
|
.replace("_", " ")
|
||||||
|
|
|
||||||
|
|
@ -286,7 +286,7 @@ class RustMatrixClient(
|
||||||
override suspend fun getUrl(url: String): Result<ByteArray> = withContext(sessionDispatcher) {
|
override suspend fun getUrl(url: String): Result<ByteArray> = withContext(sessionDispatcher) {
|
||||||
runCatchingExceptions {
|
runCatchingExceptions {
|
||||||
innerClient.getUrl(url)
|
innerClient.getUrl(url)
|
||||||
}
|
}.mapFailure { it.mapClientException() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) {
|
override suspend fun getRoom(roomId: RoomId): BaseRoom? = withContext(sessionDispatcher) {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
package io.element.android.libraries.wellknown.api
|
package io.element.android.libraries.wellknown.api
|
||||||
|
|
||||||
interface SessionWellknownRetriever {
|
interface SessionWellknownRetriever {
|
||||||
suspend fun getWellKnown(): WellKnown?
|
suspend fun getWellKnown(): WellknownRetrieverResult<WellKnown>
|
||||||
suspend fun getElementWellKnown(): ElementWellKnown?
|
suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,6 @@
|
||||||
package io.element.android.libraries.wellknown.api
|
package io.element.android.libraries.wellknown.api
|
||||||
|
|
||||||
interface WellknownRetriever {
|
interface WellknownRetriever {
|
||||||
suspend fun getWellKnown(baseUrl: String): WellKnown?
|
suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult<WellKnown>
|
||||||
suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown?
|
suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.wellknown.api
|
||||||
|
|
||||||
|
sealed interface WellknownRetrieverResult<out T> {
|
||||||
|
/**
|
||||||
|
* Well-known data has been successfully retrieved.
|
||||||
|
*/
|
||||||
|
data class Success<out T>(val data: T) : WellknownRetrieverResult<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Well-known data is not found (file does not exist server side, we got a 404).
|
||||||
|
*/
|
||||||
|
data object NotFound : WellknownRetrieverResult<Nothing>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Any other error.
|
||||||
|
*/
|
||||||
|
data class Error(val exception: Exception) : WellknownRetrieverResult<Nothing>
|
||||||
|
|
||||||
|
fun dataOrNull(): T? = when (this) {
|
||||||
|
is Success<T> -> data
|
||||||
|
is Error -> null
|
||||||
|
NotFound -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,9 +12,11 @@ import io.element.android.libraries.androidutils.json.JsonProvider
|
||||||
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
import io.element.android.libraries.core.extensions.mapCatchingExceptions
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.exception.ClientException
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
||||||
import io.element.android.libraries.wellknown.api.WellKnown
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
|
|
@ -24,29 +26,40 @@ class DefaultSessionWellknownRetriever(
|
||||||
) : SessionWellknownRetriever {
|
) : SessionWellknownRetriever {
|
||||||
private val domain by lazy { matrixClient.userIdServerName() }
|
private val domain by lazy { matrixClient.userIdServerName() }
|
||||||
|
|
||||||
override suspend fun getWellKnown(): WellKnown? {
|
override suspend fun getWellKnown(): WellknownRetrieverResult<WellKnown> {
|
||||||
val url = "https://$domain/.well-known/matrix/client"
|
val url = "https://$domain/.well-known/matrix/client"
|
||||||
return matrixClient
|
return matrixClient
|
||||||
.getUrl(url)
|
.getUrl(url)
|
||||||
.mapCatchingExceptions {
|
.mapCatchingExceptions {
|
||||||
val data = String(it)
|
val data = String(it)
|
||||||
json().decodeFromString(InternalWellKnown.serializer(), data)
|
json().decodeFromString<InternalWellKnown>(data).map()
|
||||||
}
|
}
|
||||||
.onFailure { Timber.e(it, "Failed to retrieve .well-known from $domain") }
|
.toWellknownRetrieverResult()
|
||||||
.map { it.map() }
|
|
||||||
.getOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getElementWellKnown(): ElementWellKnown? {
|
override suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown> {
|
||||||
val url = "https://$domain/.well-known/element/element.json"
|
val url = "https://$domain/.well-known/element/element.json"
|
||||||
return matrixClient
|
return matrixClient
|
||||||
.getUrl(url)
|
.getUrl(url)
|
||||||
.mapCatchingExceptions {
|
.mapCatchingExceptions {
|
||||||
val data = String(it)
|
val data = String(it)
|
||||||
json().decodeFromString(InternalElementWellKnown.serializer(), data)
|
json().decodeFromString<InternalElementWellKnown>(data).map()
|
||||||
}
|
}
|
||||||
.onFailure { Timber.e(it, "Failed to retrieve Element .well-known from $domain") }
|
.toWellknownRetrieverResult()
|
||||||
.map { it.map() }
|
|
||||||
.getOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T> Result<T>.toWellknownRetrieverResult(): WellknownRetrieverResult<T> = fold(
|
||||||
|
onSuccess = {
|
||||||
|
WellknownRetrieverResult.Success(it)
|
||||||
|
},
|
||||||
|
onFailure = {
|
||||||
|
Timber.e(it, "Failed to retrieve Element .well-known from $domain")
|
||||||
|
// This check on message value is not ideal but this is what we got from the SDK.
|
||||||
|
if ((it as? ClientException.Generic)?.message?.contains("404") == true) {
|
||||||
|
WellknownRetrieverResult.NotFound
|
||||||
|
} else {
|
||||||
|
WellknownRetrieverResult.Error(it as Exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,45 +9,71 @@ package io.element.android.libraries.wellknown.impl
|
||||||
|
|
||||||
import dev.zacsweers.metro.AppScope
|
import dev.zacsweers.metro.AppScope
|
||||||
import dev.zacsweers.metro.ContributesBinding
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import io.element.android.libraries.core.extensions.runCatchingExceptions
|
||||||
import io.element.android.libraries.core.uri.ensureProtocol
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
import io.element.android.libraries.network.RetrofitFactory
|
import io.element.android.libraries.network.RetrofitFactory
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellKnown
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
|
import retrofit2.HttpException
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultWellknownRetriever(
|
class DefaultWellknownRetriever(
|
||||||
private val retrofitFactory: RetrofitFactory,
|
private val retrofitFactory: RetrofitFactory,
|
||||||
) : WellknownRetriever {
|
) : WellknownRetriever {
|
||||||
override suspend fun getWellKnown(baseUrl: String): WellKnown? {
|
override suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult<WellKnown> {
|
||||||
val wellknownApi = buildWellknownApi(baseUrl) ?: return null
|
return buildWellknownApi(baseUrl)
|
||||||
return try {
|
.map { wellknownApi ->
|
||||||
wellknownApi.getWellKnown().map()
|
try {
|
||||||
} catch (e: Exception) {
|
val result = wellknownApi.getWellKnown().map()
|
||||||
Timber.e(e, "Failed to retrieve well-known data for $baseUrl")
|
WellknownRetrieverResult.Success(result)
|
||||||
null
|
} catch (e: Exception) {
|
||||||
}
|
Timber.e(e, "Failed to retrieve well-known data for $baseUrl")
|
||||||
|
if ((e as? HttpException)?.code() == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||||
|
WellknownRetrieverResult.NotFound
|
||||||
|
} else {
|
||||||
|
WellknownRetrieverResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fold(
|
||||||
|
onSuccess = { it },
|
||||||
|
onFailure = { WellknownRetrieverResult.Error(it as Exception) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown? {
|
override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown> {
|
||||||
val wellknownApi = buildWellknownApi(baseUrl) ?: return null
|
return buildWellknownApi(baseUrl)
|
||||||
return try {
|
.map { wellknownApi ->
|
||||||
wellknownApi.getElementWellKnown().map()
|
try {
|
||||||
} catch (e: Exception) {
|
val result = wellknownApi.getElementWellKnown().map()
|
||||||
Timber.e(e, "Failed to retrieve Element well-known data for $baseUrl")
|
WellknownRetrieverResult.Success(result)
|
||||||
null
|
} catch (e: Exception) {
|
||||||
}
|
// Is it a 404?
|
||||||
|
Timber.e(e, "Failed to retrieve Element well-known data for $baseUrl")
|
||||||
|
if ((e as? HttpException)?.code() == HttpURLConnection.HTTP_NOT_FOUND) {
|
||||||
|
WellknownRetrieverResult.NotFound
|
||||||
|
} else {
|
||||||
|
WellknownRetrieverResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fold(
|
||||||
|
onSuccess = { it },
|
||||||
|
onFailure = { WellknownRetrieverResult.Error(it as Exception) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildWellknownApi(accountProviderUrl: String): WellknownAPI? {
|
private fun buildWellknownApi(accountProviderUrl: String): Result<WellknownAPI> {
|
||||||
return try {
|
return runCatchingExceptions {
|
||||||
retrofitFactory.create(accountProviderUrl.ensureProtocol())
|
retrofitFactory.create(accountProviderUrl.ensureProtocol())
|
||||||
.create(WellknownAPI::class.java)
|
.create(WellknownAPI::class.java)
|
||||||
} catch (e: Exception) {
|
}.onFailure { e ->
|
||||||
// If the base URL is not valid, we cannot retrieve the well-known data
|
// If the base URL is not valid, we cannot retrieve the well-known data
|
||||||
Timber.e(e, "Failed to create Retrofit instance for $accountProviderUrl")
|
Timber.e(e, "Failed to create Retrofit instance for $accountProviderUrl")
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellKnown
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
import io.element.android.libraries.wellknown.api.WellKnownBaseConfig
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
import io.element.android.tests.testutils.lambda.value
|
import io.element.android.tests.testutils.lambda.value
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
|
@ -29,9 +30,11 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
getUrlLambda = getUrlLambda,
|
getUrlLambda = getUrlLambda,
|
||||||
)
|
)
|
||||||
assertThat(sut.getWellKnown()).isEqualTo(
|
assertThat(sut.getWellKnown()).isEqualTo(
|
||||||
WellKnown(
|
WellknownRetrieverResult.Success(
|
||||||
homeServer = null,
|
WellKnown(
|
||||||
identityServer = null,
|
homeServer = null,
|
||||||
|
identityServer = null,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
getUrlLambda.assertions().isCalledOnce()
|
getUrlLambda.assertions().isCalledOnce()
|
||||||
|
|
@ -55,13 +58,15 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assertThat(sut.getWellKnown()).isEqualTo(
|
assertThat(sut.getWellKnown()).isEqualTo(
|
||||||
WellKnown(
|
WellknownRetrieverResult.Success(
|
||||||
homeServer = WellKnownBaseConfig(
|
WellKnown(
|
||||||
baseURL = "https://example.org",
|
homeServer = WellKnownBaseConfig(
|
||||||
),
|
baseURL = "https://example.org",
|
||||||
identityServer = WellKnownBaseConfig(
|
),
|
||||||
baseURL = "https://identity.example.org",
|
identityServer = WellKnownBaseConfig(
|
||||||
),
|
baseURL = "https://identity.example.org",
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -81,13 +86,15 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assertThat(sut.getWellKnown()).isEqualTo(
|
assertThat(sut.getWellKnown()).isEqualTo(
|
||||||
WellKnown(
|
WellknownRetrieverResult.Success(
|
||||||
homeServer = WellKnownBaseConfig(
|
WellKnown(
|
||||||
baseURL = "https://example.org",
|
homeServer = WellKnownBaseConfig(
|
||||||
),
|
baseURL = "https://example.org",
|
||||||
identityServer = WellKnownBaseConfig(
|
),
|
||||||
baseURL = null,
|
identityServer = WellKnownBaseConfig(
|
||||||
),
|
baseURL = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -110,13 +117,15 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertThat(sut.getWellKnown()).isEqualTo(
|
assertThat(sut.getWellKnown()).isEqualTo(
|
||||||
WellKnown(
|
WellknownRetrieverResult.Success(
|
||||||
homeServer = WellKnownBaseConfig(
|
WellKnown(
|
||||||
baseURL = "https://example.org",
|
homeServer = WellKnownBaseConfig(
|
||||||
),
|
baseURL = "https://example.org",
|
||||||
identityServer = WellKnownBaseConfig(
|
),
|
||||||
baseURL = "https://identity.example.org",
|
identityServer = WellKnownBaseConfig(
|
||||||
),
|
baseURL = "https://identity.example.org",
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -135,7 +144,7 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assertThat(sut.getWellKnown()).isNull()
|
assertThat(sut.getWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -145,7 +154,7 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
Result.failure(AN_EXCEPTION)
|
Result.failure(AN_EXCEPTION)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assertThat(sut.getWellKnown()).isNull()
|
assertThat(sut.getWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -157,11 +166,13 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
getUrlLambda = getUrlLambda,
|
getUrlLambda = getUrlLambda,
|
||||||
)
|
)
|
||||||
assertThat(sut.getElementWellKnown()).isEqualTo(
|
assertThat(sut.getElementWellKnown()).isEqualTo(
|
||||||
ElementWellKnown(
|
WellknownRetrieverResult.Success(
|
||||||
registrationHelperUrl = null,
|
ElementWellKnown(
|
||||||
enforceElementPro = null,
|
registrationHelperUrl = null,
|
||||||
rageshakeUrl = null,
|
enforceElementPro = null,
|
||||||
brandColor = null,
|
rageshakeUrl = null,
|
||||||
|
brandColor = null,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
getUrlLambda.assertions().isCalledOnce()
|
getUrlLambda.assertions().isCalledOnce()
|
||||||
|
|
@ -183,11 +194,13 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assertThat(sut.getElementWellKnown()).isEqualTo(
|
assertThat(sut.getElementWellKnown()).isEqualTo(
|
||||||
ElementWellKnown(
|
WellknownRetrieverResult.Success(
|
||||||
registrationHelperUrl = "a_registration_url",
|
ElementWellKnown(
|
||||||
enforceElementPro = true,
|
registrationHelperUrl = "a_registration_url",
|
||||||
rageshakeUrl = "a_rageshake_url",
|
enforceElementPro = true,
|
||||||
brandColor = "#FF0000",
|
rageshakeUrl = "a_rageshake_url",
|
||||||
|
brandColor = "#FF0000",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -207,11 +220,13 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
assertThat(sut.getElementWellKnown()).isEqualTo(
|
assertThat(sut.getElementWellKnown()).isEqualTo(
|
||||||
ElementWellKnown(
|
WellknownRetrieverResult.Success(
|
||||||
registrationHelperUrl = "a_registration_url",
|
ElementWellKnown(
|
||||||
enforceElementPro = true,
|
registrationHelperUrl = "a_registration_url",
|
||||||
rageshakeUrl = "a_rageshake_url",
|
enforceElementPro = true,
|
||||||
brandColor = null,
|
rageshakeUrl = "a_rageshake_url",
|
||||||
|
brandColor = null,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -228,7 +243,7 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assertThat(sut.getElementWellKnown()).isNull()
|
assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -238,7 +253,7 @@ class DefaultSessionWellknownRetrieverTest {
|
||||||
Result.failure(AN_EXCEPTION)
|
Result.failure(AN_EXCEPTION)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
assertThat(sut.getElementWellKnown()).isNull()
|
assertThat(sut.getElementWellKnown()).isInstanceOf(WellknownRetrieverResult.Error::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDefaultSessionWellknownRetriever(
|
private fun createDefaultSessionWellknownRetriever(
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,18 @@ package io.element.android.features.wellknown.test
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
import io.element.android.libraries.wellknown.api.SessionWellknownRetriever
|
||||||
import io.element.android.libraries.wellknown.api.WellKnown
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
class FakeSessionWellknownRetriever(
|
class FakeSessionWellknownRetriever(
|
||||||
private val getWellKnownResult: () -> WellKnown? = { null },
|
private val getWellKnownResult: () -> WellknownRetrieverResult<WellKnown> = { WellknownRetrieverResult.NotFound },
|
||||||
private val getElementWellKnownResult: () -> ElementWellKnown? = { null },
|
private val getElementWellKnownResult: () -> WellknownRetrieverResult<ElementWellKnown> = { WellknownRetrieverResult.NotFound },
|
||||||
) : SessionWellknownRetriever {
|
) : SessionWellknownRetriever {
|
||||||
override suspend fun getWellKnown(): WellKnown? = simulateLongTask {
|
override suspend fun getWellKnown(): WellknownRetrieverResult<WellKnown> = simulateLongTask {
|
||||||
getWellKnownResult()
|
getWellKnownResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getElementWellKnown(): ElementWellKnown? = simulateLongTask {
|
override suspend fun getElementWellKnown(): WellknownRetrieverResult<ElementWellKnown> = simulateLongTask {
|
||||||
getElementWellKnownResult()
|
getElementWellKnownResult()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,18 @@ package io.element.android.features.wellknown.test
|
||||||
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
import io.element.android.libraries.wellknown.api.ElementWellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellKnown
|
import io.element.android.libraries.wellknown.api.WellKnown
|
||||||
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
import io.element.android.libraries.wellknown.api.WellknownRetriever
|
||||||
|
import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
|
||||||
import io.element.android.tests.testutils.simulateLongTask
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
class FakeWellknownRetriever(
|
class FakeWellknownRetriever(
|
||||||
private val getWellKnownResult: (String) -> WellKnown? = { null },
|
private val getWellKnownResult: (String) -> WellknownRetrieverResult<WellKnown> = { WellknownRetrieverResult.NotFound },
|
||||||
private val getElementWellKnownResult: (String) -> ElementWellKnown? = { null },
|
private val getElementWellKnownResult: (String) -> WellknownRetrieverResult<ElementWellKnown> = { WellknownRetrieverResult.NotFound },
|
||||||
) : WellknownRetriever {
|
) : WellknownRetriever {
|
||||||
override suspend fun getWellKnown(baseUrl: String): WellKnown? = simulateLongTask {
|
override suspend fun getWellKnown(baseUrl: String): WellknownRetrieverResult<WellKnown> = simulateLongTask {
|
||||||
getWellKnownResult(baseUrl)
|
getWellKnownResult(baseUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getElementWellKnown(baseUrl: String): ElementWellKnown? = simulateLongTask {
|
override suspend fun getElementWellKnown(baseUrl: String): WellknownRetrieverResult<ElementWellKnown> = simulateLongTask {
|
||||||
getElementWellKnownResult(baseUrl)
|
getElementWellKnownResult(baseUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,6 @@ class KonsistPreviewTest {
|
||||||
"FocusedEventPreview",
|
"FocusedEventPreview",
|
||||||
"GradientFloatingActionButtonCircleShapePreview",
|
"GradientFloatingActionButtonCircleShapePreview",
|
||||||
"HeaderFooterPageScrollablePreview",
|
"HeaderFooterPageScrollablePreview",
|
||||||
"IconsCompoundPreview",
|
|
||||||
"IconsOtherPreview",
|
"IconsOtherPreview",
|
||||||
"MarkdownTextComposerEditPreview",
|
"MarkdownTextComposerEditPreview",
|
||||||
"MatrixBadgeAtomInfoPreview",
|
"MatrixBadgeAtomInfoPreview",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d627cd374fe0746d2fc84fffecf6a1636fb68784ed5edae3b830fe4ee2b3751a
|
oid sha256:9807bf17918c39f2e6e4cddf87237fa26f63732f48210b0f803114bcf4c98451
|
||||||
size 61398
|
size 61360
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:1ad13a47522e6365928e7209e515a47a7e4fafc28ad87361dffdf92c95b3ff06
|
||||||
|
size 62563
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:70979d3b81a521b6208da2f3a95187bc7b3ae0ea52e3dd35e2bbd91df14c007f
|
oid sha256:ade5d824f6f466960931abcbc86cb9a286cbf6bd9a3adf875f446dc094e291d5
|
||||||
size 63318
|
size 63393
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f2cfda5b044d11e4e75fa83c9bad7b7f03a328422881cbededfaee8ab091d1db
|
||||||
|
size 64586
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5c68e00a3f0697a9f179fe8541876fbd371da1d8b55e72c02fbb92d64814863a
|
|
||||||
size 67653
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:0332e0bbea7856b02adb7a660c758a313235351891428fdfb1816d0fa26df4d6
|
|
||||||
size 68540
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:4c798bec96147f50016dad808d51de76c24339a8b9afe0119993dadd7dbff845
|
|
||||||
size 73047
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:5cf492cd78477c2ce4c296fb6baa2af8baed6549fb51f58c06da9d4181aa8ac9
|
|
||||||
size 73796
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:a3ab14628f86c883effa49f51c02255a96a0050a9b4261322b2cf596ed5298ed
|
|
||||||
size 73233
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f2151c4bbd49af17c8af90b946c94ec40b3f51edd25157abc5e87e1b4f7b7922
|
|
||||||
size 53698
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:50113ec351ed12a7ee31b5b9333dabbcce242bf762d9d09d41301484e04f33ff
|
|
||||||
size 64549
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:78f6f70dbc14f5b035f6f7d3cc042328be0894771443f188c9bf5581e40bcf59
|
|
||||||
size 65190
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:cab2c2d5b07a795080fc887c6c8776a9a8630d9b2f6e5fde7ebb9f5f462fc41c
|
|
||||||
size 69708
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:ffb53277e999c57096a8d11a03b2835d30ba7f0e8b83db85d5d1a417bbe35502
|
|
||||||
size 70680
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:d21b4e71d6d52f1e9d844dbff96dcec86f5a04871b861ce0b619f173edca66cd
|
|
||||||
size 69772
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
|
||||||
oid sha256:f209a674db2371d1e85ec2b6d92468f92595f6b87948db19dd7aba8432221b67
|
|
||||||
size 51793
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e71f4f5c0d8f299ff91699612604f5b285160f5830a2b1e3e0c4092e85bd8aa7
|
oid sha256:49f3e81d0a713630723c463c8705463e1ba04266624f71d6e29cd7d3a693c115
|
||||||
size 14064
|
size 13789
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e526b48b8f340e5e5c31e0e976286a5d64f27928d3cdf00b3175e5840bc494f3
|
oid sha256:76ecf55d5a354374db7b9ac14fb64fae7637b35b2908efcf73163b514710fcb0
|
||||||
size 13282
|
size 13127
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue