1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-07 21:37:02 +09:00

DROID-3446 Vault | New space or chat creation flow (#2481)

This commit is contained in:
Konstantin Ivanov 2025-06-02 09:53:21 +02:00 committed by GitHub
parent c9f5ec3c3f
commit 78fb9002f7
Signed by: github
GPG key ID: B5690EEEBB952194
13 changed files with 595 additions and 235 deletions

View file

@ -6,16 +6,13 @@ import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.spaces.CreateSpaceViewModel
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment
import dagger.Binds
import dagger.Component
import dagger.Module
import dagger.Provides
@Component(
dependencies = [CreateSpaceDependencies::class],
@ -50,5 +47,4 @@ interface CreateSpaceDependencies : ComponentDependencies {
fun dispatchers(): AppCoroutineDispatchers
fun spaceManager(): SpaceManager
fun analyticSpaceHelper(): AnalyticSpaceHelperDelegate
fun spaceViewContainer(): SpaceViewSubscriptionContainer
}

View file

@ -4,6 +4,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
@ -12,6 +15,8 @@ import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.parseImagePath
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
@ -29,6 +34,8 @@ class CreateSpaceFragment : BaseBottomSheetComposeFragment() {
private val vm by viewModels<CreateSpaceViewModel> { factory }
private val spaceType get() = argString(ARG_SPACE_TYPE)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -39,11 +46,33 @@ class CreateSpaceFragment : BaseBottomSheetComposeFragment() {
MaterialTheme(
typography = typography
) {
val imagePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
if (uri != null) {
vm.onImageSelected(url = uri.parseImagePath(context))
}
}
)
CreateSpaceScreen(
spaceIconView = vm.spaceIconView.collectAsState().value,
onCreate = vm::onCreateSpace,
onSpaceIconClicked = vm::onSpaceIconClicked,
isLoading = vm.isInProgress.collectAsState()
onCreate = { name, isSpaceLevelChatSwitchChecked ->
vm.onCreateSpace(
name = name,
withChat = spaceType == TYPE_CHAT
)
},
onSpaceIconUploadClicked = {
imagePickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
},
onSpaceIconRemoveClicked = {
vm.onSpaceIconRemovedClicked()
},
isLoading = vm.isInProgress.collectAsState(),
isChatSpace = spaceType == TYPE_CHAT
)
LaunchedEffect(Unit) { vm.toasts.collect { toast(it) } }
LaunchedEffect(Unit) {
@ -99,4 +128,11 @@ class CreateSpaceFragment : BaseBottomSheetComposeFragment() {
override fun releaseDependencies() {
componentManager().createSpaceComponent.release()
}
companion object {
const val ARG_SPACE_TYPE = "arg.space_type"
const val TYPE_SPACE = "space"
const val TYPE_CHAT = "chat"
}
}

View file

@ -1,76 +1,98 @@
package com.anytypeio.anytype.ui.spaces
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Name
import com.anytypeio.anytype.core_models.PRIVATE_SPACE_TYPE
import com.anytypeio.anytype.core_models.SystemColor
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.features.SpaceIconView
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.BodySemiBold
import com.anytypeio.anytype.core_ui.views.ButtonPrimaryLoading
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.core_ui.views.Title2
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.ui_settings.space.TypeOfSpace
@Composable
fun CreateSpaceScreen(
spaceIconView: SpaceIconView.Placeholder,
spaceIconView: SpaceIconView,
onCreate: (Name, IsSpaceLevelChatSwitchChecked) -> Unit,
onSpaceIconClicked: () -> Unit,
isLoading: State<Boolean>
onSpaceIconUploadClicked: () -> Unit,
onSpaceIconRemoveClicked: () -> Unit,
isLoading: State<Boolean>,
isChatSpace: Boolean = false
) {
var isSpaceLevelChatSwitchChecked = remember { mutableStateOf(false) }
val input = remember {
mutableStateOf("")
var innerValue by rememberSaveable(stateSaver = TextFieldValue.Saver) {
mutableStateOf(TextFieldValue(""))
}
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
Box(
modifier = Modifier.fillMaxSize()
modifier = Modifier
.fillMaxSize()
.background(
color = colorResource(id = R.color.background_primary),
shape = RoundedCornerShape(16.dp)
)
.imePadding()
) {
Dragger(
modifier = Modifier
@ -83,44 +105,101 @@ fun CreateSpaceScreen(
.verticalScroll(rememberScrollState())
.padding(top = 16.dp)
) {
Header()
Spacer(modifier = Modifier.height(16.dp))
Header(isChatSpace = isChatSpace)
Spacer(modifier = Modifier.height(8.dp))
SpaceIcon(
modifier = Modifier.align(Alignment.CenterHorizontally),
spaceIconView = spaceIconView.copy(
name = input.value.ifEmpty {
stringResource(id = R.string.s)
}
),
onSpaceIconClicked = onSpaceIconClicked
spaceIconView = when (spaceIconView) {
is SpaceIconView.Placeholder -> spaceIconView.copy(
name = innerValue.text.ifEmpty {
stringResource(id = R.string.u)
}
)
else -> spaceIconView
},
onSpaceIconUploadClicked = onSpaceIconUploadClicked,
onSpaceIconRemoveClicked = onSpaceIconRemoveClicked
)
Spacer(modifier = Modifier.height(10.dp))
SpaceNameInput(
input = input,
onActionDone = {
focusManager.clearFocus()
onCreate(input.value, isSpaceLevelChatSwitchChecked.value)
}
Spacer(modifier = Modifier.height(6.dp))
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally),
text = stringResource(id = R.string.create_space_change_icon),
style = BodyCalloutMedium,
color = colorResource(id = R.color.glyph_active)
)
Divider()
Section(title = stringResource(id = R.string.type))
TypeOfSpace(spaceType = PRIVATE_SPACE_TYPE)
Divider()
if (BuildConfig.DEBUG) {
DebugCreateSpaceLevelChatToggle(isSpaceLevelChatSwitchChecked)
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier
.fillMaxWidth()
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.focusRequester(focusRequester),
value = innerValue,
onValueChange = {
innerValue = it
},
placeholder = {
Text(
text = stringResource(id = R.string.untitled),
style = BodySemiBold,
color = colorResource(id = R.color.text_tertiary)
)
},
label = {
Text(
text = stringResource(id = R.string.name),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary)
)
},
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
onCreate(innerValue.text, isSpaceLevelChatSwitchChecked.value)
}
),
textStyle = BodySemiBold.copy(
color = colorResource(id = R.color.text_primary)
),
shape = RoundedCornerShape(size = 12.dp),
colors = TextFieldDefaults.colors(
cursorColor = colorResource(id = R.color.text_primary),
focusedContainerColor = colorResource(id = R.color.transparent),
unfocusedContainerColor = colorResource(id = R.color.transparent),
focusedIndicatorColor = colorResource(id = R.color.shape_primary),
unfocusedIndicatorColor = colorResource(id = R.color.shape_tertiary),
)
)
}
Spacer(modifier = Modifier.height(78.dp))
}
CreateSpaceButton(
onCreate = { name ->
ButtonPrimaryLoading(
onClick = {
focusManager.clearFocus()
onCreate(name, isSpaceLevelChatSwitchChecked.value)
keyboardController?.hide()
onCreate(innerValue.text, isSpaceLevelChatSwitchChecked.value)
},
input = input,
modifier = Modifier.align(Alignment.BottomCenter),
isLoading = isLoading
text = stringResource(id = R.string.create),
size = ButtonSize.Large,
modifierBox = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(bottom = 8.dp),
modifierButton = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp),
loading = isLoading.value,
enabled = innerValue.text.isNotEmpty()
)
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
@Composable
@ -147,43 +226,14 @@ private fun DebugCreateSpaceLevelChatToggle(isChatToggleChecked: MutableState<Bo
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Enable space-level chat (dev mode)",
color = colorResource(id = com.anytypeio.anytype.ui_settings.R.color.text_primary),
color = colorResource(id = R.color.text_primary),
style = BodyRegular
)
}
}
@Composable
private fun CreateSpaceButton(
modifier: Modifier,
onCreate: (Name) -> Unit,
input: State<String>,
isLoading: State<Boolean>
) {
Box(
modifier = modifier
.height(78.dp)
.fillMaxWidth()
) {
ButtonPrimaryLoading(
onClick = { onCreate(input.value) },
text = stringResource(id = R.string.create),
size = ButtonSize.Large,
modifierButton = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
,
modifierBox = Modifier
.padding(bottom = 10.dp)
.align(Alignment.BottomCenter)
,
loading = isLoading.value
)
}
}
@Composable
fun Header() {
fun Header(isChatSpace: Boolean = false) {
Box(
modifier = Modifier
.height(48.dp)
@ -191,8 +241,8 @@ fun Header() {
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = stringResource(id = R.string.create_space),
style = Title2,
text = stringResource(id = if (isChatSpace) R.string.create_chat else R.string.create_space),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
}
@ -202,94 +252,65 @@ fun Header() {
fun SpaceIcon(
modifier: Modifier,
spaceIconView: SpaceIconView,
onSpaceIconClicked: () -> Unit
onSpaceIconUploadClicked: () -> Unit,
onSpaceIconRemoveClicked: () -> Unit,
) {
val context = LocalContext.current
val isIconMenuExpanded = remember {
mutableStateOf(false)
}
Box(modifier = modifier.wrapContentSize()) {
SpaceIconView(
icon = spaceIconView,
onSpaceIconClick = onSpaceIconClicked
)
}
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SpaceNameInput(
input: MutableState<String>,
onActionDone: () -> Unit
) {
val focusRequester = FocusRequester()
Box(
modifier = Modifier
.height(72.dp)
.fillMaxWidth()
) {
BasicTextField(
value = input.value,
onValueChange = { input.value = it },
keyboardActions = KeyboardActions(
onDone = { onActionDone() }
),
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, bottom = 12.dp)
.align(Alignment.BottomStart)
.focusRequester(focusRequester)
,
maxLines = 1,
singleLine = true,
textStyle = HeadlineHeading.copy(
color = colorResource(id = R.color.text_primary)
),
cursorBrush = SolidColor(
colorResource(id = R.color.cursor_color)
),
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = input.value,
innerTextField = innerTextField,
singleLine = true,
enabled = true,
isError = false,
placeholder = {
Text(
text = stringResource(R.string.space_name),
style = HeadlineHeading
)
},
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = colorResource(id = R.color.text_primary),
backgroundColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
placeholderColor = colorResource(id = R.color.text_tertiary)
),
contentPadding = PaddingValues(
start = 0.dp,
top = 0.dp,
end = 0.dp,
bottom = 0.dp
),
border = {},
interactionSource = remember { MutableInteractionSource() },
visualTransformation = VisualTransformation.None
)
onSpaceIconClick = {
isIconMenuExpanded.value = !isIconMenuExpanded.value
}
)
Text(
color = colorResource(id = R.color.text_secondary),
style = Caption1Regular,
modifier = Modifier.padding(
start = 20.dp,
top = 11.dp
),
text = stringResource(id = R.string.space_name)
)
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
DropdownMenu(
modifier = Modifier,
expanded = isIconMenuExpanded.value,
offset = DpOffset(x = 0.dp, y = 6.dp),
onDismissRequest = {
isIconMenuExpanded.value = false
},
shape = RoundedCornerShape(10.dp),
containerColor = colorResource(id = R.color.background_secondary)
) {
if (ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)) {
DropdownMenuItem(
onClick = {
onSpaceIconUploadClicked()
isIconMenuExpanded.value = false
},
) {
Text(
text = stringResource(R.string.profile_settings_apply_upload_image),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
if (spaceIconView is SpaceIconView.Image) {
Divider(
paddingStart = 0.dp,
paddingEnd = 0.dp,
)
DropdownMenuItem(
onClick = {
isIconMenuExpanded.value = false
onSpaceIconRemoveClicked()
},
) {
Text(
text = stringResource(R.string.remove_image),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
}
}
}
@ -299,9 +320,11 @@ fun Section(
color: Color = colorResource(id = R.color.text_secondary),
textPaddingStart: Dp = 20.dp
) {
Box(modifier = Modifier
.height(52.dp)
.fillMaxWidth()) {
Box(
modifier = Modifier
.height(52.dp)
.fillMaxWidth()
) {
Text(
modifier = Modifier
.padding(
@ -330,4 +353,21 @@ fun UseCase() {
}
}
typealias IsSpaceLevelChatSwitchChecked = Boolean
typealias IsSpaceLevelChatSwitchChecked = Boolean
@DefaultPreviews
@Composable
fun CreateSpaceScreenPreview() {
val state = remember { mutableStateOf(false) }
CreateSpaceScreen(
spaceIconView = SpaceIconView.Placeholder(
color = SystemColor.RED,
name = "My Space"
),
onCreate = { _, _ -> },
onSpaceIconUploadClicked = {},
onSpaceIconRemoveClicked = {},
isChatSpace = true,
isLoading = state
)
}

View file

@ -0,0 +1,139 @@
package com.anytypeio.anytype.ui.vault
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.TextOverflow
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.noRippleThrottledClickable
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.Title1
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChooseSpaceTypeScreen(
onCreateChatClicked: () -> Unit,
onCreateSpaceClicked: () -> Unit,
onDismiss: () -> Unit = {}
) {
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
ModalBottomSheet(
modifier = Modifier
.fillMaxWidth(),
onDismissRequest = onDismiss,
sheetState = sheetState,
containerColor = Color.Transparent,
contentColor = Color.Transparent,
dragHandle = null,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp)
.background(shape = RoundedCornerShape(16.dp), color = colorResource(id = R.color.widget_background))
) {
Dragger(modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(vertical = 8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(72.dp)
.padding(horizontal = 16.dp)
.noRippleThrottledClickable {
onCreateChatClicked()
},
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.ic_space_type_chat),
contentDescription = "Create Chat",
modifier = Modifier.size(56.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = stringResource(id = R.string.vault_create_chat),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
Text(
text = stringResource(id = R.string.vault_create_chat_description),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.height(72.dp)
.padding(horizontal = 16.dp)
.noRippleThrottledClickable {
onCreateSpaceClicked()
},
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.ic_space_type_space),
contentDescription = "Create Space",
modifier = Modifier.size(56.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(
text = stringResource(id = R.string.vault_create_space),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
Text(
text = stringResource(id = R.string.vault_create_space_description),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@DefaultPreviews
@Composable
private fun ChooseSpaceTypeScreenPreview() {
ChooseSpaceTypeScreen(
onCreateChatClicked = {},
onCreateSpaceClicked = {}
)
}

View file

@ -5,14 +5,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
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 androidx.navigation.NavOptions
import androidx.navigation.NavOptions.*
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_utils.ext.argOrNull
@ -31,6 +35,10 @@ import com.anytypeio.anytype.ui.home.HomeScreenFragment
import com.anytypeio.anytype.ui.multiplayer.RequestJoinSpaceFragment
import com.anytypeio.anytype.ui.payments.MembershipFragment
import com.anytypeio.anytype.ui.settings.typography
import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment
import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.ARG_SPACE_TYPE
import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.TYPE_CHAT
import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.TYPE_SPACE
import javax.inject.Inject
import timber.log.Timber
@ -51,14 +59,30 @@ class VaultFragment : BaseComposeFragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(typography = typography) {
VaultScreen(
spaces = vm.spaces.collectAsStateWithLifecycle().value,
onSpaceClicked = vm::onSpaceClicked,
onCreateSpaceClicked = vm::onCreateSpaceClicked,
onSettingsClicked = vm::onSettingsClicked,
onOrderChanged = vm::onOrderChanged,
profile = vm.profileView.collectAsStateWithLifecycle().value
)
Box(modifier = Modifier.fillMaxSize()) {
VaultScreen(
spaces = vm.spaces.collectAsStateWithLifecycle().value,
onSpaceClicked = vm::onSpaceClicked,
onCreateSpaceClicked = vm::onChooseSpaceTypeClicked,
onSettingsClicked = vm::onSettingsClicked,
onOrderChanged = vm::onOrderChanged,
profile = vm.profileView.collectAsStateWithLifecycle().value
)
if (vm.showChooseSpaceType.collectAsStateWithLifecycle().value) {
ChooseSpaceTypeScreen(
onCreateChatClicked = {
vm.onCreateChatClicked()
},
onCreateSpaceClicked = {
vm.onCreateSpaceClicked()
},
onDismiss = {
vm.onChooseSpaceTypeDismissed()
}
)
}
}
}
LaunchedEffect(Unit) {
vm.commands.collect { command -> proceed(command) }
@ -100,10 +124,21 @@ class VaultFragment : BaseComposeFragment() {
is Command.CreateNewSpace -> {
runCatching {
findNavController().navigate(
R.id.actionCreateSpaceFromVault
R.id.actionCreateSpaceFromVault,
bundleOf(ARG_SPACE_TYPE to TYPE_SPACE)
)
}.onFailure {
Timber.e(it, "Error while opening create-space screen from vault")
Timber.e(it, "Error while opening create space screen from vault")
}
}
Command.CreateChat -> {
runCatching {
findNavController().navigate(
R.id.actionCreateChatFromVault,
bundleOf(ARG_SPACE_TYPE to TYPE_CHAT)
)
}.onFailure {
Timber.e(it, "Error while opening create chat screen from vault")
}
}
is Command.OpenProfileSettings -> {
@ -135,7 +170,7 @@ class VaultFragment : BaseComposeFragment() {
findNavController().navigate(
R.id.paymentsScreen,
MembershipFragment.args(command.tierId),
NavOptions.Builder().setLaunchSingleTop(true).build()
Builder().setLaunchSingleTop(true).build()
)
}
is Command.Deeplink.DeepLinkToObjectNotWorking -> {

View file

@ -320,11 +320,18 @@
<action
android:id="@+id/actionCreateSpaceFromVault"
app:destination="@id/createSpaceScreen" />
<action
android:id="@+id/actionCreateChatFromVault"
app:destination="@id/createSpaceScreen" />
</fragment>
<dialog
android:id="@+id/createSpaceScreen"
android:name="com.anytypeio.anytype.ui.spaces.CreateSpaceFragment">
<argument
android:name="arg.space_type"
android:defaultValue="space"
app:argType="string" />
<action
android:id="@+id/exitToVaultAction"
app:popUpTo="@+id/vaultScreen"

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="56"
android:viewportHeight="56">
<path
android:pathData="M28,28m-28,0a28,28 0,1 1,56 0a28,28 0,1 1,-56 0"
android:fillColor="#46AFFF"/>
<path
android:pathData="M28,14C36.837,14 44,20.044 44,27.5C44,34.956 36.837,41 28,41C27.067,41 26.152,40.931 25.263,40.802C25.01,40.765 24.752,40.821 24.537,40.958C22.361,42.344 19.694,43.345 17.111,43.771C16.205,43.921 15.744,42.832 16.367,42.157C17.128,41.334 17.824,40.486 18.47,39.625C18.825,39.151 18.67,38.479 18.176,38.154C14.418,35.684 12,31.831 12,27.5C12,20.044 19.163,14 28,14Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M28,27.5m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#46BCFF"/>
<path
android:pathData="M21.5,27.5m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#46BCFF"/>
<path
android:pathData="M34.5,27.5m-2,0a2,2 0,1 1,4 0a2,2 0,1 1,-4 0"
android:fillColor="#46BCFF"/>
</vector>

File diff suppressed because one or more lines are too long

View file

@ -916,7 +916,9 @@
<string name="exiting_please_wait">Exiting... please wait</string>
<string name="loading_please_wait">Loading... please wait</string>
<string name="personal">Default</string>
<string name="create_space">Create a space</string>
<string name="create_space">Create space</string>
<string name="create_chat">Create chat</string>
<string name="create_space_change_icon">Change icon</string>
<string name="generic_error">Something went wrong. Please try again.</string>
<string name="type">Type</string>
@ -2070,4 +2072,9 @@ Please provide specific details of your needs here.</string>
<string name="notifications_modal_success_button">Enable notifications</string>
<string name="notifications_modal_cancel_button">Not now</string>
<string name="vault_create_chat">Chat</string>
<string name="vault_create_chat_description">For real-time conversations</string>
<string name="vault_create_space">Space</string>
<string name="vault_create_space_description">For organized content and data</string>
</resources>

View file

@ -481,6 +481,7 @@ interface MiddlewareService {
@Throws(Exception::class)
fun workspaceOpen(request: Rpc.Workspace.Open.Request): Rpc.Workspace.Open.Response
@Throws(Exception::class)
fun workspaceSetInfo(request: Rpc.Workspace.SetInfo.Request): Rpc.Workspace.SetInfo.Response
@Throws(Exception::class)

View file

@ -8,16 +8,17 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.EventsPropertiesKey
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.analytics.props.Props
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.SystemColor
import com.anytypeio.anytype.core_models.Url
import com.anytypeio.anytype.core_models.primitives.Space
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.media.UploadFile
import com.anytypeio.anytype.domain.spaces.CreateSpace
import com.anytypeio.anytype.domain.spaces.SetSpaceDetails
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.BuildConfig
import com.anytypeio.anytype.presentation.common.BaseViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
@ -29,14 +30,15 @@ class CreateSpaceViewModel(
private val createSpace: CreateSpace,
private val spaceManager: SpaceManager,
private val analytics: Analytics,
private val spaceViewContainer: SpaceViewSubscriptionContainer
private val uploadFile: UploadFile,
private val setSpaceDetails: SetSpaceDetails
) : BaseViewModel() {
val isInProgress = MutableStateFlow(false)
val commands = MutableSharedFlow<Command>(replay = 0)
val spaceIconView : MutableStateFlow<SpaceIconView.Placeholder> = MutableStateFlow(
val spaceIconView : MutableStateFlow<SpaceIconView> = MutableStateFlow(
SpaceIconView.Placeholder(
color = SystemColor.entries.random()
)
@ -50,8 +52,13 @@ class CreateSpaceViewModel(
val isDismissed = MutableStateFlow(false)
fun onCreateSpace(name: String, isSpaceLevelChatSwitchChecked: Boolean) {
Timber.d("onCreateSpace, isSpaceLevelChatSwitchChecked: $isSpaceLevelChatSwitchChecked")
fun onImageSelected(url: Url) {
Timber.d("onImageSelected: $url")
spaceIconView.value = SpaceIconView.Image(url = url)
}
fun onCreateSpace(name: String, withChat: Boolean) {
Timber.d("onCreateSpace, withChat: $withChat")
if (isDismissed.value) {
return
}
@ -59,55 +66,88 @@ class CreateSpaceViewModel(
sendToast("Please wait...")
return
}
val numberOfActiveSpaces = spaceViewContainer.get().filter { it.isActive }.size
viewModelScope.launch {
createSpace.stream(
CreateSpace.Params(
details = mapOf(
Relations.NAME to name,
Relations.ICON_OPTION to spaceIconView.value.color.index.toDouble()
),
shouldApplyEmptyUseCase = true,
withChat = BuildConfig.DEBUG && isSpaceLevelChatSwitchChecked
)
).collect { result ->
val params = CreateSpace.Params(
details = mapOf(
Relations.NAME to name,
Relations.ICON_OPTION to when (val icon = spaceIconView.value) {
is SpaceIconView.Placeholder -> icon.color.index.toDouble()
else -> SystemColor.SKY.index.toDouble()
}
),
shouldApplyEmptyUseCase = true,
withChat = withChat
)
createSpace.stream(params = params).collect { result ->
result.fold(
onLoading = { isInProgress.value = true },
onSuccess = { response ->
val space = response.space.id
analytics.sendEvent(
eventName = EventsDictionary.createSpace,
props = Props(
mapOf(EventsPropertiesKey.route to EventsDictionary.Routes.navigation)
)
)
setNewSpaceAsCurrentSpace(space)
Timber.d("Successfully created space: $space").also {
isInProgress.value = false
commands.emit(
Command.SwitchSpace(
space = Space(space),
startingObject = response.startingObject
)
)
}
},
onFailure = {
Timber.e(it, "Error while creating space").also {
sendToast("Error while creating space, please try again.")
isInProgress.value = false
}
}
onSuccess = { onSpaceCreated(it) },
onFailure = { onError(it) }
)
}
}
}
private suspend fun setNewSpaceAsCurrentSpace(space: Id) {
spaceManager.set(space)
private suspend fun onSpaceCreated(response: com.anytypeio.anytype.core_models.Command.CreateSpace.Result) {
val spaceId = response.space.id
analytics.sendEvent(
eventName = EventsDictionary.createSpace,
props = Props(
mapOf(EventsPropertiesKey.route to EventsDictionary.Routes.navigation)
)
)
spaceManager.set(spaceId)
when (val icon = spaceIconView.value) {
is SpaceIconView.Image -> uploadAndSetIcon(
url = icon.url,
spaceId = spaceId,
startingObject = response.startingObject
)
else -> finishCreation(spaceId, response.startingObject)
}
}
fun onSpaceIconClicked() {
private suspend fun uploadAndSetIcon(url: Url, spaceId: Id, startingObject: Id?) {
uploadFile.async(
UploadFile.Params(
path = url,
space = Space(spaceId),
type = Block.Content.File.Type.IMAGE,
createTypeWidgetIfMissing = false
)
).fold(
onSuccess = { file ->
setSpaceDetails.async(
SetSpaceDetails.Params(
space = Space(spaceId),
details = mapOf(Relations.ICON_IMAGE to file.id)
)
)
finishCreation(spaceId, startingObject)
},
onFailure = { onError(it) }
)
}
private suspend fun finishCreation(spaceId: Id, startingObject: Id?) {
Timber.d("Space created: %s", spaceId)
isInProgress.value = false
commands.emit(
Command.SwitchSpace(
space = Space(spaceId),
startingObject = startingObject
)
)
}
private fun onError(error: Throwable) {
Timber.e(error, "Error creating space")
sendToast("Error while creating space, please try again.")
isInProgress.value = false
}
fun onSpaceIconRemovedClicked() {
proceedWithResettingRandomSpaceGradient()
}
@ -121,7 +161,8 @@ class CreateSpaceViewModel(
private val createSpace: CreateSpace,
private val spaceManager: SpaceManager,
private val analytics: Analytics,
private val spaceViewContainer: SpaceViewSubscriptionContainer
private val uploadFile: UploadFile,
private val setSpaceDetails: SetSpaceDetails
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
@ -130,7 +171,8 @@ class CreateSpaceViewModel(
createSpace = createSpace,
spaceManager = spaceManager,
analytics = analytics,
spaceViewContainer = spaceViewContainer
uploadFile = uploadFile,
setSpaceDetails = setSpaceDetails
) as T
}

View file

@ -8,7 +8,7 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
sealed class SpaceIconView {
data object Loading : SpaceIconView()
data class Placeholder(
val color: SystemColor = SystemColor.YELLOW,
val color: SystemColor = SystemColor.SKY,
val name: String = ""
): SpaceIconView()
data class Image(val url: Url) : SpaceIconView()

View file

@ -77,6 +77,7 @@ class VaultViewModel(
val spaces = MutableStateFlow<List<VaultSpaceView>>(emptyList())
val commands = MutableSharedFlow<Command>(replay = 0)
val showChooseSpaceType = MutableStateFlow(false)
val profileView = profileContainer.observe().map { obj ->
AccountProfile.Data(
@ -179,8 +180,30 @@ class VaultViewModel(
viewModelScope.launch { setVaultSpaceOrder.async(params = order) }
}
fun onChooseSpaceTypeClicked() {
viewModelScope.launch {
showChooseSpaceType.value = true
}
}
fun onCreateSpaceClicked() {
viewModelScope.launch { commands.emit(Command.CreateNewSpace) }
viewModelScope.launch {
showChooseSpaceType.value = false
commands.emit(Command.CreateNewSpace)
}
}
fun onCreateChatClicked() {
viewModelScope.launch {
showChooseSpaceType.value = false
commands.emit(Command.CreateChat)
}
}
fun onChooseSpaceTypeDismissed() {
viewModelScope.launch {
showChooseSpaceType.value = false
}
}
fun onResume(deeplink: DeepLinkResolver.Action? = null) {
@ -410,6 +433,7 @@ class VaultViewModel(
data class EnterSpaceHomeScreen(val space: Space): Command()
data class EnterSpaceLevelChat(val space: Space, val chat: Id): Command()
data object CreateNewSpace: Command()
data object CreateChat: Command()
data object OpenProfileSettings: Command()
sealed class Deeplink : Command() {