From 300709aaa272d9590041084c7c5df86d549e4957 Mon Sep 17 00:00:00 2001 From: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com> Date: Tue, 20 May 2025 16:39:00 +0200 Subject: [PATCH] DROID-3659 Multiplayer | Invite link without approve (#2407) --- .../anytypeio/anytype/ui/main/MainActivity.kt | 28 +- .../multiplayer/RequestJoinSpaceFragment.kt | 65 +++-- .../core_models/multiplayer/InviteType.kt | 7 + .../core_models/multiplayer/Multiplayer.kt | 3 +- .../core_ui/features/multiplayer/Joining.kt | 240 ++++++++++++------ .../res/drawable/ic_join_without_approve.xml | 9 + .../auth/repo/block/BlockDataRepository.kt | 13 +- .../data/auth/repo/block/BlockRemote.kt | 7 +- .../domain/block/repo/BlockRepository.kt | 8 +- .../multiplayer/GenerateSpaceInviteLink.kt | 21 +- localization/src/main/res/values/strings.xml | 7 + .../middleware/block/BlockMiddleware.kt | 13 +- .../middleware/interactor/Middleware.kt | 17 +- .../anytype/middleware/mappers/Alias.kt | 4 +- .../mappers/ToMiddlewareModelMappers.kt | 7 + .../multiplayer/RequestJoinSpaceViewModel.kt | 14 +- .../multiplayer/ShareSpaceViewModel.kt | 45 +++- 17 files changed, 375 insertions(+), 133 deletions(-) create mode 100644 core-models/src/main/java/com/anytypeio/anytype/core_models/multiplayer/InviteType.kt create mode 100644 core-ui/src/main/res/drawable/ic_join_without_approve.xml diff --git a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt index f10fce4173..b67c274f5f 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt @@ -460,14 +460,10 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr if (BuildConfig.DEBUG) Timber.d("Proceeding with share intent: $intent") when { intent.type == Mimetype.MIME_TEXT_PLAIN.value -> { - val raw = intent.getStringExtra(Intent.EXTRA_TEXT) ?: intent.dataString - if (raw != null) { - if (checkDeepLink && DefaultDeepLinkResolver.isDeepLink(raw)) { - vm.onNewDeepLink(DefaultDeepLinkResolver.resolve(raw)) - } else if (raw.isNotEmpty()) { - vm.onIntentTextShare(raw) - } - } + handleTextShare( + intent = intent, + checkDeepLink = checkDeepLink + ) } intent.type?.startsWith(SHARE_IMAGE_INTENT_PATTERN) == true -> { proceedWithImageShareIntent(intent) @@ -485,6 +481,22 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr } } + private fun handleTextShare(intent: Intent, checkDeepLink: Boolean) { + val raw = intent.getStringExtra(Intent.EXTRA_TEXT) ?: intent.dataString ?: return + + when { + checkDeepLink && DefaultDeepLinkResolver.isDeepLink(raw) -> { + vm.onNewDeepLink(DefaultDeepLinkResolver.resolve(raw)) + } + raw.isNotEmpty() && !DefaultDeepLinkResolver.isDeepLink(raw) -> { + vm.onIntentTextShare(raw) + } + else -> { + Timber.d("handleTextShare, skip handle intent :$raw") + } + } + } + private fun proceedWithFileShareIntent(intent: Intent) { if (intent.action == Intent.ACTION_SEND_MULTIPLE) { vm.onIntentMultipleFilesShare(intent.parseActionSendMultipleUris()) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt index f090b6e433..04fab586d1 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/multiplayer/RequestJoinSpaceFragment.kt @@ -8,12 +8,17 @@ import android.view.View import android.view.ViewGroup import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material.MaterialTheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.colorResource @@ -27,6 +32,8 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE import com.anytypeio.anytype.core_models.multiplayer.MultiplayerError import com.anytypeio.anytype.core_ui.features.multiplayer.JoinSpaceScreen +import com.anytypeio.anytype.core_ui.features.multiplayer.JoinSpaceWithoutApproveScreen +import com.anytypeio.anytype.core_ui.features.multiplayer.JoiningLoadingState import com.anytypeio.anytype.core_ui.foundation.AlertConfig import com.anytypeio.anytype.core_ui.foundation.Announcement import com.anytypeio.anytype.core_ui.foundation.BUTTON_SECONDARY @@ -40,7 +47,6 @@ import com.anytypeio.anytype.di.common.componentManager 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.presentation.spaces.Command import com.anytypeio.anytype.ui.home.HomeScreenFragment import com.anytypeio.anytype.ui.notifications.NotificationPermissionPromptDialog import com.anytypeio.anytype.ui.settings.typography @@ -89,39 +95,64 @@ class RequestJoinSpaceFragment : BaseBottomSheetComposeFragment() { } MaterialTheme(typography = typography) { val showModal = vm.showEnableNotificationDialog.collectAsStateWithLifecycle().value + val isLoadingInvite = vm.showLoadingInviteProgress.collectAsStateWithLifecycle().value when(val state = vm.state.collectAsStateWithLifecycle().value) { is TypedViewState.Loading, is TypedViewState.Success -> { val isLoading: Boolean val spaceName: String val createdByName: String + val withoutApprove : Boolean if (state is TypedViewState.Loading) { isLoading = true spaceName = stringResource(R.string.three_dots_text_placeholder) createdByName = stringResource(R.string.three_dots_text_placeholder) + withoutApprove = false } else { isLoading = vm.isRequestInProgress.collectAsStateWithLifecycle().value with(state as TypedViewState.Success) { spaceName = state.data.spaceName createdByName = state.data.creatorName + withoutApprove = state.data.withoutApprove } } - if (!showModal) { - JoinSpaceScreen( - isLoading = isLoading, - onRequestJoinSpaceClicked = vm::onRequestToJoinClicked, - spaceName = spaceName, - createdByName = createdByName - ) - } else { - ModalBottomSheet( - onDismissRequest = { - vm.onNotificationPromptDismissed() - }, - dragHandle = {}, - containerColor = colorResource(id = R.color.background_secondary), - sheetState = bottomSheetState - ) { + ModalBottomSheet( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars), + onDismissRequest = { + if (!isLoadingInvite) { + dismiss() + } + }, + dragHandle = {}, + containerColor = colorResource(id = R.color.background_secondary), + sheetState = bottomSheetState + ) { + if (isLoadingInvite) { + JoiningLoadingState( + onCancelLoadingInviteClicked = { + vm.onCancelLoadingInviteClicked() + dismiss() + } + ) + } else if (!showModal) { + if (withoutApprove) { + JoinSpaceWithoutApproveScreen( + isLoading = isLoading, + onRequestJoinSpaceClicked = vm::onRequestToJoinClicked, + spaceName = spaceName, + createdByName = createdByName + ) + } else { + JoinSpaceScreen( + isLoading = isLoading, + onRequestJoinSpaceClicked = vm::onRequestToJoinClicked, + spaceName = spaceName, + createdByName = createdByName, + ) + } + } else { Prompt( title = stringResource(R.string.notifications_prompt_get_notified), description = stringResource(R.string.notifications_prompt_description), diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/multiplayer/InviteType.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/multiplayer/InviteType.kt new file mode 100644 index 0000000000..4b1f3cd1b4 --- /dev/null +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/multiplayer/InviteType.kt @@ -0,0 +1,7 @@ +package com.anytypeio.anytype.core_models.multiplayer + +enum class InviteType(val code: Int) { + MEMBER(0), + GUEST(1), + WITHOUT_APPROVE(2); +} \ No newline at end of file diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/multiplayer/Multiplayer.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/multiplayer/Multiplayer.kt index 32d780aa25..95e56af38f 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/multiplayer/Multiplayer.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/multiplayer/Multiplayer.kt @@ -13,7 +13,8 @@ data class SpaceInviteView( val space: SpaceId, val spaceName: String, val creatorName: String, - val spaceIconContentId: String + val spaceIconContentId: String, + val withoutApprove: Boolean ) enum class ParticipantStatus( diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/multiplayer/Joining.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/multiplayer/Joining.kt index 823b6047d7..637265eeff 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/multiplayer/Joining.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/features/multiplayer/Joining.kt @@ -1,60 +1,35 @@ package com.anytypeio.anytype.core_ui.features.multiplayer -import androidx.compose.foundation.border +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.anytypeio.anytype.core_ui.ColorTextInputCursor import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.extensions.throttledClick -import com.anytypeio.anytype.core_ui.foundation.AlertConfig -import com.anytypeio.anytype.core_ui.foundation.AlertIcon import com.anytypeio.anytype.core_ui.foundation.Dragger -import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_BLUE -import com.anytypeio.anytype.core_ui.views.BodyRegular import com.anytypeio.anytype.core_ui.views.ButtonPrimaryLoading +import com.anytypeio.anytype.core_ui.views.ButtonSecondary import com.anytypeio.anytype.core_ui.views.ButtonSize -import com.anytypeio.anytype.core_ui.views.Caption1Medium import com.anytypeio.anytype.core_ui.views.Caption1Regular import com.anytypeio.anytype.core_ui.views.HeadlineHeading -import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular - -@Composable -@Preview -fun JoinSpaceScreenPreview() { - JoinSpaceScreen( - onRequestJoinSpaceClicked = {}, - spaceName = "Anytype Android App", - createdByName = "Konstantin" - ) -} - -@Composable -@Preview -fun JoinSpaceScreenPreviewWithEmptyNames() { - JoinSpaceScreen( - onRequestJoinSpaceClicked = {}, - spaceName = "", - createdByName = "" - ) -} +import com.anytypeio.anytype.core_ui.views.Title2 @Composable fun JoinSpaceScreen( @@ -64,7 +39,9 @@ fun JoinSpaceScreen( isLoading: Boolean = false ) { Column( - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) ) { Dragger( modifier = Modifier @@ -72,22 +49,21 @@ fun JoinSpaceScreen( .padding(vertical = 6.dp) ) Spacer(modifier = Modifier.height(16.dp)) - AlertIcon( - icon = AlertConfig.Icon( - gradient = GRADIENT_TYPE_BLUE, - icon = R.drawable.ic_alert_message - ) + Image( + modifier = Modifier.align(Alignment.CenterHorizontally), + painter = painterResource(R.drawable.ic_join_without_approve), + contentDescription = "Join without approve" ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(15.dp)) Text( text = stringResource(R.string.multiplayer_join_a_space), style = HeadlineHeading, color = colorResource(id = R.color.text_primary), modifier = Modifier.align(Alignment.CenterHorizontally) ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(8.dp)) Text( - style = BodyRegular, + style = Title2, color = colorResource(id = R.color.text_primary), modifier = Modifier.padding(horizontal = 48.dp), text = stringResource( @@ -97,9 +73,9 @@ fun JoinSpaceScreen( ), textAlign = TextAlign.Center ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(19.dp)) Box( - modifier = Modifier.padding(start = 20.dp, end = 20.dp) + modifier = Modifier ) { ButtonPrimaryLoading( onClick = throttledClick( @@ -124,47 +100,153 @@ fun JoinSpaceScreen( } @Composable -private fun CommentBox(commentInputValue: String): String { - var commentInputValue1 = commentInputValue - Box( +fun JoinSpaceWithoutApproveScreen( + onRequestJoinSpaceClicked: () -> Unit, + spaceName: String, + createdByName: String, + isLoading: Boolean = false +) { + Column( modifier = Modifier - .height(128.dp) .fillMaxWidth() - .padding(horizontal = 20.dp) - .border(width = 1.dp, color = colorResource(id = R.color.shape_primary)) - .clip(RoundedCornerShape(10.dp)) + .padding(horizontal = 16.dp) ) { + Dragger( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(vertical = 6.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Image( + modifier = Modifier.align(Alignment.CenterHorizontally), + painter = painterResource(R.drawable.ic_join_without_approve), + contentDescription = "Join without approve" + ) + Spacer(modifier = Modifier.height(15.dp)) Text( - text = stringResource(R.string.multiplayer_private_comment_for_a_space_owner), - style = Caption1Medium, - color = colorResource(id = R.color.text_secondary), - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, top = 10.dp) - ) - TextField( - value = commentInputValue1, - onValueChange = { commentInputValue1 = it }, - textStyle = PreviewTitle2Regular.copy( - color = colorResource(id = R.color.text_primary) + text = stringResource( + R.string.multiplayer_request_to_join_without_approve_title, + spaceName ), + style = HeadlineHeading, + color = colorResource(id = R.color.text_primary), modifier = Modifier .fillMaxWidth() - .padding(top = 31.dp), - placeholder = { - Text( - text = stringResource(R.string.multiplayer_tap_to_write_request_to_join_comment), - color = colorResource(id = R.color.text_tertiary) - ) - }, - colors = OutlinedTextFieldDefaults.colors( - disabledBorderColor = Color.Transparent, - errorBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent, - cursorColor = ColorTextInputCursor - ) + .padding(horizontal = 16.dp), + textAlign = TextAlign.Center ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + style = Title2, + color = colorResource(id = R.color.text_primary), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = stringResource( + id = R.string.multiplayer_request_to_join_without_approve_desc, + spaceName.ifEmpty { stringResource(id = R.string.untitled) }, + createdByName.ifEmpty { stringResource(id = R.string.untitled) } + ), + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(19.dp)) + ButtonPrimaryLoading( + onClick = throttledClick( + onClick = { onRequestJoinSpaceClicked() } + ), + size = ButtonSize.Large, + text = stringResource(R.string.multiplayer_request_to_join_without_approve_button), + modifierButton = Modifier.fillMaxWidth(), + loading = isLoading + ) + Spacer(modifier = Modifier.height(8.dp)) + ButtonSecondary( + onClick = throttledClick( + onClick = { onRequestJoinSpaceClicked() } + ), + text = stringResource(R.string.cancel), + modifier = Modifier.fillMaxWidth(), + size = ButtonSize.Large, + ) + Spacer(modifier = Modifier.height(16.dp)) } - return commentInputValue1 +} + +@Composable +fun JoiningLoadingState( + onCancelLoadingInviteClicked: () -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Spacer(modifier = Modifier.height(27.dp)) + CircularProgressIndicator( + modifier = Modifier + .size(56.dp), + color = colorResource(R.color.shape_secondary), + trackColor = colorResource(R.color.shape_primary) + ) + Spacer(modifier = Modifier.height(15.dp)) + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + color = colorResource(id = R.color.text_secondary), + textAlign = TextAlign.Center, + text = stringResource(R.string.multiplayer_request_to_join_loading_text), + style = Title2 + ) + Spacer(modifier = Modifier.height(19.dp)) + ButtonSecondary( + modifier = Modifier + .fillMaxWidth() + .padding(top = 12.dp, start = 16.dp, end = 16.dp), + onClick = { + onCancelLoadingInviteClicked() + }, + size = ButtonSize.Large, + text = stringResource(R.string.cancel), + ) + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +@DefaultPreviews +fun JoinSpaceScreenPreviewLoading() { + JoiningLoadingState( + onCancelLoadingInviteClicked = {} + ) +} + +@Composable +@DefaultPreviews +fun JoinSpaceScreenPreview() { + JoinSpaceScreen( + onRequestJoinSpaceClicked = {}, + spaceName = "Anytype Android App", + createdByName = "Konstantin" + ) +} + +@Composable +@DefaultPreviews +fun JoinSpaceScreenPreviewWithEmptyNames() { + JoinSpaceScreen( + onRequestJoinSpaceClicked = {}, + spaceName = "", + createdByName = "" + ) +} + +@Composable +@DefaultPreviews +fun JoinSpaceScreenPreviewWithoutApprove() { + JoinSpaceWithoutApproveScreen( + onRequestJoinSpaceClicked = {}, + spaceName = "", + createdByName = "" + ) } \ No newline at end of file diff --git a/core-ui/src/main/res/drawable/ic_join_without_approve.xml b/core-ui/src/main/res/drawable/ic_join_without_approve.xml new file mode 100644 index 0000000000..20cfbd8d05 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_join_without_approve.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt index 3d09e43e1e..d90263af0a 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -37,6 +37,7 @@ import com.anytypeio.anytype.core_models.membership.EmailVerificationStatus import com.anytypeio.anytype.core_models.membership.GetPaymentUrlResponse import com.anytypeio.anytype.core_models.membership.Membership import com.anytypeio.anytype.core_models.membership.MembershipTierData +import com.anytypeio.anytype.core_models.multiplayer.InviteType import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteLink import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteView import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions @@ -910,8 +911,16 @@ class BlockDataRepository( remote.makeSpaceShareable(space) } - override suspend fun generateSpaceInviteLink(space: SpaceId): SpaceInviteLink { - return remote.generateSpaceInviteLink(space) + override suspend fun generateSpaceInviteLink( + space: SpaceId, + inviteType: InviteType, + permissions: SpaceMemberPermissions + ): SpaceInviteLink { + return remote.generateSpaceInviteLink( + space = space, + inviteType = inviteType, + permissions = permissions + ) } override suspend fun revokeSpaceInviteLink(space: SpaceId) { diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt index 7d422c4ee2..05e06d199d 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt @@ -37,6 +37,7 @@ import com.anytypeio.anytype.core_models.membership.EmailVerificationStatus import com.anytypeio.anytype.core_models.membership.GetPaymentUrlResponse import com.anytypeio.anytype.core_models.membership.Membership import com.anytypeio.anytype.core_models.membership.MembershipTierData +import com.anytypeio.anytype.core_models.multiplayer.InviteType import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteLink import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteView import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions @@ -398,7 +399,11 @@ interface BlockRemote { suspend fun deleteRelationOption(command: Command.DeleteRelationOptions) suspend fun makeSpaceShareable(space: SpaceId) - suspend fun generateSpaceInviteLink(space: SpaceId) : SpaceInviteLink + suspend fun generateSpaceInviteLink( + space: SpaceId, + inviteType: InviteType, + permissions: SpaceMemberPermissions + ): SpaceInviteLink suspend fun revokeSpaceInviteLink(space: SpaceId) suspend fun approveSpaceRequest( space: SpaceId, diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt index cb76b1a95b..ff238bd938 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt @@ -37,6 +37,7 @@ import com.anytypeio.anytype.core_models.membership.EmailVerificationStatus import com.anytypeio.anytype.core_models.membership.GetPaymentUrlResponse import com.anytypeio.anytype.core_models.membership.Membership import com.anytypeio.anytype.core_models.membership.MembershipTierData +import com.anytypeio.anytype.core_models.multiplayer.InviteType import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteLink import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteView import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions @@ -44,6 +45,7 @@ import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.domain.base.Result import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet +import com.anytypeio.anytype.domain.multiplayer.Permissions import com.anytypeio.anytype.domain.page.Redo import com.anytypeio.anytype.domain.page.Undo @@ -443,7 +445,11 @@ interface BlockRepository { suspend fun deleteRelationOption(command: Command.DeleteRelationOptions) suspend fun makeSpaceShareable(space: SpaceId) - suspend fun generateSpaceInviteLink(space: SpaceId) : SpaceInviteLink + suspend fun generateSpaceInviteLink( + space: SpaceId, + inviteType: InviteType, + permissions: SpaceMemberPermissions + ): SpaceInviteLink suspend fun revokeSpaceInviteLink(space: SpaceId) suspend fun approveSpaceRequest( space: SpaceId, diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/GenerateSpaceInviteLink.kt b/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/GenerateSpaceInviteLink.kt index 41b6fa336e..324c4ace94 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/GenerateSpaceInviteLink.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/GenerateSpaceInviteLink.kt @@ -1,6 +1,8 @@ package com.anytypeio.anytype.domain.multiplayer +import com.anytypeio.anytype.core_models.multiplayer.InviteType import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteLink +import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.base.ResultInteractor @@ -8,10 +10,21 @@ import com.anytypeio.anytype.domain.block.repo.BlockRepository import javax.inject.Inject class GenerateSpaceInviteLink @Inject constructor( - private val dispatchers: AppCoroutineDispatchers, + dispatchers: AppCoroutineDispatchers, private val repo: BlockRepository -): ResultInteractor(dispatchers.io) { - override suspend fun doWork(params: SpaceId): SpaceInviteLink = repo.generateSpaceInviteLink( - space = params +) : ResultInteractor(dispatchers.io) { + + override suspend fun doWork(params: Params): SpaceInviteLink { + return repo.generateSpaceInviteLink( + space = params.space, + inviteType = params.inviteType, + permissions = params.permissions + ) + } + + data class Params( + val space: SpaceId, + val inviteType: InviteType, + val permissions: SpaceMemberPermissions ) } \ No newline at end of file diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index b0586c0230..0750f26bdd 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1357,6 +1357,13 @@ Join a space Private comment for a space owner Once the space owner approves your request, you\'ll join the space with the access rights owner determined. + + Join %1$s + You\'ve been invited to join %1$s, created by %2$s + Join Space + + Hang tight — we’re setting things up for you. This should only take a moment. + Request to join You\'ve been invited to join %1$s space, created by %2$s. Send a request so space owner can let you in. Tap to write your comment diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt index 460ed796fb..4f21c3da07 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt @@ -38,6 +38,7 @@ import com.anytypeio.anytype.core_models.membership.EmailVerificationStatus import com.anytypeio.anytype.core_models.membership.GetPaymentUrlResponse import com.anytypeio.anytype.core_models.membership.Membership import com.anytypeio.anytype.core_models.membership.MembershipTierData +import com.anytypeio.anytype.core_models.multiplayer.InviteType import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteLink import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteView import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions @@ -872,8 +873,16 @@ class BlockMiddleware( middleware.makeSpaceShareable(space = space) } - override suspend fun generateSpaceInviteLink(space: SpaceId): SpaceInviteLink { - return middleware.generateSpaceInviteLink(space) + override suspend fun generateSpaceInviteLink( + space: SpaceId, + inviteType: InviteType, + permissions: SpaceMemberPermissions + ): SpaceInviteLink { + return middleware.generateSpaceInviteLink( + space = space, + inviteType = inviteType, + permissions = permissions + ) } override suspend fun revokeSpaceInviteLink(space: SpaceId) { diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt index 0c43bbce2e..f39984ede4 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt @@ -47,6 +47,7 @@ import com.anytypeio.anytype.core_models.membership.EmailVerificationStatus import com.anytypeio.anytype.core_models.membership.GetPaymentUrlResponse import com.anytypeio.anytype.core_models.membership.Membership import com.anytypeio.anytype.core_models.membership.MembershipTierData +import com.anytypeio.anytype.core_models.multiplayer.InviteType import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteLink import com.anytypeio.anytype.core_models.multiplayer.SpaceInviteView import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions @@ -70,6 +71,7 @@ import com.anytypeio.anytype.middleware.mappers.toCoreLinkPreview import com.anytypeio.anytype.middleware.mappers.toCoreModel import com.anytypeio.anytype.middleware.mappers.toCoreModelSearchResults import com.anytypeio.anytype.middleware.mappers.toCoreModels +import com.anytypeio.anytype.middleware.mappers.toMiddleware import com.anytypeio.anytype.middleware.mappers.toMiddlewareModel import com.anytypeio.anytype.middleware.mappers.toMw import com.anytypeio.anytype.middleware.mappers.toPayload @@ -2388,16 +2390,22 @@ class Middleware @Inject constructor( } @Throws(Exception::class) - fun generateSpaceInviteLink(space: SpaceId) : SpaceInviteLink { + fun generateSpaceInviteLink( + space: SpaceId, + inviteType: InviteType, + permissions: SpaceMemberPermissions + ): SpaceInviteLink { val request = Rpc.Space.InviteGenerate.Request( - spaceId = space.id + spaceId = space.id, + inviteType = inviteType.toMiddleware(), + permissions = permissions.toMw() ) logRequestIfDebug(request) val (response, time) = measureTimedValue { service.spaceInviteGenerate(request) } logResponseIfDebug(response, time) return SpaceInviteLink( contentId = response.inviteCid, - fileKey= response.inviteFileKey + fileKey = response.inviteFileKey ) } @@ -2511,7 +2519,8 @@ class Middleware @Inject constructor( space = SpaceId(response.spaceId), creatorName = response.creatorName, spaceName = response.spaceName, - spaceIconContentId = response.spaceIconCid + spaceIconContentId = response.spaceIconCid, + withoutApprove = response.inviteType == anytype.model.InviteType.WithoutApprove ) } diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/Alias.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/Alias.kt index cf55b96ef4..5ee4c1f5c8 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/Alias.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/Alias.kt @@ -114,4 +114,6 @@ typealias MSyncStatusUpdate = Space.SyncStatus.Update typealias MDeviceNetworkType = anytype.model.DeviceNetworkType -typealias MLinkPreview = anytype.model.LinkPreview \ No newline at end of file +typealias MLinkPreview = anytype.model.LinkPreview + +typealias MInviteType = anytype.model.InviteType \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToMiddlewareModelMappers.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToMiddlewareModelMappers.kt index d4f9eeb9a3..a88693493a 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToMiddlewareModelMappers.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToMiddlewareModelMappers.kt @@ -18,6 +18,7 @@ import com.anytypeio.anytype.core_models.Relation import com.anytypeio.anytype.core_models.chats.Chat import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod import com.anytypeio.anytype.core_models.membership.NameServiceNameType +import com.anytypeio.anytype.core_models.multiplayer.InviteType import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions @@ -637,3 +638,9 @@ fun DeviceNetworkType.mw(): MDeviceNetworkType = when(this) { DeviceNetworkType.NOT_CONNECTED -> MDeviceNetworkType.NOT_CONNECTED } +fun InviteType.toMiddleware(): MInviteType = when (this) { + InviteType.MEMBER -> MInviteType.Member + InviteType.GUEST -> MInviteType.Guest + InviteType.WITHOUT_APPROVE -> MInviteType.WithoutApprove +} + diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt index 689d2314f0..4d553bfa74 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/RequestJoinSpaceViewModel.kt @@ -27,6 +27,8 @@ import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.common.TypedViewState import javax.inject.Inject +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch @@ -50,6 +52,8 @@ class RequestJoinSpaceViewModel( val isRequestInProgress = MutableStateFlow(false) val showEnableNotificationDialog = MutableStateFlow(false) val commands = MutableSharedFlow(0) + val showLoadingInviteProgress = MutableStateFlow(false) + private var getSpaceInviteViewJob: Job? = null init { Timber.i("RequestJoinSpaceViewModel, init") @@ -60,7 +64,8 @@ class RequestJoinSpaceViewModel( val fileKey = spaceInviteResolver.parseFileKey(params.link) val contentId = spaceInviteResolver.parseContentId(params.link) if (fileKey != null && contentId != null) { - viewModelScope.launch { + showLoadingInviteProgress.value = true + getSpaceInviteViewJob = viewModelScope.launch { getSpaceInviteView.async( GetSpaceInviteView.Params( inviteContentId = contentId, @@ -68,6 +73,7 @@ class RequestJoinSpaceViewModel( ) ).fold( onSuccess = { view -> + showLoadingInviteProgress.value = false val isAlreadyMember = checkIsUserSpaceMember .async(view.space) .getOrDefault(false) @@ -85,6 +91,7 @@ class RequestJoinSpaceViewModel( } }, onFailure = { e -> + showLoadingInviteProgress.value = false if (e is SpaceInviteError) { when(e) { is SpaceInviteError.InvalidInvite -> { @@ -117,6 +124,11 @@ class RequestJoinSpaceViewModel( } } + fun onCancelLoadingInviteClicked() { + getSpaceInviteViewJob?.cancel() + showLoadingInviteProgress.value = false + } + fun onRequestToJoinClicked() { when(val curr = state.value) { is TypedViewState.Success -> { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/ShareSpaceViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/ShareSpaceViewModel.kt index de5747f940..1a94beab7a 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/ShareSpaceViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/multiplayer/ShareSpaceViewModel.kt @@ -23,6 +23,7 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.ext.isPossibleToUpgradeNumberOfSpaceMembers import com.anytypeio.anytype.core_models.membership.TierId +import com.anytypeio.anytype.core_models.multiplayer.InviteType import com.anytypeio.anytype.core_models.multiplayer.MultiplayerError import com.anytypeio.anytype.core_models.multiplayer.ParticipantStatus import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType @@ -203,7 +204,10 @@ class ShareSpaceViewModel( } } - private fun proceedWithGeneratingInviteLink() { + private fun proceedWithGeneratingInviteLink( + inviteType: InviteType = InviteType.MEMBER, + permissions: SpaceMemberPermissions = SpaceMemberPermissions.READER + ) { viewModelScope.launch { if (spaceAccessType.value == SpaceAccessType.PRIVATE) { makeSpaceShareable.async( @@ -212,27 +216,44 @@ class ShareSpaceViewModel( onSuccess = { analytics.sendEvent(eventName = EventsDictionary.shareSpace) Timber.d("Successfully made space shareable") + generateInviteLink( + inviteType = inviteType, + permissions = permissions + ) }, onFailure = { Timber.e(it, "Error while making space shareable") proceedWithMultiplayerError(it) } ) - } - generateSpaceInviteLink - .async(vmParams.space) - .fold( - onSuccess = { link -> - shareLinkViewState.value = ShareLinkViewState.Shared(link = link.scheme) - }, - onFailure = { - Timber.e(it, "Error while generating invite link") - proceedWithMultiplayerError(it) - } + } else { + generateInviteLink( + inviteType = inviteType, + permissions = permissions ) + } } } + private suspend fun generateInviteLink(inviteType: InviteType, permissions: SpaceMemberPermissions) { + generateSpaceInviteLink.async( + params = GenerateSpaceInviteLink.Params( + space = vmParams.space, + inviteType = inviteType, + permissions = permissions + ) + ).fold( + onSuccess = { inviteLink -> + shareLinkViewState.value = ShareLinkViewState.Shared(inviteLink.scheme) + Timber.d("Successfully generated invite link") + }, + onFailure = { + Timber.e(it, "Error while generating invite link") + proceedWithMultiplayerError(it) + } + ) + } + fun onShareInviteLinkClicked() { viewModelScope.launch { when (val value = shareLinkViewState.value) {