Invite users to existing rooms (#441)

Invite users to existing rooms

Scope:

- Allow inviting from the room detail screen and the member list
- Invite option is only shown if the user has the correct power level
- Search flow the same as creating a new room, allowing multi-select
- Existing room members/invitees are disabled with a custom caption
- Sending is asynchronous, an error dialog will appear wherever the
  user is if necessary

Closes #245
This commit is contained in:
Chris Smith 2023-05-23 10:23:24 +01:00 committed by GitHub
parent 6825d8ac2b
commit 198d6d4c56
85 changed files with 1668 additions and 69 deletions

View file

@ -17,24 +17,30 @@
package io.element.android.appnav.root
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import io.element.android.features.rageshake.api.crash.CrashDetectionPresenter
import io.element.android.features.rageshake.api.detection.RageshakeDetectionPresenter
import io.element.android.libraries.architecture.Presenter
import io.element.android.services.apperror.api.AppErrorStateService
import javax.inject.Inject
class RootPresenter @Inject constructor(
private val crashDetectionPresenter: CrashDetectionPresenter,
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
private val appErrorStateService: AppErrorStateService,
) : Presenter<RootState> {
@Composable
override fun present(): RootState {
val rageshakeDetectionState = rageshakeDetectionPresenter.present()
val crashDetectionState = crashDetectionPresenter.present()
val appErrorState by appErrorStateService.appErrorStateFlow.collectAsState()
return RootState(
rageshakeDetectionState = rageshakeDetectionState,
crashDetectionState = crashDetectionState,
errorState = appErrorState,
)
}
}

View file

@ -19,9 +19,11 @@ package io.element.android.appnav.root
import androidx.compose.runtime.Immutable
import io.element.android.features.rageshake.api.crash.CrashDetectionState
import io.element.android.features.rageshake.api.detection.RageshakeDetectionState
import io.element.android.services.apperror.api.AppErrorState
@Immutable
data class RootState(
val rageshakeDetectionState: RageshakeDetectionState,
val crashDetectionState: CrashDetectionState,
val errorState: AppErrorState,
)

View file

@ -19,6 +19,8 @@ package io.element.android.appnav.root
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.rageshake.api.crash.aCrashDetectionState
import io.element.android.features.rageshake.api.detection.aRageshakeDetectionState
import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.aAppErrorState
open class RootStateProvider : PreviewParameterProvider<RootState> {
override val values: Sequence<RootState>
@ -30,6 +32,9 @@ open class RootStateProvider : PreviewParameterProvider<RootState> {
aRootState().copy(
rageshakeDetectionState = aRageshakeDetectionState().copy(showDialog = true),
crashDetectionState = aCrashDetectionState().copy(crashDetected = false),
),
aRootState().copy(
errorState = aAppErrorState(),
)
)
}
@ -37,4 +42,5 @@ open class RootStateProvider : PreviewParameterProvider<RootState> {
fun aRootState() = RootState(
rageshakeDetectionState = aRageshakeDetectionState(),
crashDetectionState = aCrashDetectionState(),
errorState = AppErrorState.NoError,
)

View file

@ -31,6 +31,7 @@ import io.element.android.features.rageshake.api.detection.RageshakeDetectionVie
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.services.apperror.impl.AppErrorView
@Composable
fun RootView(
@ -60,6 +61,9 @@ fun RootView(
state = state.crashDetectionState,
onOpenBugReport = ::onOpenBugReport,
)
AppErrorView(
state = state.errorState,
)
}
}

View file

@ -28,6 +28,9 @@ import io.element.android.features.rageshake.test.crash.FakeCrashDataStore
import io.element.android.features.rageshake.test.rageshake.FakeRageShake
import io.element.android.features.rageshake.test.rageshake.FakeRageshakeDataStore
import io.element.android.features.rageshake.test.screenshot.FakeScreenshotHolder
import io.element.android.services.apperror.api.AppErrorState
import io.element.android.services.apperror.api.AppErrorStateService
import io.element.android.services.apperror.impl.DefaultAppErrorStateService
import kotlinx.coroutines.test.runTest
import org.junit.Test
@ -44,7 +47,32 @@ class RootPresenterTest {
}
}
private fun createPresenter(): RootPresenter {
@Test
fun `present - passes app error state`() = runTest {
val presenter = createPresenter(
appErrorService = DefaultAppErrorStateService().apply {
showError("Bad news", "Something bad happened")
}
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
val initialState = awaitItem()
assertThat(initialState.errorState).isInstanceOf(AppErrorState.Error::class.java)
val initialErrorState = initialState.errorState as AppErrorState.Error
assertThat(initialErrorState.title).isEqualTo("Bad news")
assertThat(initialErrorState.body).isEqualTo("Something bad happened")
initialErrorState.dismiss()
assertThat(awaitItem().errorState).isInstanceOf(AppErrorState.NoError::class.java)
}
}
private fun createPresenter(
appErrorService: AppErrorStateService = DefaultAppErrorStateService()
): RootPresenter {
val crashDataStore = FakeCrashDataStore()
val rageshakeDataStore = FakeRageshakeDataStore()
val rageshake = FakeRageShake()
@ -63,6 +91,7 @@ class RootPresenterTest {
return RootPresenter(
crashDetectionPresenter = crashDetectionPresenter,
rageshakeDetectionPresenter = rageshakeDetectionPresenter,
appErrorStateService = appErrorService,
)
}
}