diff --git a/app/gradle.properties b/app/gradle.properties index 57c26abc45..f3ffb627d2 100644 --- a/app/gradle.properties +++ b/app/gradle.properties @@ -1,4 +1,4 @@ version.versionMajor=0 version.versionMinor=41 -version.versionPatch=0 +version.versionPatch=5 version.useDatedVersionName=false \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/settings/ProfileDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/ProfileDI.kt index 88603abe86..ce95896757 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/settings/ProfileDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/ProfileDI.kt @@ -15,6 +15,7 @@ import com.anytypeio.anytype.domain.icon.RemoveObjectIcon import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.networkmode.GetNetworkMode import com.anytypeio.anytype.domain.`object`.SetObjectDetails import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager @@ -58,7 +59,8 @@ object ProfileModule { getNetworkMode: GetNetworkMode, profileSubscriptionManager: ProfileSubscriptionManager, removeObjectIcon: RemoveObjectIcon, - notificationPermissionManager: NotificationPermissionManager + notificationPermissionManager: NotificationPermissionManager, + userPermissionProvider: UserPermissionProvider ): ProfileSettingsViewModel.Factory = ProfileSettingsViewModel.Factory( analytics = analytics, container = storelessSubscriptionContainer, @@ -70,7 +72,8 @@ object ProfileModule { getNetworkMode = getNetworkMode, profileSubscriptionManager = profileSubscriptionManager, removeObjectIcon = removeObjectIcon, - notificationPermissionManager = notificationPermissionManager + notificationPermissionManager = notificationPermissionManager, + userPermissionProvider = userPermissionProvider ) @Provides diff --git a/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt index cbc9c18906..cdbd0216a5 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/editor/EditorFragment.kt @@ -862,14 +862,12 @@ open class EditorFragment : NavigationFragment(R.layout.f } open fun setupWindowInsetAnimation() { - if (BuildConfig.USE_NEW_WINDOW_INSET_API && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - binding.toolbar.syncTranslationWithImeVisibility( - dispatchMode = DISPATCH_MODE_STOP - ) - binding.chooseTypeWidget.syncTranslationWithImeVisibility( - dispatchMode = DISPATCH_MODE_STOP - ) - } + binding.toolbar.syncTranslationWithImeVisibility( + dispatchMode = DISPATCH_MODE_STOP + ) + binding.chooseTypeWidget.syncTranslationWithImeVisibility( + dispatchMode = DISPATCH_MODE_STOP + ) } private fun onApplyScrollAndMoveClicked() { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/OnboardingFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/OnboardingFragment.kt index bf50fa225c..c8917a33ba 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/onboarding/OnboardingFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/onboarding/OnboardingFragment.kt @@ -11,9 +11,7 @@ import android.os.Build import android.os.Bundle import android.os.PersistableBundle import android.view.LayoutInflater -import android.view.View import android.view.ViewGroup -import android.view.WindowManager import androidx.activity.compose.BackHandler import androidx.activity.compose.ManagedActivityResultLauncher import androidx.activity.compose.rememberLauncherForActivityResult @@ -49,8 +47,6 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.core.os.bundleOf -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.fragment.compose.content @@ -71,8 +67,6 @@ import com.anytypeio.anytype.core_ui.views.BaseAlertDialog import com.anytypeio.anytype.core_utils.ext.argOrNull import com.anytypeio.anytype.core_utils.ext.shareFirstFileFromPath import com.anytypeio.anytype.core_utils.ext.toast -import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK -import com.anytypeio.anytype.core_utils.insets.RootViewDeferringInsetsCallback import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.ext.daggerViewModel import com.anytypeio.anytype.presentation.onboarding.OnboardingStartViewModel @@ -154,12 +148,7 @@ class OnboardingFragment : Fragment() { Box( modifier = Modifier .fillMaxSize() - .then( - if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) - Modifier.windowInsetsPadding(insets = WindowInsets.systemBars) - else - Modifier - ) + .windowInsetsPadding(insets = WindowInsets.systemBars) ) { val currentPage = remember { mutableStateOf(OnboardingPage.AUTH) } //BackgroundCircle() @@ -183,24 +172,6 @@ class OnboardingFragment : Fragment() { } } - private fun onApplyWindowRootInsets(view: View) { - if ( Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) { - return - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val deferringInsetsListener = RootViewDeferringInsetsCallback( - persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), - deferredInsetTypes = 0 - ) - - ViewCompat.setWindowInsetsAnimationCallback(view, deferringInsetsListener) - ViewCompat.setOnApplyWindowInsetsListener(view, deferringInsetsListener) - } else { - // Enabling workaround to prevent background circle with video shrinking. - activity?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING); - } - } - @Composable private fun Onboarding( currentPage: MutableState, diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt index 7fe8cc0a15..bd79f3d6f6 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/sets/ObjectSetFragment.kt @@ -469,21 +469,19 @@ open class ObjectSetFragment : } private fun setupWindowInsetAnimation() { - if (BuildConfig.USE_NEW_WINDOW_INSET_API && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - binding.bottomToolbarBox.syncTranslationWithImeVisibility( - dispatchMode = DISPATCH_MODE_STOP - ) - title.syncFocusWithImeVisibility() - binding.viewerEditWidget.syncTranslationWithImeVisibility( - dispatchMode = DISPATCH_MODE_STOP - ) - binding.templatesWidget.syncTranslationWithImeVisibility( - dispatchMode = DISPATCH_MODE_STOP - ) - binding.titleWidget.syncTranslationWithImeVisibility( - dispatchMode = DISPATCH_MODE_STOP - ) - } + binding.bottomToolbarBox.syncTranslationWithImeVisibility( + dispatchMode = DISPATCH_MODE_STOP + ) + title.syncFocusWithImeVisibility() + binding.viewerEditWidget.syncTranslationWithImeVisibility( + dispatchMode = DISPATCH_MODE_STOP + ) + binding.templatesWidget.syncTranslationWithImeVisibility( + dispatchMode = DISPATCH_MODE_STOP + ) + binding.titleWidget.syncTranslationWithImeVisibility( + dispatchMode = DISPATCH_MODE_STOP + ) } private fun setupGridAdapters() { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/ChooseSpaceTypeScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/ChooseSpaceTypeScreen.kt index bac68b6ed7..27341120e2 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/ChooseSpaceTypeScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/ChooseSpaceTypeScreen.kt @@ -96,7 +96,7 @@ fun ChooseSpaceTypeScreen( Text( text = stringResource(id = R.string.vault_create_chat_description), style = Caption1Regular, - color = colorResource(id = R.color.text_secondary), + color = colorResource(id = R.color.control_transparent_secondary), maxLines = 2, overflow = TextOverflow.Ellipsis ) @@ -129,7 +129,7 @@ fun ChooseSpaceTypeScreen( Text( text = stringResource(id = R.string.vault_create_space_description), style = Caption1Regular, - color = colorResource(id = R.color.text_secondary), + color = colorResource(id = R.color.control_transparent_secondary), maxLines = 2, overflow = TextOverflow.Ellipsis ) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt index 2e3d374885..983186e2d0 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultFragment.kt @@ -9,7 +9,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.material.MaterialTheme import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.res.stringResource import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import androidx.fragment.compose.content diff --git a/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/DataViewWidget.kt b/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/DataViewWidget.kt index 45c0ea64bd..4bb632e0f3 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/DataViewWidget.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/widgets/types/DataViewWidget.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -32,10 +33,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextIndent import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.anytypeio.anytype.R import com.anytypeio.anytype.core_models.Id @@ -53,6 +59,7 @@ import com.anytypeio.anytype.presentation.editor.cover.CoverColor import com.anytypeio.anytype.presentation.editor.cover.CoverView import com.anytypeio.anytype.presentation.home.InteractionMode import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.anytypeio.anytype.presentation.objects.custom_icon.CustomIconColor import com.anytypeio.anytype.presentation.widgets.DropDownMenuAction import com.anytypeio.anytype.presentation.widgets.ViewId import com.anytypeio.anytype.presentation.widgets.Widget @@ -222,6 +229,8 @@ fun GalleryWidgetCard( item(key = element.obj.id) { GalleryWidgetItemCard( item = element, + showIcon = item.showIcon, + showCover = item.showCover, onItemClicked = { onWidgetObjectClicked(element.obj) } @@ -229,9 +238,13 @@ fun GalleryWidgetCard( } if (idx == item.elements.lastIndex) { item { + // Height should match gallery items based on showCover + val seeAllHeight = if (item.showCover) 136.dp else 54.dp + Box( modifier = Modifier - .size(136.dp) + .width(136.dp) + .height(seeAllHeight) .border( width = 1.dp, color = colorResource(id = R.color.shape_transparent_primary), @@ -393,34 +406,29 @@ fun ListWidgetElement( @Composable private fun GalleryWidgetItemCard( item: WidgetView.SetOfObjects.Element, + showIcon: Boolean = false, + showCover: Boolean = true, onItemClicked: () -> Unit ) { val isImageType = item.obj.layout == ObjectType.Layout.IMAGE val hasCover = item.cover != null + + val cardHeight = if (showCover) 136.dp else 54.dp - Box( - modifier = Modifier - .size(136.dp) - .clip(RoundedCornerShape(8.dp)) - .clickable { - onItemClicked() - } - ) { - Box( - modifier = Modifier - .fillMaxSize() - .border( - width = 1.dp, - color = colorResource(id = R.color.shape_transparent_primary), - shape = RoundedCornerShape(8.dp) - ) - ) - - when { - // Case 1: Image type - show full 136x136 image - isImageType -> { - when (val cover = item.cover) { - is CoverView.Image -> { + when { + isImageType -> { + when (val cover = item.cover) { + // Case 1: Image type - show full 136x136 image + is CoverView.Image -> { + Box( + modifier = Modifier + .width(136.dp) + .height(cardHeight) + .clip(RoundedCornerShape(8.dp)) + .clickable { + onItemClicked() + } + ) { AsyncImage( model = cover.url, contentDescription = "Image object", @@ -429,16 +437,45 @@ private fun GalleryWidgetItemCard( .clip(RoundedCornerShape(8.dp)), contentScale = ContentScale.Crop, ) + GalleryItemBorder() } - else -> { - // Fallback for image type without cover - TitleOnlyContent(item) + } + // Case 2: Image type - Fallback for image type without cover + else -> { + Box( + modifier = Modifier + .height(cardHeight) + .width(136.dp) + .clip(RoundedCornerShape(8.dp)) + .clickable { + onItemClicked() + } + ) { + GalleryIconTitleContent( + modifier = Modifier + .fillMaxSize() + .padding(top = 9.dp, start = 12.dp, end = 12.dp), + item = item, + showIcon = showIcon, + showCover = showCover + ) + GalleryItemBorder() } } } + } - // Case 2: Has cover - show 136x80 cover + title at bottom - hasCover -> { + // Case 3: Has cover - show 136x80 cover + title at bottom + hasCover -> { + Box( + modifier = Modifier + .width(136.dp) + .height(cardHeight) + .clip(RoundedCornerShape(8.dp)) + .clickable { + onItemClicked() + } + ) { Column( modifier = Modifier.fillMaxSize() ) { @@ -455,7 +492,10 @@ private fun GalleryWidgetItemCard( .fillMaxSize() .background( color = Color(cover.coverColor.color), - shape = RoundedCornerShape(topEnd = 8.dp, topStart = 8.dp) + shape = RoundedCornerShape( + topEnd = 8.dp, + topStart = 8.dp + ) ) ) } @@ -468,7 +508,10 @@ private fun GalleryWidgetItemCard( Brush.horizontalGradient( colors = gradient(cover.gradient) ), - shape = RoundedCornerShape(topEnd = 8.dp, topStart = 8.dp) + shape = RoundedCornerShape( + topEnd = 8.dp, + topStart = 8.dp + ) ) ) } @@ -494,52 +537,109 @@ private fun GalleryWidgetItemCard( Box( modifier = Modifier .fillMaxWidth() - .height(56.dp) - .padding(top = 8.dp, start = 12.dp, end = 12.dp), + .height(56.dp), contentAlignment = Alignment.TopStart ) { - Text( - text = when (val name = item.name) { - is WidgetView.Name.Default -> name.prettyPrintName - is WidgetView.Name.Bundled -> stringResource(id = name.source.res()) - WidgetView.Name.Empty -> stringResource(id = R.string.untitled) - }, - style = Caption1Medium, - color = colorResource(id = R.color.text_primary), - maxLines = 2, - overflow = TextOverflow.Ellipsis + GalleryIconTitleContent( + modifier = Modifier + .fillMaxSize() + .padding(top = 8.dp, start = 12.dp, end = 12.dp), + item = item, + showIcon = showIcon, + showCover = showCover ) } } + GalleryItemBorder() } + } - // Case 3: No cover - show 136x136 with centered title - else -> { - TitleOnlyContent(item) + // Case 3: No cover - show 136x54 with title and icon + else -> { + Box( + modifier = Modifier + .width(136.dp) + .height(cardHeight) + .clip(RoundedCornerShape(8.dp)) + .clickable { + onItemClicked() + } + ) { + GalleryIconTitleContent( + modifier = Modifier + .fillMaxSize() + .padding(top = 8.dp, start = 12.dp, end = 12.dp), + item = item, + showIcon = showIcon, + showCover = showCover + ) + GalleryItemBorder() } } } } @Composable -private fun TitleOnlyContent(item: WidgetView.SetOfObjects.Element) { +private fun BoxScope.GalleryItemBorder() { Box( modifier = Modifier .fillMaxSize() - .padding(top = 9.dp, start = 12.dp, end = 12.dp), - contentAlignment = Alignment.TopStart + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_transparent_primary), + shape = RoundedCornerShape(8.dp) + ) + ) +} + +@Composable +private fun GalleryIconTitleContent( + modifier: Modifier = Modifier, + item: WidgetView.SetOfObjects.Element, + showIcon: Boolean = false, + showCover: Boolean = true +) { + val hasIcon = showIcon && item.objectIcon != ObjectIcon.None + + Box( + modifier = modifier ) { + // Show icon when showIcon is true and icon is not None + if (hasIcon) { + ListWidgetObjectIcon( + icon = item.objectIcon, + iconSize = 16.dp, + iconWithoutBackgroundMaxSize = 20.dp, + modifier = Modifier + .align(Alignment.TopStart) + ) + } + + val titleText = when (val name = item.name) { + is WidgetView.Name.Default -> name.prettyPrintName + is WidgetView.Name.Bundled -> stringResource(id = name.source.res()) + WidgetView.Name.Empty -> stringResource(id = R.string.untitled) + } + Text( - text = when (val name = item.name) { - is WidgetView.Name.Default -> name.prettyPrintName - is WidgetView.Name.Bundled -> stringResource(id = name.source.res()) - WidgetView.Name.Empty -> stringResource(id = R.string.untitled) + text = buildAnnotatedString { + withStyle( + style = ParagraphStyle( + textIndent = TextIndent( + firstLine = if (hasIcon) 20.sp else 0.sp, + restLine = 0.sp + ) + ) + ) { + append(titleText) + } }, style = Caption1Medium, color = colorResource(id = R.color.text_primary), - textAlign = TextAlign.Start, maxLines = 2, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .align(Alignment.TopStart) ) } } @@ -596,8 +696,9 @@ fun GalleryWidgetItemCard_WithImageCover_Preview() { @Composable fun GalleryWidgetItemCard_WithColorCover_Preview() { GalleryWidgetItemCard( + showIcon = true, item = WidgetView.SetOfObjects.Element( - objectIcon = ObjectIcon.None, + objectIcon = ObjectIcon.TypeIcon.Default.DEFAULT, obj = ObjectWrapper.Basic( map = mapOf( Relations.NAME to "Meeting Notes", @@ -665,8 +766,12 @@ fun GalleryWidgetItemCard_NoCover_Preview() { @Composable fun GalleryWidgetItemCard_NoCoverShort_Preview() { GalleryWidgetItemCard( + showIcon = true, item = WidgetView.SetOfObjects.Element( - objectIcon = ObjectIcon.None, + objectIcon = ObjectIcon.TypeIcon.Default( + rawValue = "american-football", + color = CustomIconColor.Blue + ), obj = ObjectWrapper.Basic( map = mapOf( Relations.NAME to "Tasks", @@ -674,7 +779,7 @@ fun GalleryWidgetItemCard_NoCoverShort_Preview() { ) ), name = WidgetView.Name.Default( - prettyPrintName = "Tasks" + prettyPrintName = "Buy, study, and share this game as an example of video games as true art." ), cover = null ), diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/PermittedConditions.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/PermittedConditions.kt index b98dec528c..bac3c3cbf3 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/PermittedConditions.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/PermittedConditions.kt @@ -5,5 +5,6 @@ val PermittedConditions = listOf( Block.Content.DataView.Filter.Condition.IN, Block.Content.DataView.Filter.Condition.EQUAL, Block.Content.DataView.Filter.Condition.GREATER_OR_EQUAL, - Block.Content.DataView.Filter.Condition.LESS_OR_EQUAL + Block.Content.DataView.Filter.Condition.LESS_OR_EQUAL, + Block.Content.DataView.Filter.Condition.EXACT_IN ) \ No newline at end of file diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/ext/BlockExt.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/ext/BlockExt.kt index 1e8138fb85..8146b31575 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/ext/BlockExt.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/ext/BlockExt.kt @@ -217,8 +217,8 @@ fun Block.addMention( color = content.color, isChecked = content.isChecked, align = content.align, - iconEmoji = null, - iconImage = null + iconEmoji = content.iconEmoji ?: "", + iconImage = content.iconImage ?: "" ) ) } diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/membership/MembershipModels.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/membership/MembershipModels.kt index f1876b3e2a..073fb11539 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/membership/MembershipModels.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/membership/MembershipModels.kt @@ -51,6 +51,10 @@ data class MembershipTierData( val androidManageUrl: String? ) +class MembershipTiers { + data class Event(val tiers: List) +} + enum class MembershipPeriodType { PERIOD_TYPE_UNKNOWN, PERIOD_TYPE_UNLIMITED, diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt index 02db41dbde..1e0e24e50a 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt @@ -108,22 +108,23 @@ fun Option( Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier + .fillMaxWidth() .height(52.dp) + .padding(horizontal = 16.dp) .clickable(onClick = onClick) ) { Image( - painterResource(image), + painter = painterResource(image), contentDescription = "Option icon", - modifier = Modifier.padding( - start = 20.dp - ) + modifier = Modifier + .size(24.dp) ) Text( text = text, color = colorResource(R.color.text_primary), modifier = Modifier.padding( - start = 12.dp + start = 10.dp ), style = BodyRegular ) @@ -131,7 +132,11 @@ fun Option( modifier = Modifier.weight(1.0f, true), contentAlignment = Alignment.CenterEnd ) { - Arrow() + Image( + painterResource(R.drawable.ic_arrow_forward), + contentDescription = "Arrow forward", + modifier = Modifier + ) } } } @@ -143,54 +148,33 @@ fun OptionMembership( onClick: () -> Unit = {}, membershipStatus: MembershipStatus? ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .noRippleThrottledClickable { onClick() } - + Box( + modifier = Modifier.fillMaxWidth() ) { - Image( - painterResource(image), - contentDescription = "Option icon", - modifier = Modifier.padding( - start = 20.dp - ) - ) - Text( - text = text, - color = colorResource(R.color.text_primary), - modifier = Modifier.padding( - start = 12.dp - ), - style = BodyRegular - ) when (membershipStatus?.status) { Status.STATUS_ACTIVE -> { Box( - modifier = Modifier.weight(1.0f, true), - contentAlignment = Alignment.CenterEnd + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 34.dp) ) { Text( - modifier = Modifier - .padding(horizontal = 38.dp), - text = membershipStatus.tiers.firstOrNull { it.id == membershipStatus.activeTier?.value }?.name.orEmpty(), + modifier = Modifier, + text = membershipStatus.tiers.firstOrNull { it.id == membershipStatus.activeTier.value }?.name.orEmpty(), color = colorResource(R.color.text_secondary), style = BodyRegular ) - Arrow() } } else -> { Box( - modifier = Modifier.weight(1.0f, true), - contentAlignment = Alignment.CenterEnd + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 34.dp) ) { Text( - modifier = Modifier - .padding(horizontal = 38.dp), + modifier = Modifier, text = stringResource(id = R.string.membership_price_pending), color = colorResource(R.color.text_secondary), style = BodyRegular @@ -198,6 +182,40 @@ fun OptionMembership( } } } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .padding(horizontal = 16.dp) + .clickable(onClick = onClick) + + ) { + Image( + painter = painterResource(image), + contentDescription = "Option icon", + modifier = Modifier + .size(24.dp) + ) + Text( + text = text, + color = colorResource(R.color.text_primary), + modifier = Modifier.padding( + start = 10.dp + ), + style = BodyRegular + ) + Box( + modifier = Modifier.weight(1.0f, true), + contentAlignment = Alignment.CenterEnd + ) { + Image( + painterResource(R.drawable.ic_arrow_forward), + contentDescription = "Arrow forward", + modifier = Modifier + ) + } + } } } @@ -207,7 +225,7 @@ fun Arrow() { painterResource(R.drawable.ic_arrow_forward), contentDescription = "Arrow forward", modifier = Modifier.padding( - end = 20.dp + end = 16.dp ) ) } @@ -358,43 +376,51 @@ fun OptionWithBadge( onClick: () -> Unit = {} ) { Box( - modifier = Modifier - .fillMaxWidth() - .height(52.dp) - .clickable(onClick = onClick) + modifier = Modifier.fillMaxWidth() ) { + if (showBadge) { + Image( + painter = painterResource(R.drawable.ic_attention_red_18), + contentDescription = "Badge", + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 40.dp) + .size(18.dp) + ) + } Row( - modifier = Modifier.fillMaxHeight(), verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(52.dp) + .padding(horizontal = 16.dp) + .clickable(onClick = onClick) + ) { Image( painter = painterResource(image), contentDescription = "Option icon", modifier = Modifier - .padding(start = 20.dp) - .size(28.dp) + .size(24.dp) ) Text( text = text, color = colorResource(R.color.text_primary), - modifier = Modifier.padding(start = 12.dp), + modifier = Modifier.padding( + start = 10.dp + ), style = BodyRegular ) - } - Row( - modifier = Modifier.align(Alignment.CenterEnd), - verticalAlignment = Alignment.CenterVertically - ) { - if (showBadge) { + Box( + modifier = Modifier.weight(1.0f, true), + contentAlignment = Alignment.CenterEnd + ) { Image( - painter = painterResource(R.drawable.ic_attention_red_18), - contentDescription = "Badge", + painterResource(R.drawable.ic_arrow_forward), + contentDescription = "Arrow forward", modifier = Modifier - .padding(end = 12.dp) - .size(18.dp) ) } - Arrow() } } } diff --git a/core-ui/src/main/res/drawable/ic_account_identity_16.xml b/core-ui/src/main/res/drawable/ic_account_identity_16.xml new file mode 100644 index 0000000000..6015b935e0 --- /dev/null +++ b/core-ui/src/main/res/drawable/ic_account_identity_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/core-ui/src/main/res/drawable/ic_space_type_space.xml b/core-ui/src/main/res/drawable/ic_space_type_space.xml index ac3947b7f3..a75f5902a2 100644 --- a/core-ui/src/main/res/drawable/ic_space_type_space.xml +++ b/core-ui/src/main/res/drawable/ic_space_type_space.xml @@ -4,8 +4,8 @@ android:viewportWidth="56" android:viewportHeight="56"> + android:pathData="M10,0L46,0A10,10 0,0 1,56 10L56,46A10,10 0,0 1,46 56L10,56A10,10 0,0 1,0 46L0,10A10,10 0,0 1,10 0z" + android:fillColor="@color/palette_system_blue"/> diff --git a/core-ui/src/main/res/layout/item_block_title.xml b/core-ui/src/main/res/layout/item_block_title.xml index 3c254df2e6..26c1212ba5 100644 --- a/core-ui/src/main/res/layout/item_block_title.xml +++ b/core-ui/src/main/res/layout/item_block_title.xml @@ -60,6 +60,7 @@ android:adjustViewBounds="true" android:background="@color/shape_tertiary" android:padding="4dp" + android:scaleType="centerCrop" android:transitionName="@string/logo_transition" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/insets/WindowInsetExt.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/insets/WindowInsetExt.kt index ca0955de52..51c5bec9a8 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/insets/WindowInsetExt.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/insets/WindowInsetExt.kt @@ -52,4 +52,4 @@ fun View.doOnApplyWindowInsets( } } -const val EDGE_TO_EDGE_MIN_SDK = Build.VERSION_CODES.TIRAMISU \ No newline at end of file +const val EDGE_TO_EDGE_MIN_SDK = Build.VERSION_CODES.O \ No newline at end of file diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseComposeFragment.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseComposeFragment.kt index c205f87e85..b991b4d5fd 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseComposeFragment.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseComposeFragment.kt @@ -1,6 +1,5 @@ package com.anytypeio.anytype.core_utils.ui -import android.os.Build import android.os.Bundle import android.view.View import androidx.core.view.ViewCompat @@ -8,7 +7,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope -import com.anytypeio.anytype.core_utils.BuildConfig import com.anytypeio.anytype.core_utils.ext.cancel import com.anytypeio.anytype.core_utils.insets.RootViewDeferringInsetsCallback import kotlinx.coroutines.Job @@ -43,14 +41,12 @@ abstract class BaseComposeFragment : Fragment() { } open fun onApplyWindowRootInsets(view: View) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val deferringInsetsListener = RootViewDeferringInsetsCallback( - persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), - deferredInsetTypes = WindowInsetsCompat.Type.ime() - ) - ViewCompat.setWindowInsetsAnimationCallback(view, deferringInsetsListener) - ViewCompat.setOnApplyWindowInsetsListener(view, deferringInsetsListener) - } + val deferringInsetsListener = RootViewDeferringInsetsCallback( + persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), + deferredInsetTypes = WindowInsetsCompat.Type.ime() + ) + ViewCompat.setWindowInsetsAnimationCallback(view, deferringInsetsListener) + ViewCompat.setOnApplyWindowInsetsListener(view, deferringInsetsListener) } protected fun DialogFragment.showChildFragment(tag: String? = null) { diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseFragment.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseFragment.kt index 784ecefca1..d3a7c1e04e 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseFragment.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ui/BaseFragment.kt @@ -90,14 +90,12 @@ abstract class BaseFragment( } open fun onApplyWindowRootInsets() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - val deferringInsetsListener = RootViewDeferringInsetsCallback( - persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), - deferredInsetTypes = WindowInsetsCompat.Type.ime() - ) - ViewCompat.setWindowInsetsAnimationCallback(binding.root, deferringInsetsListener) - ViewCompat.setOnApplyWindowInsetsListener(binding.root, deferringInsetsListener) - } + val deferringInsetsListener = RootViewDeferringInsetsCallback( + persistentInsetTypes = WindowInsetsCompat.Type.systemBars(), + deferredInsetTypes = WindowInsetsCompat.Type.ime() + ) + ViewCompat.setWindowInsetsAnimationCallback(binding.root, deferringInsetsListener) + ViewCompat.setOnApplyWindowInsetsListener(binding.root, deferringInsetsListener) } protected fun DialogFragment.showChildFragment(tag: String? = null) { diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/event/MembershipRemoteChannel.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/event/MembershipRemoteChannel.kt index bbc504e024..cd226d0816 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/event/MembershipRemoteChannel.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/event/MembershipRemoteChannel.kt @@ -1,11 +1,13 @@ package com.anytypeio.anytype.data.auth.event import com.anytypeio.anytype.core_models.membership.Membership +import com.anytypeio.anytype.core_models.membership.MembershipTiers import com.anytypeio.anytype.domain.workspace.MembershipChannel import kotlinx.coroutines.flow.Flow interface MembershipRemoteChannel { fun observe(): Flow> + fun observeTiers(): Flow> } class MembershipDateChannel( @@ -15,4 +17,8 @@ class MembershipDateChannel( override fun observe(): Flow> { return channel.observe() } + + override fun observeTiers(): Flow> { + return channel.observeTiers() + } } \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/DefaultUserPermissionProvider.kt b/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/DefaultUserPermissionProvider.kt index cc93b141fa..c80da05338 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/DefaultUserPermissionProvider.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/multiplayer/DefaultUserPermissionProvider.kt @@ -49,6 +49,11 @@ interface UserPermissionProvider { */ fun observe(space: SpaceId) : Flow + /** + * @return Flow of the current user's [ObjectWrapper.SpaceMember] or null if not available. + */ + fun getCurrent() : Flow + /** * Provide permissions of the current user in all available spaces. * Maps space to permissions. @@ -80,6 +85,10 @@ class DefaultUserPermissionProvider @Inject constructor( } } + override fun getCurrent(): Flow { + return members.map { all -> all.firstOrNull() } + } + override fun start() { logger.logInfo("Starting DefaultUserPermissionProvider") clear() @@ -118,7 +127,8 @@ class DefaultUserPermissionProvider @Inject constructor( Relations.ID, Relations.SPACE_ID, Relations.IDENTITY, - Relations.PARTICIPANT_PERMISSIONS + Relations.PARTICIPANT_PERMISSIONS, + Relations.GLOBAL_NAME ) ) ).map { results -> diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/MembershipChannel.kt b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/MembershipChannel.kt index 675ea5e8d1..2f5448ab79 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/MembershipChannel.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/MembershipChannel.kt @@ -1,8 +1,10 @@ package com.anytypeio.anytype.domain.workspace import com.anytypeio.anytype.core_models.membership.Membership +import com.anytypeio.anytype.core_models.membership.MembershipTiers import kotlinx.coroutines.flow.Flow interface MembershipChannel { fun observe(): Flow> + fun observeTiers(): Flow> } \ No newline at end of file diff --git a/domain/src/test/java/com/anytypeio/anytype/domain/ext/BlockMentionUpdateTest.kt b/domain/src/test/java/com/anytypeio/anytype/domain/ext/BlockMentionUpdateTest.kt index 9e3c8864d6..fb38446fc0 100644 --- a/domain/src/test/java/com/anytypeio/anytype/domain/ext/BlockMentionUpdateTest.kt +++ b/domain/src/test/java/com/anytypeio/anytype/domain/ext/BlockMentionUpdateTest.kt @@ -81,7 +81,9 @@ class BlockMentionUpdateTest { text = "NewPage ", color = "red", align = Block.Align.AlignCenter, - isChecked = true + isChecked = true, + iconEmoji = "", + iconImage = "" ), children = emptyList(), backgroundColor = "lime" @@ -341,7 +343,9 @@ class BlockMentionUpdateTest { ) ), style = Block.Content.Text.Style.P, - text = "page about Avant-Garde Jazz music" + text = "page about Avant-Garde Jazz music", + iconEmoji = "", + iconImage = "" ), children = emptyList() ) @@ -437,7 +441,9 @@ class BlockMentionUpdateTest { ) ), style = Block.Content.Text.Style.P, - text = "Avant-Garde Jazz page about music" + text = "Avant-Garde Jazz page about music", + iconEmoji = "", + iconImage = "" ), children = emptyList() ) @@ -533,7 +539,9 @@ class BlockMentionUpdateTest { ) ), style = Block.Content.Text.Style.P, - text = "Avant page about music" + text = "Avant page about music", + iconEmoji = "", + iconImage = "" ), children = emptyList() ) @@ -615,7 +623,9 @@ class BlockMentionUpdateTest { ) ), style = Block.Content.Text.Style.P, - text = "page about Avant-Garde Jazz " + text = "page about Avant-Garde Jazz ", + iconEmoji = "", + iconImage = "" ), children = emptyList() ) diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt index 6e4dc9c263..d4afc5e2f6 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt @@ -82,6 +82,7 @@ import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.views.Caption1Medium import com.anytypeio.anytype.core_ui.views.Caption1Regular import com.anytypeio.anytype.core_ui.views.ContentMiscChat +import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.feature_chats.R import com.anytypeio.anytype.feature_chats.presentation.ChatView import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel.ChatBoxMode @@ -115,7 +116,7 @@ fun ChatBox( onUrlInserted: (Url) -> Unit, onImageCaptured: (Uri) -> Unit, onVideoCaptured: (Uri) -> Unit, - onAttachmentMenuTriggered: () -> Unit + onAttachmentMenuTriggered: () -> Unit, ) { val context = LocalContext.current @@ -131,6 +132,9 @@ fun ChatBox( val uploadFileLauncher = rememberLauncherForActivityResult( ActivityResultContracts.OpenMultipleDocuments() ) { uris -> + if (uris.size > ChatConfig.MAX_ATTACHMENT_COUNT) { + context.toast(context.getString(R.string.chats_warning_you_can_upload_only_10_files_at_a_time)) + } onChatBoxFilePicked(uris.take(ChatConfig.MAX_ATTACHMENT_COUNT)) } diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/ListScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/ListScreen.kt index 81728afbac..d41444319a 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/ListScreen.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/fields/ui/ListScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentSize @@ -139,7 +140,9 @@ fun FieldsMainScreen( contentColor = colorResource(id = R.color.background_primary), topBar = { TopBar( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .systemBarsPadding(), uiTitleState = uiTitleState, uiIconState = uiIconState, onBackClick = { diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/TypeScreen.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/TypeScreen.kt index d8d76afca9..adc628f411 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/TypeScreen.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/ui/TypeScreen.kt @@ -1,6 +1,5 @@ package com.anytypeio.anytype.feature_object_type.ui -import android.os.Build import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets @@ -17,7 +16,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_ui.syncstatus.SpaceSyncStatusScreen -import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK import com.anytypeio.anytype.feature_object_type.ui.header.TopToolbar import com.anytypeio.anytype.presentation.sync.SyncStatusWidgetState @@ -28,13 +26,9 @@ fun TopBarContent( onTypeEvent: (TypeEvent) -> Unit ) { // Use windowInsetsPadding if running on a recent SDK - val modifier = if (Build.VERSION.SDK_INT >= EDGE_TO_EDGE_MIN_SDK) { - Modifier - .windowInsetsPadding(WindowInsets.statusBars) - .fillMaxWidth() - } else { - Modifier.fillMaxWidth() - } + val modifier = Modifier + .windowInsetsPadding(WindowInsets.statusBars) + .fillMaxWidth() Column(modifier = modifier) { TopToolbar( diff --git a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt index a606dd48f6..8706f75d25 100644 --- a/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt +++ b/feature-object-type/src/main/java/com/anytypeio/anytype/feature_object_type/viewmodel/ObjectTypeViewModel.kt @@ -689,7 +689,7 @@ class ObjectTypeViewModel( val params = SetObjectDetails.Params( ctx = vmParams.objectId, details = mapOf( - Relations.ICON_EMOJI to null, + Relations.ICON_EMOJI to "", Relations.ICON_NAME to iconName, Relations.ICON_OPTION to newColor?.iconOption?.toDouble() ) @@ -710,8 +710,8 @@ class ObjectTypeViewModel( val params = SetObjectDetails.Params( ctx = vmParams.objectId, details = mapOf( - Relations.ICON_EMOJI to null, - Relations.ICON_NAME to null, + Relations.ICON_EMOJI to "", + Relations.ICON_NAME to "", Relations.ICON_OPTION to null ) ) diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt index e262965a19..f20c052aae 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileScreen.kt @@ -5,7 +5,9 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -15,6 +17,8 @@ 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.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField @@ -49,11 +53,16 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.rememberAsyncImagePainter +import com.anytypeio.anytype.core_models.membership.Membership +import com.anytypeio.anytype.core_models.membership.MembershipConstants +import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.foundation.Option @@ -63,6 +72,7 @@ import com.anytypeio.anytype.core_ui.views.BodyRegular import com.anytypeio.anytype.core_ui.views.Caption1Regular import com.anytypeio.anytype.core_ui.views.Title1 import com.anytypeio.anytype.core_models.membership.MembershipStatus +import com.anytypeio.anytype.core_models.membership.TierId import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.foundation.Arrow import com.anytypeio.anytype.core_ui.foundation.OptionWithBadge @@ -109,7 +119,8 @@ fun ProfileSettingsScreen( onNameSet = onNameChange, onProfileIconClick = onProfileIconClick, clearProfileImage = clearProfileImage, - onTitleClicked = onHeaderTitleClicked + onTitleClicked = onHeaderTitleClicked, + onIdentityClicked = onMembershipClicked ) } item { @@ -133,7 +144,7 @@ fun ProfileSettingsScreen( ) } item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } item { OptionWithBadge( @@ -144,7 +155,7 @@ fun ProfileSettingsScreen( ) } item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } item { @@ -155,7 +166,7 @@ fun ProfileSettingsScreen( ) } item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } if (showMembership?.isShowing == true) { item { @@ -167,7 +178,7 @@ fun ProfileSettingsScreen( ) } item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } } @@ -183,7 +194,7 @@ fun ProfileSettingsScreen( ) } item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } item { @@ -194,7 +205,7 @@ fun ProfileSettingsScreen( ) } item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } item { Option( @@ -204,7 +215,7 @@ fun ProfileSettingsScreen( ) } item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } item { @@ -220,7 +231,7 @@ fun ProfileSettingsScreen( } if (isDebugEnabled) { item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } item { Option( @@ -231,7 +242,7 @@ fun ProfileSettingsScreen( } } item { - Divider(paddingStart = 60.dp) + Divider(paddingStart = 16.dp, paddingEnd = 16.dp) } item { LogoutButton(onLogoutClicked, isLogoutInProgress) @@ -250,6 +261,8 @@ private fun LogoutButton( Row( modifier = Modifier .height(52.dp) + .fillMaxWidth() + .padding(horizontal = 16.dp) .clickable { onLogoutClicked() }, @@ -258,9 +271,7 @@ private fun LogoutButton( Image( painter = painterResource(R.drawable.ic_settings_log_out), contentDescription = "Option icon", - modifier = Modifier.padding( - start = 20.dp - ) + modifier = Modifier.size(24.dp) ) Box( modifier = Modifier @@ -291,7 +302,11 @@ private fun LogoutButton( modifier = Modifier, contentAlignment = Alignment.CenterEnd ) { - Arrow() + Image( + painter = painterResource(R.drawable.ic_arrow_forward), + contentDescription = "Arrow forward", + modifier = Modifier + ) } } } @@ -307,7 +322,7 @@ fun Section(name: String) { Text( text = name, modifier = Modifier.padding( - start = 20.dp, + start = 16.dp, bottom = 8.dp ), color = colorResource(R.color.text_secondary), @@ -383,7 +398,8 @@ private fun Header( onProfileIconClick: () -> Unit, onNameSet: (String) -> Unit, clearProfileImage: () -> Unit, - onTitleClicked: () -> Unit + onTitleClicked: () -> Unit, + onIdentityClicked: () -> Unit ) { when (account) { is AccountProfile.Data -> { @@ -391,7 +407,7 @@ private fun Header( Dragger() } Box(modifier = modifier.padding(top = 12.dp, bottom = 28.dp)) { - ProfileTitleBlock(onTitleClicked) + ProfileTitleBlock(account, onIdentityClicked) } Box(modifier = modifier.padding(bottom = 16.dp)) { ProfileImageBlock( @@ -428,7 +444,7 @@ fun ProfileNameBlock( } } - Column(modifier = modifier.padding(start = 20.dp)) { + Column(modifier = modifier.padding(start = 16.dp)) { Text( text = stringResource(id = R.string.name), color = colorResource(id = R.color.text_secondary), @@ -495,20 +511,65 @@ fun ProfileNameBlock( } @Composable -fun ProfileTitleBlock( +fun BoxScope.ProfileTitleBlock( + account: AccountProfile.Data, onClick: () -> Unit ) { - Text( - text = stringResource(R.string.settings), - style = Title1, - color = colorResource(id = R.color.text_primary), - modifier = Modifier.noRippleClickable { - onClick() + val globalName = account.globalName + val identity = account.identity + Box( + modifier = Modifier.fillMaxWidth() + .padding(horizontal = 32.dp), + contentAlignment = Alignment.Center + ) { + Row( + modifier = Modifier + .height(22.dp) + .wrapContentWidth() + .align(Alignment.Center), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (!globalName.isNullOrEmpty()) { + Image( + modifier = Modifier.size(18.dp), + painter = painterResource(R.drawable.ic_account_name_18), + contentDescription = "Account any name" + ) + Text( + text = globalName, + style = Caption1Regular, + color = colorResource(id = R.color.text_primary), + textAlign = TextAlign.Center, + overflow = TextOverflow.MiddleEllipsis, + maxLines = 1, + modifier = Modifier + .wrapContentWidth() + .noRippleClickable { onClick() } + ) + } else { + Image( + modifier = Modifier.size(18.dp), + painter = painterResource(R.drawable.ic_account_identity_16), + contentDescription = "Account any name", + contentScale = ContentScale.Fit + ) + Text( + text = identity.orEmpty(), + style = Caption1Regular, + color = colorResource(id = R.color.text_primary), + overflow = TextOverflow.MiddleEllipsis, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .width(108.dp) + .noRippleClickable { onClick() } + ) + } } - ) + } } - @Composable fun ProfileImageBlock( name: String, @@ -621,14 +682,24 @@ private fun ProfileSettingPreview() { onProfileIconClick = {}, account = AccountProfile.Data( "Walter", - icon = ProfileIconView.Placeholder("Walter") + icon = ProfileIconView.Placeholder("Walter"), + //globalName = "Konstantin.any", + identity = "hdjsakjflkjdshlfkdsjkhfjkasdhjkfhdskjhfjksdhakjfhsadjkhfkjlasdhjkfhjsdhfjkhsadj" ), onAppearanceClicked = {}, onDataManagementClicked = {}, onAboutClicked = {}, onSpacesClicked = {}, onMembershipClicked = {}, - membershipStatus = null, + membershipStatus = MembershipStatus( + status = Membership.Status.STATUS_ACTIVE, + activeTier = TierId(MembershipConstants.CO_CREATOR_ID), + dateEnds = 1714199910, + paymentMethod = MembershipPaymentMethod.METHOD_CRYPTO, + anyName = "AnyName1983", + tiers = listOf(), + formattedDateEnds = "formattedDateEnds-fdsfadsfdsafs" + ), showMembership = ShowMembership(true), clearProfileImage = {}, onDebugClicked = {}, diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt index 683b4d2a94..ca5ccfec2e 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/account/ProfileSettingsViewModel.kt @@ -18,6 +18,7 @@ import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon import com.anytypeio.anytype.domain.icon.SetImageIcon import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.networkmode.GetNetworkMode import com.anytypeio.anytype.domain.`object`.SetObjectDetails import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager @@ -31,11 +32,12 @@ import com.anytypeio.anytype.ui_settings.BuildConfig import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import timber.log.Timber -import kotlinx.coroutines.flow.StateFlow class ProfileSettingsViewModel( private val analytics: Analytics, @@ -48,7 +50,8 @@ class ProfileSettingsViewModel( private val getNetworkMode: GetNetworkMode, private val profileContainer: ProfileSubscriptionManager, private val removeObjectIcon: RemoveObjectIcon, - private val notificationPermissionManager: NotificationPermissionManager + private val notificationPermissionManager: NotificationPermissionManager, + private val userPermissionProvider: UserPermissionProvider ) : BaseViewModel() { private val jobs = mutableListOf() @@ -70,16 +73,21 @@ class ProfileSettingsViewModel( } }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(STOP_SUBSCRIPTION_TIMEOUT), true) - val profileData = profileContainer.observe().map { obj -> - AccountProfile.Data( - name = obj.name.orEmpty(), - icon = obj.profileIcon(urlBuilder) + val profileData = profileContainer + .observe() + .combine(userPermissionProvider.getCurrent()) { profile, spaceMember -> + Timber.d("profileData, profile:[$profile], spaceMember:[$spaceMember]") + AccountProfile.Data( + name = profile.name.orEmpty(), + icon = profile.profileIcon(urlBuilder), + identity = spaceMember?.identity, + globalName = spaceMember?.globalName + ) + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(STOP_SUBSCRIPTION_TIMEOUT), + AccountProfile.Idle ) - }.stateIn( - viewModelScope, - SharingStarted.WhileSubscribed(STOP_SUBSCRIPTION_TIMEOUT), - AccountProfile.Idle - ) init { viewModelScope.launch { @@ -205,7 +213,8 @@ class ProfileSettingsViewModel( private val getNetworkMode: GetNetworkMode, private val profileSubscriptionManager: ProfileSubscriptionManager, private val removeObjectIcon: RemoveObjectIcon, - private val notificationPermissionManager: NotificationPermissionManager + private val notificationPermissionManager: NotificationPermissionManager, + private val userPermissionProvider: UserPermissionProvider ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { @@ -220,7 +229,8 @@ class ProfileSettingsViewModel( getNetworkMode = getNetworkMode, profileContainer = profileSubscriptionManager, removeObjectIcon = removeObjectIcon, - notificationPermissionManager = notificationPermissionManager + notificationPermissionManager = notificationPermissionManager, + userPermissionProvider = userPermissionProvider ) as T } } diff --git a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/Items.kt b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/Items.kt index f1c297e542..8b11c93c36 100644 --- a/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/Items.kt +++ b/feature-ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/new_settings/Items.kt @@ -580,18 +580,24 @@ fun NewSettingsTextField( val focusRequester = remember { FocusRequester() } - val textFieldValue = TextFieldValue( - text = value, - selection = TextRange(value.length) - ) + val textFieldValue = remember(value) { + mutableStateOf( + TextFieldValue( + text = value, + selection = TextRange(value.length) + ) + ) + } + BasicTextField( - value = textFieldValue, + value = textFieldValue.value, modifier = Modifier .padding(top = 4.dp) .fillMaxWidth() .focusRequester(focusRequester) , onValueChange = { update -> + textFieldValue.value = update onValueChange(update.text) }, enabled = isEditEnabled, diff --git a/feature-ui-settings/src/main/res/drawable/ic_account_name_18.xml b/feature-ui-settings/src/main/res/drawable/ic_account_name_18.xml new file mode 100644 index 0000000000..7d598712f5 --- /dev/null +++ b/feature-ui-settings/src/main/res/drawable/ic_account_name_18.xml @@ -0,0 +1,15 @@ + + + + diff --git a/localization/src/main/res/values-be-rBY/strings.xml b/localization/src/main/res/values-be-rBY/strings.xml index 89ef4c3ab7..ab7e5ae4d6 100644 --- a/localization/src/main/res/values-be-rBY/strings.xml +++ b/localization/src/main/res/values-be-rBY/strings.xml @@ -816,7 +816,7 @@ Property settings Выдаліць This object has no links to other objects. - No objects inside + Тут пакуль няма аб\'ектаў Гэты выгляд даных не мае аб\'ектаў.\nПаспрабуйце стварыць новы. Гэты віджэт не мае аб\'ектаў.\nПаспрабуйце стварыць новы. Эмодзі @@ -2026,6 +2026,7 @@ We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored Адмацаваць + See all My Sites There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values-de-rDE/strings.xml b/localization/src/main/res/values-de-rDE/strings.xml index 0501930d10..f83d32871e 100644 --- a/localization/src/main/res/values-de-rDE/strings.xml +++ b/localization/src/main/res/values-de-rDE/strings.xml @@ -812,7 +812,7 @@ Eigenschaftseinstellungen Entfernen Dieses Objekt ist mit keinem anderen Objekt verlinkt - Keine Objekte innerhalb + Hier gibt es keine Objekte Diese Datenansicht hat keine Objekte.\nVersuche ein neues zu erstellen. Dieses Widget hat keine Objekte.\nVersuche ein neues zu erstellen. Emoji @@ -2000,6 +2000,7 @@ Bitte mache hier genaue Angaben zu deinen Bedarf. We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. Dieses Widget kann nicht wiederhergestellt werden Loslösen + See all Meine Seiten Es gibt noch keine veröffentlichten Objekte Hier ist es leer diff --git a/localization/src/main/res/values-es-rES/strings.xml b/localization/src/main/res/values-es-rES/strings.xml index a72f4c51f2..35427d20fb 100644 --- a/localization/src/main/res/values-es-rES/strings.xml +++ b/localization/src/main/res/values-es-rES/strings.xml @@ -812,7 +812,7 @@ Ajustes de la propiedad Borrar Este objeto no tiene enlaces a otros objetos. - No contiene objetos + Aquí no hay objetos Esta vista de datos no tiene objetos.\nPrueba a crear alguno. Este widget no tiene objetos.\nPrueba a crear alguno. Emoji @@ -2000,6 +2000,7 @@ En concreto, Hemos rediseñado el funcionamiento de los widgets y este widget ya no es compatible. Una vez desanclado, este widget no se puede restaurar. Este widget no se podrá restaurar Desanclar + Ver todo Mis sitios Aún no hay ningún objeto publicado Aquí no hay nada diff --git a/localization/src/main/res/values-fr-rFR/strings.xml b/localization/src/main/res/values-fr-rFR/strings.xml index 4ca12e8945..14f8b14ca9 100644 --- a/localization/src/main/res/values-fr-rFR/strings.xml +++ b/localization/src/main/res/values-fr-rFR/strings.xml @@ -812,7 +812,7 @@ Paramètres de la propriété Supprimer Cet objet n\'a pas de liens vers d\'autres objets. - No objects inside + Il n\'y a aucun objet ici Cette vue de données n\'a aucun objet.\nEssayez d\'en créer un nouveau. Ce widget n\'a pas d\'objets.\nEssayez d\'en créer un nouveau. Emoji @@ -1997,6 +1997,7 @@ Merci de décrire ici vos besoins spécifiques. We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored Désépingler + See all My Sites There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values-in-rID/strings.xml b/localization/src/main/res/values-in-rID/strings.xml index e19315a03b..6c54b1ef58 100644 --- a/localization/src/main/res/values-in-rID/strings.xml +++ b/localization/src/main/res/values-in-rID/strings.xml @@ -810,7 +810,7 @@ Pengaturan properti Hapus Objek ini tidak punya tautan ke objek lain. - Tidak ada objek + Tidak ada objek di sini This data view has no objects.\nTry to create a new one. This widget has no objects.\nTry to create a new one. Emoji @@ -1986,6 +1986,7 @@ Harap berikan detail spesifik kebutuhan Anda di sini. Kami telah meredesain konsep gawit, dan gawit ini sudah tidak didukung lagi. Begitu dihapus, gawit ini tidak dapat dikembalikan. Gawit Tak Dapat Dikembalikan Lepas sematan + Lihat semua Situsku Belum ada objek diterbitkan Di sini kosong diff --git a/localization/src/main/res/values-it-rIT/strings.xml b/localization/src/main/res/values-it-rIT/strings.xml index 5c7d92a627..d490e4b4db 100644 --- a/localization/src/main/res/values-it-rIT/strings.xml +++ b/localization/src/main/res/values-it-rIT/strings.xml @@ -753,7 +753,7 @@ Questo oggetto non esiste Back to dashboard Indietro - Query not found for this type. + Query non trovata per questo tipo. Crea Nuovo %1$s Chiudi @@ -785,9 +785,9 @@ Without template Error while searching for images on Unsplash. Please try again later. Non dimenticare di prendere nota della tua Chiave dalle impostazioni e salvarla. - Unlock your object to add new property + Sblocca il tuo oggetto per aggiungere una nuova proprietà Unlock your object to edit relations - Select property type + Seleziona tipo di proprietà Ripristina La tua cassaforte è stata eliminata. @@ -809,10 +809,10 @@ moved to Chiave copiata Type settings - Property settings + Impostazioni della proprietà Rimuovi Questo oggetto non ha collegamenti con altri oggetti. - Nessun oggetto all\'interno + Non ci sono oggetti qui This data view has no objects.\nTry to create a new one. This widget has no objects.\nTry to create a new one. Emoji @@ -1000,7 +1000,7 @@ Widget con una vista a elenco compatta Lista Vista - Widget with a Query or Collection layout + Widget con layout Query o Collezione Compact list Cambia fonte Cambia Tipo di Widget @@ -1650,10 +1650,10 @@ Please provide specific details of your needs here. Sort by Z → A A → Z - Newest first - Oldest first - Date updated - Date created + Prima i più recenti + Prima i più vecchi + Data di aggiornamento + Data di creazione Data ultimo utilizzo Nome Apri come Oggetto @@ -2001,6 +2001,7 @@ Please provide specific details of your needs here. Abbiamo riprogettato il funzionamento dei widget e questo widget non è più supportato. Una volta sganciato, questo widget non può essere ripristinato. Questo Widget Non Può Essere Ripristinato Togli + Mostra tutto I miei Siti Non ci sono ancora oggetti pubblicati Qui è vuoto diff --git a/localization/src/main/res/values-ja-rJP/strings.xml b/localization/src/main/res/values-ja-rJP/strings.xml index 496ad6d97e..67460216e1 100644 --- a/localization/src/main/res/values-ja-rJP/strings.xml +++ b/localization/src/main/res/values-ja-rJP/strings.xml @@ -810,7 +810,7 @@ プロパティ設定 削除 他のオブジェクトとのリンクがありません。 - No objects inside + オブジェクトがありません オブジェクトがありません。\n新しく作成してみましょう。 オブジェクトがありません。\n新しく作成してみましょう。 絵文字 @@ -1988,6 +1988,7 @@ We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored ピン留め解除 + See all サイト There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values-nl-rNL/strings.xml b/localization/src/main/res/values-nl-rNL/strings.xml index c5ae6abbc8..e915d1cebe 100644 --- a/localization/src/main/res/values-nl-rNL/strings.xml +++ b/localization/src/main/res/values-nl-rNL/strings.xml @@ -812,7 +812,7 @@ Eigenschap instellingen Verwijder Dit object heeft geen koppelingen naar andere objecten. - Bevat geen objecten + Er zijn hier geen objecten Deze gegevensweergave heeft geen objecten.\nProbeer een nieuwe te maken. Deze widget heeft geen objecten.\nProbeer een nieuwe te maken. Emoji @@ -2000,6 +2000,7 @@ Geef alsjeblieft specifieke details van jouw wensen hier aan. We hebben opnieuw ontworpen hoe widgets werken, en deze widget wordt niet langer ondersteund. Eenmaal verwijderd kan deze widget niet worden hersteld. Deze widget kan niet worden hersteld Losmaken + Bekijk alle Mijn sites Er zijn nog geen gepubliceerde objecten Het is hier leeg diff --git a/localization/src/main/res/values-no-rNO/strings.xml b/localization/src/main/res/values-no-rNO/strings.xml index 0b92ee7f64..79c34c0e9e 100644 --- a/localization/src/main/res/values-no-rNO/strings.xml +++ b/localization/src/main/res/values-no-rNO/strings.xml @@ -812,7 +812,7 @@ Property settings Fjern This object has no links to other objects. - No objects inside + Det er ingen objekter her This data view has no objects.\nTry to create a new one. This widget has no objects.\nTry to create a new one. Emoji @@ -2000,6 +2000,7 @@ Please provide specific details of your needs here. We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored Løsne + See all My Sites There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values-pt-rBR/strings.xml b/localization/src/main/res/values-pt-rBR/strings.xml index 9f4a60b900..20b3f33e7c 100644 --- a/localization/src/main/res/values-pt-rBR/strings.xml +++ b/localization/src/main/res/values-pt-rBR/strings.xml @@ -10,7 +10,7 @@ Aparência Armazenamento de arquivos Chave - Login Key + Chave de acesso Limpar cache de arquivos Relatório de Depuração de Sincronização Depuração @@ -31,7 +31,7 @@ Configurações Misc Preferências - Application + Aplicação Vault & Key Você salvou a sua chave? Você precisará dela para entrar. Guarde-a em um local seguro. Se você perdê-la, você não poderá mais acessar sua conta. @@ -52,7 +52,7 @@ Entrar no Espaço Aplicar cor sólida aleatória Carregue uma imagem - Change icon color + Alterar cor do ícone Carregue uma imagem Remover imagem Excluir Espaço @@ -68,10 +68,10 @@ Obtenha mais espaço Nome Configurações do espaço - Failed to change space type. Please try again. - Failed to update wallpaper. Please try again. - Failed to update space name. Please try again. - Failed to update space icon. Please try again. + Não foi possível alterar o tipo do espaço. Tente novamente. + Falha ao atualizar plano de fundo. Tente novamente. + Falha ao atualizar o nome do espaço. Tente novamente. + Falha ao atualizar o ícone de espaço. Tente novamente. Gerenciamento de dados Após solicitar a exclusão do seu vault, você terá 30 dias para cancelar esta solicitação. Após 30 dias, os dados criptografados do vault serão removidos permanentemente do nosso nó de backup e você não conseguirá acessar o Anytype em novos dispositivos. Zona de perigo @@ -812,7 +812,7 @@ Configurações da propriedade Remover Esse objeto não possui ligações para outros objetos. - No objects inside + Não há objetos aqui Esta visualização de dados não tem objetos. \nTente criar objetos. Esse componente não tem objetos. \nTente criar um objeto. Emoji @@ -836,9 +836,9 @@ Carregando... aguarde por favor. Padrão Criar espaço - Create chat + Criar conversa Alterar ícone - Chat name + Nome da conversa Nome do espaço Algo deu errado. Por favor, tente novamente. Tipo @@ -1997,6 +1997,7 @@ forneça detalhes específicos das suas necessidades aqui. We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored Desafixar + See all My Sites There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values-ru-rRU/strings.xml b/localization/src/main/res/values-ru-rRU/strings.xml index 3fd4445088..75b8e1e533 100644 --- a/localization/src/main/res/values-ru-rRU/strings.xml +++ b/localization/src/main/res/values-ru-rRU/strings.xml @@ -842,7 +842,7 @@ Создать пространство Создать чат Изменить иконку - Chat name + Имя чата Название пространства Произошла ошибка. Попробуйте ещё раз. Тип @@ -962,15 +962,15 @@ Зелёный фон %1$s - Tell Us About Yourself + Расскажите нам о себе Select one role or background that best fits you - Artist / Content Creator - Consultant - Designer - Software Developer + Артист + Консультант + Дизайнер + Разработчик ПО Founder / Entrepreneur - Manager / IT Professional + Менеджер/Специалист ИТ Marketer Researcher / Academic Student @@ -2029,6 +2029,7 @@ We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored Открепить + See all My Sites There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values-tr-rTR/strings.xml b/localization/src/main/res/values-tr-rTR/strings.xml index 18a6c888d8..e5cda96e52 100644 --- a/localization/src/main/res/values-tr-rTR/strings.xml +++ b/localization/src/main/res/values-tr-rTR/strings.xml @@ -247,7 +247,7 @@ Alanlar İndir Görüntüle - Görüntülenmeler + Görünümler Emoji seç logo geçi̇şi̇ Şuradan bağla @@ -812,7 +812,7 @@ Özellik ayarları Kaldır Bu nesnenin diğer nesnelerle bağlantısı yok. - İçinde nesne yok + Burada hiç nesne yok Bu veri görünümünde nesne yok.\nYeni bir tane oluşturmayı deneyin. Bu widget\'ın nesnesi yok.\nYeni bir tane oluşturmayı deneyin. Emoji @@ -2000,6 +2000,7 @@ Lütfen ihtiyaçlarınızla ilgili özel ayrıntıları burada belirtin.Bu widget artık desteklenmiyor çünkü widget\'ların çalışma şeklini yeniden tasarladık. Sabitlemesi kaldırıldıktan sonra bu widget geri yüklenemez. Bu Widget Geri Yüklenemez Sabitlemeyi kaldır + Tümünü gör Sitelerim Henüz yayınlanmış nesne bulunmamaktadır Burası boş diff --git a/localization/src/main/res/values-uk-rUA/strings.xml b/localization/src/main/res/values-uk-rUA/strings.xml index fb6c1f310b..5e64801b4f 100644 --- a/localization/src/main/res/values-uk-rUA/strings.xml +++ b/localization/src/main/res/values-uk-rUA/strings.xml @@ -816,7 +816,7 @@ Property settings Вилучити This object has no links to other objects. - No objects inside + There are no objects here This data view has no objects.\nTry to create a new one. This widget has no objects.\nTry to create a new one. Емоджі @@ -2026,6 +2026,7 @@ Please provide specific details of your needs here. We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored Unpin + See all My Sites There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values-zh-rCN/strings.xml b/localization/src/main/res/values-zh-rCN/strings.xml index a30c4b7efa..8a90ac12a4 100644 --- a/localization/src/main/res/values-zh-rCN/strings.xml +++ b/localization/src/main/res/values-zh-rCN/strings.xml @@ -810,7 +810,7 @@ 属性设置 移除 此对象没有链接到其他对象。 - No objects inside + 这里没有对象 此数据视图没有对象。\n尝试创建一个新的对象。 此小部件没有对象。\n尝试创建一个新小部件。 Emoji @@ -1983,6 +1983,7 @@ We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored 取消固定 + See all My Sites There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values-zh-rTW/strings.xml b/localization/src/main/res/values-zh-rTW/strings.xml index 25b150efa4..6f1ff3a522 100644 --- a/localization/src/main/res/values-zh-rTW/strings.xml +++ b/localization/src/main/res/values-zh-rTW/strings.xml @@ -810,7 +810,7 @@ Property settings 移除 This object has no links to other objects. - No objects inside + 這裡沒有物件 該資料檢視沒有物件。\n請嘗試建立一個新的。 此小工具內沒有物件。\n請嘗試建立一個新的。 Emoji @@ -1987,6 +1987,7 @@ Please provide specific details of your needs here. We\'ve redesigned how widgets work, and this widget is no longer supported. Once unpinned, this widget cannot be restored. This Widget Cannot Be Restored 取消釘選 + See all My Sites There are no published objects yet It\'s empty here diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 987f9d49bc..83b4e1bb2e 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1890,7 +1890,7 @@ Please provide specific details of your needs here. Error getting version history Error getting members New object - My Spaces + Channels Unread All Pinned @@ -2215,9 +2215,9 @@ Please provide specific details of your needs here. Not now New Chat - Group chat with content organization + For real-time conversations New Space - Hub for advanced data management + For organized content and data Ideas need conversations @@ -2338,5 +2338,6 @@ Please provide specific details of your needs here. Approve + You can upload only 10 files at a time \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MembershipMiddlewareChannel.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MembershipMiddlewareChannel.kt index b6af4c4f1b..bf02258957 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MembershipMiddlewareChannel.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MembershipMiddlewareChannel.kt @@ -1,6 +1,7 @@ package com.anytypeio.anytype.middleware.interactor import com.anytypeio.anytype.core_models.membership.Membership +import com.anytypeio.anytype.core_models.membership.MembershipTiers import com.anytypeio.anytype.data.auth.event.MembershipRemoteChannel import com.anytypeio.anytype.middleware.EventProxy import com.anytypeio.anytype.middleware.mappers.toCoreModel @@ -34,4 +35,21 @@ class MembershipMiddlewareChannel( } }.filter { events -> events.isNotEmpty() } } + + override fun observeTiers(): Flow> { + return eventsProxy.flow() + .mapNotNull { emission -> + emission.messages.mapNotNull { message -> + when { + message.membershipTiersUpdate != null -> { + val event = message.membershipTiersUpdate + checkNotNull(event) + val tiers = event.tiers.map { it.toCoreModel() } + MembershipTiers.Event(tiers = tiers) + } + else -> null + } + } + }.filter { events -> events.isNotEmpty() } + } } \ No newline at end of file diff --git a/payments/src/main/java/com/anytypeio/anytype/payments/viewmodel/MembershipViewModel.kt b/payments/src/main/java/com/anytypeio/anytype/payments/viewmodel/MembershipViewModel.kt index 9329123924..59bc2faab9 100644 --- a/payments/src/main/java/com/anytypeio/anytype/payments/viewmodel/MembershipViewModel.kt +++ b/payments/src/main/java/com/anytypeio/anytype/payments/viewmodel/MembershipViewModel.kt @@ -50,6 +50,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import timber.log.Timber @@ -96,6 +97,8 @@ class MembershipViewModel( val anyEmailState = TextFieldState(initialText = "") + private val forceRefreshTrigger = MutableStateFlow(false) + init { Timber.i("MembershipViewModel, init") viewModelScope.launch { @@ -111,17 +114,19 @@ class MembershipViewModel( viewModelScope.launch { val account = getAccount.async(Unit) val accountId = account.getOrNull()?.id.orEmpty() - combine( - membershipProvider.status() - .onEach { setupBillingClient(it) }, - billingProducts, - billingPurchases - ) { membershipStatus, billingProducts, billingPurchases -> - Timber.d("TierResult: " + - "\n----------------------------\nmembershipStatus:[$membershipStatus]," + - "\n----------------------------\nbillingProducts:[$billingProducts]," + - "\n----------------------------\nbillingPurchases:[$billingPurchases]") - MainResult(membershipStatus, billingProducts, billingPurchases) + forceRefreshTrigger.flatMapLatest { forceRefresh -> + combine( + membershipProvider.status(forceRefresh = forceRefresh) + .onEach { setupBillingClient(it) }, + billingProducts, + billingPurchases + ) { membershipStatus, billingProducts, billingPurchases -> + Timber.d("TierResult: " + + "\n----------------------------\nmembershipStatus:[$membershipStatus]," + + "\n----------------------------\nbillingProducts:[$billingProducts]," + + "\n----------------------------\nbillingPurchases:[$billingPurchases]") + MainResult(membershipStatus, billingProducts, billingPurchases) + } }.collect { (membershipStatus, billingClientState, purchases) -> val newState = membershipStatus.toMainView( billingClientState = billingClientState, @@ -166,7 +171,9 @@ class MembershipViewModel( private fun checkPurchaseStatus(billingPurchaseState: BillingPurchaseState) { if (billingPurchaseState is BillingPurchaseState.HasPurchases && billingPurchaseState.isNewPurchase) { Timber.d("Billing purchase state: $billingPurchaseState") - //Got new purchase, show success screen + //Got new purchase, force refresh membership status and tiers + forceRefreshTrigger.value = true + //Show success screen val tierView = (tierState.value as? MembershipTierState.Visible)?.tier ?: return proceedWithHideTier() welcomeState.value = WelcomeState.Initial(tierView) @@ -177,6 +184,11 @@ class MembershipViewModel( ) } proceedWithNavigation(MembershipNavigation.Welcome) + // Reset force refresh trigger after a delay to allow normal updates + viewModelScope.launch { + delay(FORCE_REFRESH_RESET_DELAY_MS) + forceRefreshTrigger.value = false + } } } @@ -507,10 +519,17 @@ class MembershipViewModel( verifyMembershipEmailCode.async(VerifyMembershipEmailCode.Params(code)).fold( onSuccess = { Timber.d("Email code verified") + // Force refresh membership status and tiers after email verification + forceRefreshTrigger.value = true codeState.value = MembershipEmailCodeState.Visible.Success delay(500) proceedWithNavigation(MembershipNavigation.Dismiss) proceedWithGettingEmailStatus() + // Reset force refresh trigger + viewModelScope.launch { + delay(FORCE_REFRESH_RESET_DELAY_MS) + forceRefreshTrigger.value = false + } }, onFailure = { error -> Timber.e("Error verifying email code: $error") @@ -782,6 +801,7 @@ class MembershipViewModel( companion object { + const val FORCE_REFRESH_RESET_DELAY_MS = 1000L const val EXPECTED_SUBSCRIPTION_PURCHASE_LIST_SIZE = 1 const val NAME_VALIDATION_DELAY = 300L } diff --git a/payments/src/test/java/com/anytypeio/anytype/payments/provider/MembershipProviderTest.kt b/payments/src/test/java/com/anytypeio/anytype/payments/provider/MembershipProviderTest.kt index dbc0f8a861..7134b18e0d 100644 --- a/payments/src/test/java/com/anytypeio/anytype/payments/provider/MembershipProviderTest.kt +++ b/payments/src/test/java/com/anytypeio/anytype/payments/provider/MembershipProviderTest.kt @@ -2,8 +2,8 @@ package com.anytypeio.anytype.payments.provider import androidx.arch.core.executor.testing.InstantTaskExecutorRule import app.cash.turbine.turbineScope -import com.anytypeio.anytype.core_models.Command import com.anytypeio.anytype.core_models.membership.Membership +import com.anytypeio.anytype.core_models.membership.MembershipTiers import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository @@ -91,8 +91,16 @@ class MembershipProviderTest { emit(listOf(event2)) } + val tierEvent1 = MembershipTiers.Event(listOf(tierData2, tierData)) + val tierEvent2 = MembershipTiers.Event(listOf(tierData2, tierData)) + val tierEventList = flow { + emit(listOf(tierEvent1)) + emit(listOf(tierEvent2)) + } + membershipChannel.stub { on { observe() } doReturn eventList + on { observeTiers() } doReturn tierEventList } whenever(repo.membershipStatus(any())).thenReturn(membership) whenever( @@ -101,15 +109,8 @@ class MembershipProviderTest { DATE_FORMAT ) ).thenReturn("01-01-1970") - val command = Command.Membership.GetTiers( - noCache = true, - locale = "en" - ) - repo.stub { - onBlocking { membershipGetTiers(command) } doReturn listOf(tierData2, tierData) - - } whenever(localeProvider.language()).thenReturn("en") + whenever(repo.membershipGetTiers(any())).thenReturn(listOf(tierData2, tierData)) awaitAccountStartManager.setState(AwaitAccountStartManager.State.Started) val membershipProviderFlow = provider.status().testIn(backgroundScope) @@ -135,7 +136,8 @@ class MembershipProviderTest { membershipProviderFlow2.awaitItem() membershipProviderFlow2.awaitItem() - verify(repo, times(6)).membershipGetTiers(command) + // Each call to status() fetches tiers once initially, then uses events for updates + verify(repo, times(2)).membershipGetTiers(any()) } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt index 9675090c51..3536fb86c3 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt @@ -119,6 +119,8 @@ import com.anytypeio.anytype.presentation.notifications.NotificationPermissionMa import com.anytypeio.anytype.presentation.objects.getCreateObjectParams import com.anytypeio.anytype.presentation.search.Subscriptions import com.anytypeio.anytype.presentation.sets.prefillNewObjectDetails +import com.anytypeio.anytype.presentation.sets.resolveSetByRelationPrefilledObjectData +import com.anytypeio.anytype.presentation.sets.resolveTemplateForDataViewObject import com.anytypeio.anytype.presentation.sets.resolveTypeAndActiveViewTemplate import com.anytypeio.anytype.presentation.sets.state.ObjectState.Companion.VIEW_DEFAULT_OBJECT_TYPE import com.anytypeio.anytype.presentation.spaces.SpaceIconView @@ -2438,6 +2440,31 @@ class HomeScreenViewModel( ) } + /** + * Creates a new object in a Widget by clicking the "+" button. + * + * This method handles TWO distinct use cases: + * 1. **Layout.SET** - "Query by Type" Widgets [Pinned section]: + * The [dataViewSourceObj] is the ObjectType obtained from the Set's `setOf` field via lookup. + * 2. **Layout.OBJECT_TYPE** - Object Type Widgets [Object Types section]: + * The [dataViewSourceObj] IS the ObjectType view itself (no lookup needed). + * + * In both cases, the created object will: + * - Have the type specified by [dataViewSourceObj] (the ObjectType) + * - Be prefilled with values from active view filters (when filters use permitted conditions, + * see [com.anytypeio.anytype.core_models.PermittedConditions]) + * - Use a template with the following priority: + * 1. Viewer's custom template (if set) + * 2. ObjectType's default template (if viewer template is not set) + * 3. No template (if both are null/empty) + * + * @param dataViewSourceObj The ObjectType for the new object. Source depends on layout: + * - Layout.SET: The ObjectType from the Set's `setOf` field (obtained via lookup) + * - Layout.OBJECT_TYPE: The widget's source object itself (already the ObjectType) + * @param viewer The active view/viewer containing filters, template settings, and display configuration + * @param dv The DataView content with relation links used for proper filter value formatting + * @param navigate If true, navigates to the created object after successful creation + */ private suspend fun proceedWithCreatingDataViewObject( dataViewSourceObj: ObjectWrapper.Basic, viewer: Block.Content.DataView.Viewer, @@ -2445,24 +2472,22 @@ class HomeScreenViewModel( navigate: Boolean = false ) { Timber.d("proceedWithCreatingDataViewObject, dataViewSourceObj: $dataViewSourceObj") - val dataViewSourceType = dataViewSourceObj.uniqueKey - val (_, defaultTemplate) = resolveTypeAndActiveViewTemplate( - viewer, - storeOfObjectTypes - ) val prefilled = viewer.prefillNewObjectDetails( storeOfRelations = storeOfRelations, dataViewRelationLinks = dv.relationLinks, dateProvider = dateProvider ) - val type = TypeKey(dataViewSourceType ?: VIEW_DEFAULT_OBJECT_TYPE) + val type = TypeKey(dataViewSourceObj.uniqueKey ?: VIEW_DEFAULT_OBJECT_TYPE) val space = vmParams.spaceId.id val startTime = System.currentTimeMillis() createDataViewObject.async( params = CreateDataViewObject.Params.SetByType( type = type, filters = viewer.filters, - template = defaultTemplate, + template = resolveTemplateForDataViewObject( + viewer = viewer, + setOfObject = dataViewSourceObj + ), prefilled = prefilled ).also { Timber.d("Calling with params: $it") @@ -2491,6 +2516,80 @@ class HomeScreenViewModel( ) } + /** + * Creates a new object in a "Set by Relation" Widget. + * + * This method handles Sets where the `setOf` field points to a Relation object + * (not an ObjectType). In this case: + * - The type comes from the viewer's `defaultObjectType` (similar to Collections) + * - The relation itself is added to the created object with an appropriate default value + * - Template resolution follows the standard priority (Viewer → ObjectType → null) + * + * @param relationObj The Relation object from the Set's `setOf` field + * @param viewer The active view/viewer containing filters, template settings, and display configuration + * @param dv The DataView content with relation links used for proper filter value formatting + * @param navigate If true, navigates to the created object after successful creation + */ + private suspend fun proceedWithCreatingSetByRelationObject( + relationObj: ObjectWrapper.Relation, + viewer: Block.Content.DataView.Viewer, + dv: DV, + navigate: Boolean = false + ) { + Timber.d("proceedWithCreatingSetByRelationObject, relationObj Id: ${relationObj.id}, relationObj Key: ${relationObj.uniqueKey}") + + // Get type from viewer's defaultObjectType (not from the relation) + val (defaultObjectType, defaultTemplate) = resolveTypeAndActiveViewTemplate( + activeView = viewer, + storeOfObjectTypes = storeOfObjectTypes + ) + + val type = TypeKey(defaultObjectType?.uniqueKey ?: VIEW_DEFAULT_OBJECT_TYPE) + + // Get prefilled data including the relation itself with default value + val prefilled = viewer.resolveSetByRelationPrefilledObjectData( + storeOfRelations = storeOfRelations, + dateProvider = dateProvider, + objSetByRelation = relationObj, + dataViewRelationLinks = dv.relationLinks + ) + + val space = vmParams.spaceId.id + val startTime = System.currentTimeMillis() + + createDataViewObject.async( + params = CreateDataViewObject.Params.SetByRelation( + type = type, + filters = viewer.filters, + template = defaultTemplate, + prefilled = prefilled + ).also { + Timber.d("Calling SetByRelation with params: $it") + } + ).fold( + onSuccess = { result -> + Timber.d("Successfully created Set by Relation object with id: ${result.objectId}") + viewModelScope.sendAnalyticsObjectCreateEvent( + analytics = analytics, + route = EventsDictionary.Routes.widget, + startTime = startTime, + view = null, + objType = type.key, + spaceParams = provideParams(space) + ) + if (navigate) { + val wrapper = ObjectWrapper.Basic(result.struct.orEmpty()) + if (wrapper.isValid) { + proceedWithNavigation(wrapper.navigation()) + } + } + }, + onFailure = { + Timber.e(it, "Error while creating Set by Relation object for widget") + } + ) + } + private suspend fun proceedWithAddingObjectToCollection( viewer: Block.Content.DataView.Viewer, dv: DV, @@ -2503,8 +2602,8 @@ class HomeScreenViewModel( ) val (defaultObjectType, defaultTemplate) = resolveTypeAndActiveViewTemplate( - viewer, - storeOfObjectTypes + activeView = viewer, + storeOfObjectTypes = storeOfObjectTypes ) val defaultObjectTypeUniqueKey = TypeKey(defaultObjectType?.uniqueKey ?: VIEW_DEFAULT_OBJECT_TYPE) @@ -2612,12 +2711,30 @@ class HomeScreenViewModel( Timber.w("Data view source is missing or not valid") return@fold } - proceedWithCreatingDataViewObject( - dataViewSourceObj = dataViewSourceObj, - viewer = viewer, - dv = dv, - navigate = navigate - ) + // Check if this is a Set by ObjectType or Set by Relation + when (dataViewSourceObj.layout) { + ObjectType.Layout.OBJECT_TYPE -> { + // Set by Type: setOf points to an ObjectType + proceedWithCreatingDataViewObject( + dataViewSourceObj = dataViewSourceObj, + viewer = viewer, + dv = dv, + navigate = navigate + ) + } + ObjectType.Layout.RELATION -> { + // Set by Relation: setOf points to a Relation + proceedWithCreatingSetByRelationObject( + relationObj = ObjectWrapper.Relation(dataViewSourceObj.map), + viewer = viewer, + dv = dv, + navigate = navigate + ) + } + else -> { + Timber.w("Unsupported setOf layout: ${dataViewSourceObj.layout}") + } + } } ObjectType.Layout.OBJECT_TYPE -> { if (!dataViewObject.isValid) { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/membership/provider/MembershipProvider.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/membership/provider/MembershipProvider.kt index 217cb0e925..bdaa175828 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/membership/provider/MembershipProvider.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/membership/provider/MembershipProvider.kt @@ -14,6 +14,7 @@ import com.anytypeio.anytype.domain.workspace.MembershipChannel import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest @@ -24,7 +25,7 @@ import timber.log.Timber interface MembershipProvider { - fun status(): Flow + fun status(forceRefresh: Boolean = false): Flow fun activeTier(): Flow class Default( @@ -37,13 +38,13 @@ interface MembershipProvider { ) : MembershipProvider { @OptIn(ExperimentalCoroutinesApi::class) - override fun status(): Flow { + override fun status(forceRefresh: Boolean): Flow { return awaitAccountStartManager.state().flatMapLatest { state -> when (state) { AwaitAccountStartManager.State.Started -> buildStatusFlow( - initial = proceedWithGettingMembership() + initialMembership = proceedWithGettingMembership(noCache = forceRefresh), + initialTiers = proceedWithGettingTiers(noCache = forceRefresh) ) - AwaitAccountStartManager.State.Init -> emptyFlow() AwaitAccountStartManager.State.Stopped -> emptyFlow() } @@ -58,7 +59,6 @@ interface MembershipProvider { AwaitAccountStartManager.State.Started -> buildActiveTierFlow( initial = proceedWithGettingMembership() ) - AwaitAccountStartManager.State.Init -> emptyFlow() AwaitAccountStartManager.State.Stopped -> emptyFlow() } @@ -80,24 +80,32 @@ interface MembershipProvider { } private fun buildStatusFlow( - initial: Membership? + initialMembership: Membership?, + initialTiers: List ): Flow { - return membershipChannel + val membershipFlow = membershipChannel .observe() - .scan(initial) { _, events -> - events.lastOrNull()?.membership + .scan(initialMembership) { previous, events -> + events.lastOrNull()?.membership ?: previous }.filterNotNull() - .map { membership -> - val tiers = proceedWithGettingTiers() + + val tiersFlow = membershipChannel + .observeTiers() + .scan(initialTiers) { previous, events -> + events.lastOrNull()?.tiers ?: previous + } + + return combine(membershipFlow, tiersFlow) { membership, tiers -> + val filteredTiers = tiers .filter { tier -> shouldShowTier(tier, membership.tier) } .sortedBy { it.id } - val newStatus = toMembershipStatus( - membership = membership, - tiers = tiers - ) - Timber.d("MembershipProvider, newState: $newStatus") - newStatus - } + val newStatus = toMembershipStatus( + membership = membership, + tiers = filteredTiers + ) + Timber.d("MembershipProvider, newState: $newStatus") + newStatus + } } /** @@ -123,16 +131,16 @@ interface MembershipProvider { return !tier.androidProductId.isNullOrBlank() } - private suspend fun proceedWithGettingMembership(): Membership? { + private suspend fun proceedWithGettingMembership(noCache: Boolean = false): Membership? { val command = Command.Membership.GetStatus( - noCache = true + noCache = noCache ) return repo.membershipStatus(command) } - private suspend fun proceedWithGettingTiers(): List { + private suspend fun proceedWithGettingTiers(noCache: Boolean = false): List { val tiersParams = Command.Membership.GetTiers( - noCache = true, + noCache = noCache, locale = localeProvider.language() ) return repo.membershipGetTiers(tiersParams) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt index 5248a0f55d..27fa004e7d 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/menu/ObjectMenuViewModelBase.kt @@ -49,7 +49,6 @@ import com.anytypeio.anytype.presentation.util.Dispatcher import com.anytypeio.anytype.presentation.util.downloader.DebugGoroutinesShareDownloader import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader import com.anytypeio.anytype.presentation.widgets.findWidgetBlockForObject -import com.anytypeio.anytype.presentation.widgets.parseWidgets import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/profile/ProfileIconView.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/profile/ProfileIconView.kt index 29545568c1..7ea462abab 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/profile/ProfileIconView.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/profile/ProfileIconView.kt @@ -24,6 +24,8 @@ sealed class AccountProfile { data object Idle: AccountProfile() class Data( val name: String, - val icon: ProfileIconView + val icon: ProfileIconView, + val identity: String? = null, + val globalName: String? = null ): AccountProfile() } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt index 3e2e5e0d58..1bf51c223a 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sets/ObjectSetExtension.kt @@ -134,24 +134,47 @@ fun ObjectState.DataView.Collection.getObjectOrderIds(currentViewerId: String): return dataViewContent.objectOrders.find { it.view == currentViewerId }?.ids ?: emptyList() } -fun List.updateFormatForSubscription(relationLinks: List): List { - return map { f: DVFilter -> - val r = relationLinks.firstOrNull { it.key == f.relation } - if (r != null && r.format == RelationFormat.DATE) { - f.copy(relationFormat = r.format) - } else if (r != null && r.format == RelationFormat.OBJECT) { +/** + * Transforms a filter by applying the appropriate relation format. + * + * For DATE formats: adds the relationFormat to the filter + * For OBJECT formats: adds the relationFormat AND normalizes EQUAL condition to IN + * + * @param filter The filter to transform + * @param format The relation format to apply (null means no transformation) + * @return The transformed filter, or the original if format is null or not DATE/OBJECT + */ +private fun transformFilterWithFormat(filter: DVFilter, format: RelationFormat?): DVFilter { + return when (format) { + RelationFormat.DATE -> { + filter.copy(relationFormat = format) + } + RelationFormat.OBJECT -> { // Temporary workaround for normalizing filter condition for object filters - f.copy( - relationFormat = r.format, - condition = if (f.condition == DVFilterCondition.EQUAL) { + filter.copy( + relationFormat = format, + condition = if (filter.condition == DVFilterCondition.EQUAL) { DVFilterCondition.IN } else { - f.condition + filter.condition } ) - } else { - f } + else -> filter + } +} + +fun List.updateFormatForSubscription(relationLinks: List): List { + return map { filter -> + val relation = relationLinks.firstOrNull { it.key == filter.relation } + transformFilterWithFormat(filter, relation?.format) + } +} + +suspend fun List.updateFormatForSubscription(storeOfRelations: StoreOfRelations): List { + return map { filter -> + val relation = storeOfRelations.getByKey(filter.relation) + transformFilterWithFormat(filter, relation?.format) } } @@ -469,6 +492,36 @@ suspend fun resolveTypeAndActiveViewTemplate( } } +/** + * Resolves template for creating objects in a View of an Objects with Layouts: SET or OBJECT_TYPE. + * Template resolution priority: + * 1. Check Viewer's defaultTemplate first + * 2. If viewer template is null/empty, check setOfObject's defaultTemplateId + * 3. If both are null/empty, return null (no template for object creation) + * + * @param viewer The viewer from which to check for template + * @param setOfObject The DataView setOf object containing the default template + * @return Template ID if found, null if both viewer and setOfObject's templates are empty + */ +fun resolveTemplateForDataViewObject( + viewer: Block.Content.DataView.Viewer, + setOfObject: ObjectWrapper.Basic +): Id? { + // First check Viewer for template + return if (!viewer.defaultTemplate.isNullOrEmpty()) { + viewer.defaultTemplate + } else { + // If viewer template is not present, check ObjectType + val objectTypeTemplate = setOfObject.getSingleValue(Relations.DEFAULT_TEMPLATE_ID) + if (!objectTypeTemplate.isNullOrEmpty()) { + objectTypeTemplate + } else { + // If both are null/empty, don't use template for object creation + null + } + } +} + fun ObjectState.DataView.isChangingDefaultTypeAvailable(): Boolean { return when (this) { is ObjectState.DataView.Collection -> true diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt index 8707d38c0d..98ed611349 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/spaces/SpaceSettingsViewModel.kt @@ -800,7 +800,7 @@ class SpaceSettingsViewModel( details = mapOf( Relations.ICON_IMAGE to file.id, Relations.ICON_OPTION to null, - Relations.ICON_EMOJI to null + Relations.ICON_EMOJI to "" ) ) ).fold( diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/DataViewListWidgetContainer.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/DataViewListWidgetContainer.kt index bb7080195f..6cd2b9fbb7 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/DataViewListWidgetContainer.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/DataViewListWidgetContainer.kt @@ -27,6 +27,7 @@ import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.relations.cover import com.anytypeio.anytype.presentation.search.ObjectSearchConstants import com.anytypeio.anytype.presentation.sets.subscription.updateWithRelationFormat +import com.anytypeio.anytype.presentation.sets.updateFormatForSubscription import com.anytypeio.anytype.presentation.widgets.WidgetView.Gallery import com.anytypeio.anytype.presentation.widgets.WidgetView.Name.Default import com.anytypeio.anytype.presentation.widgets.WidgetView.SetOfObjects @@ -286,7 +287,7 @@ class DataViewListWidgetContainer( * Builds a ViewerContext containing object data, data view configuration, and search parameters. * Handles viewer selection, limit resolution, and parameter parsing for widget data subscription. */ - private fun buildViewerContextCommon( + private suspend fun buildViewerContextCommon( obj: ObjectView, activeViewerId: Id?, isCompact: Boolean @@ -326,8 +327,12 @@ class DataViewListWidgetContainer( addAll(defaultKeys) addAll(dataViewKeys) }.distinct(), + // IMPORTANT: updateFormatForSubscription is required to enrich filters with proper + // property formats from StoreOfRelations. ObjectView doesn't include formats in + // DataView details, so we must always fetch the latest Property Format from Store. + // This is critical for DATE and OBJECT relation types to get correct subscription results. filters = buildList { - addAll(targetView?.filters.orEmpty()) + addAll(targetView?.filters?.updateFormatForSubscription(storeOfRelations).orEmpty()) addAll(ObjectSearchConstants.defaultDataViewFilters()) }, limit = subscriptionLimit, @@ -349,8 +354,12 @@ class DataViewListWidgetContainer( addAll(defaultKeys) addAll(dataViewKeys) }.distinct(), + // IMPORTANT: updateFormatForSubscription is required to enrich filters with proper + // property formats from StoreOfRelations. ObjectView doesn't include formats in + // DataView details, so we must always fetch the latest Property Format from Store. + // This is critical for DATE and OBJECT relation types to get correct subscription results. filters = buildList { - addAll(targetView?.filters.orEmpty()) + addAll(targetView?.filters?.updateFormatForSubscription(storeOfRelations).orEmpty()) addAll(ObjectSearchConstants.defaultDataViewFilters()) }, limit = subscriptionLimit, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt index 75c661641f..73881240a8 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/Widget.kt @@ -267,18 +267,18 @@ suspend fun List.parseWidgets( val sourceContent = child.content if (sourceContent is Block.Content.Link) { val target = sourceContent.target - val raw = details[target] ?: mapOf(Relations.ID to sourceContent.target) + val raw = details[target].orEmpty() val targetObj = ObjectWrapper.Basic(raw) - val icon = targetObj.objectIcon( - builder = urlBuilder, - objType = storeOfObjectTypes.getTypeOfObject(targetObj) - ) val source = if (BundledWidgetSourceIds.ids.contains(target)) { target.bundled() } else { Widget.Source.Default(obj = targetObj) } if (source.hasValidSource() && !WidgetConfig.excludedTypes.contains(source.type)) { + val icon = targetObj.objectIcon( + builder = urlBuilder, + objType = storeOfObjectTypes.getTypeOfObject(targetObj) + ) when (source) { is Widget.Source.Bundled.AllObjects -> { add( diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/ParseWidgetLimitTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/ParseWidgetLimitTest.kt index 2f1808cc44..284381e2a7 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/ParseWidgetLimitTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/ParseWidgetLimitTest.kt @@ -30,7 +30,9 @@ class ParseWidgetLimitTest { layout = Block.Content.Widget.Layout.TREE ) - val widgetLink = StubLinkToObjectBlock() + val widgetLink = StubLinkToObjectBlock( + target = source.id + ) val widgetBlock = Block( id = MockDataFactory.randomUuid(), @@ -52,7 +54,7 @@ class ParseWidgetLimitTest { put(source.id, source.map) }, config = StubConfig(), - urlBuilder, + urlBuilder = urlBuilder, storeOfObjectTypes = storeOfObjectTypes ) @@ -70,7 +72,9 @@ class ParseWidgetLimitTest { layout = Block.Content.Widget.Layout.LIST ) - val widgetLink = StubLinkToObjectBlock() + val widgetLink = StubLinkToObjectBlock( + target = source.id + ) val widgetBlock = Block( id = MockDataFactory.randomUuid(), @@ -110,7 +114,9 @@ class ParseWidgetLimitTest { layout = Block.Content.Widget.Layout.COMPACT_LIST ) - val widgetLink = StubLinkToObjectBlock() + val widgetLink = StubLinkToObjectBlock( + target = source.id + ) val widgetBlock = Block( id = MockDataFactory.randomUuid(), diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/ParseWidgetTest.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/ParseWidgetTest.kt index 4c5e522dd1..bbf1ad326c 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/ParseWidgetTest.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/ParseWidgetTest.kt @@ -1,6 +1,8 @@ package com.anytypeio.anytype.presentation.home import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.Relations import com.anytypeio.anytype.core_models.StubConfig import com.anytypeio.anytype.core_models.StubLinkToObjectBlock import com.anytypeio.anytype.core_models.StubObject @@ -171,10 +173,90 @@ class ParseWidgetTest { result.first().id == secondWidgetBlock.id } } + @Test - fun `should hide widgets with invalid source because of empty details`()= runTest { + fun `should hide widgets with deleted or archived source because of empty details`() = runTest { + + val invalidSource = StubObject( + isArchived = true, + isDeleted = true + ) + + val validSource = StubObject( + isArchived = false, + isDeleted = false + ) - val invalidSource = StubObject() + val widgetContent = Block.Content.Widget( + layout = listOf( + Block.Content.Widget.Layout.TREE, + Block.Content.Widget.Layout.LIST, + Block.Content.Widget.Layout.LINK, + Block.Content.Widget.Layout.COMPACT_LIST + ).random() + ) + + val firstWidgetLink = StubLinkToObjectBlock( + target = invalidSource.id + ) + + val secondWidgetLink = StubLinkToObjectBlock( + target = validSource.id + ) + + val firstWidgetBlock = Block( + id = MockDataFactory.randomUuid(), + content = widgetContent, + children = listOf(firstWidgetLink.id), + fields = Block.Fields.empty() + ) + + val secondWidgetBlock = Block( + id = MockDataFactory.randomUuid(), + content = widgetContent, + children = listOf(secondWidgetLink.id), + fields = Block.Fields.empty() + ) + + val smartBlock = StubSmartBlock( + id = HomeScreenViewModelTest.WIDGET_OBJECT_ID, + children = listOf(firstWidgetBlock.id, secondWidgetBlock.id) + ) + + val blocks = listOf( + smartBlock, + firstWidgetBlock, + firstWidgetLink, + secondWidgetBlock, + secondWidgetLink + ) + + val result = blocks.parseWidgets( + root = smartBlock.id, + details = buildMap { + put(invalidSource.id, emptyMap()) + put(validSource.id, validSource.map) + }, + config = StubConfig(), + urlBuilder, + storeOfObjectTypes = storeOfObjectTypes + ) + + assertTrue { + result.size == 1 + } + + assertTrue { + result.first().id == secondWidgetBlock.id + } + } + + @Test + fun `should hide widgets with source having empty details`() = runTest { + + val invalidSource = ObjectWrapper.Basic( + mapOf(Relations.ID to MockDataFactory.randomUuid()) + ) val validSource = StubObject( isArchived = false, diff --git a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/UserPermissionProviderStub.kt b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/UserPermissionProviderStub.kt index 8df44e933c..9ddbf8f5de 100644 --- a/presentation/src/test/java/com/anytypeio/anytype/presentation/home/UserPermissionProviderStub.kt +++ b/presentation/src/test/java/com/anytypeio/anytype/presentation/home/UserPermissionProviderStub.kt @@ -1,6 +1,7 @@ package com.anytypeio.anytype.presentation.home import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider @@ -28,4 +29,8 @@ class UserPermissionProviderStub : UserPermissionProvider { override fun all(): Flow> { return flowOf(emptyMap()) } + + override fun getCurrent(): Flow { + return flowOf(null) + } } diff --git a/versioning.gradle b/versioning.gradle index efc8075083..ec94bd3931 100644 --- a/versioning.gradle +++ b/versioning.gradle @@ -98,7 +98,7 @@ ext.getBuildVersionName = { def date = getCurrentDate() return "${versionMajor}.${versionMinor}.${versionPatch}-${date}" } else { - return "${versionMajor}.${versionMinor}.${versionPatch}-alpha" + return "${versionMajor}.${versionMinor}.${versionPatch}" } }