Merge branch 'develop' into fix/1874-empty-formatted-body

This commit is contained in:
hayaksi1
2026-08-30 02:04:57 +03:00
612 changed files with 5683 additions and 2482 deletions
@@ -8,6 +8,6 @@
package io.element.android.features.analytics.api
sealed interface AnalyticsOptInEvents {
data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvents
sealed interface AnalyticsOptInEvent {
data class EnableAnalytics(val isEnabled: Boolean) : AnalyticsOptInEvent
}
@@ -8,11 +8,11 @@
package io.element.android.features.analytics.api.preferences
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
data class AnalyticsPreferencesState(
val applicationName: String,
val isEnabled: Boolean,
val policyUrl: String,
val eventSink: (AnalyticsOptInEvents) -> Unit,
val eventSink: (AnalyticsOptInEvent) -> Unit,
)
@@ -13,7 +13,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.features.analytics.api.R
import io.element.android.libraries.designsystem.components.LINK_TAG
import io.element.android.libraries.designsystem.components.list.ListItemContent
@@ -30,7 +30,7 @@ fun AnalyticsPreferencesView(
modifier: Modifier = Modifier,
) {
fun onEnabledChanged(isEnabled: Boolean) {
state.eventSink(AnalyticsOptInEvents.EnableAnalytics(isEnabled = isEnabled))
state.eventSink(AnalyticsOptInEvent.EnableAnalytics(isEnabled = isEnabled))
}
val supportingText = stringResource(
@@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.services.analytics.api.AnalyticsService
@@ -28,9 +28,9 @@ class AnalyticsOptInPresenter(
override fun present(): AnalyticsOptInState {
val localCoroutineScope = rememberCoroutineScope()
fun handleEvent(event: AnalyticsOptInEvents) {
fun handleEvent(event: AnalyticsOptInEvent) {
when (event) {
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
is AnalyticsOptInEvent.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
}
localCoroutineScope.launch {
analyticsService.setDidAskUserConsent()
@@ -8,10 +8,10 @@
package io.element.android.features.analytics.impl
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
data class AnalyticsOptInState(
val applicationName: String,
val hasPolicyLink: Boolean,
val eventSink: (AnalyticsOptInEvents) -> Unit
val eventSink: (AnalyticsOptInEvent) -> Unit
)
@@ -28,7 +28,7 @@ import androidx.compose.ui.unit.dp
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -55,11 +55,11 @@ fun AnalyticsOptInView(
val eventSink = state.eventSink
fun onAcceptTerms() {
eventSink(AnalyticsOptInEvents.EnableAnalytics(true))
eventSink(AnalyticsOptInEvent.EnableAnalytics(true))
}
fun onDeclineTerms() {
eventSink(AnalyticsOptInEvents.EnableAnalytics(false))
eventSink(AnalyticsOptInEvent.EnableAnalytics(false))
}
BackHandler(onBack = ::onDeclineTerms)
@@ -13,7 +13,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.features.analytics.api.preferences.AnalyticsPreferencesState
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.core.meta.BuildMeta
@@ -31,9 +31,9 @@ class AnalyticsPreferencesPresenter(
val localCoroutineScope = rememberCoroutineScope()
val isEnabled = analyticsService.userConsentFlow.collectAsState(initial = false)
fun handleEvent(event: AnalyticsOptInEvents) {
fun handleEvent(event: AnalyticsOptInEvent) {
when (event) {
is AnalyticsOptInEvents.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
is AnalyticsOptInEvent.EnableAnalytics -> localCoroutineScope.setIsEnabled(event.isEnabled)
}
}
@@ -12,7 +12,7 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
@@ -37,7 +37,7 @@ class AnalyticsOptInPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(true))
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
assertThat(analyticsService.userConsentFlow.first()).isTrue()
}
@@ -55,7 +55,7 @@ class AnalyticsOptInPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(analyticsService.didAskUserConsentFlow.first()).isFalse()
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(false))
assertThat(analyticsService.didAskUserConsentFlow.first()).isTrue()
assertThat(analyticsService.userConsentFlow.first()).isFalse()
}
@@ -13,7 +13,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.AnalyticsConfig
import io.element.android.features.analytics.api.AnalyticsOptInEvents
import io.element.android.features.analytics.api.AnalyticsOptInEvent
import io.element.android.libraries.matrix.test.core.aBuildMeta
import io.element.android.services.analytics.test.FakeAnalyticsService
import io.element.android.tests.testutils.WarmUpRule
@@ -67,9 +67,9 @@ class AnalyticsPreferencesPresenterTest {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.isEnabled).isTrue()
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(false))
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(false))
assertThat(awaitItem().isEnabled).isFalse()
initialState.eventSink.invoke(AnalyticsOptInEvents.EnableAnalytics(true))
initialState.eventSink.invoke(AnalyticsOptInEvent.EnableAnalytics(true))
assertThat(awaitItem().isEnabled).isTrue()
}
}
@@ -21,7 +21,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.createroom.impl.R
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.features.invitepeople.api.InvitePeopleStatePreviewParam
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
@@ -56,7 +56,7 @@ fun AddPeopleView(
Button(
text = stringResource(CommonStrings.action_finish),
onClick = {
state.eventSink(InvitePeopleEvents.SendInvites)
state.eventSink(InvitePeopleEvent.SendInvites)
},
enabled = state.canInvite,
modifier = Modifier
@@ -11,13 +11,13 @@ package io.element.android.features.createroom.impl.configureroom
import io.element.android.libraries.matrix.api.spaces.SpaceRoom
import io.element.android.libraries.matrix.ui.media.AvatarAction
sealed interface ConfigureRoomEvents {
data class RoomNameChanged(val name: String) : ConfigureRoomEvents
data class TopicChanged(val topic: String) : ConfigureRoomEvents
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvents
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvents
data object CreateRoom : ConfigureRoomEvents
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvents
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvents
data object CancelCreateRoom : ConfigureRoomEvents
sealed interface ConfigureRoomEvent {
data class RoomNameChanged(val name: String) : ConfigureRoomEvent
data class TopicChanged(val topic: String) : ConfigureRoomEvent
data class JoinRuleChanged(val joinRuleItem: JoinRuleItem) : ConfigureRoomEvent
data class RoomAddressChanged(val roomAddress: String) : ConfigureRoomEvent
data object CreateRoom : ConfigureRoomEvent
data class HandleAvatarAction(val action: AvatarAction) : ConfigureRoomEvent
data class SetParentSpace(val space: SpaceRoom?) : ConfigureRoomEvent
data object CancelCreateRoom : ConfigureRoomEvent
}
@@ -188,14 +188,14 @@ class ConfigureRoomPresenter(
localCoroutineScope.createRoom(config, createRoomAction)
}
fun handleEvent(event: ConfigureRoomEvents) {
fun handleEvent(event: ConfigureRoomEvent) {
when (event) {
is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name)
is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvents.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
is ConfigureRoomEvents.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvents.CreateRoom -> createRoom(createRoomConfig)
is ConfigureRoomEvents.HandleAvatarAction -> {
is ConfigureRoomEvent.RoomNameChanged -> dataStore.setRoomName(event.name)
is ConfigureRoomEvent.TopicChanged -> dataStore.setTopic(event.topic)
is ConfigureRoomEvent.JoinRuleChanged -> dataStore.setJoinRule(event.joinRuleItem)
is ConfigureRoomEvent.RoomAddressChanged -> dataStore.setRoomAddress(event.roomAddress)
is ConfigureRoomEvent.CreateRoom -> createRoom(createRoomConfig)
is ConfigureRoomEvent.HandleAvatarAction -> {
when (event.action) {
AvatarAction.ChoosePhoto -> galleryImagePicker.launch()
AvatarAction.TakePhoto -> if (cameraPermissionState.permissionGranted) {
@@ -207,10 +207,10 @@ class ConfigureRoomPresenter(
AvatarAction.Remove -> dataStore.setAvatarUri(uri = null)
}
}
is ConfigureRoomEvents.SetParentSpace -> {
is ConfigureRoomEvent.SetParentSpace -> {
dataStore.setParentSpace(event.space, false)
}
ConfigureRoomEvents.CancelCreateRoom -> {
ConfigureRoomEvent.CancelCreateRoom -> {
createRoomAction.value = AsyncAction.Uninitialized
}
}
@@ -26,7 +26,7 @@ data class ConfigureRoomState(
val homeserverName: String,
val availableJoinRules: ImmutableList<JoinRuleItem>,
val spaces: ImmutableList<SpaceRoom>,
val eventSink: (ConfigureRoomEvents) -> Unit
val eventSink: (ConfigureRoomEvent) -> Unit
) {
val isValid: Boolean = config.roomName?.isNotEmpty() == true &&
(config.visibilityState is RoomVisibilityState.Private || roomAddressValidity == RoomAddressValidity.Valid) &&
@@ -143,7 +143,7 @@ fun aConfigureRoomState(
)
},
spaces: List<SpaceRoom> = emptyList(),
eventSink: (ConfigureRoomEvents) -> Unit = { },
eventSink: (ConfigureRoomEvent) -> Unit = { },
) = ConfigureRoomState(
config = config,
isSpace = isSpace,
@@ -96,7 +96,7 @@ fun ConfigureRoomView(
onBackClick = onBackClick,
onNextClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.CreateRoom)
state.eventSink(ConfigureRoomEvent.CreateRoom)
},
)
}
@@ -114,20 +114,20 @@ fun ConfigureRoomView(
avatarUri = state.config.avatarUri,
roomName = state.config.roomName.orEmpty(),
onAvatarClick = ::onAvatarClick,
onChangeRoomName = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) },
onChangeRoomName = { state.eventSink(ConfigureRoomEvent.RoomNameChanged(it)) },
)
Spacer(modifier = Modifier.height(16.dp))
RoomTopic(
modifier = Modifier.padding(horizontal = 16.dp),
topic = state.config.topic.orEmpty(),
onTopicChange = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) },
onTopicChange = { state.eventSink(ConfigureRoomEvent.TopicChanged(it)) },
)
Spacer(modifier = Modifier.height(16.dp))
if (!state.isSpace && state.spaces.isNotEmpty()) {
SelectParentSpaceOptions(
spaces = state.spaces,
selectedSpace = state.config.parentSpace,
onSelectSpace = { state.eventSink(ConfigureRoomEvents.SetParentSpace(it)) },
onSelectSpace = { state.eventSink(ConfigureRoomEvent.SetParentSpace(it)) },
)
}
RoomJoinRuleOptions(
@@ -136,7 +136,7 @@ fun ConfigureRoomView(
parentSpace = state.config.parentSpace,
onOptionClick = {
focusManager.clearFocus()
state.eventSink(ConfigureRoomEvents.JoinRuleChanged(it))
state.eventSink(ConfigureRoomEvent.JoinRuleChanged(it))
},
)
if (state.config.visibilityState !is RoomVisibilityState.Private) {
@@ -146,7 +146,7 @@ fun ConfigureRoomView(
address = state.config.visibilityState.roomAddress().getOrNull().orEmpty(),
homeserverName = state.homeserverName,
addressValidity = state.roomAddressValidity,
onAddressChange = { state.eventSink(ConfigureRoomEvents.RoomAddressChanged(it)) },
onAddressChange = { state.eventSink(ConfigureRoomEvent.RoomAddressChanged(it)) },
label = null,
supportingText = stringResource(R.string.screen_create_room_room_address_section_footer),
)
@@ -158,7 +158,7 @@ fun ConfigureRoomView(
actions = state.avatarActions,
isVisible = isAvatarActionsSheetVisible.value,
onDismiss = { isAvatarActionsSheetVisible.value = false },
onSelectAction = { state.eventSink(ConfigureRoomEvents.HandleAvatarAction(it)) }
onSelectAction = { state.eventSink(ConfigureRoomEvent.HandleAvatarAction(it)) }
)
AsyncActionView(
@@ -170,8 +170,8 @@ fun ConfigureRoomView(
},
onSuccess = { onCreateRoomSuccess(it) },
errorMessage = { stringResource(if (isSpace) R.string.screen_create_room_error_creating_space else R.string.screen_create_room_error_creating_room) },
onRetry = { state.eventSink(ConfigureRoomEvents.CreateRoom) },
onErrorDismiss = { state.eventSink(ConfigureRoomEvents.CancelCreateRoom) },
onRetry = { state.eventSink(ConfigureRoomEvent.CreateRoom) },
onErrorDismiss = { state.eventSink(ConfigureRoomEvent.CancelCreateRoom) },
)
PermissionsView(
@@ -12,7 +12,7 @@ import androidx.core.net.toUri
import app.cash.turbine.TurbineTestContext
import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.CreatedRoom
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvents
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomEvent
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomPresenter
import io.element.android.features.createroom.impl.configureroom.ConfigureRoomState
import io.element.android.features.createroom.impl.configureroom.CreateRoomConfig
@@ -106,14 +106,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
assertThat(initialState.isValid).isFalse()
// Room name not empty
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
initialState.eventSink(ConfigureRoomEvent.RoomNameChanged(A_ROOM_NAME))
var newState: ConfigureRoomState = awaitItem()
config = config.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(config)
assertThat(newState.isValid).isTrue()
// Clear room name
newState.eventSink(ConfigureRoomEvents.RoomNameChanged(""))
newState.eventSink(ConfigureRoomEvent.RoomNameChanged(""))
newState = awaitItem()
config = config.copy(roomName = null)
assertThat(newState.config).isEqualTo(config)
@@ -136,13 +136,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
var expectedConfig = CreateRoomConfig()
assertThat(initialState.config).isEqualTo(expectedConfig)
// Room name
initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME))
initialState.eventSink(ConfigureRoomEvent.RoomNameChanged(A_ROOM_NAME))
var newState = awaitItem()
expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME)
assertThat(newState.config).isEqualTo(expectedConfig)
// Room topic
newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE))
newState.eventSink(ConfigureRoomEvent.TopicChanged(A_MESSAGE))
newState = awaitItem()
expectedConfig = expectedConfig.copy(topic = A_MESSAGE)
assertThat(newState.config).isEqualTo(expectedConfig)
@@ -153,7 +153,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// From gallery
val uriFromGallery = AN_URI_FROM_GALLERY
pickerProvider.givenResult(uriFromGallery.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.ChoosePhoto))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromGallery)
assertThat(newState.config).isEqualTo(expectedConfig)
@@ -161,7 +161,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
val uriFromCamera = AN_URI_FROM_CAMERA
pickerProvider.givenResult(uriFromCamera.toUri())
assertThat(newState.cameraPermissionState.permissionGranted).isFalse()
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem()
assertThat(newState.cameraPermissionState.showDialog).isTrue()
permissionsPresenter.setPermissionGranted()
@@ -173,18 +173,18 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// Do it again, no permission is requested
val uriFromCamera2 = AN_URI_FROM_CAMERA_2
pickerProvider.givenResult(uriFromCamera2.toUri())
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.TakePhoto))
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.TakePhoto))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = uriFromCamera2)
assertThat(newState.config).isEqualTo(expectedConfig)
// Remove
newState.eventSink(ConfigureRoomEvents.HandleAvatarAction(AvatarAction.Remove))
newState.eventSink(ConfigureRoomEvent.HandleAvatarAction(AvatarAction.Remove))
newState = awaitItem()
expectedConfig = expectedConfig.copy(avatarUri = null)
assertThat(newState.config).isEqualTo(expectedConfig)
// Room privacy
newState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
newState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
newState = awaitItem()
expectedConfig = expectedConfig.copy(
visibilityState = RoomVisibilityState.Public(
@@ -208,7 +208,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
matrixClient.givenCreateRoomResult(createRoomResult)
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
@@ -239,13 +239,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// Use a public parent space so AskToJoin is a valid option
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(parentSpace))
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
@@ -282,13 +282,13 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
// Use a public parent space so AskToJoin is a valid option
val parentSpace = aSpaceRoom(joinRule = JoinRule.Public)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(parentSpace))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(parentSpace))
assertThat(awaitItem().config.parentSpace).isEqualTo(parentSpace)
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.AskToJoin))
assertThat(awaitItem().config.visibilityState.joinRuleItem).isEqualTo(JoinRuleItem.PublicVisibility.AskToJoin)
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
// We immediately receive the room power levels info needed for adding the child to a space
val powerLevels = RoomPowerLevels(
@@ -339,7 +339,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
matrixClient.givenCreateRoomResult(createRoomResult)
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
skipItems(2)
val analyticsEvent = analyticsService.capturedEvents.filterIsInstance<CreatedRoom>().firstOrNull()
@@ -369,14 +369,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
mediaPreProcessor.givenResult(Result.success(MediaUploadInfo.Image(file, mockk(), mockk())))
matrixClient.givenUploadMediaResult(Result.failure(AN_EXCEPTION))
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat(analyticsService.capturedEvents.filterIsInstance<CreatedRoom>()).isEmpty()
matrixClient.givenUploadMediaResult(Result.success(AN_AVATAR_URL))
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
stateAfterCreateRoom.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Success::class.java)
@@ -399,14 +399,14 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
fakeMatrixClient.givenCreateRoomResult(createRoomResult)
// Create
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterCreateRoom = awaitItem()
assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(AsyncAction.Failure::class.java)
assertThat((stateAfterCreateRoom.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Retry
stateAfterCreateRoom.eventSink(ConfigureRoomEvents.CreateRoom)
stateAfterCreateRoom.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Loading::class.java)
val stateAfterRetry = awaitItem()
@@ -414,7 +414,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
assertThat((stateAfterRetry.createRoomAction as? AsyncAction.Failure)?.error).isEqualTo(createRoomResult.exceptionOrNull())
// Cancel
stateAfterRetry.eventSink(ConfigureRoomEvents.CancelCreateRoom)
stateAfterRetry.eventSink(ConfigureRoomEvent.CancelCreateRoom)
assertThat(awaitItem().createRoomAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
}
}
@@ -430,9 +430,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("invalid address"))
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("invalid address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -450,9 +450,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -470,9 +470,9 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
)
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1)
initialState.eventSink(ConfigureRoomEvents.RoomAddressChanged("address"))
initialState.eventSink(ConfigureRoomEvent.RoomAddressChanged("address"))
skipItems(1)
advanceUntilIdle()
awaitItem().also { state ->
@@ -488,22 +488,22 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
val initialState = initialState()
// First change the join rule to public
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
assertThat(awaitItem().config.visibilityState).isInstanceOf(RoomVisibilityState.Public::class.java)
// Then check changing the parent space resets it to private
// (via LaunchedEffect fallback since Public is not in availableJoinRules for non-public parent)
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(aSpaceRoom()))
skipItems(1) // Skip intermediate state
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
// If we change the join rule back to public
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PublicVisibility.Public))
skipItems(1) // Skip intermediate state (Public is still invalid)
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
// Then remove the parent space, the join rule stays private
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(null))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(null))
assertThat(awaitItem().config.visibilityState).isEqualTo(RoomVisibilityState.Private(JoinRuleItem.PrivateVisibility.Private))
}
}
@@ -514,7 +514,7 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.SetParentSpace(aSpaceRoom()))
initialState.eventSink(ConfigureRoomEvent.SetParentSpace(aSpaceRoom()))
assertThat(awaitError())
}
@@ -534,8 +534,8 @@ class ConfigureRoomPresenterTest : RobolectricTest() {
presenter.test {
val initialState = initialState()
initialState.eventSink(ConfigureRoomEvents.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
initialState.eventSink(ConfigureRoomEvents.CreateRoom)
initialState.eventSink(ConfigureRoomEvent.JoinRuleChanged(JoinRuleItem.PrivateVisibility.Private))
initialState.eventSink(ConfigureRoomEvent.CreateRoom)
assertThat(awaitItem().createRoomAction.isLoading()).isTrue()
assertThat(awaitItem().createRoomAction.isSuccess()).isTrue()
@@ -8,9 +8,9 @@
package io.element.android.features.logout.impl
sealed interface AccountDeactivationEvents {
data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvents
data class SetPassword(val password: String) : AccountDeactivationEvents
data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvents
data object CloseDialogs : AccountDeactivationEvents
sealed interface AccountDeactivationEvent {
data class SetEraseData(val eraseData: Boolean) : AccountDeactivationEvent
data class SetPassword(val password: String) : AccountDeactivationEvent
data class DeactivateAccount(val isRetry: Boolean) : AccountDeactivationEvent
data object CloseDialogs : AccountDeactivationEvent
}
@@ -34,19 +34,19 @@ class AccountDeactivationPresenter(
val formState = remember { mutableStateOf(DeactivateFormState.Default) }
fun handleEvent(event: AccountDeactivationEvents) {
fun handleEvent(event: AccountDeactivationEvent) {
when (event) {
is AccountDeactivationEvents.SetEraseData -> {
is AccountDeactivationEvent.SetEraseData -> {
updateFormState(formState) {
copy(eraseData = event.eraseData)
}
}
is AccountDeactivationEvents.SetPassword -> {
is AccountDeactivationEvent.SetPassword -> {
updateFormState(formState) {
copy(password = event.password)
}
}
is AccountDeactivationEvents.DeactivateAccount ->
is AccountDeactivationEvent.DeactivateAccount ->
if (action.value.isConfirming() || event.isRetry) {
localCoroutineScope.deactivateAccount(
formState = formState.value,
@@ -55,7 +55,7 @@ class AccountDeactivationPresenter(
} else {
action.value = AsyncAction.ConfirmingNoParams
}
AccountDeactivationEvents.CloseDialogs -> {
AccountDeactivationEvent.CloseDialogs -> {
action.value = AsyncAction.Uninitialized
}
}
@@ -15,7 +15,7 @@ import kotlinx.parcelize.Parcelize
data class AccountDeactivationState(
val deactivateFormState: DeactivateFormState,
val accountDeactivationAction: AsyncAction<Unit>,
val eventSink: (AccountDeactivationEvents) -> Unit,
val eventSink: (AccountDeactivationEvent) -> Unit,
) {
val submitEnabled: Boolean
get() = accountDeactivationAction is AsyncAction.Uninitialized &&
@@ -45,7 +45,7 @@ internal fun aDeactivateFormState(
internal fun anAccountDeactivationState(
deactivateFormState: DeactivateFormState = aDeactivateFormState(),
accountDeactivationAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (AccountDeactivationEvents) -> Unit = {},
eventSink: (AccountDeactivationEvent) -> Unit = {},
) = AccountDeactivationState(
deactivateFormState = deactivateFormState,
accountDeactivationAction = accountDeactivationAction,
@@ -102,14 +102,14 @@ fun AccountDeactivationView(
Content(
state = state,
onSubmitClick = {
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
}
)
Spacer(modifier = Modifier.height(32.dp))
Buttons(
state = state,
onSubmitClick = {
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
}
)
}
@@ -117,13 +117,13 @@ fun AccountDeactivationView(
AccountDeactivationActionDialog(
state.accountDeactivationAction,
onConfirmClick = {
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
},
onRetryClick = {
eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true))
eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = true))
},
onDismissDialog = {
eventSink(AccountDeactivationEvents.CloseDialogs)
eventSink(AccountDeactivationEvent.CloseDialogs)
},
)
}
@@ -239,7 +239,7 @@ private fun Content(
headline = stringResource(R.string.screen_deactivate_account_delete_all_messages),
value = eraseData,
onChange = {
eventSink(AccountDeactivationEvents.SetEraseData(it))
eventSink(AccountDeactivationEvent.SetEraseData(it))
},
enabled = !isLoading,
)
@@ -275,7 +275,7 @@ private fun Content(
onValueChange = {
val sanitized = it.sanitize()
passwordFieldState = sanitized
eventSink(AccountDeactivationEvents.SetPassword(sanitized))
eventSink(AccountDeactivationEvent.SetPassword(sanitized))
},
placeholder = stringResource(CommonStrings.common_password),
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
@@ -47,11 +47,11 @@ class AccountDeactivationPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(initialState.deactivateFormState).isEqualTo(DeactivateFormState.Default)
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
val updatedState = awaitItem()
assertThat(updatedState.deactivateFormState).isEqualTo(DeactivateFormState.Default.copy(eraseData = true))
assertThat(updatedState.submitEnabled).isFalse()
updatedState.eventSink(AccountDeactivationEvents.SetPassword("password"))
updatedState.eventSink(AccountDeactivationEvent.SetPassword("password"))
val updatedState2 = awaitItem()
assertThat(updatedState2.deactivateFormState).isEqualTo(DeactivateFormState(password = "password", eraseData = true))
assertThat(updatedState2.submitEnabled).isTrue()
@@ -71,12 +71,12 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
skipItems(1)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
@@ -98,20 +98,20 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
skipItems(2)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
recorder.assertions().isCalledOnce().with(value("password"), value(true))
// Retry
finalState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = true))
finalState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = true))
val finalState2 = awaitItem()
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
assertThat(awaitItem().accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
@@ -131,20 +131,20 @@ class AccountDeactivationPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(AccountDeactivationEvents.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvents.SetEraseData(true))
initialState.eventSink(AccountDeactivationEvent.SetPassword("password"))
initialState.eventSink(AccountDeactivationEvent.SetEraseData(true))
skipItems(2)
initialState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
initialState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState = awaitItem()
assertThat(updatedState.accountDeactivationAction).isEqualTo(AsyncAction.ConfirmingNoParams)
updatedState.eventSink(AccountDeactivationEvents.DeactivateAccount(isRetry = false))
updatedState.eventSink(AccountDeactivationEvent.DeactivateAccount(isRetry = false))
val updatedState2 = awaitItem()
assertThat(updatedState2.accountDeactivationAction).isEqualTo(AsyncAction.Loading)
val finalState = awaitItem()
assertThat(finalState.accountDeactivationAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
recorder.assertions().isCalledOnce().with(value("password"), value(true))
// Cancel
finalState.eventSink(AccountDeactivationEvents.CloseDialogs)
finalState.eventSink(AccountDeactivationEvent.CloseDialogs)
val finalState2 = awaitItem()
assertThat(finalState2.accountDeactivationAction).isEqualTo(AsyncAction.Uninitialized)
}
@@ -35,7 +35,7 @@ import org.robolectric.annotation.Config
class AccountDeactivationViewTest : RobolectricTest() {
@Test
fun `clicking on back invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>(expectEvents = false)
ensureCalledOnce {
setAccountDeactivationView(
state = anAccountDeactivationState(eventSink = eventsRecorder),
@@ -48,7 +48,7 @@ class AccountDeactivationViewTest : RobolectricTest() {
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on Deactivate emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -58,12 +58,12 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_delete)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(false))
}
@Test
fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -74,12 +74,12 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(false))
}
@Test
fun `clicking on retry on the confirmation dialog emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -90,24 +90,24 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true))
eventsRecorder.assertSingle(AccountDeactivationEvent.DeactivateAccount(true))
}
@Test
fun `switching on the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
eventSink = eventsRecorder,
),
)
clickOn(R.string.screen_deactivate_account_delete_all_messages)
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true))
eventsRecorder.assertSingle(AccountDeactivationEvent.SetEraseData(true))
}
@Test
fun `switching off the erase all switch emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -117,13 +117,13 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_deactivate_account_delete_all_messages)
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false))
eventsRecorder.assertSingle(AccountDeactivationEvent.SetEraseData(false))
}
@Config(qualifiers = "h1024dp")
@Test
fun `typing text in the password field emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
val eventsRecorder = EventsRecorder<AccountDeactivationEvent>()
setAccountDeactivationView(
state = anAccountDeactivationState(
deactivateFormState = aDeactivateFormState(
@@ -133,7 +133,7 @@ class AccountDeactivationViewTest : RobolectricTest() {
),
)
onNodeWithTag(TestTags.loginPassword.value).performTextInput("A")
eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD"))
eventsRecorder.assertSingle(AccountDeactivationEvent.SetPassword("A$A_PASSWORD"))
}
}
@@ -0,0 +1,20 @@
/*
* Copyright (c) 2026 Element Creations 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.features.enterprise.api
import android.app.Activity
/**
* A hook that is called during app startup, from the `MainActivity`.
*
* Note that the hook is run in the background: it will not block startup of the app.
*/
fun interface AppStartupHook {
/** @param activity The activity which is active while the app starts (i.e. the `MainActivity`). */
suspend fun onAppStartup(activity: Activity)
}
@@ -8,6 +8,6 @@
package io.element.android.features.forward.impl
sealed interface ForwardMessagesEvents {
data object ClearError : ForwardMessagesEvents
sealed interface ForwardMessagesEvent {
data object ClearError : ForwardMessagesEvent
}
@@ -48,9 +48,9 @@ class ForwardMessagesPresenter(
@Composable
override fun present(): ForwardMessagesState {
fun handleEvent(event: ForwardMessagesEvents) {
fun handleEvent(event: ForwardMessagesEvent) {
when (event) {
ForwardMessagesEvents.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
ForwardMessagesEvent.ClearError -> forwardingActionState.value = AsyncAction.Uninitialized
}
}
@@ -13,5 +13,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
data class ForwardMessagesState(
val forwardAction: AsyncAction<List<RoomId>>,
val eventSink: (ForwardMessagesEvents) -> Unit
val eventSink: (ForwardMessagesEvent) -> Unit
)
@@ -32,7 +32,7 @@ open class ForwardMessagesStatePreviewParam : PreviewParameterProvider<ForwardMe
fun aForwardMessagesState(
forwardAction: AsyncAction<List<RoomId>> = AsyncAction.Uninitialized,
eventSink: (ForwardMessagesEvents) -> Unit = {}
eventSink: (ForwardMessagesEvent) -> Unit = {}
) = ForwardMessagesState(
forwardAction = forwardAction,
eventSink = eventSink
@@ -31,7 +31,7 @@ fun ForwardMessagesView(
stringResource(id = CommonStrings.error_unknown)
},
onErrorDismiss = {
state.eventSink(ForwardMessagesEvents.ClearError)
state.eventSink(ForwardMessagesEvent.ClearError)
},
)
}
@@ -86,7 +86,7 @@ class ForwardMessagesPresenterTest {
val failedForwardState = awaitItem()
assertThat(failedForwardState.forwardAction.isFailure()).isTrue()
// Then clear error
failedForwardState.eventSink(ForwardMessagesEvents.ClearError)
failedForwardState.eventSink(ForwardMessagesEvent.ClearError)
assertThat(awaitItem().forwardAction.isUninitialized()).isTrue()
forwardEventLambda.assertions().isCalledOnce()
}
@@ -29,7 +29,7 @@ import org.junit.Test
class ForwardMessagesViewTest : RobolectricTest() {
@Test
fun `cancel error emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>()
val eventsRecorder = EventsRecorder<ForwardMessagesEvent>()
setForwardMessagesView(
aForwardMessagesState(
forwardAction = AsyncAction.Failure(AN_EXCEPTION),
@@ -37,13 +37,13 @@ class ForwardMessagesViewTest : RobolectricTest() {
),
)
pressTag(TestTags.dialogPositive.value)
eventsRecorder.assertSingle(ForwardMessagesEvents.ClearError)
eventsRecorder.assertSingle(ForwardMessagesEvent.ClearError)
}
@Test
fun `success invokes onForwardSuccess`() = runAndroidComposeUiTest {
val data = listOf(A_ROOM_ID)
val eventsRecorder = EventsRecorder<ForwardMessagesEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<ForwardMessagesEvent>(expectEvents = false)
ensureCalledOnceWithParam<List<RoomId>?>(data) { callback ->
setForwardMessagesView(
aForwardMessagesState(
@@ -8,7 +8,7 @@
package io.element.android.features.ftue.impl.notifications
sealed interface NotificationsOptInEvents {
data object ContinueClicked : NotificationsOptInEvents
data object NotNowClicked : NotificationsOptInEvents
sealed interface NotificationsOptInEvent {
data object ContinueClicked : NotificationsOptInEvent
data object NotNowClicked : NotificationsOptInEvent
}
@@ -52,16 +52,16 @@ class NotificationsOptInPresenter(
override fun present(): NotificationsOptInState {
val notificationsPermissionsState = postNotificationPermissionsPresenter.present()
fun handleEvent(event: NotificationsOptInEvents) {
fun handleEvent(event: NotificationsOptInEvent) {
when (event) {
NotificationsOptInEvents.ContinueClicked -> {
NotificationsOptInEvent.ContinueClicked -> {
if (notificationsPermissionsState.permissionGranted) {
callback.onNotificationsOptInFinished()
} else {
notificationsPermissionsState.eventSink(PermissionsEvent.RequestPermissions)
}
}
NotificationsOptInEvents.NotNowClicked -> {
NotificationsOptInEvent.NotNowClicked -> {
if (buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
appCoroutineScope.setPermissionDenied()
}
@@ -12,5 +12,5 @@ import io.element.android.libraries.permissions.api.PermissionsState
data class NotificationsOptInState(
val notificationsPermissionState: PermissionsState,
val eventSink: (NotificationsOptInEvents) -> Unit
val eventSink: (NotificationsOptInEvent) -> Unit
)
@@ -86,14 +86,14 @@ private fun NotificationsOptInFooter(state: NotificationsOptInState) {
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_ok),
onClick = {
state.eventSink(NotificationsOptInEvents.ContinueClicked)
state.eventSink(NotificationsOptInEvent.ContinueClicked)
}
)
TextButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_not_now),
onClick = {
state.eventSink(NotificationsOptInEvents.NotNowClicked)
state.eventSink(NotificationsOptInEvent.NotNowClicked)
}
)
}
@@ -15,7 +15,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import dev.zacsweers.metro.Inject
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.architecture.Presenter
@@ -67,7 +67,7 @@ class ChooseSelfVerificationModePresenter(
fun handleEvent(event: ChooseSelfVerificationModeEvent) {
when (event) {
ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvents.Logout(ignoreSdkError = false))
ChooseSelfVerificationModeEvent.SignOut -> directLogoutState.eventSink(DirectLogoutEvent.Logout(ignoreSdkError = false))
}
}
@@ -55,7 +55,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
initialState.eventSink(NotificationsOptInEvent.ContinueClicked)
assertThat(awaitItem().notificationsPermissionState.showDialog).isTrue()
}
}
@@ -70,7 +70,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.ContinueClicked)
initialState.eventSink(NotificationsOptInEvent.ContinueClicked)
assertThat(isFinished).isTrue()
}
}
@@ -86,7 +86,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
initialState.eventSink(NotificationsOptInEvent.NotNowClicked)
assertThat(isFinished).isTrue()
}
}
@@ -105,7 +105,7 @@ class NotificationsOptInPresenterTest {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(NotificationsOptInEvents.NotNowClicked)
initialState.eventSink(NotificationsOptInEvent.NotNowClicked)
// Allow background coroutines to run
runCurrent()
@@ -9,7 +9,7 @@
package io.element.android.features.ftue.impl.sessionverification.choosemode
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.direct.DirectLogoutEvents
import io.element.android.features.logout.api.direct.DirectLogoutEvent
import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.libraries.architecture.AsyncData
@@ -112,7 +112,7 @@ class ChooseSessionVerificationModePresenterTest {
@Test
fun `sing out action triggers a direct logout`() = runTest {
val logoutEventRecorder = lambdaRecorder<DirectLogoutEvents, Unit> {}
val logoutEventRecorder = lambdaRecorder<DirectLogoutEvent, Unit> {}
val logoutPresenter = Presenter<DirectLogoutState> {
aDirectLogoutState(eventSink = logoutEventRecorder)
}
@@ -121,7 +121,7 @@ class ChooseSessionVerificationModePresenterTest {
val initial = awaitItem()
initial.eventSink(ChooseSelfVerificationModeEvent.SignOut)
logoutEventRecorder.assertions().isCalledOnce()
.with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
.with(value(DirectLogoutEvent.Logout(ignoreSdkError = false)))
}
}
@@ -16,7 +16,7 @@ import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.push.api.battery.BatteryOptimizationEvents
import io.element.android.libraries.push.api.battery.BatteryOptimizationEvent
import io.element.android.libraries.push.api.battery.BatteryOptimizationState
import io.element.android.libraries.push.api.battery.aBatteryOptimizationState
@@ -31,8 +31,8 @@ internal fun BatteryOptimizationBanner(
description = stringResource(R.string.banner_battery_optimization_content_android),
type = AnnouncementType.Actionable(
actionText = stringResource(R.string.banner_battery_optimization_submit_android),
onActionClick = { state.eventSink(BatteryOptimizationEvents.RequestDisableOptimizations) },
onDismissClick = { state.eventSink(BatteryOptimizationEvents.Dismiss) },
onActionClick = { state.eventSink(BatteryOptimizationEvent.RequestDisableOptimizations) },
onDismissClick = { state.eventSink(BatteryOptimizationEvent.Dismiss) },
),
)
}
@@ -16,7 +16,7 @@ import io.element.android.libraries.designsystem.components.Announcement
import io.element.android.libraries.designsystem.components.AnnouncementType
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvents
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsEvent
import io.element.android.libraries.fullscreenintent.api.FullScreenIntentPermissionsState
import io.element.android.libraries.fullscreenintent.api.aFullScreenIntentPermissionsState
import io.element.android.libraries.ui.strings.CommonStrings
@@ -31,8 +31,8 @@ fun FullScreenIntentPermissionBanner(
description = stringResource(R.string.full_screen_intent_banner_message),
type = AnnouncementType.Actionable(
actionText = stringResource(CommonStrings.action_continue),
onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvents.Dismiss) },
onActionClick = { state.eventSink(FullScreenIntentPermissionsEvents.OpenSettings) },
onDismissClick = { state.eventSink(FullScreenIntentPermissionsEvent.Dismiss) },
onActionClick = { state.eventSink(FullScreenIntentPermissionsEvent.OpenSettings) },
),
modifier = modifier.roomListBannerPadding(),
)
@@ -53,8 +53,6 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
@@ -80,7 +78,7 @@ import io.element.android.libraries.designsystem.modifiers.backgroundVerticalGra
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
import io.element.android.libraries.designsystem.text.AdaptativeTitle
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
import io.element.android.libraries.designsystem.theme.components.DropdownMenuItem
import io.element.android.libraries.designsystem.theme.components.Icon
@@ -141,15 +139,21 @@ fun HomeTopBar(
else -> stringResource(selectedNavigationItem.labelRes)
}
}
HomeNavigationBarItem.Spaces -> stringResource(selectedNavigationItem.labelRes)
HomeNavigationBarItem.Spaces -> null
}
displayTitle?.let {
val style = when (spaceFiltersState) {
// Space name
is SpaceFiltersState.Selected -> ElementTheme.typography.fontHeadingSmMedium
// "Chats"
else -> ElementTheme.typography.fontHeadingLgBold
}
AdaptativeTitle(
title = displayTitle,
style = style,
twoLinesStyle = ElementTheme.typography.fontHeadingSmMedium,
)
}
Text(
modifier = Modifier.semantics {
heading()
},
style = ElementTheme.typography.aliasScreenTitle,
text = displayTitle,
)
},
navigationIcon = {
NavigationIcon(
@@ -179,7 +183,9 @@ fun HomeTopBar(
TopAppBarScrollBehaviorLayout(scrollBehavior = scrollBehavior) {
RoomListFiltersView(
state = filtersState,
modifier = Modifier.padding(bottom = 16.dp).padding(contentPadding)
modifier = Modifier
.padding(bottom = 16.dp)
.padding(contentPadding)
)
}
}
@@ -348,45 +354,45 @@ private fun AccountIcon(
),
contentAlignment = Alignment.Center,
) {
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
val avatarData by remember(matrixUser) {
derivedStateOf {
matrixUser.getAvatarData(size = AvatarSize.CurrentUserTopBar)
}
val statusEmoji = matrixUser.displayedStatus?.toEmojiText()
val avatarModifier = if (statusEmoji != null) {
Modifier.eraseStatusEmojiBackground(
parentSize = AvatarSize.CurrentUserTopBar.dp,
layoutDirection = LocalLayoutDirection.current,
)
} else {
Modifier
}
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
modifier = avatarModifier,
contentDescription = if (isCurrentAccount) {
if (showAvatarIndicator) {
stringResource(CommonStrings.a11y_settings_with_required_action)
} else {
stringResource(CommonStrings.common_settings)
}
} else {
null
},
}
val statusEmoji = matrixUser.displayedStatus?.toEmojiText()
val avatarModifier = if (statusEmoji != null) {
Modifier.eraseStatusEmojiBackground(
parentSize = AvatarSize.CurrentUserTopBar.dp,
layoutDirection = LocalLayoutDirection.current,
)
if (statusEmoji != null) {
StatusEmojiBadge(
emoji = statusEmoji,
modifier = Modifier.align(Alignment.BottomEnd),
)
}
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier.align(Alignment.TopEnd)
)
}
} else {
Modifier
}
Avatar(
avatarData = avatarData,
avatarType = AvatarType.User,
modifier = avatarModifier,
contentDescription = if (isCurrentAccount) {
if (showAvatarIndicator) {
stringResource(CommonStrings.a11y_settings_with_required_action)
} else {
stringResource(CommonStrings.common_settings)
}
} else {
null
},
)
if (statusEmoji != null) {
StatusEmojiBadge(
emoji = statusEmoji,
modifier = Modifier.align(Alignment.BottomEnd),
)
}
if (showAvatarIndicator) {
RedIndicatorAtom(
modifier = Modifier.align(Alignment.TopEnd)
)
}
}
}
@@ -19,6 +19,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.matrix.api.room.CallIntentConsensus
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
import io.element.android.libraries.matrix.api.room.RoomInfo
import io.element.android.libraries.matrix.api.roomlist.LatestEventValue
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.ui.model.dmUserStatus
@@ -47,7 +48,7 @@ class RoomListRoomSummaryFactory(
mode = DateFormatterMode.TimeOrDate,
useRelative = true,
),
latestEvent = computeLatestEvent(roomSummary.latestEvent, roomInfo.isDm),
latestEvent = computeLatestEvent(roomSummary.latestEvent, roomInfo.hasOnlyTwoMembers()),
avatarData = avatarData,
userDefinedNotificationMode = roomInfo.userDefinedNotificationMode,
hasRoomCall = roomInfo.hasRoomCall,
@@ -81,6 +82,10 @@ class RoomListRoomSummaryFactory(
)
}
private fun RoomInfo.hasOnlyTwoMembers(): Boolean {
return isDm || activeMembersCount <= 2
}
private fun computeLatestEvent(latestEvent: LatestEventValue, dm: Boolean): LatestEvent {
return when (latestEvent) {
is LatestEventValue.None -> {
@@ -37,8 +37,8 @@ import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.into
import io.element.android.features.home.impl.spacefilters.selectedFilter
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.AcceptInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents.DeclineInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent.AcceptInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent.DeclineInvite
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
@@ -7,9 +7,6 @@
package io.element.android.features.home.impl.search
import androidx.compose.runtime.Immutable
@Immutable
sealed interface GlobalSearchEvent {
object ClearQuery : GlobalSearchEvent
object ToggleSearchVisibility : GlobalSearchEvent
@@ -8,4 +8,4 @@
package io.element.android.features.home.impl.spaces
sealed interface HomeSpacesEvents
sealed interface HomeSpacesEvent
@@ -39,7 +39,7 @@ class HomeSpacesPresenter(
seenInvitesStore.seenRoomIds().map { it.toImmutableSet() }
}.collectAsState(persistentSetOf())
fun handleEvent(event: HomeSpacesEvents) {
fun handleEvent(event: HomeSpacesEvent) {
// when (event) { }
}
@@ -19,7 +19,7 @@ data class HomeSpacesState(
val seenSpaceInvites: ImmutableSet<RoomId>,
val hideInvitesAvatar: Boolean,
val canExploreSpaces: Boolean,
val eventSink: (HomeSpacesEvents) -> Unit,
val eventSink: (HomeSpacesEvent) -> Unit,
)
sealed interface CurrentSpace {
@@ -43,7 +43,7 @@ internal fun aHomeSpacesState(
seenSpaceInvites: Set<RoomId> = emptySet(),
hideInvitesAvatar: Boolean = false,
canExploreSpaces: Boolean = true,
eventSink: (HomeSpacesEvents) -> Unit = {},
eventSink: (HomeSpacesEvent) -> Unit = {},
) = HomeSpacesState(
space = space,
spaceRooms = spaceRooms.toImmutableList(),
@@ -143,7 +143,7 @@ private fun EmptySpaceHomeView(
)
Text(
text = stringResource(CommonStrings.screen_space_list_empty_state_title),
style = ElementTheme.typography.fontHeadingLgBold,
style = ElementTheme.typography.fontHeadingMdBold,
color = ElementTheme.colors.textPrimary,
textAlign = TextAlign.Center,
)
@@ -8,10 +8,36 @@
package io.element.android.features.home.impl.datasource
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.eventformatter.api.RoomLatestEventFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLatestEventFormatter
import io.element.android.libraries.matrix.test.room.aRoomInfo
import io.element.android.libraries.matrix.test.room.aRoomSummary
import org.junit.Test
class RoomListRoomSummaryFactoryTest {
@Test
fun `a room with two members does not prefix its preview with the sender`() {
val formatter = FakeRoomLatestEventFormatter()
val factory = aRoomListRoomSummaryFactory(roomLatestEventFormatter = formatter)
factory.create(aRoomSummary(info = aRoomInfo(isDm = false, activeMembersCount = 2)))
assertThat(formatter.lastIsDmRoom).isTrue()
}
@Test
fun `a room with more than two members prefixes its preview with the sender`() {
val formatter = FakeRoomLatestEventFormatter()
val factory = aRoomListRoomSummaryFactory(roomLatestEventFormatter = formatter)
factory.create(aRoomSummary(info = aRoomInfo(isDm = false, activeMembersCount = 3)))
assertThat(formatter.lastIsDmRoom).isFalse()
}
}
fun aRoomListRoomSummaryFactory(
dateFormatter: DateFormatter = FakeDateFormatter { _, _, _ -> "Today" },
@@ -25,7 +25,7 @@ import io.element.android.features.home.impl.search.aRoomListSearchState
import io.element.android.features.home.impl.spacefilters.SpaceFiltersState
import io.element.android.features.home.impl.spacefilters.aDisabledSpaceFiltersState
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.test.InMemorySeenInvitesStore
@@ -474,7 +474,7 @@ class RoomListPresenterTest {
@Test
fun `present - when a room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvent -> }
val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
@@ -513,8 +513,8 @@ class RoomListPresenterTest {
assert(eventSinkRecorder)
.isCalledExactly(2)
.withSequence(
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false))),
listOf(value(AcceptDeclineInviteEvent.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false))),
)
}
}
@@ -13,7 +13,7 @@ import io.element.android.features.invite.api.InviteData
/**
* Events sent to the accept/decline invite presenter, from whichever screen is showing the invitation.
*/
interface AcceptDeclineInviteEvents {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
interface AcceptDeclineInviteEvent {
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvent
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvent
}
@@ -14,5 +14,5 @@ import io.element.android.libraries.matrix.api.core.RoomId
data class AcceptDeclineInviteState(
val acceptAction: AsyncAction<RoomId>,
val declineAction: AsyncAction<RoomId>,
val eventSink: (AcceptDeclineInviteEvents) -> Unit,
val eventSink: (AcceptDeclineInviteEvent) -> Unit,
)
@@ -14,7 +14,7 @@ import io.element.android.libraries.matrix.api.core.RoomId
fun anAcceptDeclineInviteState(
acceptAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
declineAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (AcceptDeclineInviteEvents) -> Unit = {},
eventSink: (AcceptDeclineInviteEvent) -> Unit = {},
) = AcceptDeclineInviteState(
acceptAction = acceptAction,
declineAction = declineAction,
@@ -15,7 +15,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import dev.zacsweers.metro.Inject
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
@@ -40,13 +40,13 @@ class AcceptDeclineInvitePresenter(
val declinedAction: MutableState<AsyncAction<RoomId>> =
remember { mutableStateOf(AsyncAction.Uninitialized) }
fun handleEvent(event: AcceptDeclineInviteEvents) {
fun handleEvent(event: AcceptDeclineInviteEvent) {
when (event) {
is AcceptDeclineInviteEvents.AcceptInvite -> {
is AcceptDeclineInviteEvent.AcceptInvite -> {
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
}
is AcceptDeclineInviteEvents.DeclineInvite -> {
is AcceptDeclineInviteEvent.DeclineInvite -> {
val inviteData = event.invite
if (event.shouldConfirm) {
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
@@ -58,11 +58,11 @@ class AcceptDeclineInvitePresenter(
)
}
}
is InternalAcceptDeclineInviteEvents.ClearAcceptActionState -> {
is InternalAcceptDeclineInviteEvent.ClearAcceptActionState -> {
acceptedAction.value = AsyncAction.Uninitialized
}
is InternalAcceptDeclineInviteEvents.ClearDeclineActionState -> {
is InternalAcceptDeclineInviteEvent.ClearDeclineActionState -> {
declinedAction.value = AsyncAction.Uninitialized
}
}
@@ -14,7 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.PreviewParameter
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
@@ -37,11 +37,11 @@ fun AcceptDeclineInviteView(
AsyncActionView(
async = state.acceptAction,
onSuccess = { roomId ->
state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearAcceptActionState)
onAcceptInviteSuccess(roomId)
},
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.ClearAcceptActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearAcceptActionState)
},
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
@@ -57,11 +57,11 @@ fun AcceptDeclineInviteView(
AsyncActionView(
async = state.declineAction,
onSuccess = { roomId ->
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
onDeclineInviteSuccess(roomId)
},
onErrorDismiss = {
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
},
errorTitle = {
stringResource(CommonStrings.common_something_went_wrong)
@@ -77,7 +77,7 @@ fun AcceptDeclineInviteView(
blockUser = confirming.blockUser,
onConfirmClick = {
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(
AcceptDeclineInviteEvent.DeclineInvite(
confirming.inviteData,
blockUser = confirming.blockUser,
shouldConfirm = false
@@ -85,7 +85,7 @@ fun AcceptDeclineInviteView(
)
},
onDismissClick = {
state.eventSink(InternalAcceptDeclineInviteEvents.ClearDeclineActionState)
state.eventSink(InternalAcceptDeclineInviteEvent.ClearDeclineActionState)
}
)
}
@@ -8,9 +8,9 @@
package io.element.android.features.invite.impl.acceptdecline
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
data object ClearAcceptActionState : InternalAcceptDeclineInviteEvents
data object ClearDeclineActionState : InternalAcceptDeclineInviteEvents
sealed interface InternalAcceptDeclineInviteEvent : AcceptDeclineInviteEvent {
data object ClearAcceptActionState : InternalAcceptDeclineInviteEvent
data object ClearDeclineActionState : InternalAcceptDeclineInviteEvent
}
@@ -8,10 +8,10 @@
package io.element.android.features.invite.impl.declineandblock
sealed interface DeclineAndBlockEvents {
data class UpdateReportReason(val reason: String) : DeclineAndBlockEvents
data object ToggleReportRoom : DeclineAndBlockEvents
data object ToggleBlockUser : DeclineAndBlockEvents
data object Decline : DeclineAndBlockEvents
data object ClearDeclineAction : DeclineAndBlockEvents
sealed interface DeclineAndBlockEvent {
data class UpdateReportReason(val reason: String) : DeclineAndBlockEvent
data object ToggleReportRoom : DeclineAndBlockEvent
data object ToggleBlockUser : DeclineAndBlockEvent
data object Decline : DeclineAndBlockEvent
data object ClearDeclineAction : DeclineAndBlockEvent
}
@@ -49,13 +49,13 @@ class DeclineAndBlockPresenter(
val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: DeclineAndBlockEvents) {
fun handleEvent(event: DeclineAndBlockEvent) {
when (event) {
DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
DeclineAndBlockEvents.ToggleBlockUser -> blockUser = !blockUser
DeclineAndBlockEvents.ToggleReportRoom -> reportRoom = !reportRoom
is DeclineAndBlockEvents.UpdateReportReason -> reportReason = event.reason
DeclineAndBlockEvent.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
DeclineAndBlockEvent.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
DeclineAndBlockEvent.ToggleBlockUser -> blockUser = !blockUser
DeclineAndBlockEvent.ToggleReportRoom -> reportRoom = !reportRoom
is DeclineAndBlockEvent.UpdateReportReason -> reportReason = event.reason
}
}
@@ -15,7 +15,7 @@ data class DeclineAndBlockState(
val reportReason: String,
val blockUser: Boolean,
val declineAction: AsyncAction<Unit>,
val eventSink: (DeclineAndBlockEvents) -> Unit
val eventSink: (DeclineAndBlockEvent) -> Unit
) {
val canDecline = blockUser || reportRoom && reportReason.isNotEmpty()
}
@@ -36,7 +36,7 @@ fun aDeclineAndBlockState(
reportReason: String = "",
blockUser: Boolean = false,
declineAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
eventSink: (DeclineAndBlockEvents) -> Unit = {},
eventSink: (DeclineAndBlockEvent) -> Unit = {},
) = DeclineAndBlockState(
reportRoom = reportRoom,
reportReason = reportReason,
@@ -56,8 +56,8 @@ fun DeclineAndBlockView(
async = state.declineAction,
onSuccess = { onDeclineSuccess() },
errorMessage = { stringResource(CommonStrings.error_unknown) },
onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) },
onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) }
onRetry = { state.eventSink(DeclineAndBlockEvent.Decline) },
onErrorDismiss = { state.eventSink(DeclineAndBlockEvent.ClearDeclineAction) }
)
Scaffold(
@@ -89,7 +89,7 @@ fun DeclineAndBlockView(
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_description))
},
onClick = {
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
state.eventSink(DeclineAndBlockEvent.ToggleBlockUser)
},
trailingContent = ListItemContent.Switch(checked = state.blockUser)
)
@@ -104,7 +104,7 @@ fun DeclineAndBlockView(
Text(text = stringResource(R.string.screen_decline_and_block_report_user_option_description))
},
onClick = {
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
state.eventSink(DeclineAndBlockEvent.ToggleReportRoom)
},
trailingContent = ListItemContent.Switch(checked = state.reportRoom)
)
@@ -113,7 +113,7 @@ fun DeclineAndBlockView(
Spacer(modifier = Modifier.height(24.dp))
TextField(
value = state.reportReason,
onValueChange = { state.eventSink(DeclineAndBlockEvents.UpdateReportReason(it)) },
onValueChange = { state.eventSink(DeclineAndBlockEvent.UpdateReportReason(it)) },
placeholder = stringResource(R.string.screen_decline_and_block_report_user_reason_placeholder),
minLines = 3,
enabled = !isDeclining,
@@ -132,7 +132,7 @@ fun DeclineAndBlockView(
enabled = !isDeclining && state.canDecline,
onClick = {
focusManager.clearFocus(force = true)
state.eventSink(DeclineAndBlockEvents.Decline)
state.eventSink(DeclineAndBlockEvent.Decline)
},
modifier = Modifier
.fillMaxWidth()
@@ -10,7 +10,7 @@ package io.element.android.features.invite.impl.acceptdecline
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
import io.element.android.features.invite.impl.AcceptInvite
import io.element.android.features.invite.impl.DeclineInvite
@@ -51,13 +51,13 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
InternalAcceptDeclineInviteEvents.ClearDeclineActionState
InternalAcceptDeclineInviteEvent.ClearDeclineActionState
)
}
awaitItem().also { state ->
@@ -78,20 +78,20 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.ClearDeclineActionState
InternalAcceptDeclineInviteEvent.ClearDeclineActionState
)
}
awaitItem().also { state ->
@@ -114,13 +114,13 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
)
}
awaitItem().also { state ->
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, blockUser = false))
state.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
@@ -146,7 +146,7 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
AcceptDeclineInviteEvent.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
@@ -155,7 +155,7 @@ class AcceptDeclineInvitePresenterTest {
awaitItem().also { state ->
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(
InternalAcceptDeclineInviteEvents.ClearAcceptActionState
InternalAcceptDeclineInviteEvent.ClearAcceptActionState
)
}
awaitItem().also { state ->
@@ -178,7 +178,7 @@ class AcceptDeclineInvitePresenterTest {
val inviteData = anInviteData()
awaitItem().also { state ->
state.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
AcceptDeclineInviteEvent.AcceptInvite(inviteData)
)
}
awaitItem().also { state ->
@@ -53,21 +53,21 @@ class DeclineAndBlockPresenterTest {
assertThat(state.blockUser).isTrue()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isTrue()
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
state.eventSink(DeclineAndBlockEvent.ToggleBlockUser)
}
awaitItem().also { state ->
assertThat(state.reportRoom).isFalse()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isFalse()
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
state.eventSink(DeclineAndBlockEvent.ToggleReportRoom)
}
awaitItem().also { state ->
assertThat(state.reportRoom).isTrue()
assertThat(state.blockUser).isFalse()
assertThat(state.reportReason).isEmpty()
assertThat(state.canDecline).isFalse()
state.eventSink(DeclineAndBlockEvents.UpdateReportReason("Spam"))
state.eventSink(DeclineAndBlockEvent.UpdateReportReason("Spam"))
}
awaitItem().also { state ->
assertThat(state.reportRoom).isTrue()
@@ -87,7 +87,7 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
@@ -110,12 +110,12 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(DeclineAndBlockEvents.ClearDeclineAction)
state.eventSink(DeclineAndBlockEvent.ClearDeclineAction)
}
awaitItem().also { state ->
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
@@ -137,7 +137,7 @@ class DeclineAndBlockPresenterTest {
)
presenter.test {
awaitItem().also { state ->
state.eventSink(DeclineAndBlockEvents.Decline)
state.eventSink(DeclineAndBlockEvent.Decline)
}
assertThat(awaitItem().declineAction.isLoading()).isTrue()
awaitItem().also { state ->
@@ -30,7 +30,7 @@ import org.junit.Test
class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>(expectEvents = false)
ensureCalledOnce {
setDeclineAndBlockView(
aDeclineAndBlockState(
@@ -44,7 +44,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `a successful decline invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>(expectEvents = false)
ensureCalledOnce {
setDeclineAndBlockView(
aDeclineAndBlockState(
@@ -58,7 +58,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `clicking on decline when enabled emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>()
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = true,
@@ -66,12 +66,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline)
eventsRecorder.assertSingle(DeclineAndBlockEvent.Decline)
}
@Test
fun `clicking on decline when disabled does not emit event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>(expectEvents = false)
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = false,
@@ -85,7 +85,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
@Test
fun `clicking on block option emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>()
setDeclineAndBlockView(
aDeclineAndBlockState(
blockUser = true,
@@ -93,12 +93,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_decline_and_block_block_user_option_title)
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser)
eventsRecorder.assertSingle(DeclineAndBlockEvent.ToggleBlockUser)
}
@Test
fun `clicking on report room option emits the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>()
setDeclineAndBlockView(
aDeclineAndBlockState(
reportRoom = true,
@@ -106,12 +106,12 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_report_room)
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom)
eventsRecorder.assertSingle(DeclineAndBlockEvent.ToggleReportRoom)
}
@Test
fun `typing text in the reason field emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
val eventsRecorder = EventsRecorder<DeclineAndBlockEvent>()
setDeclineAndBlockView(
aDeclineAndBlockState(
reportRoom = true,
@@ -120,7 +120,7 @@ class DeclineAndBlockViewTest : RobolectricTest() {
),
)
onNodeWithText("").performTextInput("Spam!")
eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!"))
eventsRecorder.assertSingle(DeclineAndBlockEvent.UpdateReportReason("Spam!"))
}
}
@@ -11,8 +11,8 @@ package io.element.android.features.invitepeople.api
/**
* Events the invite people UI sends to its presenter through [InvitePeopleState.eventSink].
*/
interface InvitePeopleEvents {
data object SendInvites : InvitePeopleEvents
data object CloseSearch : InvitePeopleEvents
data object ClearError : InvitePeopleEvents
interface InvitePeopleEvent {
data object SendInvites : InvitePeopleEvent
data object CloseSearch : InvitePeopleEvent
data object ClearError : InvitePeopleEvent
}
@@ -28,5 +28,5 @@ interface InvitePeopleState {
val createRoomFromDmAction: AsyncAction<RoomId>
/** Where the UI sends its events. */
val eventSink: (InvitePeopleEvents) -> Unit
val eventSink: (InvitePeopleEvent) -> Unit
}
@@ -27,7 +27,7 @@ private data class PreviewInvitePeopleState(
override val isSearchActive: Boolean,
override val sendInvitesAction: AsyncAction<Unit>,
override val createRoomFromDmAction: AsyncAction<RoomId>,
override val eventSink: (InvitePeopleEvents) -> Unit,
override val eventSink: (InvitePeopleEvent) -> Unit,
) : InvitePeopleState
private fun aPreviewInvitePeopleState(
@@ -35,7 +35,7 @@ private fun aPreviewInvitePeopleState(
isSearchActive: Boolean = false,
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
createRoomFromDmAction: AsyncAction<RoomId> = AsyncAction.Uninitialized,
eventSink: (InvitePeopleEvents) -> Unit = {},
eventSink: (InvitePeopleEvent) -> Unit = {},
) = PreviewInvitePeopleState(
canInvite = canInvite,
isSearchActive = isSearchActive,
@@ -8,12 +8,12 @@
package io.element.android.features.invitepeople.impl
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.matrix.api.user.MatrixUser
sealed interface DefaultInvitePeopleEvents : InvitePeopleEvents {
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvents
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvents
data object DismissUnknownUsersModal : DefaultInvitePeopleEvents
data object RemoveUnknownUsers : DefaultInvitePeopleEvents
sealed interface DefaultInvitePeopleEvent : InvitePeopleEvent {
data class ToggleUser(val user: MatrixUser) : DefaultInvitePeopleEvent
data class OnSearchActiveChanged(val active: Boolean) : DefaultInvitePeopleEvent
data object DismissUnknownUsersModal : DefaultInvitePeopleEvent
data object RemoveUnknownUsers : DefaultInvitePeopleEvent
}
@@ -24,7 +24,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedFactory
import dev.zacsweers.metro.AssistedInject
import dev.zacsweers.metro.ContributesBinding
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.libraries.architecture.AsyncAction
@@ -179,26 +179,26 @@ class DefaultInvitePeoplePresenter(
)
}
fun handleEvent(event: InvitePeopleEvents) {
fun handleEvent(event: InvitePeopleEvent) {
when (event) {
// Dedicated `when` for exhaustivity.
is DefaultInvitePeopleEvents -> when (event) {
is DefaultInvitePeopleEvents.OnSearchActiveChanged -> {
is DefaultInvitePeopleEvent -> when (event) {
is DefaultInvitePeopleEvent.OnSearchActiveChanged -> {
searchActive = event.active
if (!event.active) {
queryState.clearText()
}
}
is DefaultInvitePeopleEvents.ToggleUser -> {
is DefaultInvitePeopleEvent.ToggleUser -> {
selectedUsers.toggleUser(event.user)
searchResults.toggleUser(event.user)
// suggestions will automatically update via derivedStateOf when selectedUsers changes
}
is DefaultInvitePeopleEvents.DismissUnknownUsersModal -> {
is DefaultInvitePeopleEvent.DismissUnknownUsersModal -> {
sendInvitesAction.value = AsyncAction.Uninitialized
}
is DefaultInvitePeopleEvents.RemoveUnknownUsers -> {
is DefaultInvitePeopleEvent.RemoveUnknownUsers -> {
val usersToRemove = selectedUsers.value.filter { it in unknownUsers }
usersToRemove.forEach { user ->
selectedUsers.toggleUser(user)
@@ -207,7 +207,7 @@ class DefaultInvitePeoplePresenter(
sendInvitesAction.value = AsyncAction.Uninitialized
}
}
is InvitePeopleEvents.SendInvites -> {
is InvitePeopleEvent.SendInvites -> {
if (unknownUsers.isNotEmpty() && sendInvitesAction.value !is ConfirmingUnknownUserInvitation) {
sendInvitesAction.value = ConfirmingUnknownUserInvitation(
unknownUsers
@@ -224,11 +224,11 @@ class DefaultInvitePeoplePresenter(
}
}
}
is InvitePeopleEvents.CloseSearch -> {
is InvitePeopleEvent.CloseSearch -> {
searchActive = false
queryState.clearText()
}
is InvitePeopleEvents.ClearError -> {
is InvitePeopleEvent.ClearError -> {
sendInvitesAction.value = AsyncAction.Uninitialized
createRoomFromDmAction.value = AsyncAction.Uninitialized
}
@@ -9,7 +9,7 @@
package io.element.android.features.invitepeople.impl
import androidx.compose.foundation.text.input.TextFieldState
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.features.invitepeople.api.InvitePeopleState
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
@@ -29,5 +29,5 @@ data class DefaultInvitePeopleState(
override val sendInvitesAction: AsyncAction<Unit>,
override val createRoomFromDmAction: AsyncAction<RoomId>,
val suggestions: ImmutableList<InvitableUser>,
override val eventSink: (InvitePeopleEvents) -> Unit
override val eventSink: (InvitePeopleEvent) -> Unit
) : InvitePeopleState
@@ -30,7 +30,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonRowMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
@@ -102,7 +102,7 @@ private fun InvitePeopleContentView(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
fun toggleUser(user: MatrixUser) {
state.eventSink(DefaultInvitePeopleEvents.ToggleUser(user))
state.eventSink(DefaultInvitePeopleEvent.ToggleUser(user))
}
InvitePeopleSearchBar(
@@ -114,7 +114,7 @@ private fun InvitePeopleContentView(
active = state.isSearchActive,
onActiveChange = {
state.eventSink(
DefaultInvitePeopleEvents.OnSearchActiveChanged(
DefaultInvitePeopleEvent.OnSearchActiveChanged(
it
)
)
@@ -144,7 +144,7 @@ private fun InvitePeopleContentView(
CheckableUserRow(
checked = invitableUser.isSelected,
onCheckedChange = {
state.eventSink(DefaultInvitePeopleEvents.ToggleUser(invitableUser.matrixUser))
state.eventSink(DefaultInvitePeopleEvent.ToggleUser(invitableUser.matrixUser))
},
data = CheckableUserRowData.Resolved(
avatarData = invitableUser.matrixUser.getAvatarData(AvatarSize.UserListItem),
@@ -163,9 +163,9 @@ private fun InvitePeopleContentView(
if (state.sendInvitesAction is ConfirmingUnknownUserInvitation) {
InvitePeopleConfirmModal(
users = state.sendInvitesAction.users,
onDismiss = { state.eventSink.invoke(DefaultInvitePeopleEvents.DismissUnknownUsersModal) },
onInvite = { state.eventSink.invoke(InvitePeopleEvents.SendInvites) },
onRemove = { state.eventSink.invoke(DefaultInvitePeopleEvents.RemoveUnknownUsers) }
onDismiss = { state.eventSink.invoke(DefaultInvitePeopleEvent.DismissUnknownUsersModal) },
onInvite = { state.eventSink.invoke(InvitePeopleEvent.SendInvites) },
onRemove = { state.eventSink.invoke(DefaultInvitePeopleEvent.RemoveUnknownUsers) }
)
}
}
@@ -11,7 +11,7 @@ package io.element.android.features.invitepeople.impl
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat
import io.element.android.features.invitepeople.api.InvitePeopleEvents
import io.element.android.features.invitepeople.api.InvitePeopleEvent
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
@@ -88,13 +88,13 @@ internal class DefaultInvitePeoplePresenterTest {
val initialState = awaitItem()
skipItems(1)
initialState.eventSink(DefaultInvitePeopleEvents.OnSearchActiveChanged(true))
initialState.eventSink(DefaultInvitePeopleEvent.OnSearchActiveChanged(true))
val resultState = awaitItemAsDefault()
assertThat(resultState.isSearchActive).isTrue()
resultState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
assertThat(awaitItemAsDefault().searchQuery.text.toString()).isEqualTo("some query")
resultState.eventSink(InvitePeopleEvents.CloseSearch)
resultState.eventSink(InvitePeopleEvent.CloseSearch)
skipItems(1)
awaitItemAsDefault().also {
assertThat(it.isSearchActive).isFalse()
@@ -290,18 +290,18 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser()))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser())
// Toggling a different user also adds them
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser(id = A_USER_ID_2.value)))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(
aMatrixUser(),
aMatrixUser(id = A_USER_ID_2.value)
)
// Toggling the first user removes them
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(aMatrixUser()))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(aMatrixUser()))
assertThat(awaitItemAsDefault().selectedUsers).containsExactly(aMatrixUser(id = A_USER_ID_2.value))
}
}
@@ -319,7 +319,7 @@ internal class DefaultInvitePeoplePresenterTest {
val selectedUser = aMatrixUser(displayName = "John Doe")
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
initialState.searchQuery.setTextAndPlaceCursorAtEnd("some query")
skipItems(1)
@@ -373,7 +373,7 @@ internal class DefaultInvitePeoplePresenterTest {
}
// And then a user is toggled
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
@@ -414,13 +414,13 @@ internal class DefaultInvitePeoplePresenterTest {
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
skipItems(1)
// And then a user is toggled
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
// The results are updated...
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
// Send invites
initialState.eventSink(InvitePeopleEvents.SendInvites)
initialState.eventSink(InvitePeopleEvent.SendInvites)
// Can't invite in the loading state
awaitItem().run {
@@ -468,13 +468,13 @@ internal class DefaultInvitePeoplePresenterTest {
repository.emitStateWithUsers(users = aMatrixUserList() + selectedUser)
skipItems(1)
// And then a user is toggled
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(selectedUser))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(selectedUser))
skipItems(1)
val resultState = awaitItemAsDefault()
// The results are updated...
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
// Send invites
initialState.eventSink(InvitePeopleEvents.SendInvites)
initialState.eventSink(InvitePeopleEvent.SendInvites)
// Can't invite in the loading state
awaitItem().run {
@@ -645,17 +645,17 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
// If we do not have their identity cached, or fail to fetch it, we should mark them as unknown.
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
eventSink(InvitePeopleEvents.SendInvites)
eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -668,7 +668,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(InvitePeopleEvents.SendInvites)
eventSink(InvitePeopleEvent.SendInvites)
}
delay(1_000)
@@ -712,11 +712,11 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
// And the search is matching Alice and Bob
@@ -738,7 +738,7 @@ internal class DefaultInvitePeoplePresenterTest {
searchResults.users().map { Pair(it.matrixUser, it.isSelected) }
).containsExactly(Pair(alice, true), Pair(bob, true))
eventSink(InvitePeopleEvents.SendInvites)
eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -751,7 +751,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(DefaultInvitePeopleEvents.RemoveUnknownUsers)
eventSink(DefaultInvitePeopleEvent.RemoveUnknownUsers)
}
// Selecting "remove" should remove all unknown users, but keeps those who are known.
@@ -794,16 +794,16 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// When we toggle a user not in the list, they are added, and we fetch their identity.
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(bob))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(bob))
delay(100)
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvents.ToggleUser(charlie))
awaitItemAsDefault().eventSink(DefaultInvitePeopleEvent.ToggleUser(charlie))
delay(100)
awaitItemAsDefault().run {
assertThat(selectedUsers).containsExactly(alice, bob, charlie)
eventSink(InvitePeopleEvents.SendInvites)
eventSink(InvitePeopleEvent.SendInvites)
}
getUserIdentityResult.assertions().isCalledExactly(3).withSequence(
@@ -816,7 +816,7 @@ internal class DefaultInvitePeoplePresenterTest {
awaitItemAsDefault().run {
assertThat(sendInvitesAction).isInstanceOf(ConfirmingUnknownUserInvitation::class.java)
assertThat(canInvite).isTrue()
eventSink(DefaultInvitePeopleEvents.DismissUnknownUsersModal)
eventSink(DefaultInvitePeopleEvent.DismissUnknownUsersModal)
}
// Dismissing should not modify the selection at all
@@ -853,10 +853,10 @@ internal class DefaultInvitePeoplePresenterTest {
skipItems(1)
// We want to add a new user to a DM
initialState.eventSink(DefaultInvitePeopleEvents.ToggleUser(alice))
initialState.eventSink(DefaultInvitePeopleEvent.ToggleUser(alice))
// And we send the invites
initialState.eventSink(InvitePeopleEvents.SendInvites)
initialState.eventSink(InvitePeopleEvent.SendInvites)
skipItems(1)
@@ -10,15 +10,15 @@ package io.element.android.features.joinroom.impl
import io.element.android.features.invite.api.InviteData
sealed interface JoinRoomEvents {
data object RetryFetchingContent : JoinRoomEvents
data object DismissErrorAndHideContent : JoinRoomEvents
data object JoinRoom : JoinRoomEvents
data object KnockRoom : JoinRoomEvents
data object ForgetRoom : JoinRoomEvents
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
data object ClearActionStates : JoinRoomEvents
data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvents
data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvents
sealed interface JoinRoomEvent {
data object RetryFetchingContent : JoinRoomEvent
data object DismissErrorAndHideContent : JoinRoomEvent
data object JoinRoom : JoinRoomEvent
data object KnockRoom : JoinRoomEvent
data object ForgetRoom : JoinRoomEvent
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvent
data class UpdateKnockMessage(val message: String) : JoinRoomEvent
data object ClearActionStates : JoinRoomEvent
data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvent
data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvent
}
@@ -26,7 +26,7 @@ import dev.zacsweers.metro.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
@@ -151,37 +151,37 @@ class JoinRoomPresenter(
contentState.markRoomInviteAsSeen()
}
fun handleEvent(event: JoinRoomEvents) {
fun handleEvent(event: JoinRoomEvent) {
when (event) {
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
is JoinRoomEvents.AcceptInvite -> {
JoinRoomEvent.JoinRoom -> coroutineScope.joinRoom(joinAction)
is JoinRoomEvent.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
is JoinRoomEvent.AcceptInvite -> {
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.AcceptInvite(event.inviteData)
AcceptDeclineInviteEvent.AcceptInvite(event.inviteData)
)
}
is JoinRoomEvents.DeclineInvite -> {
is JoinRoomEvent.DeclineInvite -> {
acceptDeclineInviteState.eventSink(
AcceptDeclineInviteEvents.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true)
AcceptDeclineInviteEvent.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true)
)
}
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
JoinRoomEvents.RetryFetchingContent -> {
is JoinRoomEvent.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
JoinRoomEvent.RetryFetchingContent -> {
retryCount++
}
JoinRoomEvents.ClearActionStates -> {
JoinRoomEvent.ClearActionStates -> {
knockAction.value = AsyncAction.Uninitialized
joinAction.value = AsyncAction.Uninitialized
cancelKnockAction.value = AsyncAction.Uninitialized
forgetRoomAction.value = AsyncAction.Uninitialized
}
is JoinRoomEvents.UpdateKnockMessage -> {
is JoinRoomEvent.UpdateKnockMessage -> {
knockMessage = event.message.take(MAX_KNOCK_MESSAGE_LENGTH)
}
JoinRoomEvents.DismissErrorAndHideContent -> {
JoinRoomEvent.DismissErrorAndHideContent -> {
isDismissingContent = true
}
JoinRoomEvents.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction)
JoinRoomEvent.ForgetRoom -> coroutineScope.forgetRoom(forgetRoomAction)
}
}
@@ -37,7 +37,7 @@ data class JoinRoomState(
val knockMessage: String,
val hideInviteAvatars: Boolean,
val canReportRoom: Boolean,
val eventSink: (JoinRoomEvents) -> Unit
val eventSink: (JoinRoomEvent) -> Unit
) {
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoom.Failures.UnauthorizedJoin
val joinAuthorisationStatus = when (contentState) {
@@ -207,7 +207,7 @@ fun aJoinRoomState(
knockMessage: String = "",
hideInviteAvatars: Boolean = false,
canReportRoom: Boolean = true,
eventSink: (JoinRoomEvents) -> Unit = {}
eventSink: (JoinRoomEvent) -> Unit = {}
) = JoinRoomState(
roomIdOrAlias = roomIdOrAlias,
contentState = contentState,
@@ -115,33 +115,33 @@ fun JoinRoomView(
contentState = state.contentState,
knockMessage = state.knockMessage,
hideAvatarsImages = state.hideAvatarsImages,
onKnockMessageUpdate = { state.eventSink(JoinRoomEvents.UpdateKnockMessage(it)) },
onKnockMessageUpdate = { state.eventSink(JoinRoomEvent.UpdateKnockMessage(it)) },
)
},
footer = {
JoinRoomFooter(
joinAuthorisationStatus = state.joinAuthorisationStatus,
onAcceptInvite = { inviteData ->
state.eventSink(JoinRoomEvents.AcceptInvite(inviteData))
state.eventSink(JoinRoomEvent.AcceptInvite(inviteData))
},
onDeclineInvite = { inviteData, blockUser ->
if (state.canReportRoom && blockUser) {
onDeclineInviteAndBlockUser(inviteData)
} else {
state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, blockUser = blockUser))
state.eventSink(JoinRoomEvent.DeclineInvite(inviteData, blockUser = blockUser))
}
},
onJoinRoom = {
state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvent.JoinRoom)
},
onKnockRoom = {
state.eventSink(JoinRoomEvents.KnockRoom)
state.eventSink(JoinRoomEvent.KnockRoom)
},
onCancelKnock = {
state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = true))
state.eventSink(JoinRoomEvent.CancelKnock(requiresConfirmation = true))
},
onForgetRoom = {
state.eventSink(JoinRoomEvents.ForgetRoom)
state.eventSink(JoinRoomEvent.ForgetRoom)
},
onGoBack = onBackClick,
)
@@ -152,9 +152,9 @@ fun JoinRoomView(
RetryDialog(
title = stringResource(R.string.screen_join_room_loading_alert_title),
content = stringResource(CommonStrings.error_network_or_server_issue),
onRetry = { state.eventSink(JoinRoomEvents.RetryFetchingContent) },
onRetry = { state.eventSink(JoinRoomEvent.RetryFetchingContent) },
onDismiss = {
state.eventSink(JoinRoomEvents.DismissErrorAndHideContent)
state.eventSink(JoinRoomEvent.DismissErrorAndHideContent)
onBackClick()
}
)
@@ -166,7 +166,7 @@ fun JoinRoomView(
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onJoinSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
}
AsyncActionView(
@@ -174,19 +174,19 @@ fun JoinRoomView(
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onKnockSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
AsyncActionView(
async = state.forgetAction,
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
onSuccess = { onForgetSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
AsyncActionView(
async = state.cancelKnockAction,
onSuccess = { onCancelKnockSuccess() },
onErrorDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onErrorDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
errorTitle = { stringResource(CommonStrings.common_something_went_wrong) },
errorMessage = { stringResource(CommonStrings.error_network_or_server_issue) },
confirmationDialog = {
@@ -195,8 +195,8 @@ fun JoinRoomView(
title = stringResource(R.string.screen_join_room_cancel_knock_alert_title),
submitText = stringResource(R.string.screen_join_room_cancel_knock_alert_confirmation),
cancelText = stringResource(CommonStrings.action_no),
onSubmitClick = { state.eventSink(JoinRoomEvents.CancelKnock(requiresConfirmation = false)) },
onDismiss = { state.eventSink(JoinRoomEvents.ClearActionStates) },
onSubmitClick = { state.eventSink(JoinRoomEvent.CancelKnock(requiresConfirmation = false)) },
onDismiss = { state.eventSink(JoinRoomEvent.ClearActionStates) },
)
},
)
@@ -12,7 +12,7 @@ import com.google.common.truth.Truth.assertThat
import im.vector.app.features.analytics.plan.JoinedRoom
import io.element.android.features.invite.api.InviteData
import io.element.android.features.invite.api.SeenInvitesStore
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvent
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
import io.element.android.features.invite.api.toInviteData
@@ -365,7 +365,7 @@ class JoinRoomPresenterTest {
@Test
fun `present - when room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvent -> }
val acceptDeclinePresenter = Presenter {
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
}
@@ -388,14 +388,14 @@ class JoinRoomPresenterTest {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.AcceptInvite(inviteData))
state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, false))
state.eventSink(JoinRoomEvent.AcceptInvite(inviteData))
state.eventSink(JoinRoomEvent.DeclineInvite(inviteData, false))
assert(eventSinkRecorder)
.isCalledExactly(2)
.withSequence(
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))),
listOf(value(AcceptDeclineInviteEvent.AcceptInvite(inviteData))),
listOf(value(AcceptDeclineInviteEvent.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))),
)
}
}
@@ -422,7 +422,7 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
@@ -453,14 +453,14 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Failure(AN_EXCEPTION))
state.eventSink(JoinRoomEvents.ClearActionStates)
state.eventSink(JoinRoomEvent.ClearActionStates)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Uninitialized)
@@ -480,7 +480,7 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.JoinRoom)
state.eventSink(JoinRoomEvent.JoinRoom)
}
awaitItem().also { state ->
assertThat(state.joinAction).isEqualTo(AsyncAction.Loading)
@@ -683,17 +683,17 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.UpdateKnockMessage(knockMessage))
state.eventSink(JoinRoomEvent.UpdateKnockMessage(knockMessage))
}
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.KnockRoom)
state.eventSink(JoinRoomEvent.KnockRoom)
}
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.knockAction).isEqualTo(AsyncAction.Success(Unit))
fakeKnockRoom.lambda = knockRoomFailure
state.eventSink(JoinRoomEvents.KnockRoom)
state.eventSink(JoinRoomEvent.KnockRoom)
}
assertThat(awaitItem().knockAction).isEqualTo(AsyncAction.Loading)
@@ -731,17 +731,17 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.CancelKnock(true))
state.eventSink(JoinRoomEvent.CancelKnock(true))
}
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.ConfirmingNoParams)
state.eventSink(JoinRoomEvents.CancelKnock(false))
state.eventSink(JoinRoomEvent.CancelKnock(false))
}
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.cancelKnockAction).isEqualTo(AsyncAction.Success(Unit))
cancelKnockRoom.lambda = cancelKnockRoomFailure
state.eventSink(JoinRoomEvents.CancelKnock(false))
state.eventSink(JoinRoomEvent.CancelKnock(false))
}
assertThat(awaitItem().cancelKnockAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
@@ -778,14 +778,14 @@ class JoinRoomPresenterTest {
presenter.test {
skipItems(1)
awaitItem().also { state ->
state.eventSink(JoinRoomEvents.ForgetRoom)
state.eventSink(JoinRoomEvent.ForgetRoom)
}
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
awaitItem().also { state ->
assertThat(state.forgetAction).isEqualTo(AsyncAction.Success(Unit))
fakeForgetRoom.lambda = forgetRoomFailure
state.eventSink(JoinRoomEvents.ForgetRoom)
state.eventSink(JoinRoomEvent.ForgetRoom)
}
assertThat(awaitItem().forgetAction).isEqualTo(AsyncAction.Loading)
@@ -34,7 +34,7 @@ import org.junit.Test
class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<JoinRoomEvent>(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -48,7 +48,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Join room on CanJoin room emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanJoin),
@@ -56,12 +56,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_join_action)
eventsRecorder.assertSingle(JoinRoomEvents.JoinRoom)
eventsRecorder.assertSingle(JoinRoomEvent.JoinRoom)
}
@Test
fun `clicking on Knock room on CanKnock room emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -70,12 +70,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.KnockRoom)
eventsRecorder.assertSingle(JoinRoomEvent.KnockRoom)
}
@Test
fun `clicking on closing Knock error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -84,12 +84,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `clicking on cancel knock request emit the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
@@ -97,12 +97,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_cancel_knock_action)
eventsRecorder.assertSingle(JoinRoomEvents.CancelKnock(true))
eventsRecorder.assertSingle(JoinRoomEvent.CancelKnock(true))
}
@Test
fun `clicking on closing Cancel Knock error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked),
@@ -111,12 +111,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `clicking on closing Join error emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.CanKnock),
@@ -125,12 +125,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_ok)
eventsRecorder.assertSingle(JoinRoomEvents.ClearActionStates)
eventsRecorder.assertSingle(JoinRoomEvent.ClearActionStates)
}
@Test
fun `when joining room is successful, the expected callback is invoked`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<JoinRoomEvent>(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -144,7 +144,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
val inviteData = anInviteData()
setJoinRoomView(
aJoinRoomState(
@@ -153,12 +153,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData))
eventsRecorder.assertSingle(JoinRoomEvent.AcceptInvite(inviteData))
}
@Test
fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
val inviteData = anInviteData()
setJoinRoomView(
aJoinRoomState(
@@ -167,13 +167,13 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false))
eventsRecorder.assertSingle(JoinRoomEvent.DeclineInvite(inviteData, false))
}
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() {
runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<JoinRoomEvent>(expectEvents = false)
val inviteData = anInviteData()
val joinRoomState = aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
@@ -192,7 +192,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
val inviteData = anInviteData()
val joinRoomState = aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
@@ -201,12 +201,12 @@ class JoinRoomViewTest : RobolectricTest() {
)
setJoinRoomView(state = joinRoomState)
clickOn(R.string.screen_join_room_decline_and_block_button_title)
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true))
eventsRecorder.assertSingle(JoinRoomEvent.DeclineInvite(inviteData, true))
}
@Test
fun `clicking on Retry when an error occurs emits the expected Event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aFailureContentState(),
@@ -214,12 +214,12 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(JoinRoomEvents.RetryFetchingContent)
eventsRecorder.assertSingle(JoinRoomEvent.RetryFetchingContent)
}
@Test
fun `clicking on ok when user is unauthorized the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<JoinRoomEvent>(expectEvents = false)
ensureCalledOnce {
setJoinRoomView(
aJoinRoomState(
@@ -235,7 +235,7 @@ class JoinRoomViewTest : RobolectricTest() {
@Test
fun `clicking on forget when user is banned invokes the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
val eventsRecorder = EventsRecorder<JoinRoomEvent>()
setJoinRoomView(
aJoinRoomState(
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(null, null)),
@@ -243,7 +243,7 @@ class JoinRoomViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_join_room_forget_action)
eventsRecorder.assertSingle(JoinRoomEvents.ForgetRoom)
eventsRecorder.assertSingle(JoinRoomEvent.ForgetRoom)
}
}
@@ -8,7 +8,7 @@
package io.element.android.features.knockrequests.impl.banner
sealed interface KnockRequestsBannerEvents {
data object AcceptSingleRequest : KnockRequestsBannerEvents
data object Dismiss : KnockRequestsBannerEvents
sealed interface KnockRequestsBannerEvent {
data object AcceptSingleRequest : KnockRequestsBannerEvent
data object Dismiss : KnockRequestsBannerEvent
}
@@ -53,15 +53,15 @@ class KnockRequestsBannerPresenter(
}
}
fun handleEvent(event: KnockRequestsBannerEvents) {
fun handleEvent(event: KnockRequestsBannerEvent) {
when (event) {
is KnockRequestsBannerEvents.AcceptSingleRequest -> {
is KnockRequestsBannerEvent.AcceptSingleRequest -> {
sessionCoroutineScope.acceptSingleKnockRequest(
knockRequests = knockRequests,
displayAcceptError = showAcceptError,
)
}
is KnockRequestsBannerEvents.Dismiss -> {
is KnockRequestsBannerEvent.Dismiss -> {
sessionCoroutineScope.launch {
knockRequestsService.markAllKnockRequestsAsSeen()
}
@@ -20,7 +20,7 @@ data class KnockRequestsBannerState(
val knockRequests: ImmutableList<KnockRequestPresentable>,
val displayAcceptError: Boolean,
val canAccept: Boolean,
val eventSink: (KnockRequestsBannerEvents) -> Unit,
val eventSink: (KnockRequestsBannerEvent) -> Unit,
) {
val subtitle = knockRequests.singleOrNull()?.userId?.value
val reason = knockRequests.singleOrNull()?.reason
@@ -64,7 +64,7 @@ fun aKnockRequestsBannerState(
displayAcceptError: Boolean = false,
canAccept: Boolean = true,
isVisible: Boolean = true,
eventSink: (KnockRequestsBannerEvents) -> Unit = {}
eventSink: (KnockRequestsBannerEvent) -> Unit = {}
) = KnockRequestsBannerState(
knockRequests = knockRequests.toImmutableList(),
displayAcceptError = displayAcceptError,
@@ -109,11 +109,11 @@ private fun KnockRequestsBannerContent(
modifier: Modifier = Modifier,
) {
fun onDismissClick() {
state.eventSink(KnockRequestsBannerEvents.Dismiss)
state.eventSink(KnockRequestsBannerEvent.Dismiss)
}
fun onAcceptClick() {
state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
state.eventSink(KnockRequestsBannerEvent.AcceptSingleRequest)
}
Column(
@@ -10,12 +10,12 @@ package io.element.android.features.knockrequests.impl.list
import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable
sealed interface KnockRequestsListEvents {
data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents
data object AcceptAll : KnockRequestsListEvents
data object ResetCurrentAction : KnockRequestsListEvents
data object RetryCurrentAction : KnockRequestsListEvents
data object ConfirmCurrentAction : KnockRequestsListEvents
sealed interface KnockRequestsListEvent {
data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvent
data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvent
data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvent
data object AcceptAll : KnockRequestsListEvent
data object ResetCurrentAction : KnockRequestsListEvent
data object RetryCurrentAction : KnockRequestsListEvent
data object ConfirmCurrentAction : KnockRequestsListEvent
}
@@ -39,28 +39,28 @@ class KnockRequestsListPresenter(
val coroutineScope = rememberCoroutineScope()
fun handleEvent(event: KnockRequestsListEvents) {
fun handleEvent(event: KnockRequestsListEvent) {
when (event) {
KnockRequestsListEvents.AcceptAll -> {
KnockRequestsListEvent.AcceptAll -> {
currentAction = KnockRequestsAction.AcceptAll
}
is KnockRequestsListEvents.Accept -> {
is KnockRequestsListEvent.Accept -> {
currentAction = KnockRequestsAction.Accept(event.knockRequest)
}
is KnockRequestsListEvents.Decline -> {
is KnockRequestsListEvent.Decline -> {
currentAction = KnockRequestsAction.Decline(event.knockRequest)
}
is KnockRequestsListEvents.DeclineAndBan -> {
is KnockRequestsListEvent.DeclineAndBan -> {
currentAction = KnockRequestsAction.DeclineAndBan(event.knockRequest)
}
KnockRequestsListEvents.ResetCurrentAction -> {
KnockRequestsListEvent.ResetCurrentAction -> {
asyncAction.value = AsyncAction.Uninitialized
currentAction = KnockRequestsAction.None
}
KnockRequestsListEvents.RetryCurrentAction -> {
KnockRequestsListEvent.RetryCurrentAction -> {
coroutineScope.executeAction(currentAction, asyncAction, isActionConfirmed = true)
}
KnockRequestsListEvents.ConfirmCurrentAction -> {
KnockRequestsListEvent.ConfirmCurrentAction -> {
coroutineScope.executeAction(currentAction, asyncAction, isActionConfirmed = true)
}
}
@@ -20,7 +20,7 @@ data class KnockRequestsListState(
val currentAction: KnockRequestsAction,
val asyncAction: AsyncAction<Unit>,
val permissions: KnockRequestPermissions,
val eventSink: (KnockRequestsListEvents) -> Unit,
val eventSink: (KnockRequestsListEvent) -> Unit,
) {
val canAcceptAll = permissions.canAccept && knockRequests is AsyncData.Success && knockRequests.data.size > 1
}
@@ -140,7 +140,7 @@ fun aKnockRequestsListState(
canDecline = true,
canBan = true,
),
eventSink: (KnockRequestsListEvents) -> Unit = {},
eventSink: (KnockRequestsListEvent) -> Unit = {},
) = KnockRequestsListState(
knockRequests = knockRequests,
currentAction = currentAction,
@@ -108,15 +108,15 @@ private fun KnockRequestsListContent(
modifier: Modifier = Modifier,
) {
fun onAcceptClick(knockRequest: KnockRequestPresentable) {
state.eventSink(KnockRequestsListEvents.Accept(knockRequest))
state.eventSink(KnockRequestsListEvent.Accept(knockRequest))
}
fun onDeclineClick(knockRequest: KnockRequestPresentable) {
state.eventSink(KnockRequestsListEvents.Decline(knockRequest))
state.eventSink(KnockRequestsListEvent.Decline(knockRequest))
}
fun onBanClick(knockRequest: KnockRequestPresentable) {
state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequest))
state.eventSink(KnockRequestsListEvent.DeclineAndBan(knockRequest))
}
var bottomPaddingInPixels by remember { mutableIntStateOf(0) }
@@ -160,19 +160,19 @@ private fun KnockRequestsListContent(
currentAction = state.currentAction,
asyncAction = state.asyncAction,
onConfirm = {
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
},
onRetry = {
state.eventSink(KnockRequestsListEvents.RetryCurrentAction)
state.eventSink(KnockRequestsListEvent.RetryCurrentAction)
},
onDismiss = {
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
},
)
if (state.canAcceptAll) {
KnockRequestsAcceptAll(
onClick = {
state.eventSink(KnockRequestsListEvents.AcceptAll)
state.eventSink(KnockRequestsListEvent.AcceptAll)
},
onHeightChange = { height ->
bottomPaddingInPixels = height
@@ -146,7 +146,7 @@ import org.junit.Test
presenter.test {
skipItems(2)
awaitItem().also { state ->
state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
state.eventSink(KnockRequestsBannerEvent.AcceptSingleRequest)
}
awaitItem().also { state ->
assertThat(state.isVisible).isFalse()
@@ -182,7 +182,7 @@ import org.junit.Test
skipItems(2)
awaitItem().also { state ->
assertThat(state.knockRequests).hasSize(1)
state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
state.eventSink(KnockRequestsBannerEvent.AcceptSingleRequest)
}
awaitItem().also { state ->
assertThat(state.isVisible).isFalse()
@@ -206,7 +206,7 @@ import org.junit.Test
presenter.test {
skipItems(2)
awaitItem().also { state ->
state.eventSink(KnockRequestsBannerEvents.Dismiss)
state.eventSink(KnockRequestsBannerEvent.Dismiss)
}
advanceUntilIdle()
assert(markAsSeenLambda).isCalledExactly(3)
@@ -29,7 +29,7 @@ import org.junit.Test
class KnockRequestsBannerViewTest : RobolectricTest() {
@Test
fun `clicking on view on single request invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvent>(expectEvents = false)
ensureCalledOnce {
setKnockRequestsBannerView(
state = aKnockRequestsBannerState(
@@ -43,7 +43,7 @@ class KnockRequestsBannerViewTest : RobolectricTest() {
@Test
fun `clicking on view all when multiple requests invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvent>(expectEvents = false)
ensureCalledOnce {
setKnockRequestsBannerView(
state = aKnockRequestsBannerState(
@@ -62,19 +62,19 @@ class KnockRequestsBannerViewTest : RobolectricTest() {
@Test
fun `clicking on accept on a single request emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvent>()
setKnockRequestsBannerView(
state = aKnockRequestsBannerState(
eventSink = eventsRecorder,
),
)
clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest)
eventsRecorder.assertSingle(KnockRequestsBannerEvent.AcceptSingleRequest)
}
@Test
fun `clicking on dismiss emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsBannerEvent>()
setKnockRequestsBannerView(
state = aKnockRequestsBannerState(
eventSink = eventsRecorder,
@@ -82,7 +82,7 @@ class KnockRequestsBannerViewTest : RobolectricTest() {
)
val close = activity!!.getString(CommonStrings.action_close)
onNodeWithContentDescription(close).performClick()
eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss)
eventsRecorder.assertSingle(KnockRequestsBannerEvent.Dismiss)
}
}
@@ -65,7 +65,7 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable))
state.eventSink(KnockRequestsListEvent.Accept(knockRequestPresentable))
}
skipItems(1)
awaitItem().also { state ->
@@ -75,7 +75,7 @@ class KnockRequestsListPresenterTest {
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -99,7 +99,7 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable))
state.eventSink(KnockRequestsListEvent.Accept(knockRequestPresentable))
}
skipItems(1)
awaitItem().also { state ->
@@ -109,14 +109,14 @@ class KnockRequestsListPresenterTest {
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(KnockRequestsListEvents.RetryCurrentAction)
state.eventSink(KnockRequestsListEvent.RetryCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(1)
awaitItem().also { state ->
@@ -140,21 +140,21 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
state.eventSink(KnockRequestsListEvents.Decline(knockRequestPresentable))
state.eventSink(KnockRequestsListEvent.Decline(knockRequestPresentable))
}
skipItems(1)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
assertThat(state.currentAction).isEqualTo(KnockRequestsAction.Decline(knockRequestPresentable))
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -178,21 +178,21 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequestPresentable))
state.eventSink(KnockRequestsListEvent.DeclineAndBan(knockRequestPresentable))
}
skipItems(1)
awaitItem().also { state ->
val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!!
assertThat(state.currentAction).isEqualTo(KnockRequestsAction.DeclineAndBan(knockRequestPresentable))
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -220,20 +220,20 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
assertThat(state.canAcceptAll).isTrue()
state.eventSink(KnockRequestsListEvents.AcceptAll)
state.eventSink(KnockRequestsListEvent.AcceptAll)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.currentAction).isEqualTo(KnockRequestsAction.AcceptAll)
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java)
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -262,20 +262,20 @@ class KnockRequestsListPresenterTest {
skipItems(2)
awaitItem().also { state ->
assertThat(state.canAcceptAll).isTrue()
state.eventSink(KnockRequestsListEvents.AcceptAll)
state.eventSink(KnockRequestsListEvent.AcceptAll)
}
skipItems(1)
awaitItem().also { state ->
assertThat(state.currentAction).isEqualTo(KnockRequestsAction.AcceptAll)
assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java)
state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction)
state.eventSink(KnockRequestsListEvent.ConfirmCurrentAction)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java)
}
awaitItem().also { state ->
assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java)
state.eventSink(KnockRequestsListEvents.ResetCurrentAction)
state.eventSink(KnockRequestsListEvent.ResetCurrentAction)
}
skipItems(2)
awaitItem().also { state ->
@@ -31,7 +31,7 @@ import org.junit.Test
class KnockRequestsListViewTest : RobolectricTest() {
@Test
fun `clicking on back invoke the expected callback`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>(expectEvents = false)
val eventsRecorder = EventsRecorder<KnockRequestsListEvent>(expectEvents = false)
ensureCalledOnce {
setKnockRequestsListView(
aKnockRequestsListState(
@@ -45,7 +45,7 @@ class KnockRequestsListViewTest : RobolectricTest() {
@Test
fun `clicking on accept emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsListEvent>()
val knockRequest = aKnockRequestPresentable()
setKnockRequestsListView(
aKnockRequestsListState(
@@ -54,12 +54,12 @@ class KnockRequestsListViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_accept)
eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest))
eventsRecorder.assertSingle(KnockRequestsListEvent.Accept(knockRequest))
}
@Test
fun `clicking on decline emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsListEvent>()
val knockRequest = aKnockRequestPresentable()
setKnockRequestsListView(
aKnockRequestsListState(
@@ -68,12 +68,12 @@ class KnockRequestsListViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_decline)
eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest))
eventsRecorder.assertSingle(KnockRequestsListEvent.Decline(knockRequest))
}
@Test
fun `clicking on decline and ban emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsListEvent>()
val knockRequest = aKnockRequestPresentable()
setKnockRequestsListView(
aKnockRequestsListState(
@@ -82,12 +82,12 @@ class KnockRequestsListViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title)
eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest))
eventsRecorder.assertSingle(KnockRequestsListEvent.DeclineAndBan(knockRequest))
}
@Test
fun `clicking on accept all emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsListEvent>()
val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
setKnockRequestsListView(
aKnockRequestsListState(
@@ -96,12 +96,12 @@ class KnockRequestsListViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_knock_requests_list_accept_all_button_title)
eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll)
eventsRecorder.assertSingle(KnockRequestsListEvent.AcceptAll)
}
@Test
fun `retry on async view retry emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsListEvent>()
val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
setKnockRequestsListView(
aKnockRequestsListState(
@@ -112,12 +112,12 @@ class KnockRequestsListViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_retry)
eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction)
eventsRecorder.assertSingle(KnockRequestsListEvent.RetryCurrentAction)
}
@Test
fun `canceling async view emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsListEvent>()
val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
setKnockRequestsListView(
aKnockRequestsListState(
@@ -128,12 +128,12 @@ class KnockRequestsListViewTest : RobolectricTest() {
),
)
clickOn(CommonStrings.action_cancel)
eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction)
eventsRecorder.assertSingle(KnockRequestsListEvent.ResetCurrentAction)
}
@Test
fun `confirming async view emit the expected event`() = runAndroidComposeUiTest {
val eventsRecorder = EventsRecorder<KnockRequestsListEvents>()
val eventsRecorder = EventsRecorder<KnockRequestsListEvent>()
val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable())
setKnockRequestsListView(
aKnockRequestsListState(
@@ -144,7 +144,7 @@ class KnockRequestsListViewTest : RobolectricTest() {
),
)
clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title)
eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction)
eventsRecorder.assertSingle(KnockRequestsListEvent.ConfirmCurrentAction)
}
}
@@ -41,9 +41,9 @@ class DefaultPermissionsPresenter(
permissionsRequested = true
}
fun handleEvent(event: PermissionsEvents) {
fun handleEvent(event: PermissionsEvent) {
when (event) {
PermissionsEvents.RequestPermissions -> {
PermissionsEvent.RequestPermissions -> {
multiplePermissionsState.launchMultiplePermissionRequest()
}
}
@@ -8,6 +8,6 @@
package io.element.android.features.location.impl.common.permissions
sealed interface PermissionsEvents {
data object RequestPermissions : PermissionsEvents
sealed interface PermissionsEvent {
data object RequestPermissions : PermissionsEvent
}
@@ -12,7 +12,7 @@ data class PermissionsState(
val permissions: Permissions,
val shouldShowRationale: Boolean,
val permissionsAlreadyRequested: Boolean,
val eventSink: (PermissionsEvents) -> Unit,
val eventSink: (PermissionsEvent) -> Unit,
) {
sealed interface Permissions {
data object AllGranted : Permissions
@@ -29,7 +29,7 @@ import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.SendLiveLocationPermissions
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.location.impl.common.checkLocationConstraints
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsEvent
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.sendLiveLocationPermissions
@@ -111,7 +111,7 @@ class ShareLocationPresenter(
sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED
)
if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
permissionsState.eventSink(PermissionsEvent.RequestPermissions)
}
trackUserPosition = locationConstraints is LocationConstraintsCheck.Success
dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
@@ -137,7 +137,7 @@ class ShareLocationPresenter(
}
else -> {
if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
permissionsState.eventSink(PermissionsEvent.RequestPermissions)
}
dialogState = ShareLocationState.Dialog.Constraints(locationConstraints.toDialogState())
}
@@ -195,7 +195,7 @@ class ShareLocationPresenter(
}
ShareLocationEvent.RequestPermissions -> {
dialogState = ShareLocationState.Dialog.None
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
permissionsState.eventSink(PermissionsEvent.RequestPermissions)
}
}
}
@@ -32,7 +32,7 @@ import io.element.android.features.location.impl.common.MapDefaults
import io.element.android.features.location.impl.common.SendLiveLocationPermissions
import io.element.android.features.location.impl.common.actions.LocationActions
import io.element.android.features.location.impl.common.checkLocationConstraints
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
import io.element.android.features.location.impl.common.permissions.PermissionsEvent
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
import io.element.android.features.location.impl.common.permissions.PermissionsState
import io.element.android.features.location.impl.common.toDialogState
@@ -99,7 +99,7 @@ class ShowLocationPresenter(
sendLiveLocationPermissions = SendLiveLocationPermissions.GRANTED
)
if (locationConstraints is LocationConstraintsCheck.PermissionShouldBeRequested) {
permissionsState.eventSink(PermissionsEvents.RequestPermissions)
permissionsState.eventSink(PermissionsEvent.RequestPermissions)
}
isTrackMyLocation = locationConstraints is LocationConstraintsCheck.Success
dialogState = locationConstraints.toDialogState()
@@ -130,7 +130,7 @@ class ShowLocationPresenter(
locationActions.openLocationSettings()
dialogState = LocationConstraintsDialogState.None
}
ShowLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvents.RequestPermissions)
ShowLocationEvent.RequestPermissions -> permissionsState.eventSink(PermissionsEvent.RequestPermissions)
ShowLocationEvent.StopLocationSharing -> coroutineScope.launch {
liveLocationShareManager.stopShare(joinedRoom.roomId)
}
@@ -11,9 +11,9 @@ package io.element.android.features.location.impl.common.permissions
import androidx.compose.runtime.Composable
class FakePermissionsPresenter : PermissionsPresenter {
val events = mutableListOf<PermissionsEvents>()
val events = mutableListOf<PermissionsEvent>()
private fun handleEvent(event: PermissionsEvents) {
private fun handleEvent(event: PermissionsEvent) {
events += event
}

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