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

DROID-2378 Membership | Enhancement | Introduce incentives on share-space screen (#1302)

This commit is contained in:
Konstantin Ivanov 2024-06-21 12:51:40 +02:00 committed by GitHub
parent d6d871fe57
commit bc5102337c
Signed by: github
GPG key ID: B5690EEEBB952194
17 changed files with 923 additions and 295 deletions

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
!.idea/vcs.xml
!.idea/copyright
*.iml
/app/release
.gradle
/local.properties
/configuration.properties

View file

@ -8,6 +8,7 @@ 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.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.multiplayer.SpaceJoinRequestViewModel
@ -57,4 +58,5 @@ interface SpaceJoinRequestDependencies : ComponentDependencies {
fun spaceManager(): SpaceManager
fun analytics(): Analytics
fun analyticSpaceHelper(): AnalyticSpaceHelperDelegate
fun userPermissions(): UserPermissionProvider
}

View file

@ -65,7 +65,9 @@ class ShareSpaceFragment : BaseBottomSheetComposeFragment() {
onGenerateInviteLinkClicked = vm::onGenerateSpaceInviteLink,
onMoreInfoClicked = vm::onMoreInfoClicked,
onShareQrCodeClicked = vm::onShareQrCodeClicked,
onDeleteLinkClicked = vm::onDeleteLinkClicked
onDeleteLinkClicked = vm::onDeleteLinkClicked,
incentiveState = vm.showIncentive.collectAsStateWithLifecycle().value,
onIncentiveClicked = vm::onIncentiveClicked
)
}
LaunchedEffect(Unit) {
@ -181,6 +183,9 @@ class ShareSpaceFragment : BaseBottomSheetComposeFragment() {
val msg = getString(R.string.multiplayer_toast_permission_not_allowed)
toast(msg)
}
Command.ShowMembershipScreen -> {
findNavController().navigate(R.id.paymentsScreen)
}
}
}

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.core_ui.features.multiplayer
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@ -37,7 +38,8 @@ import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.Title1
@Composable
@Preview
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Mode")
@Preview(backgroundColor = 0x000000, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
fun ShareInviteLinkCardPreview() {
ShareInviteLinkCard(
link = "https://anytype.io/ibafyrfhfsag6rea3ifffsasssa3ifffsasssga3ifffsasssga3ifffsas",
@ -49,7 +51,8 @@ fun ShareInviteLinkCardPreview() {
}
@Composable
@Preview
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Mode")
@Preview(backgroundColor = 0x000000, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
fun GenerateInviteLinkCardPreview() {
GenerateInviteLinkCard(
modifier = Modifier,
@ -120,14 +123,7 @@ fun ShareInviteLinkCard(
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.multiplayer_share_invite_link_description),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.padding(horizontal = 20.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.padding(horizontal = 20.dp)
@ -140,12 +136,19 @@ fun ShareInviteLinkCard(
style = BodyCalloutRegular,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = colorResource(id = R.color.text_secondary),
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth()
)
}
Divider()
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(10.dp))
Text(
text = stringResource(R.string.multiplayer_share_invite_link_description),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_secondary),
modifier = Modifier.padding(horizontal = 20.dp)
)
Spacer(modifier = Modifier.height(20.dp))
Box(
modifier = Modifier
.fillMaxWidth()

View file

@ -1,10 +1,17 @@
package com.anytypeio.anytype.core_ui.features.multiplayer
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -12,17 +19,22 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -31,7 +43,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -40,6 +55,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -60,6 +76,8 @@ import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.ButtonUpgrade
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations1
import com.anytypeio.anytype.core_ui.views.Relations3
@ -68,12 +86,14 @@ import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel
import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceViewModel.ShareLinkViewState
import com.anytypeio.anytype.presentation.objects.SpaceMemberIconView
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ShareSpaceScreen(
spaceAccessType: SpaceAccessType?,
isCurrentUserOwner: Boolean,
members: List<ShareSpaceMemberView>,
shareLinkViewState: ShareLinkViewState,
incentiveState: ShareSpaceViewModel.ShareSpaceIncentiveState,
onGenerateInviteLinkClicked: () -> Unit,
onShareInviteLinkClicked: () -> Unit,
onViewRequestClicked: (ShareSpaceMemberView) -> Unit,
@ -84,142 +104,184 @@ fun ShareSpaceScreen(
onStopSharingClicked: () -> Unit,
onMoreInfoClicked: () -> Unit,
onShareQrCodeClicked: () -> Unit,
onDeleteLinkClicked: () -> Unit
onDeleteLinkClicked: () -> Unit,
onIncentiveClicked: () -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item {
Box(modifier = Modifier.fillMaxWidth()) {
Dragger(
modifier = Modifier
.align(Alignment.Center)
.padding(vertical = 6.dp)
)
}
val nestedScrollInteropConnection = rememberNestedScrollInteropConnection()
Box(
modifier = Modifier
.fillMaxSize()
.nestedScroll(nestedScrollInteropConnection)
) {
Column(
modifier = Modifier
.fillMaxSize()
) {
Box(
modifier = Modifier
.fillMaxWidth()
) {
Dragger(
modifier = Modifier
.align(Alignment.Center)
.padding(vertical = 6.dp)
)
}
item {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxWidth()) {
var isMenuExpanded by remember { mutableStateOf(false) }
Box(
modifier = Modifier
.fillMaxWidth()
) {
if (isCurrentUserOwner) {
Toolbar(title = stringResource(R.string.multiplayer_sharing))
} else {
Toolbar(title = stringResource(R.string.multiplayer_members))
}
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 16.dp)
) {
if (isCurrentUserOwner) {
Toolbar(title = stringResource(R.string.multiplayer_sharing))
} else {
Toolbar(title = stringResource(R.string.multiplayer_members))
Image(
painter = painterResource(id = R.drawable.ic_action_more),
contentDescription = "Menu button",
modifier = Modifier.noRippleClickable {
isMenuExpanded = true
}
)
}
Box(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 16.dp)
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
modifier = Modifier.background(
color = colorResource(id = R.color.background_secondary)
)
) {
if (isCurrentUserOwner) {
Image(
painter = painterResource(id = R.drawable.ic_action_more),
contentDescription = "Menu button",
modifier = Modifier.noRippleClickable {
isMenuExpanded = true
}
DropdownMenuItem(
onClick = {
onMoreInfoClicked()
isMenuExpanded = false
}
) {
Text(
text = stringResource(id = R.string.multiplayer_more_info),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.weight(1.0f)
)
}
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
modifier = Modifier.background(
color = colorResource(id = R.color.background_secondary)
if (spaceAccessType == SpaceAccessType.SHARED) {
Divider(
paddingStart = 0.dp,
paddingEnd = 0.dp
)
) {
DropdownMenuItem(
onClick = {
onMoreInfoClicked()
onStopSharingClicked()
isMenuExpanded = false
}
) {
Text(
text = stringResource(id = R.string.multiplayer_more_info),
text = stringResource(id = R.string.multiplayer_space_stop_sharing),
style = BodyRegular,
color = colorResource(id = R.color.text_primary),
color = colorResource(id = R.color.palette_dark_red),
modifier = Modifier.weight(1.0f)
)
}
if (spaceAccessType == SpaceAccessType.SHARED) {
Divider(
paddingStart = 0.dp,
paddingEnd = 0.dp
)
DropdownMenuItem(
onClick = {
onStopSharingClicked()
isMenuExpanded = false
}
) {
Text(
text = stringResource(id = R.string.multiplayer_space_stop_sharing),
style = BodyRegular,
color = colorResource(id = R.color.palette_dark_red),
modifier = Modifier.weight(1.0f)
)
}
}
}
}
}
}
if (isCurrentUserOwner) {
item {
Section(
title = stringResource(R.string.multiplayer_members_and_requests)
)
}
Section(
title = stringResource(R.string.multiplayer_members_and_requests)
)
}
members.forEachIndexed { index, member ->
item {
when(val config = member.config) {
is ShareSpaceMemberView.Config.Member -> {
SpaceMember(
member = member.obj,
isCurrentUserOwner = isCurrentUserOwner,
config = config,
onCanEditClicked = {
onCanEditClicked(member)
},
onCanViewClicked = {
onCanViewClicked(member)
},
onRemoveMemberClicked = {
onRemoveMemberClicked(member)
},
icon = member.icon,
canEditEnabled = member.canEditEnabled,
canReadEnabled = member.canReadEnabled,
isUser = member.isUser
)
Incentive(
incentiveState = incentiveState,
onIncentiveClicked = onIncentiveClicked
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
members.forEachIndexed { index, member ->
item {
when (val config = member.config) {
is ShareSpaceMemberView.Config.Member -> {
SpaceMember(
member = member.obj,
isCurrentUserOwner = isCurrentUserOwner,
config = config,
onCanEditClicked = {
onCanEditClicked(member)
},
onCanViewClicked = {
onCanViewClicked(member)
},
onRemoveMemberClicked = {
onRemoveMemberClicked(member)
},
icon = member.icon,
canEditEnabled = member.canEditEnabled,
canReadEnabled = member.canReadEnabled,
isUser = member.isUser
)
}
is ShareSpaceMemberView.Config.Request -> {
SpaceMemberRequest(
member = member.obj,
icon = member.icon,
request = config,
onViewRequestClicked = {
onViewRequestClicked(member)
},
onApproveLeaveRequestClicked = {
onApproveLeaveRequestClicked(member)
},
isUser = member.isUser
)
}
}
is ShareSpaceMemberView.Config.Request -> {
SpaceMemberRequest(
member = member.obj,
icon = member.icon,
request = config,
onViewRequestClicked = {
onViewRequestClicked(member)
},
onApproveLeaveRequestClicked = {
onApproveLeaveRequestClicked(member)
},
isUser = member.isUser
)
if (index != members.lastIndex) {
Divider()
}
}
if (index != members.lastIndex) {
Divider()
}
if (members.size > 2) {
// Workaround adding footer to prevent content invisible behind link card
item {
Spacer(modifier = Modifier.height(324.dp))
}
}
}
if (members.size > 4) {
// Workaround adding footer to prevent content invisible behind link card
item {
Spacer(modifier = Modifier.height(324.dp))
}
}
}
val density = LocalDensity.current
val draggedDownAnchorTop = with(density) { 250.dp.toPx() }
val anchors = DraggableAnchors {
DragValue.DRAGGED_DOWN at draggedDownAnchorTop
DragValue.DRAGGED_UP at 0f
}
val anchoredDraggableState = remember {
AnchoredDraggableState(
initialValue = DragValue.DRAGGED_UP,
anchors = anchors,
positionalThreshold = { distance: Float -> distance * 0.5f },
velocityThreshold = { with(density) { 100.dp.toPx() } },
animationSpec = tween()
)
}
val offset =
if (anchoredDraggableState.offset.isNaN()) 0 else anchoredDraggableState.offset.toInt()
SideEffect {
anchoredDraggableState.updateAnchors(anchors)
}
AnimatedVisibility(
visible = shareLinkViewState is ShareLinkViewState.Shared,
@ -227,7 +289,13 @@ fun ShareSpaceScreen(
exit = slideOutVertically { it },
modifier = Modifier.align(Alignment.BottomStart)
) {
Box(modifier = Modifier.padding(16.dp)) {
Box(modifier = Modifier
.padding(16.dp)
.offset {
IntOffset(x = 0, y = offset)
}
.anchoredDraggable(anchoredDraggableState, Orientation.Vertical)
) {
if (shareLinkViewState is ShareLinkViewState.Shared) {
ShareInviteLinkCard(
link = shareLinkViewState.link,
@ -253,6 +321,57 @@ fun ShareSpaceScreen(
}
}
@Composable
private fun Incentive(
incentiveState: ShareSpaceViewModel.ShareSpaceIncentiveState,
onIncentiveClicked: () -> Unit
) {
when (incentiveState) {
is ShareSpaceViewModel.ShareSpaceIncentiveState.VisibleSpaceReaders -> {
Text(
modifier = Modifier
.padding(horizontal = 20.dp),
text = stringResource(id = R.string.multiplayer_cant_add_members),
style = Caption1Regular,
color = colorResource(id = R.color.text_primary)
)
ButtonUpgrade(
modifier = Modifier
.padding(top = 12.dp, bottom = 24.dp, start = 16.dp, end = 16.dp)
.height(36.dp)
.verticalScroll(rememberScrollState()),
onClick = onIncentiveClicked,
text = stringResource(id = R.string.multiplayer_upgrade_button)
)
}
ShareSpaceViewModel.ShareSpaceIncentiveState.VisibleSpaceEditors -> {
Text(
modifier = Modifier
.padding(horizontal = 20.dp)
.verticalScroll(rememberScrollState()),
text = stringResource(id = R.string.multiplayer_cant_add_members),
style = Caption1Regular,
color = colorResource(id = R.color.text_primary)
)
ButtonUpgrade(
modifier = Modifier
.padding(top = 12.dp, bottom = 24.dp, start = 16.dp, end = 16.dp)
.height(36.dp)
.verticalScroll(rememberScrollState()),
onClick = onIncentiveClicked,
text = stringResource(id = R.string.multiplayer_upgrade_button)
)
}
ShareSpaceViewModel.ShareSpaceIncentiveState.Hidden -> {
//show nothing
}
}
}
enum class DragValue { DRAGGED_DOWN, DRAGGED_UP }
@Composable
private fun SpaceMember(
isUser: Boolean,
@ -304,16 +423,19 @@ private fun SpaceMember(
}
Spacer(modifier = Modifier.height(2.dp))
Text(
text = when(config) {
text = when (config) {
ShareSpaceMemberView.Config.Member.Writer -> {
stringResource(id = R.string.multiplayer_can_edit)
}
ShareSpaceMemberView.Config.Member.Owner -> {
stringResource(id = R.string.multiplayer_owner)
}
ShareSpaceMemberView.Config.Member.Reader -> {
stringResource(id = R.string.multiplayer_can_view)
}
else -> EMPTY_STRING_VALUE
},
style = Relations3,
@ -350,13 +472,13 @@ private fun SpaceMember(
color = colorResource(id = R.color.text_primary),
modifier = Modifier.weight(1.0f)
)
if (config is ShareSpaceMemberView.Config.Member.Reader) {
Image(
painter = painterResource(id = R.drawable.ic_dropdown_menu_check),
contentDescription = "Checked icon",
modifier = Modifier.align(Alignment.CenterVertically)
)
}
if (config is ShareSpaceMemberView.Config.Member.Reader) {
Image(
painter = painterResource(id = R.drawable.ic_dropdown_menu_check),
contentDescription = "Checked icon",
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
Divider()
DropdownMenuItem(
@ -373,13 +495,13 @@ private fun SpaceMember(
color = colorResource(id = R.color.text_primary),
modifier = Modifier.weight(1.0f)
)
if (config is ShareSpaceMemberView.Config.Member.Writer) {
Image(
painter = painterResource(id = R.drawable.ic_dropdown_menu_check),
contentDescription = "Checked icon",
modifier = Modifier.align(Alignment.CenterVertically)
)
}
if (config is ShareSpaceMemberView.Config.Member.Writer) {
Image(
painter = painterResource(id = R.drawable.ic_dropdown_menu_check),
contentDescription = "Checked icon",
modifier = Modifier.align(Alignment.CenterVertically)
)
}
}
Divider()
DropdownMenuItem(
@ -488,14 +610,15 @@ private fun SpaceMemberRequest(
}
}
Spacer(modifier = Modifier.height(2.dp))
val color = when(request) {
val color = when (request) {
ShareSpaceMemberView.Config.Request.Join -> ThemeColor.PINK
ShareSpaceMemberView.Config.Request.Leave -> ThemeColor.RED
}
val text = when(request) {
val text = when (request) {
ShareSpaceMemberView.Config.Request.Join -> stringResource(
id = R.string.multiplayer_join_request
)
ShareSpaceMemberView.Config.Request.Leave -> stringResource(
id = R.string.multiplayer_leave_request
)
@ -515,7 +638,7 @@ private fun SpaceMemberRequest(
style = Relations1
)
}
when(request) {
when (request) {
ShareSpaceMemberView.Config.Request.Join -> {
ButtonSecondary(
text = stringResource(R.string.multiplayer_view_request),
@ -526,6 +649,7 @@ private fun SpaceMemberRequest(
modifier = Modifier.align(Alignment.CenterVertically)
)
}
ShareSpaceMemberView.Config.Request.Leave -> {
ButtonSecondary(
text = stringResource(R.string.multiplayer_approve_request),
@ -580,7 +704,18 @@ fun SpaceLeaveRequestPreview() {
}
@Composable
@Preview
@Preview(
backgroundColor = 0xFFFFFFFF,
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_NO,
name = "Light Mode"
)
@Preview(
backgroundColor = 0x000000,
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_YES,
name = "Dark Mode"
)
fun ShareSpaceScreenPreview() {
ShareSpaceScreen(
shareLinkViewState = ShareSpaceViewModel.ShareLinkViewState.Shared(
@ -649,13 +784,15 @@ fun ShareSpaceScreenPreview() {
onRemoveMemberClicked = {},
onCanViewClicked = {},
onCanEditClicked = {},
isCurrentUserOwner = false,
isCurrentUserOwner = true,
onStopSharingClicked = {},
onGenerateInviteLinkClicked = {},
onMoreInfoClicked = {},
onShareQrCodeClicked = {},
onDeleteLinkClicked = {},
spaceAccessType = null
spaceAccessType = null,
incentiveState = ShareSpaceViewModel.ShareSpaceIncentiveState.VisibleSpaceReaders,
onIncentiveClicked = {}
)
}

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.core_ui.views
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.ImageView
@ -129,9 +130,8 @@ fun ButtonPrimary(
val interactionSource = remember { MutableInteractionSource() }
val isPressed = interactionSource.collectIsPressedAsState()
val backgroundColor =
if (isPressed.value) colorResource(id = R.color.button_pressed) else colorResource(
id = R.color.glyph_selected
)
if (isPressed.value) colorResource(id = R.color.glyph_button).copy(alpha = 0.15f)
else colorResource(id = R.color.glyph_button)
CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {
Button(
@ -142,8 +142,8 @@ fun ButtonPrimary(
colors = ButtonDefaults.buttonColors(
backgroundColor = backgroundColor,
contentColor = colorResource(id = R.color.button_text),
disabledBackgroundColor = colorResource(id = R.color.shape_tertiary),
disabledContentColor = colorResource(id = R.color.text_tertiary)
disabledBackgroundColor = colorResource(id = R.color.shape_secondary),
disabledContentColor = colorResource(id = R.color.text_label_inversion)
),
modifier = modifier
.defaultMinSize(minWidth = 1.dp, minHeight = 1.dp),
@ -217,9 +217,8 @@ fun ButtonPrimaryLoading(
val interactionSource = remember { MutableInteractionSource() }
val isPressed = interactionSource.collectIsPressedAsState()
val backgroundColor =
if (isPressed.value) colorResource(id = R.color.button_pressed) else colorResource(
id = R.color.glyph_selected
)
if (isPressed.value) colorResource(id = R.color.glyph_button).copy(alpha = 0.15f)
else colorResource(id = R.color.glyph_button)
CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {
Box(modifier = modifierBox, contentAlignment = Alignment.Center) {
@ -231,8 +230,8 @@ fun ButtonPrimaryLoading(
colors = ButtonDefaults.buttonColors(
backgroundColor = backgroundColor,
contentColor = colorResource(id = R.color.button_text),
disabledBackgroundColor = colorResource(id = R.color.shape_tertiary),
disabledContentColor = colorResource(id = R.color.text_tertiary)
disabledBackgroundColor = colorResource(id = R.color.shape_secondary),
disabledContentColor = colorResource(id = R.color.text_label_inversion)
),
modifier = modifierButton
.defaultMinSize(minWidth = 1.dp, minHeight = 1.dp),
@ -568,7 +567,8 @@ object NoRippleTheme : RippleTheme {
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Mode")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
fun MyPrimaryButton() {
ButtonPrimary(
onClick = {},
@ -581,7 +581,8 @@ fun MyPrimaryButton() {
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Mode")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
fun MyPrimaryButtonDisabled() {
ButtonPrimary(
onClick = {},
@ -595,7 +596,8 @@ fun MyPrimaryButtonDisabled() {
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Mode")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
fun MyPrimaryButtonDark() {
ButtonPrimaryDarkTheme(
onClick = {},
@ -608,7 +610,8 @@ fun MyPrimaryButtonDark() {
}
@Composable
@Preview
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Mode")
@Preview(backgroundColor = 0x000000, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
fun MySecondaryButton() {
ButtonSecondary(
onClick = {},
@ -621,7 +624,8 @@ fun MySecondaryButton() {
}
@Composable
@Preview
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Mode")
@Preview(backgroundColor = 0x000000, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")
fun MyWarningButton() {
ButtonWarning(
onClick = {},

View file

@ -0,0 +1,74 @@
package com.anytypeio.anytype.core_ui.views
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.R
@Composable
fun ButtonUpgrade(
text: String = "",
onClick: () -> Unit,
modifier: Modifier = Modifier,
style: TextStyle = BodyCalloutRegular
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed = interactionSource.collectIsPressedAsState()
val backgroundColor =
if (isPressed.value) colorResource(id = R.color.button_pressed) else colorResource(
id = R.color.glyph_selected
)
CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) {
Box(
modifier = modifier
.fillMaxWidth()
.background(color = backgroundColor, shape = RoundedCornerShape(18.dp))
.clickable(
interactionSource = interactionSource,
onClick = onClick,
indication = null
)
,
contentAlignment = Alignment.Center
) {
Text(
modifier = Modifier.wrapContentSize(),
text = text,
style = style,
color = colorResource(id = R.color.button_text)
)
}
}
}
@Composable
@Preview(backgroundColor = 0xFFFFFFFF, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light Mode")
@Preview(backgroundColor = 0x000000, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark Mode")fun MyButtonUpgrade() {
ButtonUpgrade(
onClick = {},
text = "✦ Upgrade",
modifier = Modifier
.padding(start = 16.dp, end = 16.dp)
.height(36.dp)
)
}

View file

@ -2,19 +2,33 @@ package com.anytypeio.anytype.domain.`object`
import com.anytypeio.anytype.core_models.ObjectWrapper.SpaceView
import com.anytypeio.anytype.core_models.ObjectWrapper.SpaceMember
import com.anytypeio.anytype.core_models.multiplayer.ParticipantStatus
import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType
import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions
fun SpaceView.canAddWriters(participants: List<SpaceMember>): Boolean {
if (!canAddReaders(participants)) return false
this.writersLimit?.let {
return it > activeWriters(participants)
} ?: return true
fun SpaceView.canAddWriters(
isCurrentUserOwner: Boolean,
participants: List<SpaceMember>
): Boolean {
if (!canAddReaders(isCurrentUserOwner, participants)) return false
if (!isCurrentUserOwner) return false
if (spaceAccessType != SpaceAccessType.SHARED) return false
if (participants.none { it.status == ParticipantStatus.JOINING }) return false
val isWritersLimitReached =
isSubscriberLimitReached(activeWriters(participants), writersLimit?.toInt())
return !isWritersLimitReached
}
fun SpaceView.canAddReaders(participants: List<SpaceMember>): Boolean {
this.readersLimit?.let {
return it > activeReaders(participants)
} ?: return true
fun SpaceView.canAddReaders(
isCurrentUserOwner: Boolean,
participants: List<SpaceMember>
): Boolean {
if (!isCurrentUserOwner) return false
if (spaceAccessType != SpaceAccessType.SHARED) return false
if (participants.none { it.status == ParticipantStatus.JOINING }) return false
val isReadersLimitReached =
isSubscriberLimitReached(activeReaders(participants), readersLimit?.toInt())
return !isReadersLimitReached
}
fun SpaceView.canChangeWriterToReader(participants: List<SpaceMember>): Boolean {
@ -22,9 +36,7 @@ fun SpaceView.canChangeWriterToReader(participants: List<SpaceMember>): Boolean
}
fun SpaceView.canChangeReaderToWriter(participants: List<SpaceMember>): Boolean {
writersLimit?.let {
return it.toInt() > activeWriters(participants)
} ?: return true
return !isSubscriberLimitReached(activeWriters(participants), writersLimit?.toInt())
}
private fun activeReaders(participants: List<SpaceMember>): Int =
@ -43,3 +55,7 @@ private fun activeWriters(participants: List<SpaceMember>): Int =
SpaceMemberPermissions.OWNER
)
}
private fun isSubscriberLimitReached(currentSubscribers: Int, subscriberLimit: Int?): Boolean {
return subscriberLimit?.let { currentSubscribers >= it } ?: false
}

View file

@ -2,6 +2,7 @@ package com.anytypeio.anytype.domain.`object`
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.multiplayer.ParticipantStatus
import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions
import junit.framework.TestCase.assertTrue
import kotlin.test.assertFalse
@ -12,6 +13,8 @@ class SpaceViewExtKtTest {
val spaceId = RandomString.make()
val isCurrentUserOwner = true
@Test
fun `1 participant, zero limits`() {
val spaceView = createSpaceView(
@ -19,220 +22,517 @@ class SpaceViewExtKtTest {
readersLimit = 0
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
)
)
assertFalse(spaceView.canAddWriters(participants))
assertFalse(spaceView.canAddReaders(participants))
assertFalse(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertFalse(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertFalse(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `1 participant, o`() {
fun `1 participant, owner + join`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 3
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertTrue(spaceView.canAddWriters(participants))
assertTrue(spaceView.canAddReaders(participants))
assertTrue(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertTrue(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertTrue(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `2 participants, o w`() {
fun `2 participants, owner writer join`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 3
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.WRITER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertTrue(spaceView.canAddWriters(participants))
assertTrue(spaceView.canAddReaders(participants))
assertTrue(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertTrue(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertTrue(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `2 participants, o r`() {
fun `2 participants, owner reader join`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 3
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.READER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertTrue(spaceView.canAddWriters(participants))
assertTrue(spaceView.canAddReaders(participants))
assertTrue(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertTrue(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertTrue(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `3 participants, o r r`() {
fun `3 participants, owner reader reader join`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 3
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.READER,
SpaceMemberPermissions.READER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertFalse(spaceView.canAddWriters(participants))
assertFalse(spaceView.canAddReaders(participants))
assertFalse(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertFalse(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertTrue(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `3 participants, o w r`() {
fun `3 participants, owner writer reader join`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 3
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.WRITER,
SpaceMemberPermissions.READER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertFalse(spaceView.canAddWriters(participants))
assertFalse(spaceView.canAddReaders(participants))
assertFalse(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertFalse(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertTrue(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `3 participants, o w w`() {
fun `3 participants, owner writer writer join`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 3
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.WRITER,
SpaceMemberPermissions.WRITER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertFalse(spaceView.canAddWriters(participants))
assertFalse(spaceView.canAddReaders(participants))
assertFalse(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertFalse(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertFalse(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `3 participants, o w w, diff limits`() {
fun `3 participants, owner writer writer join, diff limits`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 5
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.WRITER,
SpaceMemberPermissions.WRITER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertFalse(spaceView.canAddWriters(participants))
assertTrue(spaceView.canAddReaders(participants))
assertFalse(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertTrue(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertFalse(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `3 participants, o w r, diff limits`() {
fun `3 participants, owner writer reader join, diff limits`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 5
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.WRITER,
SpaceMemberPermissions.READER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertTrue(spaceView.canAddWriters(participants))
assertTrue(spaceView.canAddReaders(participants))
assertTrue(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertTrue(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertTrue(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `3 participants, o r r, diff limits`() {
fun `3 participants, owner reader reader join, diff limits`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 5
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.READER,
SpaceMemberPermissions.READER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertTrue(spaceView.canAddWriters(participants))
assertTrue(spaceView.canAddReaders(participants))
assertTrue(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertTrue(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertTrue(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `4 participants, o w w r, diff limits`() {
fun `4 participants, owner writer writer reader join, diff limits`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 5
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.WRITER,
SpaceMemberPermissions.WRITER,
SpaceMemberPermissions.READER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertFalse(spaceView.canAddWriters(participants))
assertTrue(spaceView.canAddReaders(participants))
assertFalse(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertTrue(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertFalse(spaceView.canChangeReaderToWriter(participants))
}
@Test
fun `5 participants, o w w r r, diff limits`() {
fun `5 participants, o w w r r join, diff limits`() {
val spaceView = createSpaceView(
writersLimit = 3,
readersLimit = 5
)
val participants = createParticipants(
SpaceMemberPermissions.OWNER,
SpaceMemberPermissions.WRITER,
SpaceMemberPermissions.WRITER,
SpaceMemberPermissions.READER,
SpaceMemberPermissions.READER
val participants = listOf(
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.OWNER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.WRITER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to SpaceMemberPermissions.READER.code.toDouble(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.ACTIVE.code.toDouble()
)
),
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.PARTICIPANT_STATUS to ParticipantStatus.JOINING.code.toDouble()
)
)
)
assertFalse(spaceView.canAddWriters(participants))
assertFalse(spaceView.canAddReaders(participants))
assertFalse(spaceView.canAddWriters(isCurrentUserOwner, participants))
assertFalse(spaceView.canAddReaders(isCurrentUserOwner, participants))
assertTrue(spaceView.canChangeWriterToReader(participants))
assertFalse(spaceView.canChangeReaderToWriter(participants))
}
@ -242,19 +542,10 @@ class SpaceViewExtKtTest {
map = mapOf(
Relations.ID to spaceId,
Relations.WRITERS_LIMIT to writersLimit.toDouble(),
Relations.READERS_LIMIT to readersLimit.toDouble()
Relations.READERS_LIMIT to readersLimit.toDouble(),
Relations.SPACE_ACCESS_TYPE to 2.0
)
)
private fun createParticipants(vararg permissions: SpaceMemberPermissions) = permissions.map {
ObjectWrapper.SpaceMember(
map = mapOf(
Relations.ID to RandomString.make(),
Relations.SPACE_ID to spaceId,
Relations.PARTICIPANT_PERMISSIONS to it.code.toDouble()
)
)
}
//endregion
}

View file

@ -4,7 +4,7 @@ kotlinVersion = '1.9.22'
androidxCoreVersion = "1.13.0"
androidxComposeVersion = '1.6.6'
androidxComposeVersion = '1.6.8'
composeKotlinCompilerVersion = '1.5.10'
composeMaterial3Version = '1.2.1'
composeMaterialVersion = '1.6.6'
@ -17,7 +17,7 @@ composeReorderableVersion = 'e9ef693f63'
accompanistVersion = "0.34.0"
appcompatVersion = '1.6.1'
androidXAnnotationVersion = '1.7.1'
fragmentVersion = "1.6.2"
fragmentVersion = "1.8.0"
exoplayerVersion = "2.19.1"
wireVersion = "4.9.8"
glideVersion = "4.14.2"

View file

@ -1312,7 +1312,7 @@
<!--region MULTIPLAYER -->
<string name="multiplayer_share_invite_link">Share invite link</string>
<string name="multiplayer_share_invite_link_description">Send this link to invite others. Assign access rights upon their request approval</string>
<string name="multiplayer_share_invite_link_description">Share this invite link so that others can join your Space. Once they click your link and request access, you can set their access rights.</string>
<string name="multiplayer_invite_link">Invite link</string>
<string name="multiplayer_join_a_space">Join a space</string>
<string name="multiplayer_private_comment_for_a_space_owner">Private comment for a space owner</string>
@ -1322,6 +1322,8 @@
<string name="multiplayer_tap_to_write_request_to_join_comment">Tap to write your comment</string>
<string name="multiplayer_share_space">Share space</string>
<string name="multiplayer_members_and_requests">Members and requests</string>
<string name="multiplayer_cant_add_members">You cant add more members</string>
<string name="multiplayer_upgrade_button">✦ Upgrade</string>
<string name="multiplayer_members">Members</string>
<string name="multiplayer_leave_request">Leave request</string>
<string name="multiplayer_join_request">Join request</string>

View file

@ -6,6 +6,7 @@ object MembershipConstants {
const val EXPLORER_ID = 1
const val BUILDER_ID = 4
const val CO_CREATOR_ID = 5
const val ANY_TEAM_ID = 7
const val MEMBERSHIP_LEVEL_DETAILS = "https://anytype.io/pricing"
const val PRIVACY_POLICY = "https://anytype.io/app_privacy"

View file

@ -0,0 +1,29 @@
package com.anytypeio.anytype.presentation.mapper
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.`object`.canChangeReaderToWriter
import com.anytypeio.anytype.domain.`object`.canChangeWriterToReader
import com.anytypeio.anytype.presentation.multiplayer.ShareSpaceMemberView
fun List<ObjectWrapper.SpaceMember>.toView(
spaceView: ObjectWrapper.SpaceView?,
urlBuilder: UrlBuilder,
isCurrentUserOwner: Boolean,
account: Id?
): List<ShareSpaceMemberView> {
return this.mapNotNull { spaceMember ->
if (spaceView == null) return@mapNotNull null
val canChangeReaderToWriter = spaceView.canChangeReaderToWriter(participants = this)
val canChangeWriterToReader = spaceView.canChangeWriterToReader(participants = this)
ShareSpaceMemberView.fromObject(
obj = spaceMember,
urlBuilder = urlBuilder,
canChangeWriterToReader = canChangeWriterToReader,
canChangeReaderToWriter = canChangeReaderToWriter,
includeRequests = isCurrentUserOwner,
account = account
)
}
}

View file

@ -0,0 +1,17 @@
package com.anytypeio.anytype.presentation.multiplayer
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.domain.`object`.canAddReaders
import com.anytypeio.anytype.domain.`object`.canAddWriters
fun ObjectWrapper.SpaceView.getIncentiveState(
isCurrentUserOwner: Boolean, spaceMembers: List<ObjectWrapper.SpaceMember>
): ShareSpaceViewModel.ShareSpaceIncentiveState {
val canAddReaders = canAddReaders(isCurrentUserOwner, spaceMembers)
val canAddWriters = canAddWriters(isCurrentUserOwner, spaceMembers)
return when {
!canAddReaders -> ShareSpaceViewModel.ShareSpaceIncentiveState.VisibleSpaceReaders
!canAddWriters -> ShareSpaceViewModel.ShareSpaceIncentiveState.VisibleSpaceEditors
else -> ShareSpaceViewModel.ShareSpaceIncentiveState.Hidden
}
}

View file

@ -9,7 +9,6 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.SharingSpacesTypes.
import com.anytypeio.anytype.analytics.base.EventsDictionary.SharingSpacesTypes.shareTypeQR
import com.anytypeio.anytype.analytics.base.EventsDictionary.SharingSpacesTypes.shareTypeRevoke
import com.anytypeio.anytype.analytics.base.EventsDictionary.SharingSpacesTypes.shareTypeShareLink
import com.anytypeio.anytype.analytics.base.EventsDictionary.SharingSpacesTypes.shareTypeShareQr
import com.anytypeio.anytype.analytics.base.EventsDictionary.clickSettingsSpaceShare
import com.anytypeio.anytype.analytics.base.EventsDictionary.removeSpaceMember
import com.anytypeio.anytype.analytics.base.EventsDictionary.screenRevokeShareLink
@ -45,7 +44,10 @@ import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.canChangeReaderToWriter
import com.anytypeio.anytype.domain.`object`.canChangeWriterToReader
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.mapper.toView
import com.anytypeio.anytype.presentation.objects.SpaceMemberIconView
import com.anytypeio.anytype.presentation.objects.toSpaceMembers
import com.anytypeio.anytype.presentation.objects.toSpaceView
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.getSpaceMembersSearchParams
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.getSpaceViewSearchParams
import javax.inject.Inject
@ -78,9 +80,7 @@ class ShareSpaceViewModel(
val commands = MutableSharedFlow<Command>()
val isCurrentUserOwner = MutableStateFlow(false)
val spaceAccessType = MutableStateFlow<SpaceAccessType?>(null)
private var canChangeWriterToReader = false
private var canChangeReaderToWriter = false
val showIncentive = MutableStateFlow<ShareSpaceIncentiveState>(ShareSpaceIncentiveState.Hidden)
init {
Timber.d("Share-space init with params: $params")
@ -119,34 +119,27 @@ class ShareSpaceViewModel(
container.subscribe(spaceMembersSearchParams),
isCurrentUserOwner
) { spaceResponse, membersResponse, isCurrentUserOwner ->
val spaceView = spaceResponse.firstOrNull()?.let { ObjectWrapper.SpaceView(it.map) }
val spaceMembers = membersResponse.map { ObjectWrapper.SpaceMember(it.map) }
canChangeReaderToWriter = spaceView?.canChangeReaderToWriter(spaceMembers) ?: false
canChangeWriterToReader = spaceView?.canChangeWriterToReader(spaceMembers) ?: false
val spaceViewMembers = spaceMembers.mapNotNull { m ->
ShareSpaceMemberView.fromObject(
obj = m,
urlBuilder = urlBuilder,
canChangeWriterToReader = canChangeWriterToReader,
canChangeReaderToWriter = canChangeReaderToWriter,
includeRequests = isCurrentUserOwner,
account = account
)
}
Triple(spaceView, spaceViewMembers, isCurrentUserOwner)
Triple(spaceResponse, membersResponse, isCurrentUserOwner)
}.catch {
Timber.e(
it, "Error while $SHARE_SPACE_MEMBER_SUBSCRIPTION " +
"and $SHARE_SPACE_SPACE_SUBSCRIPTION subscription"
)
}.collect { (spaceView, spaceViewMembers, isCurrentUserOwner) ->
}.collect { (spaceResponse, membersResponse, isCurrentUserOwner) ->
val spaceView = spaceResponse.toSpaceView()
val spaceMembers = membersResponse.toSpaceMembers()
spaceAccessType.value = spaceView?.spaceAccessType
setShareLinkViewState(spaceView, isCurrentUserOwner)
members.value = spaceViewMembers
members.value = spaceMembers.toView(
spaceView = spaceView,
urlBuilder = urlBuilder,
isCurrentUserOwner = isCurrentUserOwner,
account = account
)
showIncentive.value = spaceView?.getIncentiveState(
spaceMembers = spaceMembers,
isCurrentUserOwner = isCurrentUserOwner
) ?: ShareSpaceIncentiveState.Hidden
}
}
}
@ -441,6 +434,12 @@ class ShareSpaceViewModel(
}
}
fun onIncentiveClicked() {
viewModelScope.launch {
commands.emit(Command.ShowMembershipScreen)
}
}
fun onDeleteLinkAccepted() {
Timber.d("onDeleteLinkAccepted")
viewModelScope.launch {
@ -549,6 +548,13 @@ class ShareSpaceViewModel(
data object ShowDeleteLinkWarning: Command()
data object ToastPermission : Command()
data object Dismiss : Command()
data object ShowMembershipScreen : Command()
}
sealed class ShareSpaceIncentiveState {
data object Hidden : ShareSpaceIncentiveState()
data object VisibleSpaceReaders : ShareSpaceIncentiveState()
data object VisibleSpaceEditors : ShareSpaceIncentiveState()
}
companion object {

View file

@ -23,6 +23,7 @@ import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.ApproveJoinSpaceRequest
import com.anytypeio.anytype.domain.multiplayer.DeclineSpaceJoinRequest
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.domain.`object`.canAddReaders
import com.anytypeio.anytype.domain.`object`.canAddWriters
import com.anytypeio.anytype.domain.search.SearchObjects
@ -33,6 +34,7 @@ import com.anytypeio.anytype.presentation.search.ObjectSearchConstants
import com.anytypeio.anytype.presentation.search.ObjectSearchConstants.filterParticipants
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import timber.log.Timber
@ -43,16 +45,20 @@ class SpaceJoinRequestViewModel(
private val searchObjects: SearchObjects,
private val spaceManager: SpaceManager,
private val urlBuilder: UrlBuilder,
private val analytics: Analytics
private val analytics: Analytics,
private val userPermissionProvider: UserPermissionProvider
): BaseViewModel() {
val isDismissed = MutableStateFlow(false)
private val _isCurrentUserOwner = MutableStateFlow(false)
private val state = MutableStateFlow<State>(State.Init)
val viewState = MutableStateFlow<ViewState>(ViewState.Init)
init {
proceedWithUserPermissions()
viewModelScope.launch {
val config = spaceManager.getConfig()
if (config != null && config.space == params.space.id) {
@ -119,7 +125,9 @@ class SpaceJoinRequestViewModel(
}
viewModelScope.launch {
state.collect { curr ->
state.combine(_isCurrentUserOwner) { state, isCurrentUserOwner ->
state to isCurrentUserOwner
}.collect { (curr, isCurrentUserOwner) ->
viewState.value = when (curr) {
is State.Error -> ViewState.Error
is State.Init -> ViewState.Init
@ -130,8 +138,8 @@ class SpaceJoinRequestViewModel(
obj = curr.member,
urlBuilder = urlBuilder
),
canAddAsReader = curr.spaceView.canAddReaders(curr.participants),
canAddAsEditor = curr.spaceView.canAddWriters(curr.participants)
canAddAsReader = curr.spaceView.canAddReaders(isCurrentUserOwner, curr.participants),
canAddAsEditor = curr.spaceView.canAddWriters(isCurrentUserOwner, curr.participants)
)
}
}
@ -147,6 +155,16 @@ class SpaceJoinRequestViewModel(
}
}
private fun proceedWithUserPermissions() {
viewModelScope.launch {
userPermissionProvider
.observe(space = params.space)
.collect { permission ->
_isCurrentUserOwner.value = permission == SpaceMemberPermissions.OWNER
}
}
}
private suspend fun getMembers(config: Config) {
val searchMembersParams = SearchObjects.Params(
filters = filterParticipants(
@ -289,7 +307,8 @@ class SpaceJoinRequestViewModel(
private val searchObjects: SearchObjects,
private val spaceManager: SpaceManager,
private val urlBuilder: UrlBuilder,
private val analytics: Analytics
private val analytics: Analytics,
private val userPermissionProvider: UserPermissionProvider
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = SpaceJoinRequestViewModel(
@ -299,7 +318,8 @@ class SpaceJoinRequestViewModel(
searchObjects = searchObjects,
spaceManager = spaceManager,
urlBuilder = urlBuilder,
analytics = analytics
analytics = analytics,
userPermissionProvider = userPermissionProvider
) as T
}

View file

@ -16,7 +16,6 @@ import com.anytypeio.anytype.presentation.extension.getProperObjectName
import com.anytypeio.anytype.presentation.library.LibraryView
import com.anytypeio.anytype.presentation.linking.LinkToItemView
import com.anytypeio.anytype.presentation.navigation.DefaultObjectView
import com.anytypeio.anytype.presentation.objects.SupportedLayouts.fileLayouts
import com.anytypeio.anytype.presentation.relations.RelationValueView
import com.anytypeio.anytype.presentation.sets.filter.CreateFilterView
import com.anytypeio.anytype.presentation.widgets.collection.CollectionView
@ -311,4 +310,25 @@ private fun ObjectWrapper.Basic.getFileObjectIcon(): ObjectIcon {
else -> ObjectIcon.None
}
}
}
fun List<ObjectWrapper.Basic>.toSpaceMembers(): List<ObjectWrapper.SpaceMember> =
mapNotNull { basic ->
if (basic.map.isEmpty()) {
null
} else {
ObjectWrapper.SpaceMember(basic.map)
}
}
fun List<ObjectWrapper.Basic>.toSpaceView() =
if (isNotEmpty()) {
val spaceMap = first().map
if (spaceMap.isEmpty()) {
null
} else {
ObjectWrapper.SpaceView(spaceMap)
}
} else {
null
}