Promote "history sharing on invite" out of developer options (#6647)

* Enable history sharing by default, unconditionally

* Remove feature-flag dep from history viz icons in room header

* Remove feature-flag dep from warning on inviting new people

* Remove feature-flag dep from warning on starting chat with new people

* Remove `enableKeyShareOnInvite` feature flag

* Update screenshots

* Remove redundant `FakeFeatureFlagService()` invocation, per review comment

---------

Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
Richard van der Hoff 2026-04-24 11:52:21 +01:00 committed by GitHub
parent b17388d686
commit e961214c4d
63 changed files with 106 additions and 206 deletions

View file

@ -13,7 +13,6 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -37,8 +36,6 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope import io.element.android.libraries.di.annotations.SessionCoroutineScope
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.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
@ -74,7 +71,6 @@ class DefaultInvitePeoplePresenter(
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
@SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope, @SessionCoroutineScope private val sessionCoroutineScope: CoroutineScope,
private val appErrorStateService: AppErrorStateService, private val appErrorStateService: AppErrorStateService,
private val featureFlagService: FeatureFlagService,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
) : InvitePeoplePresenter { ) : InvitePeoplePresenter {
@AssistedFactory @AssistedFactory
@ -93,8 +89,6 @@ class DefaultInvitePeoplePresenter(
val showSearchLoader = rememberSaveable { mutableStateOf(false) } val showSearchLoader = rememberSaveable { mutableStateOf(false) }
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) } val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
val recentDirectRooms by produceState(emptyList(), roomMembers.value) { val recentDirectRooms by produceState(emptyList(), roomMembers.value) {
if (roomMembers.value.isSuccess()) { if (roomMembers.value.isSuccess()) {
val activeMemberIds = roomMembers.value.dataOrNull().orEmpty() val activeMemberIds = roomMembers.value.dataOrNull().orEmpty()
@ -137,12 +131,7 @@ class DefaultInvitePeoplePresenter(
val selectedUserIdentities = produceState( val selectedUserIdentities = produceState(
emptyMap<MatrixUser, IdentityState?>().toImmutableMap(), emptyMap<MatrixUser, IdentityState?>().toImmutableMap(),
selectedUsers.value, selectedUsers.value,
enableKeyShareOnInvite,
) { ) {
if (!enableKeyShareOnInvite) {
return@produceState
}
val selected = selectedUsers.value val selected = selectedUsers.value
val cached = value val cached = value
@ -213,7 +202,7 @@ class DefaultInvitePeoplePresenter(
} }
} }
is InvitePeopleEvents.SendInvites -> { is InvitePeopleEvents.SendInvites -> {
if (enableKeyShareOnInvite && unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) { if (unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) {
sendInvitesAction.value = ConfirmingUnknownUserInvitation( sendInvitesAction.value = ConfirmingUnknownUserInvitation(
unknownUsers unknownUsers
) )

View file

@ -15,9 +15,6 @@ import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
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.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -405,10 +402,14 @@ internal class DefaultInvitePeoplePresenterTest {
val inviteUserResult = lambdaRecorder<UserId, Result<Unit>> { userId: UserId -> val inviteUserResult = lambdaRecorder<UserId, Result<Unit>> { userId: UserId ->
Result.success(Unit) Result.success(Unit)
} }
val encryptionService = FakeEncryptionService(
getUserIdentityResult = { _ -> Result.success(null) },
)
val presenter = createDefaultInvitePeoplePresenter( val presenter = createDefaultInvitePeoplePresenter(
userRepository = repository, userRepository = repository,
inviteUserResult = inviteUserResult, inviteUserResult = inviteUserResult,
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClient = FakeMatrixClient(encryptionService = encryptionService),
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -451,13 +452,18 @@ internal class DefaultInvitePeoplePresenterTest {
Result.failure(AN_EXCEPTION) Result.failure(AN_EXCEPTION)
} }
val showErrorResResult = lambdaRecorder<Int, Int, Unit> { _, _ -> } val showErrorResResult = lambdaRecorder<Int, Int, Unit> { _, _ -> }
val encryptionService = FakeEncryptionService(
getUserIdentityResult = { _ -> Result.success(null) },
)
val presenter = createDefaultInvitePeoplePresenter( val presenter = createDefaultInvitePeoplePresenter(
userRepository = repository, userRepository = repository,
inviteUserResult = inviteUserResult, inviteUserResult = inviteUserResult,
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
appErrorStateService = FakeAppErrorStateService( appErrorStateService = FakeAppErrorStateService(
showErrorResResult = showErrorResResult, showErrorResResult = showErrorResResult,
) ),
matrixClient = FakeMatrixClient(encryptionService = encryptionService),
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -632,15 +638,11 @@ internal class DefaultInvitePeoplePresenterTest {
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult getUserIdentityResult = getUserIdentityResult
) )
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter( val presenter = createDefaultInvitePeoplePresenter(
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
inviteUserResult = inviteUserResult, inviteUserResult = inviteUserResult,
matrixClient = FakeMatrixClient(encryptionService = encryptionService), matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -703,15 +705,11 @@ internal class DefaultInvitePeoplePresenterTest {
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult getUserIdentityResult = getUserIdentityResult
) )
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter( val presenter = createDefaultInvitePeoplePresenter(
userRepository = repository, userRepository = repository,
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClient = FakeMatrixClient(encryptionService = encryptionService), matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
) )
presenter.test { presenter.test {
val initialState = awaitItemAsDefault() val initialState = awaitItemAsDefault()
@ -790,14 +788,10 @@ internal class DefaultInvitePeoplePresenterTest {
val encryptionService = FakeEncryptionService( val encryptionService = FakeEncryptionService(
getUserIdentityResult = getUserIdentityResult getUserIdentityResult = getUserIdentityResult
) )
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val presenter = createDefaultInvitePeoplePresenter( val presenter = createDefaultInvitePeoplePresenter(
coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true),
matrixClient = FakeMatrixClient(encryptionService = encryptionService), matrixClient = FakeMatrixClient(encryptionService = encryptionService),
featureFlagService = featureFlagService
) )
presenter.test { presenter.test {
val initialState = awaitItem() val initialState = awaitItem()
@ -878,7 +872,6 @@ fun TestScope.createDefaultInvitePeoplePresenter(
userRepository: UserRepository = FakeUserRepository(), userRepository: UserRepository = FakeUserRepository(),
coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(),
appErrorStateService: AppErrorStateService = FakeAppErrorStateService(), appErrorStateService: AppErrorStateService = FakeAppErrorStateService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
): DefaultInvitePeoplePresenter { ): DefaultInvitePeoplePresenter {
return DefaultInvitePeoplePresenter( return DefaultInvitePeoplePresenter(
@ -888,7 +881,6 @@ fun TestScope.createDefaultInvitePeoplePresenter(
coroutineDispatchers = coroutineDispatchers, coroutineDispatchers = coroutineDispatchers,
sessionCoroutineScope = backgroundScope, sessionCoroutineScope = backgroundScope,
appErrorStateService = appErrorStateService, appErrorStateService = appErrorStateService,
featureFlagService = featureFlagService,
matrixClient = matrixClient, matrixClient = matrixClient,
) )
} }

View file

@ -217,12 +217,10 @@ class MessagesPresenter(
val dmRoomMember by room.getDirectRoomMember(membersState) val dmRoomMember by room.getDirectRoomMember(membersState)
val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges val roomMemberIdentityStateChanges = identityChangeState.roomMemberIdentityStateChanges
val isKeyShareOnInviteEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
// The top bar should show a "history" icon if: // The top bar should show a "history" icon if:
// * History sharing is enabled,
// * The room is encrypted, and: // * The room is encrypted, and:
// * The room's history_visibility allows future users to see content. // * The room's history_visibility allows future users to see content.
val topBarSharedHistoryIcon = if (isKeyShareOnInviteEnabled) roomInfo.sharedHistoryIcon() else SharedHistoryIcon.NONE val topBarSharedHistoryIcon = roomInfo.sharedHistoryIcon()
LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) { LifecycleResumeEffect(dmRoomMember, roomInfo.isEncrypted) {
if (roomInfo.isEncrypted == true) { if (roomInfo.isEncrypted == true) {

View file

@ -1228,9 +1228,6 @@ class MessagesPresenterTest {
initialRoomInfo = aRoomInfo(isEncrypted = true, historyVisibility = RoomHistoryVisibility.Shared), initialRoomInfo = aRoomInfo(isEncrypted = true, historyVisibility = RoomHistoryVisibility.Shared),
), ),
), ),
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.EnableKeyShareOnInvite.key to true)
)
) )
presenter.testWithLifecycleOwner { presenter.testWithLifecycleOwner {
awaitItem() awaitItem()
@ -1249,9 +1246,6 @@ class MessagesPresenterTest {
initialRoomInfo = aRoomInfo(isEncrypted = true, historyVisibility = RoomHistoryVisibility.WorldReadable), initialRoomInfo = aRoomInfo(isEncrypted = true, historyVisibility = RoomHistoryVisibility.WorldReadable),
), ),
), ),
featureFlagService = FakeFeatureFlagService(
initialState = mapOf(FeatureFlags.EnableKeyShareOnInvite.key to true)
)
) )
presenter.testWithLifecycleOwner { presenter.testWithLifecycleOwner {
awaitItem() awaitItem()

View file

@ -168,8 +168,6 @@ class RoomDetailsPresenter(
val canReportRoom by produceState(false) { value = client.canReportRoom() } val canReportRoom by produceState(false) { value = client.canReportRoom() }
val enableKeyShareOnInvite by featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(initial = false)
return RoomDetailsState( return RoomDetailsState(
roomId = room.roomId, roomId = room.roomId,
roomName = roomName, roomName = roomName,
@ -199,7 +197,6 @@ class RoomDetailsPresenter(
isTombstoned = roomInfo.successorRoom != null, isTombstoned = roomInfo.successorRoom != null,
showDebugInfo = isDeveloperModeEnabled, showDebugInfo = isDeveloperModeEnabled,
roomVersion = roomInfo.roomVersion, roomVersion = roomInfo.roomVersion,
enableKeyShareOnInvite = enableKeyShareOnInvite,
roomHistoryVisibility = roomInfo.historyVisibility, roomHistoryVisibility = roomInfo.historyVisibility,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )

View file

@ -51,7 +51,6 @@ data class RoomDetailsState(
val isTombstoned: Boolean, val isTombstoned: Boolean,
val showDebugInfo: Boolean, val showDebugInfo: Boolean,
val roomVersion: String?, val roomVersion: String?,
val enableKeyShareOnInvite: Boolean,
val roomHistoryVisibility: RoomHistoryVisibility, val roomHistoryVisibility: RoomHistoryVisibility,
val eventSink: (RoomDetailsEvent) -> Unit val eventSink: (RoomDetailsEvent) -> Unit
) { ) {
@ -64,7 +63,7 @@ data class RoomDetailsState(
if (isPublic) { if (isPublic) {
add(RoomBadge.PUBLIC) add(RoomBadge.PUBLIC)
} }
if (enableKeyShareOnInvite && isEncrypted) { if (isEncrypted) {
when (roomHistoryVisibility) { when (roomHistoryVisibility) {
RoomHistoryVisibility.Invited, RoomHistoryVisibility.Joined -> add(RoomBadge.SHARED_HISTORY_HIDDEN) RoomHistoryVisibility.Invited, RoomHistoryVisibility.Joined -> add(RoomBadge.SHARED_HISTORY_HIDDEN)
RoomHistoryVisibility.Shared -> add(RoomBadge.SHARED_HISTORY_SHARED) RoomHistoryVisibility.Shared -> add(RoomBadge.SHARED_HISTORY_SHARED)

View file

@ -121,7 +121,6 @@ fun aRoomDetailsState(
canReportRoom: Boolean = true, canReportRoom: Boolean = true,
isTombstoned: Boolean = false, isTombstoned: Boolean = false,
showDebugInfo: Boolean = false, showDebugInfo: Boolean = false,
enableKeyShareOnInvite: Boolean = false,
roomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Shared, roomHistoryVisibility: RoomHistoryVisibility = RoomHistoryVisibility.Shared,
eventSink: (RoomDetailsEvent) -> Unit = {}, eventSink: (RoomDetailsEvent) -> Unit = {},
) = RoomDetailsState( ) = RoomDetailsState(
@ -153,7 +152,6 @@ fun aRoomDetailsState(
isTombstoned = isTombstoned, isTombstoned = isTombstoned,
showDebugInfo = showDebugInfo, showDebugInfo = showDebugInfo,
roomVersion = "12", roomVersion = "12",
enableKeyShareOnInvite = enableKeyShareOnInvite,
roomHistoryVisibility = roomHistoryVisibility, roomHistoryVisibility = roomHistoryVisibility,
eventSink = eventSink, eventSink = eventSink,
) )
@ -195,6 +193,5 @@ fun aSharedHistoryRoomDetailsState(
roomHistoryVisibility: RoomHistoryVisibility roomHistoryVisibility: RoomHistoryVisibility
) = aRoomDetailsState( ) = aRoomDetailsState(
isEncrypted = true, isEncrypted = true,
enableKeyShareOnInvite = true,
roomHistoryVisibility = roomHistoryVisibility, roomHistoryVisibility = roomHistoryVisibility,
) )

View file

@ -37,24 +37,24 @@ class RoomDetailsStateTest {
} }
@Test @Test
fun `room public encrypted should have encrypted and public badges`() { fun `room public encrypted should have encrypted, public, and history sharing shared badges`() {
val sut = aRoomDetailsState( val sut = aRoomDetailsState(
isPublic = true, isPublic = true,
isEncrypted = true, isEncrypted = true,
) )
assertThat(sut.roomBadges).isEqualTo( assertThat(sut.roomBadges).isEqualTo(
persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC) persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC, RoomBadge.SHARED_HISTORY_SHARED)
) )
} }
@Test @Test
fun `room not public encrypted should have encrypted badges`() { fun `room not public encrypted should have encrypted and history sharing shared badges`() {
val sut = aRoomDetailsState( val sut = aRoomDetailsState(
isPublic = false, isPublic = false,
isEncrypted = true, isEncrypted = true,
) )
assertThat(sut.roomBadges).isEqualTo( assertThat(sut.roomBadges).isEqualTo(
persistentListOf(RoomBadge.ENCRYPTED) persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.SHARED_HISTORY_SHARED)
) )
} }
@ -62,7 +62,6 @@ class RoomDetailsStateTest {
fun `room public not encrypted should not have history sharing badges`() { fun `room public not encrypted should not have history sharing badges`() {
val sut = aRoomDetailsState( val sut = aRoomDetailsState(
isEncrypted = false, isEncrypted = false,
enableKeyShareOnInvite = true,
roomHistoryVisibility = RoomHistoryVisibility.Shared roomHistoryVisibility = RoomHistoryVisibility.Shared
) )
assertThat(sut.roomBadges).isEqualTo( assertThat(sut.roomBadges).isEqualTo(
@ -74,7 +73,6 @@ class RoomDetailsStateTest {
fun `room public encrypted should have history sharing hidden badge`() { fun `room public encrypted should have history sharing hidden badge`() {
val sut = aRoomDetailsState( val sut = aRoomDetailsState(
isEncrypted = true, isEncrypted = true,
enableKeyShareOnInvite = true,
roomHistoryVisibility = RoomHistoryVisibility.Joined roomHistoryVisibility = RoomHistoryVisibility.Joined
) )
assertThat(sut.roomBadges).isEqualTo( assertThat(sut.roomBadges).isEqualTo(
@ -83,22 +81,9 @@ class RoomDetailsStateTest {
} }
@Test @Test
fun `room public encrypted should have history sharing shared badge`() { fun `room public encrypted with world_readable visibility should have history sharing world_readable badge`() {
val sut = aRoomDetailsState( val sut = aRoomDetailsState(
isEncrypted = true, isEncrypted = true,
enableKeyShareOnInvite = true,
roomHistoryVisibility = RoomHistoryVisibility.Shared
)
assertThat(sut.roomBadges).isEqualTo(
persistentListOf(RoomBadge.ENCRYPTED, RoomBadge.PUBLIC, RoomBadge.SHARED_HISTORY_SHARED)
)
}
@Test
fun `room public encrypted should have history sharing world_readable badge`() {
val sut = aRoomDetailsState(
isEncrypted = true,
enableKeyShareOnInvite = true,
roomHistoryVisibility = RoomHistoryVisibility.WorldReadable roomHistoryVisibility = RoomHistoryVisibility.WorldReadable
) )
assertThat(sut.roomBadges).isEqualTo( assertThat(sut.roomBadges).isEqualTo(

View file

@ -15,8 +15,6 @@ import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.features.startchat.api.StartDMAction import io.element.android.features.startchat.api.StartDMAction
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
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.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.StartDMResult import io.element.android.libraries.matrix.api.room.StartDMResult
@ -28,7 +26,6 @@ import io.element.android.services.analytics.api.AnalyticsService
class DefaultStartDMAction( class DefaultStartDMAction(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val featureFlagService: FeatureFlagService,
) : StartDMAction { ) : StartDMAction {
override suspend fun execute( override suspend fun execute(
matrixUser: MatrixUser, matrixUser: MatrixUser,
@ -50,7 +47,7 @@ class DefaultStartDMAction(
val identityState = matrixClient.encryptionService.getUserIdentity(matrixUser.userId, fallbackToServer = false).getOrNull() val identityState = matrixClient.encryptionService.getUserIdentity(matrixUser.userId, fallbackToServer = false).getOrNull()
actionState.value = ConfirmingStartDmWithMatrixUser( actionState.value = ConfirmingStartDmWithMatrixUser(
matrixUser = matrixUser, matrixUser = matrixUser,
isUserIdentityUnknown = featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite) && identityState == null isUserIdentityUnknown = identityState == null
) )
} }
} }

View file

@ -58,8 +58,6 @@ class StartChatPresenter(
featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch) featureFlagService.isFeatureEnabledFlow(FeatureFlags.RoomDirectorySearch)
}.collectAsState(initial = false) }.collectAsState(initial = false)
val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false)
fun handleEvent(event: StartChatEvents) { fun handleEvent(event: StartChatEvents) {
when (event) { when (event) {
is StartChatEvents.StartDM -> localCoroutineScope.launch { is StartChatEvents.StartDM -> localCoroutineScope.launch {
@ -78,7 +76,6 @@ class StartChatPresenter(
userListState = userListState, userListState = userListState,
startDmAction = startDmActionState.value, startDmAction = startDmActionState.value,
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
enableKeyShareOnInvite = enableKeyShareOnInvite.value,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }

View file

@ -17,6 +17,5 @@ data class StartChatState(
val userListState: UserListState, val userListState: UserListState,
val startDmAction: AsyncAction<RoomId>, val startDmAction: AsyncAction<RoomId>,
val isRoomDirectorySearchEnabled: Boolean, val isRoomDirectorySearchEnabled: Boolean,
val enableKeyShareOnInvite: Boolean,
val eventSink: (StartChatEvents) -> Unit, val eventSink: (StartChatEvents) -> Unit,
) )

View file

@ -82,6 +82,5 @@ fun aCreateRoomRootState(
userListState = userListState, userListState = userListState,
startDmAction = startDmAction, startDmAction = startDmAction,
isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled, isRoomDirectorySearchEnabled = isRoomDirectorySearchEnabled,
enableKeyShareOnInvite = false,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -130,7 +130,6 @@ fun StartChatView(
if (data is ConfirmingStartDmWithMatrixUser) { if (data is ConfirmingStartDmWithMatrixUser) {
CreateDmConfirmationBottomSheet( CreateDmConfirmationBottomSheet(
matrixUser = data.matrixUser, matrixUser = data.matrixUser,
enableKeyShareOnInvite = state.enableKeyShareOnInvite,
isUserIdentityUnknown = data.isUserIdentityUnknown, isUserIdentityUnknown = data.isUserIdentityUnknown,
onSendInvite = { onSendInvite = {
state.eventSink(StartChatEvents.StartDM(data.matrixUser)) state.eventSink(StartChatEvents.StartDM(data.matrixUser))

View file

@ -13,9 +13,6 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser import io.element.android.features.startchat.api.ConfirmingStartDmWithMatrixUser
import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncAction
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.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -88,7 +85,7 @@ class DefaultStartDMActionTest {
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized) val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
val matrixUser = aMatrixUser() val matrixUser = aMatrixUser()
action.execute(matrixUser, false, state) action.execute(matrixUser, false, state)
assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = false)) assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(matrixUser, isUserIdentityUnknown = true))
assertThat(analyticsService.capturedEvents).isEmpty() assertThat(analyticsService.capturedEvents).isEmpty()
} }
@ -107,37 +104,31 @@ class DefaultStartDMActionTest {
} }
@Test @Test
fun `when history sharing enabled, user identity fetched and identity unknown`() = runTest { fun `when user identity fetched and identity unknown`() = runTest {
val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { _ -> Result.success(null) } val getUserIdentityResult = lambdaRecorder<UserId, Result<IdentityState?>> { _ -> Result.success(null) }
val encryptionService = FakeEncryptionService(getUserIdentityResult = getUserIdentityResult) val encryptionService = FakeEncryptionService(getUserIdentityResult = getUserIdentityResult)
val matrixClient = FakeMatrixClient(encryptionService = encryptionService).apply { val matrixClient = FakeMatrixClient(encryptionService = encryptionService).apply {
givenFindDmResult(Result.success(null)) givenFindDmResult(Result.success(null))
} }
val featureFlagService = FakeFeatureFlagService().apply {
setFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite, true)
}
val action = createStartDMAction( val action = createStartDMAction(
matrixClient = matrixClient, matrixClient = matrixClient,
featureFlagService = featureFlagService
) )
val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized) val state = mutableStateOf<AsyncAction<RoomId>>(AsyncAction.Uninitialized)
action.execute(aMatrixUser(), false, state) action.execute(aMatrixUser(), false, state)
assertThat(getUserIdentityResult.assertions().isCalledOnce()) getUserIdentityResult.assertions().isCalledOnce()
assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = true)) assertThat(state.value).isEqualTo(ConfirmingStartDmWithMatrixUser(aMatrixUser(), isUserIdentityUnknown = true))
} }
private fun createStartDMAction( private fun createStartDMAction(
matrixClient: MatrixClient = FakeMatrixClient(), matrixClient: MatrixClient = FakeMatrixClient(),
analyticsService: AnalyticsService = FakeAnalyticsService(), analyticsService: AnalyticsService = FakeAnalyticsService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService()
): DefaultStartDMAction { ): DefaultStartDMAction {
return DefaultStartDMAction( return DefaultStartDMAction(
matrixClient = matrixClient, matrixClient = matrixClient,
analyticsService = analyticsService, analyticsService = analyticsService,
featureFlagService = featureFlagService,
) )
} }
} }

View file

@ -26,7 +26,6 @@ data class UserProfileState(
val dmRoomId: RoomId?, val dmRoomId: RoomId?,
val canCall: Boolean, val canCall: Boolean,
val snackbarMessage: SnackbarMessage?, val snackbarMessage: SnackbarMessage?,
val enableKeyShareOnInvite: Boolean,
val eventSink: (UserProfileEvents) -> Unit val eventSink: (UserProfileEvents) -> Unit
) { ) {
enum class ConfirmationDialog { enum class ConfirmationDialog {

View file

@ -12,7 +12,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
@ -32,8 +31,6 @@ 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.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.bool.orFalse
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.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -53,7 +50,6 @@ class UserProfilePresenter(
private val client: MatrixClient, private val client: MatrixClient,
private val startDMAction: StartDMAction, private val startDMAction: StartDMAction,
private val sessionEnterpriseService: SessionEnterpriseService, private val sessionEnterpriseService: SessionEnterpriseService,
private val featureFlagService: FeatureFlagService,
) : Presenter<UserProfileState> { ) : Presenter<UserProfileState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -105,8 +101,6 @@ class UserProfilePresenter(
} }
val userProfile by produceState<MatrixUser?>(null) { value = client.getProfile(userId).getOrNull() } val userProfile by produceState<MatrixUser?>(null) { value = client.getProfile(userId).getOrNull() }
val enableKeyShareOnInvite = featureFlagService.isFeatureEnabledFlow(FeatureFlags.EnableKeyShareOnInvite).collectAsState(false)
fun handleEvent(event: UserProfileEvents) { fun handleEvent(event: UserProfileEvents) {
when (event) { when (event) {
is UserProfileEvents.BlockUser -> { is UserProfileEvents.BlockUser -> {
@ -159,7 +153,6 @@ class UserProfilePresenter(
dmRoomId = dmRoomId, dmRoomId = dmRoomId,
canCall = canCall, canCall = canCall,
snackbarMessage = null, snackbarMessage = null,
enableKeyShareOnInvite = enableKeyShareOnInvite.value,
eventSink = ::handleEvent, eventSink = ::handleEvent,
) )
} }

View file

@ -24,7 +24,6 @@ import io.element.android.features.userprofile.api.UserProfileVerificationState
import io.element.android.features.userprofile.impl.root.UserProfilePresenter import io.element.android.features.userprofile.impl.root.UserProfilePresenter
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.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
@ -415,7 +414,6 @@ class UserProfilePresenterTest {
sessionEnterpriseService = FakeSessionEnterpriseService( sessionEnterpriseService = FakeSessionEnterpriseService(
isElementCallAvailableResult = { isElementCallAvailable }, isElementCallAvailableResult = { isElementCallAvailable },
), ),
featureFlagService = FakeFeatureFlagService()
) )
} }
} }

View file

@ -61,6 +61,5 @@ fun aUserProfileState(
dmRoomId = dmRoomId, dmRoomId = dmRoomId,
canCall = canCall, canCall = canCall,
snackbarMessage = snackbarMessage, snackbarMessage = snackbarMessage,
enableKeyShareOnInvite = false,
eventSink = eventSink, eventSink = eventSink,
) )

View file

@ -114,7 +114,6 @@ fun UserProfileView(
if (data is ConfirmingStartDmWithMatrixUser) { if (data is ConfirmingStartDmWithMatrixUser) {
CreateDmConfirmationBottomSheet( CreateDmConfirmationBottomSheet(
matrixUser = data.matrixUser, matrixUser = data.matrixUser,
enableKeyShareOnInvite = state.enableKeyShareOnInvite,
isUserIdentityUnknown = data.isUserIdentityUnknown, isUserIdentityUnknown = data.isUserIdentityUnknown,
onSendInvite = { onSendInvite = {
state.eventSink(UserProfileEvents.StartDM) state.eventSink(UserProfileEvents.StartDM)

View file

@ -52,17 +52,6 @@ enum class FeatureFlags(
defaultValue = { false }, defaultValue = { false },
isFinished = false, isFinished = false,
), ),
EnableKeyShareOnInvite(
key = "feature.enableKeyShareOnInvite",
title = "Share encrypted history with new members",
description = "When inviting a user to an encrypted room that has history visibility set to \"shared\"," +
" share encrypted history with that user, and accept encrypted history when you are invited to such a room." +
"\nRequires an app restart to take effect." +
"\n\nWARNING: this feature is EXPERIMENTAL and not all security precautions are implemented." +
" Do not enable on production accounts.",
defaultValue = { false },
isFinished = false,
),
Knock( Knock(
key = "feature.knock", key = "feature.knock",
title = "Ask to join", title = "Ask to join",

View file

@ -167,7 +167,7 @@ class RustMatrixClientFactory(
} }
) )
) )
.enableShareHistoryOnInvite(featureFlagService.isFeatureEnabled(FeatureFlags.EnableKeyShareOnInvite)) .enableShareHistoryOnInvite(true)
.threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false) .threadsEnabled(featureFlagService.isFeatureEnabled(FeatureFlags.Threads), threadSubscriptions = false)
.requestConfig( .requestConfig(
RequestConfig( RequestConfig(

View file

@ -54,18 +54,17 @@ import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun CreateDmConfirmationBottomSheet( fun CreateDmConfirmationBottomSheet(
matrixUser: MatrixUser, matrixUser: MatrixUser,
enableKeyShareOnInvite: Boolean,
isUserIdentityUnknown: Boolean, isUserIdentityUnknown: Boolean,
onSendInvite: () -> Unit, onSendInvite: () -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val titleContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) { val titleContent = if (isUserIdentityUnknown) {
stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_title) stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_title)
} else { } else {
stringResource(R.string.screen_bottom_sheet_create_dm_title) stringResource(R.string.screen_bottom_sheet_create_dm_title)
} }
val descriptionContent = if (enableKeyShareOnInvite && isUserIdentityUnknown) { val descriptionContent = if (isUserIdentityUnknown) {
stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_content) stringResource(R.string.screen_bottom_sheet_create_dm_unknown_user_content)
} else { } else {
stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName()) stringResource(R.string.screen_bottom_sheet_create_dm_message, matrixUser.getFullName())
@ -154,7 +153,6 @@ internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter(
) state: CreateDmConfirmationBottomSheetState) = ElementPreview { ) state: CreateDmConfirmationBottomSheetState) = ElementPreview {
CreateDmConfirmationBottomSheet( CreateDmConfirmationBottomSheet(
matrixUser = state.matrixUser, matrixUser = state.matrixUser,
enableKeyShareOnInvite = state.enableKeyShareOnInvite,
isUserIdentityUnknown = state.isUserIdentityUnknown, isUserIdentityUnknown = state.isUserIdentityUnknown,
onSendInvite = {}, onSendInvite = {},
onDismiss = {}, onDismiss = {},
@ -163,14 +161,12 @@ internal fun CreateDmConfirmationBottomSheetPreview(@PreviewParameter(
data class CreateDmConfirmationBottomSheetState( data class CreateDmConfirmationBottomSheetState(
val matrixUser: MatrixUser, val matrixUser: MatrixUser,
val enableKeyShareOnInvite: Boolean,
val isUserIdentityUnknown: Boolean, val isUserIdentityUnknown: Boolean,
) )
class CreateDmConfirmationBottomSheetStateProvider : PreviewParameterProvider<CreateDmConfirmationBottomSheetState> { class CreateDmConfirmationBottomSheetStateProvider : PreviewParameterProvider<CreateDmConfirmationBottomSheetState> {
override val values = sequenceOf( override val values = sequenceOf(
CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = false, isUserIdentityUnknown = false), CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), isUserIdentityUnknown = false),
CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = false), CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), isUserIdentityUnknown = true),
CreateDmConfirmationBottomSheetState(matrixUser = aMatrixUser(), enableKeyShareOnInvite = true, isUserIdentityUnknown = true),
) )
} }

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:dbbd62622843fbd1d8d35b63c7308eaed46b488e6b189e987983144e5395bd09 oid sha256:2119838c9649710465dc1b8610550ce101f3016a1a6511c3f6dadc715fb75862
size 78972 size 82975

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:31d18a5e250e531fd7b986690306366cf69eedcfdbaba28148e3edd1d36ae597 oid sha256:7ad7b9331a5d504b8d2145c4428e6b64ed838b371f1aad5c288e5545f8a20f1f
size 42912 size 45464

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a04a5aa9e35df5eb82a0a708b2b743bb83a1a9408fee91beff60d5a1ce2c6d5d oid sha256:8d74ede198f221b1138ed49bac7e02ee0fefbccd38735b09237fda95a78f6bd2
size 41599 size 44174

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ef69f889a28afe5704da86296ebf9916a7af1de751eb4c1ee4995ca3f70ea12d oid sha256:b3f483a9e05278928f806f9534720c121efa77bd11b93ea6afc8b543f51a6d7c
size 40737 size 43171

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1d4312936ba67965a8b35e714bbc3014214320311c5d077d33382ebb5aca5738 oid sha256:3045102b9695ce3c095ac81e3d3d26dbf182d433427903e280189613a74f2984
size 42172 size 44758

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4b07f6c942ab083d94c22218010b4aa646cf03e87af41fbda89b16620f7b9752 oid sha256:2bbbf23895558745ebe4fab7faee8ffb4acba07f48b652cc130c78ddf89bebef
size 42079 size 44684

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1b6db26bd05f206661a5707a972cb6f843eef82c76e5e0035775625b4a6268a4 oid sha256:1f72fb044fb52f226600ad2eb3344e8122edcce15c75800fb483f3b93a1c6e26
size 42640 size 45092

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5f77eba1f623f67eba427312523def9a45c41581dd4e047701b2c75f787fcffc oid sha256:14261c716c286b17ee2afcb652378f9703b39f67b219256f6ac3cde61cd50947
size 43178 size 45589

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:a62965a57fea6665739d8e8b2fda9e15abf119ea59f6bbd5d7d8a269512064b9 oid sha256:b1b1c3f8db4dc58dcaab91f254da2906138c09c65d4e57a77a5be9af371aaa5d
size 42421 size 44944

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:39780082d7826422d70902ac2b7859013c30b41d31f20aefc25ada8aeae196c2 oid sha256:a5d3a4bd5c340c0d439ccd7d7e24f372d89255a775a9497e5d9b8847354cafb6
size 41684 size 44236

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5917028dda35add5ed6b32fa9017a9cdcdfb8d273d230aa6d529bebfbf0b95c8 oid sha256:6d9a464a1bc24dc76d1a10ae835fb24839b521ba446a1adad3e70f4ae5f17857
size 39646 size 42076

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f65f298b3f14ecb0da53f03a20a5464a7f6ae138dfdf417ad936601a521fbb30 oid sha256:29592dcede3af29136c857b03975c2697470682371ac5d606f2e6974f89ea268
size 39601 size 42030

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:df3ceef42a59cc072e3aa21c89e9b90f0524ac422d1538804b6fcb8abf97752e oid sha256:676908cf3eabc3417f228dabd2663bb0aa3d903d1b6f0e2fe770bdd6fbb48af2
size 38090 size 40407

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0bf5c596196e64554117addd1247aa1e5be6e8095d85a595b1c2a6232ed483eb oid sha256:ae13e58b01e0e43591c0374474b33fea60574a5d8b22cb57092d15637dc58baa
size 36591 size 37920

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6900db132e6b79de1c5ac986666af46e3f7ef65a9c4cc7d847dde6962eadd129 oid sha256:a5bb345d25a8b90a34708123fc6ba72dd6aca2c09ade4c26b29e9168d65e18be
size 41352 size 42466

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b7199817c8591ccf11241204af8a39d2ed1a60dfe238b06387749d57858cfa65 oid sha256:c1d4af8779d9869938b17f26b155c584c10d119479c62918d5b819a0db30acac
size 39288 size 41720

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ed4073d3b427b2a314667f5118b571e5896f1439582b9209077ec9c62ee0e061 oid sha256:6cb0953ce6c69ad2e49213bf0dbff76a87505f88a092660b71d97ad1e75a5344
size 43097 size 45766

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:c68c895b189a41c7edf8fee58202560ef47e0a98082ddc3a346546f4f1346de7 oid sha256:c0f655ed143e7b894e37d99fc64c75d44e6483e830055e04609a9da36830278c
size 42100 size 44670

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e1879c3d54dfed55e4bb46d967c9c8c5a4f95f4e31e884afde4c9c517f35402e oid sha256:ac26ca718f2118d88315704ce4bc7960983ab2ac0c394199da2c5b3feeec4438
size 42093 size 44688

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e2b5dc5cd20ccfdb7e6a1accabf6e139ff343c16378af19232168f835b1c97df oid sha256:9a9c05a4ace7dc7013e4b02b5e652296399cb2f391ed4bacb2f74d06b68c4be4
size 43650 size 46436

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f9c7dce94908b2b0287c196f564c306a05306f058c71d75be3b298a45683bce4 oid sha256:997444daf4024495291014f1cae1cee2ba389d160c0ccae706b120dcd908996e
size 42321 size 45024

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:743fcac827e8df7742127491722c3ce617a54c0b64fd3336f939994b45376966 oid sha256:527d2c4574c8a5f6946ee94415e30152fdd9e153fd7651b5a40df789e8694d72
size 41470 size 43969

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:671ae1ee2f065705b38063cb354c4bb4a63247e01e1101dd155515b46decb660 oid sha256:7da6bfdf7bc9878e1850f8a443adae82a3a4b0aaa5574cec74081ba6f88b879e
size 42926 size 45622

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8f352ad92472cb8b152ef0660c7f66b55b956ec05bd11dbeb2c9aa543d0db2ca oid sha256:2e253e47f998774da8443926990526105fd918b79040955a3d884558a110db8a
size 42849 size 45538

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5742ffdefde6a9e3f01ba021a75eea20599cc520fa4976aa45cf1c10e394f62b oid sha256:60a989450ffb8ed428d5dd021573b821d238a06f5515f21f9d50b855889ad985
size 43367 size 46022

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:0f6be8e586f9692013ce38c483c42fb23f8c135593e17a72f3be19381719fe6c oid sha256:37e6049854cd5aaeb50f38c4604e631fb561f0f309d4c58d41becf69a01ca036
size 43967 size 46596

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7b4b470aa3f3650c26c6e1cf2491ffb7c3ea58db28dd00c0755a7f5727dc60f1 oid sha256:05a841f7590a33ffea5b09ffc0975bc9090b03cace9e22c330965ce773431211
size 43192 size 45849

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:656c300cea79355aa3b35cd59666f8e35aa4c09d69dd422e816f256cbe6d420d oid sha256:8bc90cce90b0648a340e332276fe4395aec673408aaf938bd0d744d45c89dec2
size 42685 size 45396

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:93fe116bc34f9cf8ee3da2317948a98405e36a6fb98a58b2046a049af435e913 oid sha256:c3ab8eeb4123df1feb35b4d9d9c7fbf166be943edfeb9fa23ac6549463763b10
size 40373 size 42853

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:09f89c67652965407ea147aef158d0b3281850b95cca35f7aec207693bb0af62 oid sha256:a3eee4e61c8676a547c3ccbb1d6b9ea2ce0ae30286012de6b3a4f912b908d2e0
size 40242 size 42722

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:04c7f8e85d58591c3dd6ade913b9fe0861ff7f774fdb4b46e63c9f86ef5a918a oid sha256:02c734eb60efab5050988331527aae294fc6baf0b110e3395103429a79ba059c
size 38959 size 41424

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:27918a9baa05b2cb8dee7219b6e19cc10be1421be190d1e35724d237270761af oid sha256:ced1cdb2e11bff01ffce233ddf6cf826d6db266739abcd022f83b58d8bec012f
size 37382 size 38785

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8ee0a79abd45fc9573e64b6f06491f33dfca34ddd96c0dbd3d8cf7a9052b47be oid sha256:db67d40bae68ec1ece950a06f618606fedc78aa997743b1804db4b69848ba65b
size 42072 size 43381

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e7146d365a105c86eecc34027d2b824e90ca1615fcf77ca0f49ed920dbffe22d oid sha256:6cee9f291a3257f3960ea5d1490f3f219bb0d42b01109b9b398b941524b85971
size 39903 size 42383

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:863c5db2d8fd863b057d0ddfe26bb3804d15126c2fdb58fc03c0087d79c6a3b3 oid sha256:79ac979bcc0339ed30c68c4617f082a9398edf657cc3aea1c80df04bd4b8bab2
size 44056 size 46724

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:16b815f5ec48c101a7bde78cd5a4e30e825138f359e415699ef2b14e6ea23869 oid sha256:0d71c5e3a8dd2defe2dd37fee1a22921f49bbc9a52a766e5ad082b6595c6ff75
size 42968 size 45577

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:5c47ff32beec1987fdb9f92ef6e5c9b7eca04ab20ebab576a2028225a595ba12 oid sha256:ada1e17913092a5e33b5f422a41826dd711102cfb5ab396d5d323e8e0f0b6589
size 42911 size 45590

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bb4d6bfb9c412de00a2b4956032dd42906b5451eb99e6ebb1880dc01f6b55af5 oid sha256:26bf76ccdb56d042422553f557d91d0f26d874a710f696ac106c5c2b5590d332
size 26077 size 38833

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b8c422787b67d477d3b7c8d5dee8879f33d47153dc93dd29bb3883e4ed863a41 oid sha256:8413aed02383572cfe8c481c6ba8b0db4cfb3402334c37f2d8b54a73fe4bf594
size 25232 size 37343

View file

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