mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-2382 Multiplayer | Fix | Handle corner-case scenarios when handling space invite (#1070)
This commit is contained in:
parent
5a82c78f6a
commit
680f7fde42
10 changed files with 243 additions and 44 deletions
|
@ -3,8 +3,10 @@ package com.anytypeio.anytype.di.feature.multiplayer
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.anytypeio.anytype.core_utils.di.scope.PerDialog
|
||||
import com.anytypeio.anytype.di.common.ComponentDependencies
|
||||
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver
|
||||
import com.anytypeio.anytype.domain.workspace.SpaceManager
|
||||
|
@ -54,6 +56,8 @@ object RequestJoinSpaceModule {
|
|||
|
||||
interface RequestJoinSpaceDependencies : ComponentDependencies {
|
||||
fun blockRepository(): BlockRepository
|
||||
fun auth(): AuthRepository
|
||||
fun settings(): UserSettingsRepository
|
||||
fun urlBuilder(): UrlBuilder
|
||||
fun dispatchers(): AppCoroutineDispatchers
|
||||
fun spaceManager(): SpaceManager
|
||||
|
|
|
@ -8,18 +8,26 @@ import androidx.compose.material.MaterialTheme
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE
|
||||
import com.anytypeio.anytype.core_ui.features.multiplayer.JoinSpaceScreen
|
||||
import com.anytypeio.anytype.core_ui.foundation.AlertConfig
|
||||
import com.anytypeio.anytype.core_ui.foundation.BUTTON_SECONDARY
|
||||
import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_BLUE
|
||||
import com.anytypeio.anytype.core_ui.foundation.GenericAlert
|
||||
import com.anytypeio.anytype.core_ui.foundation.Warning
|
||||
import com.anytypeio.anytype.core_utils.ext.arg
|
||||
import com.anytypeio.anytype.core_utils.ext.toast
|
||||
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.presentation.common.ViewState
|
||||
import com.anytypeio.anytype.presentation.common.TypedViewState
|
||||
import com.anytypeio.anytype.presentation.multiplayer.RequestJoinSpaceViewModel
|
||||
import com.anytypeio.anytype.presentation.multiplayer.RequestJoinSpaceViewModel.ErrorView
|
||||
import com.anytypeio.anytype.ui.settings.typography
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -42,19 +50,51 @@ class RequestJoinSpaceFragment : BaseBottomSheetComposeFragment() {
|
|||
setContent {
|
||||
MaterialTheme(typography = typography) {
|
||||
when(val state = vm.state.collectAsStateWithLifecycle().value) {
|
||||
is ViewState.Error -> {
|
||||
toast(state.error).also { dismiss() }
|
||||
}
|
||||
ViewState.Loading -> {
|
||||
is TypedViewState.Loading -> {
|
||||
// Render nothing.
|
||||
}
|
||||
is ViewState.Success -> {
|
||||
is TypedViewState.Success -> {
|
||||
JoinSpaceScreen(
|
||||
onRequestJoinSpaceClicked = vm::onRequestToJoinClicked,
|
||||
spaceName = state.data.spaceName,
|
||||
createdByName = state.data.creatorName
|
||||
)
|
||||
}
|
||||
is TypedViewState.Error -> {
|
||||
when(val err = state.error) {
|
||||
is ErrorView.AlreadySpaceMember -> {
|
||||
Warning(
|
||||
title = stringResource(id = R.string.multiplayer_already_space_member),
|
||||
subtitle = EMPTY_STRING_VALUE,
|
||||
actionButtonText = stringResource(id = R.string.multiplayer_open_space),
|
||||
cancelButtonText = stringResource(id = R.string.cancel),
|
||||
onNegativeClick = {
|
||||
dismiss()
|
||||
},
|
||||
onPositiveClick = {
|
||||
vm.onOpenSpaceClicked(err.space)
|
||||
}
|
||||
)
|
||||
}
|
||||
is ErrorView.InvalidLink -> {
|
||||
GenericAlert(
|
||||
config = AlertConfig.WithOneButton(
|
||||
title = "This link does not seem to work",
|
||||
firstButtonText = stringResource(id = R.string.button_okay),
|
||||
firstButtonType = BUTTON_SECONDARY,
|
||||
description = EMPTY_STRING_VALUE,
|
||||
icon = AlertConfig.Icon(
|
||||
gradient = GRADIENT_TYPE_BLUE,
|
||||
icon = R.drawable.ic_alert_message
|
||||
)
|
||||
),
|
||||
onFirstButtonClicked = {
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
vm.toasts.collect { toast(it) }
|
||||
|
@ -77,6 +117,12 @@ class RequestJoinSpaceFragment : BaseBottomSheetComposeFragment() {
|
|||
RequestJoinSpaceViewModel.Command.Toast.RequestSent -> {
|
||||
toast(getString(R.string.multiplayer_request_sent_toast))
|
||||
}
|
||||
RequestJoinSpaceViewModel.Command.Toast.SpaceDeleted -> {
|
||||
toast(getString(R.string.multiplayer_error_space_deleted))
|
||||
}
|
||||
RequestJoinSpaceViewModel.Command.Toast.SpaceNotFound -> {
|
||||
toast(getString(R.string.multiplayer_error_space_not_found))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,4 +44,10 @@ enum class SpaceAccessType(val code: Int) {
|
|||
PRIVATE(0),
|
||||
DEFAULT(1),
|
||||
SHARED(2)
|
||||
}
|
||||
|
||||
sealed class SpaceInviteError : Exception() {
|
||||
class SpaceNotFound : SpaceInviteError()
|
||||
class SpaceDeleted: SpaceInviteError()
|
||||
class InvalidInvite: SpaceInviteError()
|
||||
}
|
|
@ -232,17 +232,21 @@ fun Warning(
|
|||
style = HeadlineHeading,
|
||||
color = colorResource(R.color.text_primary)
|
||||
)
|
||||
Text(
|
||||
text = subtitle,
|
||||
modifier = Modifier.padding(
|
||||
top = 12.dp,
|
||||
start = 20.dp,
|
||||
end = 20.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
style = BodyCalloutRegular,
|
||||
color = colorResource(R.color.text_primary)
|
||||
)
|
||||
if (subtitle.isNotEmpty()) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
modifier = Modifier.padding(
|
||||
top = 12.dp,
|
||||
start = 20.dp,
|
||||
end = 20.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
style = BodyCalloutRegular,
|
||||
color = colorResource(R.color.text_primary)
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
|
|
|
@ -45,7 +45,7 @@ import com.anytypeio.anytype.core_ui.views.HeadlineHeading
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AlertWithTwoButtons() {
|
||||
private fun AlertWithTwoButtons() {
|
||||
GenericAlert(
|
||||
onFirstButtonClicked = {},
|
||||
onSecondButtonClicked = {},
|
||||
|
@ -66,7 +66,7 @@ fun AlertWithTwoButtons() {
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AlertWithWarningAndTwoButtons() {
|
||||
private fun AlertWithWarningAndTwoButtons() {
|
||||
GenericAlert(
|
||||
onFirstButtonClicked = {},
|
||||
onSecondButtonClicked = {},
|
||||
|
@ -86,8 +86,8 @@ fun AlertWithWarningAndTwoButtons() {
|
|||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AlertWithWarningButton() {
|
||||
@Composable
|
||||
private fun AlertWithWarningButton() {
|
||||
GenericAlert(
|
||||
onFirstButtonClicked = {},
|
||||
onSecondButtonClicked = {},
|
||||
|
@ -108,7 +108,7 @@ fun AlertWithWarningButton() {
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
fun AlertWithMessageButton() {
|
||||
private fun AlertWithMessageButton() {
|
||||
GenericAlert(
|
||||
onFirstButtonClicked = {},
|
||||
onSecondButtonClicked = {},
|
||||
|
@ -139,8 +139,10 @@ fun GenericAlert(
|
|||
if (icon != null) { AlertIcon(icon) }
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
AlertTitle(config.title)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AlertDescription(config.description)
|
||||
if (config.withDescription) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AlertDescription(config.description)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
AlertButtons(
|
||||
config = config,
|
||||
|
@ -332,6 +334,8 @@ sealed class AlertConfig {
|
|||
abstract val description: String
|
||||
abstract val icon: Icon?
|
||||
|
||||
val withDescription get() = description.isNotEmpty()
|
||||
|
||||
data class WithTwoButtons(
|
||||
override val icon: Icon?,
|
||||
override val title: String,
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
package com.anytypeio.anytype.domain.multiplayer
|
||||
|
||||
import com.anytypeio.anytype.core_models.DVFilter
|
||||
import com.anytypeio.anytype.core_models.DVFilterCondition
|
||||
import com.anytypeio.anytype.core_models.ObjectType
|
||||
import com.anytypeio.anytype.core_models.Relations
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.ResultInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class CheckIsUserSpaceMember @Inject constructor(
|
||||
private val repo: BlockRepository,
|
||||
private val auth: AuthRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
) : ResultInteractor<SpaceId, Boolean>(dispatchers.io) {
|
||||
|
||||
override suspend fun doWork(params: SpaceId): Boolean {
|
||||
val account = auth.getCurrentAccountId()
|
||||
val results = repo.searchObjects(
|
||||
limit = 1,
|
||||
filters = buildList {
|
||||
add(
|
||||
DVFilter(
|
||||
relation = Relations.LAYOUT,
|
||||
value = ObjectType.Layout.PARTICIPANT.code.toDouble(),
|
||||
condition = DVFilterCondition.EQUAL
|
||||
)
|
||||
)
|
||||
add(
|
||||
DVFilter(
|
||||
relation = Relations.IDENTITY,
|
||||
value = account,
|
||||
condition = DVFilterCondition.EQUAL
|
||||
)
|
||||
)
|
||||
add(
|
||||
DVFilter(
|
||||
relation = Relations.SPACE_ID,
|
||||
value = params.id,
|
||||
condition = DVFilterCondition.EQUAL
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
return results.isNotEmpty()
|
||||
}
|
||||
}
|
|
@ -1348,7 +1348,11 @@
|
|||
<string name="multiplayer_how_to_share_space_step_3">After approving the request, you can choose the access rights for that person.</string>
|
||||
<string name="multiplayer_delete_space_sharing_link">Delete sharing link</string>
|
||||
<string name="multiplayer_delete_link">Delete link</string>
|
||||
<string name="multiplayer_delete_link_description">New members won’t be able to join the space. You can generate a new link anytype.</string>
|
||||
<string name="multiplayer_delete_link_description">New members won’t be able to join the space. You can generate a new link anytime.</string>
|
||||
<string name="multiplayer_already_space_member">You are already a member of this space</string>
|
||||
<string name="multiplayer_open_space">Open space</string>
|
||||
<string name="multiplayer_error_space_deleted">Space deleted</string>
|
||||
<string name="multiplayer_error_space_not_found">Space not found</string>
|
||||
|
||||
<!--endregion-->
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.anytypeio.anytype.core_models.exceptions.AccountIsDeletedException
|
|||
import com.anytypeio.anytype.core_models.exceptions.LoginException
|
||||
import com.anytypeio.anytype.core_models.exceptions.MigrationNeededException
|
||||
import com.anytypeio.anytype.core_models.exceptions.NeedToUpdateApplicationException
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteError
|
||||
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
|
||||
import com.anytypeio.anytype.data.auth.exception.AnytypeNeedsUpgradeException
|
||||
import com.anytypeio.anytype.data.auth.exception.NotFoundObjectException
|
||||
|
@ -1827,7 +1828,23 @@ class MiddlewareServiceImplementation @Inject constructor(
|
|||
val response = Rpc.Space.InviteView.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Space.InviteView.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
when(error.code) {
|
||||
Rpc.Space.InviteView.Response.Error.Code.NO_SUCH_SPACE -> {
|
||||
throw SpaceInviteError.SpaceNotFound()
|
||||
}
|
||||
Rpc.Space.InviteView.Response.Error.Code.SPACE_IS_DELETED -> {
|
||||
throw SpaceInviteError.SpaceDeleted()
|
||||
}
|
||||
Rpc.Space.InviteView.Response.Error.Code.INVITE_NOT_FOUND -> {
|
||||
throw SpaceInviteError.InvalidInvite()
|
||||
}
|
||||
Rpc.Space.InviteView.Response.Error.Code.INVITE_BAD_SIGNATURE -> {
|
||||
throw SpaceInviteError.InvalidInvite()
|
||||
}
|
||||
else -> {
|
||||
throw Exception(error.description)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -3,5 +3,11 @@ package com.anytypeio.anytype.presentation.common
|
|||
sealed class ViewState<out T : Any> {
|
||||
data class Success<out T : Any>(val data: T) : ViewState<T>()
|
||||
data class Error(val error: String) : ViewState<Nothing>()
|
||||
object Loading : ViewState<Nothing>()
|
||||
data object Loading : ViewState<Nothing>()
|
||||
}
|
||||
|
||||
sealed class TypedViewState<out T : Any, out E: Any> {
|
||||
data class Success<out T : Any, out E: Any>(val data: T) : TypedViewState<T, E>()
|
||||
data class Error<out T : Any, out E: Any>(val error: E) : TypedViewState<T, E>()
|
||||
data object Loading : TypedViewState<Nothing, Nothing>()
|
||||
}
|
|
@ -3,14 +3,20 @@ package com.anytypeio.anytype.presentation.multiplayer
|
|||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteError
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteView
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_utils.ext.msg
|
||||
import com.anytypeio.anytype.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.base.getOrDefault
|
||||
import com.anytypeio.anytype.domain.multiplayer.CheckIsUserSpaceMember
|
||||
import com.anytypeio.anytype.domain.multiplayer.GetSpaceInviteView
|
||||
import com.anytypeio.anytype.domain.multiplayer.SendJoinSpaceRequest
|
||||
import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver
|
||||
import com.anytypeio.anytype.domain.spaces.SaveCurrentSpace
|
||||
import com.anytypeio.anytype.domain.workspace.SpaceManager
|
||||
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
||||
import com.anytypeio.anytype.presentation.common.ViewState
|
||||
import com.anytypeio.anytype.presentation.common.TypedViewState
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -21,10 +27,13 @@ class RequestJoinSpaceViewModel(
|
|||
private val params: Params,
|
||||
private val getSpaceInviteView: GetSpaceInviteView,
|
||||
private val sendJoinSpaceRequest: SendJoinSpaceRequest,
|
||||
private val spaceInviteResolver: SpaceInviteResolver
|
||||
private val spaceInviteResolver: SpaceInviteResolver,
|
||||
private val checkIsUserSpaceMember: CheckIsUserSpaceMember,
|
||||
private val spaceManager: SpaceManager,
|
||||
private val saveCurrentSpace: SaveCurrentSpace
|
||||
) : BaseViewModel() {
|
||||
|
||||
val state = MutableStateFlow<ViewState<SpaceInviteView>>(ViewState.Loading)
|
||||
val state = MutableStateFlow<TypedViewState<SpaceInviteView, ErrorView>>(TypedViewState.Loading)
|
||||
val commands = MutableSharedFlow<Command>(0)
|
||||
|
||||
init {
|
||||
|
@ -43,28 +52,48 @@ class RequestJoinSpaceViewModel(
|
|||
)
|
||||
).fold(
|
||||
onSuccess = { view ->
|
||||
state.value = ViewState.Success(view)
|
||||
val isAlreadyMember = checkIsUserSpaceMember
|
||||
.async(view.space)
|
||||
.getOrDefault(false)
|
||||
if (isAlreadyMember) {
|
||||
state.value = TypedViewState.Error(
|
||||
ErrorView.AlreadySpaceMember(view.space)
|
||||
)
|
||||
} else {
|
||||
state.value = TypedViewState.Success(view)
|
||||
}
|
||||
},
|
||||
onFailure = { e ->
|
||||
if (e is SpaceInviteError) {
|
||||
when(e) {
|
||||
is SpaceInviteError.InvalidInvite -> {
|
||||
state.value = TypedViewState.Error(
|
||||
ErrorView.InvalidLink
|
||||
)
|
||||
}
|
||||
is SpaceInviteError.SpaceDeleted -> {
|
||||
commands.emit(Command.Toast.SpaceDeleted)
|
||||
commands.emit(Command.Dismiss)
|
||||
}
|
||||
is SpaceInviteError.SpaceNotFound -> {
|
||||
commands.emit(Command.Toast.SpaceNotFound)
|
||||
commands.emit(Command.Dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
Timber.e(e, "Error while getting space invite view")
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Timber.e("Could not parse invite link: ${params.link}")
|
||||
state.value = ViewState.Error("Could not parse invite link: ${params.link}")
|
||||
Timber.w("Could not parse invite link: ${params.link}")
|
||||
state.value = TypedViewState.Error(ErrorView.InvalidLink)
|
||||
}
|
||||
}
|
||||
|
||||
fun onRequestToJoinClicked() {
|
||||
when(val curr = state.value) {
|
||||
is ViewState.Error -> {
|
||||
// Do nothing.
|
||||
}
|
||||
is ViewState.Loading -> {
|
||||
// Do nothing.
|
||||
}
|
||||
is ViewState.Success -> {
|
||||
is TypedViewState.Success -> {
|
||||
viewModelScope.launch {
|
||||
val fileKey = spaceInviteResolver.parseFileKey(params.link)
|
||||
val contentId = spaceInviteResolver.parseContentId(params.link)
|
||||
|
@ -89,6 +118,22 @@ class RequestJoinSpaceViewModel(
|
|||
)
|
||||
}
|
||||
}
|
||||
} else -> {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onOpenSpaceClicked(space: SpaceId) {
|
||||
viewModelScope.launch {
|
||||
val curr = spaceManager.get()
|
||||
if (curr == space.id) {
|
||||
commands.emit(Command.Dismiss)
|
||||
} else {
|
||||
spaceManager.set(space.id)
|
||||
saveCurrentSpace.async(params = SaveCurrentSpace.Params(space))
|
||||
// TODO navigate to the target space instead of dismissing
|
||||
commands.emit(Command.Dismiss)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -97,14 +142,20 @@ class RequestJoinSpaceViewModel(
|
|||
private val params: Params,
|
||||
private val getSpaceInviteView: GetSpaceInviteView,
|
||||
private val sendJoinSpaceRequest: SendJoinSpaceRequest,
|
||||
private val spaceInviteResolver: SpaceInviteResolver
|
||||
private val spaceInviteResolver: SpaceInviteResolver,
|
||||
private val checkIsUserSpaceMember: CheckIsUserSpaceMember,
|
||||
private val saveCurrentSpace: SaveCurrentSpace,
|
||||
private val spaceManager: SpaceManager
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = RequestJoinSpaceViewModel(
|
||||
params = params,
|
||||
getSpaceInviteView = getSpaceInviteView,
|
||||
sendJoinSpaceRequest = sendJoinSpaceRequest,
|
||||
spaceInviteResolver = spaceInviteResolver
|
||||
spaceInviteResolver = spaceInviteResolver,
|
||||
checkIsUserSpaceMember = checkIsUserSpaceMember,
|
||||
saveCurrentSpace = saveCurrentSpace,
|
||||
spaceManager = spaceManager
|
||||
) as T
|
||||
}
|
||||
|
||||
|
@ -112,8 +163,15 @@ class RequestJoinSpaceViewModel(
|
|||
|
||||
sealed class Command {
|
||||
sealed class Toast : Command() {
|
||||
object RequestSent : Toast()
|
||||
data object RequestSent : Toast()
|
||||
data object SpaceNotFound : Toast()
|
||||
data object SpaceDeleted : Toast()
|
||||
}
|
||||
object Dismiss: Command()
|
||||
data object Dismiss: Command()
|
||||
}
|
||||
|
||||
sealed class ErrorView {
|
||||
data object InvalidLink : ErrorView()
|
||||
data class AlreadySpaceMember(val space: SpaceId) : ErrorView()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue