1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2264 Multiplayer | Enhancement | Basic UX for generating space invite link (#921)

This commit is contained in:
Evgenii Kozlov 2024-02-24 18:27:00 +01:00 committed by GitHub
parent c075a2379b
commit 26eb17bb1f
Signed by: github
GPG key ID: B5690EEEBB952194
15 changed files with 240 additions and 22 deletions

View file

@ -1,11 +1,10 @@
package com.anytypeio.anytype.di.feature.multiplayer
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.core_utils.di.scope.CreateFromScratch
import com.anytypeio.anytype.core_utils.di.scope.PerDialog
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.di.feature.BacklinkOrAddToObjectComponent
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.workspace.SpaceManager
@ -53,4 +52,5 @@ interface ShareSpaceDependencies : ComponentDependencies {
fun blockRepository(): BlockRepository
fun urlBuilder(): UrlBuilder
fun spaceManager(): SpaceManager
fun dispatchers(): AppCoroutineDispatchers
}

View file

@ -102,7 +102,8 @@ fun HomeScreen(
onProfileClicked: () -> Unit,
onObjectCheckboxClicked: (Id, Boolean) -> Unit,
onSpaceWidgetClicked: () -> Unit,
onMove: (List<WidgetView>, FromIndex, ToIndex) -> Unit
onMove: (List<WidgetView>, FromIndex, ToIndex) -> Unit,
onSpaceShareIconClicked: (ObjectWrapper.Basic) -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
@ -121,7 +122,8 @@ fun HomeScreen(
onOpenSpacesClicked = onOpenSpacesClicked,
onSpaceWidgetClicked = onSpaceWidgetClicked,
onMove = onMove,
onObjectCheckboxClicked = onObjectCheckboxClicked
onObjectCheckboxClicked = onObjectCheckboxClicked,
onSpaceShareIconClicked = onSpaceShareIconClicked
)
AnimatedVisibility(
visible = mode is InteractionMode.Edit,
@ -188,7 +190,8 @@ private fun WidgetList(
onMove: (List<WidgetView>, FromIndex, ToIndex) -> Unit,
onObjectCheckboxClicked: (Id, Boolean) -> Unit,
onOpenSpacesClicked: () -> Unit,
onSpaceWidgetClicked: () -> Unit
onSpaceWidgetClicked: () -> Unit,
onSpaceShareIconClicked: (ObjectWrapper.Basic) -> Unit
) {
val views = remember { mutableStateOf(widgets) }
views.value = widgets
@ -225,7 +228,9 @@ private fun WidgetList(
onClick = onSpaceWidgetClicked,
name = item.space.name.orEmpty(),
icon = item.icon,
spaceType = item.type
spaceType = item.type,
onSpaceShareIconClicked = { onSpaceShareIconClicked(item.space) },
shareable = item.shareable
)
}
is WidgetView.Tree -> {

View file

@ -31,6 +31,7 @@ import com.anytypeio.anytype.presentation.home.HomeScreenViewModel.Navigation
import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction
import com.anytypeio.anytype.ui.base.navigation
import com.anytypeio.anytype.ui.main.MainActivity
import com.anytypeio.anytype.ui.multiplayer.ShareSpaceFragment
import com.anytypeio.anytype.ui.objects.creation.SelectObjectTypeFragment
import com.anytypeio.anytype.ui.settings.typography
import com.anytypeio.anytype.ui.widgets.SelectWidgetSourceFragment
@ -126,7 +127,8 @@ class HomeScreenFragment : BaseComposeFragment() {
),
onBundledWidgetClicked = vm::onBundledWidgetClicked,
onMove = vm::onMove,
onObjectCheckboxClicked = vm::onObjectCheckboxClicked
onObjectCheckboxClicked = vm::onObjectCheckboxClicked,
onSpaceShareIconClicked = vm::onSpaceShareIconClicked
)
}
}
@ -229,6 +231,12 @@ class HomeScreenFragment : BaseComposeFragment() {
arguments?.putString(DEEP_LINK_KEY, null)
findNavController().navigate(R.id.alertImportExperienceUnsupported)
}
is Command.ShareSpace -> {
findNavController().navigate(
R.id.shareSpaceScreen,
args = ShareSpaceFragment.args(command.space)
)
}
}
}

View file

@ -1,13 +1,17 @@
package com.anytypeio.anytype.ui.multiplayer
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_ui.features.multiplayer.ShareSpaceScreen
import com.anytypeio.anytype.core_utils.ext.arg
@ -36,13 +40,33 @@ class ShareSpaceFragment : BaseBottomSheetComposeFragment() {
setContent {
MaterialTheme(typography = typography) {
ShareSpaceScreen(
// TODO
viewState = vm.viewState.collectAsStateWithLifecycle().value,
onRegenerateInviteLinkClicked = vm::onRegenerateInviteLinkClicked,
onShareInviteLinkClicked = vm::onShareInviteLinkClicked
)
LaunchedEffect(Unit) {
vm.commands.collect { command ->
proceedWithCommand(command)
}
}
}
}
}
}
private fun proceedWithCommand(command: ShareSpaceViewModel.Command) {
when (command) {
is ShareSpaceViewModel.Command.ShareInviteLink -> {
val intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, command.link)
type = "text/plain"
}
startActivity(Intent.createChooser(intent, null))
}
}
}
override fun injectDependencies() {
componentManager().shareSpaceComponent.get(SpaceId(space)).inject(this)
}
@ -53,5 +77,8 @@ class ShareSpaceFragment : BaseBottomSheetComposeFragment() {
companion object {
const val SPACE_ID_KEY = "arg.share-space.space-id-key"
fun args(space: SpaceId) : Bundle = bundleOf(
SPACE_ID_KEY to space.id
)
}
}

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.ui.widgets.types
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
@ -11,24 +12,43 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.PERSONAL_SPACE_TYPE
import com.anytypeio.anytype.core_models.PRIVATE_SPACE_TYPE
import com.anytypeio.anytype.core_models.SpaceType
import com.anytypeio.anytype.core_ui.extensions.throttledClick
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.ui_settings.main.SpaceImageBlock
@Composable
@Preview
fun SpaceWidgetCardPreview() {
SpaceWidgetCard(
onClick = {},
icon = SpaceIconView.Placeholder,
name = "Research",
spaceType = PRIVATE_SPACE_TYPE,
onSpaceShareIconClicked = {},
shareable = true
)
}
@Composable
fun SpaceWidgetCard(
onClick: () -> Unit,
name: String,
icon: SpaceIconView,
spaceType: SpaceType
spaceType: SpaceType,
onSpaceShareIconClicked: () -> Unit,
shareable: Boolean
) {
Box(
modifier = Modifier
@ -82,5 +102,17 @@ fun SpaceWidgetCard(
color = colorResource(id = R.color.text_secondary),
maxLines = 1
)
if (shareable) {
Image(
painter = painterResource(id = R.drawable.ic_space_widget_share_space_icon),
contentDescription = "Space share icon",
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 20.dp)
.noRippleClickable(
onClick = throttledClick(onClick = { onSpaceShareIconClicked() })
)
)
}
}
}

View file

@ -534,5 +534,8 @@
android:label="Migration-needed screen">
</fragment>
<dialog
android:id="@+id/shareSpaceScreen"
android:name="com.anytypeio.anytype.ui.multiplayer.ShareSpaceFragment"/>
</navigation>

View file

@ -2,5 +2,7 @@ package com.anytypeio.anytype.core_models.multiplayer
data class SpaceInviteLink(
val fileKey: String,
val cid: String
)
val contentId: String
) {
val scheme = "anytype://invite/?cid=$contentId&key=$fileKey"
}

View file

@ -20,6 +20,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
import com.anytypeio.anytype.core_ui.views.ButtonPrimary
import com.anytypeio.anytype.core_ui.views.ButtonSize
@ -31,13 +32,15 @@ fun ShareInviteLinkCardPreview() {
ShareInviteLinkCard(
link = "https://anytype.io/ibafyrfhfsag6rea3ifffsasssa3ifffsasssga3ifffsasssga3ifffsas",
onShareInviteClicked = {},
onRegenerateInviteLinkClicked = {}
)
}
@Composable
fun ShareInviteLinkCard(
link: String,
onShareInviteClicked: () -> Unit
onShareInviteClicked: () -> Unit,
onRegenerateInviteLinkClicked: () -> Unit
) {
Card {
Spacer(modifier = Modifier.height(20.dp))
@ -53,7 +56,10 @@ fun ShareInviteLinkCard(
)
Image(
painter = painterResource(id = R.drawable.ic_action_replace),
contentDescription = "Regenerate-invite icon"
contentDescription = "Regenerate-invite icon",
modifier = Modifier.noRippleClickable {
onRegenerateInviteLinkClicked()
}
)
}
Spacer(modifier = Modifier.height(8.dp))

View file

@ -1,8 +1,29 @@
package com.anytypeio.anytype.core_ui.features.multiplayer
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel
@Composable
fun ShareSpaceScreen() {
// TODO
fun ShareSpaceScreen(
viewState: ShareSpaceViewModel.ViewState,
onRegenerateInviteLinkClicked: () -> Unit,
onShareInviteLinkClicked: () -> Unit
) {
Box(modifier = Modifier.wrapContentSize()) {
when(viewState) {
ShareSpaceViewModel.ViewState.Init -> {
// Do nothing.
}
is ShareSpaceViewModel.ViewState.Share -> {
ShareInviteLinkCard(
link = viewState.link,
onShareInviteClicked = onShareInviteLinkClicked,
onRegenerateInviteLinkClicked = onRegenerateInviteLinkClicked
)
}
}
}
}

View file

@ -0,0 +1,39 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,4.199m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M12,19.801m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M4.199,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M19.801,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M12.001,4.839m-1.2,0a1.2,1.2 0,1 1,2.4 0a1.2,1.2 0,1 1,-2.4 0"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M4.821,12.019m0,1.2a1.2,1.2 0,1 1,0 -2.4a1.2,1.2 0,1 1,0 2.4"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M19.184,12.019m0,1.2a1.2,1.2 0,1 1,0 -2.4a1.2,1.2 0,1 1,0 2.4"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M6.92,6.945m-0.849,0.849a1.2,1.2 0,1 1,1.697 -1.697a1.2,1.2 0,1 1,-1.697 1.697"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M17.076,17.102m-0.849,0.849a1.2,1.2 90,1 1,1.697 -1.697a1.2,1.2 0,1 1,-1.697 1.697"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M17.082,6.941m-0.849,-0.849a1.2,1.2 0,1 1,1.697 1.697a1.2,1.2 0,1 1,-1.697 -1.697"
android:fillColor="@color/glyph_active"/>
<path
android:pathData="M6.926,17.099m-0.849,-0.849a1.2,1.2 0,1 1,1.697 1.697a1.2,1.2 90,1 1,-1.697 -1.697"
android:fillColor="@color/glyph_active"/>
</vector>

View file

@ -5,8 +5,9 @@ import com.anytypeio.anytype.core_models.primitives.SpaceId
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 GenerateSpaceInviteLink(
class GenerateSpaceInviteLink @Inject constructor(
private val dispatchers: AppCoroutineDispatchers,
private val repo: BlockRepository
): ResultInteractor<SpaceId, SpaceInviteLink>(dispatchers.io) {

View file

@ -2376,7 +2376,7 @@ class Middleware @Inject constructor(
val response = service.spaceInviteGenerate(request)
if (BuildConfig.DEBUG) logResponse(response)
return SpaceInviteLink(
cid = response.inviteCid,
contentId = response.inviteCid,
fileKey= response.inviteFileKey
)
}

View file

@ -1341,6 +1341,17 @@ class HomeScreenViewModel(
}
}
fun onSpaceShareIconClicked(spaceView: ObjectWrapper.Basic) {
viewModelScope.launch {
val space = spaceView.targetSpaceId
if (space != null) {
commands.emit(Command.ShareSpace(SpaceId(space)))
} else {
sendToast("Space not found")
}
}
}
override fun onCleared() {
super.onCleared()
viewModelScope.launch {
@ -1496,6 +1507,8 @@ sealed class Command {
}
}
data class ShareSpace(val space: SpaceId) : Command()
sealed class Deeplink : Command() {
object CannotImportExperience : Deeplink()
}

View file

@ -2,24 +2,82 @@ package com.anytypeio.anytype.presentation.multiplayer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.multiplayer.GenerateSpaceInviteLink
import com.anytypeio.anytype.presentation.common.BaseViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class ShareSpaceViewModel(
private val params: Params
private val params: Params,
private val generateSpaceInviteLink: GenerateSpaceInviteLink
) : BaseViewModel() {
val viewState = MutableStateFlow<ViewState>(ViewState.Init)
val commands = MutableSharedFlow<Command>()
init {
proceedWithGeneratingInviteLink()
}
private fun proceedWithGeneratingInviteLink() {
viewModelScope.launch {
generateSpaceInviteLink
.async(params.space)
.fold(
onSuccess = { link ->
viewState.value = ViewState.Share(link = link.scheme)
},
onFailure = {
Timber.e(it, "Error while generating invite link")
}
)
}
}
fun onRegenerateInviteLinkClicked() {
proceedWithGeneratingInviteLink()
}
fun onShareInviteLinkClicked() {
viewModelScope.launch {
when(val value = viewState.value) {
ViewState.Init -> {
// Do nothing.
}
is ViewState.Share -> {
commands.emit(Command.ShareInviteLink(value.link))
}
}
}
}
class Factory @Inject constructor(
private val params: Params
private val params: Params,
private val generateSpaceInviteLink: GenerateSpaceInviteLink
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = ShareSpaceViewModel(
params = params
params = params,
generateSpaceInviteLink = generateSpaceInviteLink
) as T
}
data class Params(
val space: SpaceId
)
sealed class ViewState {
object Init : ViewState()
data class Share(val link: String): ViewState()
}
sealed class Command {
data class ShareInviteLink(val link: String) : Command()
}
}

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.presentation.widgets
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.PRIVATE_SPACE_TYPE
import com.anytypeio.anytype.core_models.SpaceType
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.presentation.editor.model.Indent
@ -94,8 +95,10 @@ sealed class WidgetView {
data class View(
val space: ObjectWrapper.Basic,
val icon: SpaceIconView,
val type: SpaceType
) : SpaceWidget()
val type: SpaceType,
) : SpaceWidget() {
val shareable: Boolean get() = type == PRIVATE_SPACE_TYPE
}
}
object Library : WidgetView() {