diff --git a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt index 2a7a278d95..18f9d2a98b 100644 --- a/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt +++ b/app/src/androidTest/java/com/anytypeio/anytype/features/sets/dv/TestObjectSetSetup.kt @@ -69,6 +69,7 @@ import com.anytypeio.anytype.presentation.common.Action import com.anytypeio.anytype.presentation.common.Delegator import com.anytypeio.anytype.presentation.editor.cover.CoverImageHashProvider import com.anytypeio.anytype.core_models.ObjectViewDetails +import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.presentation.sets.ObjectSetDatabase import com.anytypeio.anytype.presentation.sets.ObjectSetPaginator import com.anytypeio.anytype.presentation.sets.ObjectSetSession @@ -87,6 +88,7 @@ import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import org.mockito.kotlin.any import org.mockito.kotlin.doReturn @@ -244,7 +246,10 @@ abstract class TestObjectSetSetup { private val dateProvider = DateProviderImpl( defaultZoneId = ZoneId.systemDefault(), localeProvider = localeProvider, - appDefaultDateFormatProvider = AppDefaultDateFormatProviderImpl(localeProvider) + appDefaultDateFormatProvider = AppDefaultDateFormatProviderImpl(localeProvider), + stringResourceProvider = mock( + StringResourceProvider::class.java + ) ) open fun setup() { diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt index 8d41c7af91..589586aca9 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/vault/VaultDI.kt @@ -13,15 +13,17 @@ import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.domain.debugging.Logger import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.misc.AppActionManager +import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider +import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.other.DefaultSpaceInviteResolver import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate -import com.anytypeio.anytype.presentation.vault.VaultViewModel +import com.anytypeio.anytype.presentation.vault.VaultViewModelFactory import com.anytypeio.anytype.ui.vault.VaultFragment import dagger.Binds import dagger.Component @@ -53,7 +55,7 @@ object VaultModule { @PerScreen @Binds fun bindViewModelFactory( - factory: VaultViewModel.Factory + factory: VaultViewModelFactory ): ViewModelProvider.Factory @PerScreen @@ -84,4 +86,6 @@ interface VaultComponentDependencies : ComponentDependencies { fun profileContainer(): ProfileSubscriptionManager fun chatPreviewContainer(): ChatPreviewContainer fun pendingIntentStore(): PendingIntentStore + fun stringResourceProvider(): StringResourceProvider + fun dateProvider(): DateProvider } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/UtilModule.kt b/app/src/main/java/com/anytypeio/anytype/di/main/UtilModule.kt index fca1b9fb3e..c79e3ed17b 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/UtilModule.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/UtilModule.kt @@ -69,11 +69,13 @@ object UtilModule { @Singleton fun provideDateProvider( localeProvider: LocaleProvider, - appDefaultDateFormatProvider: AppDefaultDateFormatProvider + appDefaultDateFormatProvider: AppDefaultDateFormatProvider, + stringResourceProvider: StringResourceProvider ): DateProvider = DateProviderImpl( defaultZoneId = ZoneId.systemDefault(), localeProvider = localeProvider, - appDefaultDateFormatProvider = appDefaultDateFormatProvider + appDefaultDateFormatProvider = appDefaultDateFormatProvider, + stringResourceProvider = stringResourceProvider ) @JvmStatic diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultEmptyScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultEmptyScreen.kt index 00898f7f4a..724b83d043 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultEmptyScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultEmptyScreen.kt @@ -25,10 +25,11 @@ import com.anytypeio.anytype.core_ui.views.ButtonSize @Composable fun VaultEmptyState( + modifier: Modifier = Modifier, onCreateSpaceClicked: () -> Unit ) { Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, 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 3b9a78167f..053f587b6e 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 @@ -15,7 +15,6 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf import androidx.fragment.app.viewModels import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.NavOptions import androidx.navigation.NavOptions.* import androidx.navigation.fragment.findNavController import com.anytypeio.anytype.R @@ -25,9 +24,10 @@ import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.other.DefaultDeepLinkResolver +import com.anytypeio.anytype.presentation.vault.VaultCommand +import com.anytypeio.anytype.presentation.vault.VaultNavigation import com.anytypeio.anytype.presentation.vault.VaultViewModel -import com.anytypeio.anytype.presentation.vault.VaultViewModel.Navigation -import com.anytypeio.anytype.presentation.vault.VaultViewModel.Command +import com.anytypeio.anytype.presentation.vault.VaultViewModelFactory import com.anytypeio.anytype.ui.base.navigation import com.anytypeio.anytype.ui.chats.ChatFragment import com.anytypeio.anytype.ui.gallery.GalleryInstallationFragment @@ -35,7 +35,6 @@ import com.anytypeio.anytype.ui.home.HomeScreenFragment import com.anytypeio.anytype.ui.multiplayer.RequestJoinSpaceFragment import com.anytypeio.anytype.ui.payments.MembershipFragment import com.anytypeio.anytype.ui.settings.typography -import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.ARG_SPACE_TYPE import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.TYPE_CHAT import com.anytypeio.anytype.ui.spaces.CreateSpaceFragment.Companion.TYPE_SPACE @@ -47,7 +46,7 @@ class VaultFragment : BaseComposeFragment() { private val deepLink: String? get() = argOrNull(DEEP_LINK_KEY) @Inject - lateinit var factory: VaultViewModel.Factory + lateinit var factory: VaultViewModelFactory private val vm by viewModels { factory } @@ -68,7 +67,7 @@ class VaultFragment : BaseComposeFragment() { onOrderChanged = vm::onOrderChanged, profile = vm.profileView.collectAsStateWithLifecycle().value ) - + if (vm.showChooseSpaceType.collectAsStateWithLifecycle().value) { ChooseSpaceTypeScreen( onCreateChatClicked = { @@ -88,14 +87,14 @@ class VaultFragment : BaseComposeFragment() { vm.commands.collect { command -> proceed(command) } } LaunchedEffect(Unit) { - vm.navigation.collect { command -> proceed(command) } + vm.navigations.collect { command -> proceed(command) } } } } - private fun proceed(command: Command) { + private fun proceed(command: VaultCommand) { when (command) { - is Command.EnterSpaceHomeScreen -> { + is VaultCommand.EnterSpaceHomeScreen -> { runCatching { findNavController().navigate( R.id.actionOpenSpaceFromVault, @@ -108,7 +107,8 @@ class VaultFragment : BaseComposeFragment() { Timber.e(it, "Error while opening space from vault") } } - is Command.EnterSpaceLevelChat -> { + + is VaultCommand.EnterSpaceLevelChat -> { runCatching { findNavController().navigate( R.id.actionOpenChatFromVault, @@ -121,7 +121,8 @@ class VaultFragment : BaseComposeFragment() { Timber.e(it, "Error while opening space-level chat from vault") } } - is Command.CreateNewSpace -> { + + is VaultCommand.CreateNewSpace -> { runCatching { findNavController().navigate( R.id.actionCreateSpaceFromVault, @@ -131,7 +132,8 @@ class VaultFragment : BaseComposeFragment() { Timber.e(it, "Error while opening create space screen from vault") } } - Command.CreateChat -> { + + VaultCommand.CreateChat -> { runCatching { findNavController().navigate( R.id.actionCreateChatFromVault, @@ -141,7 +143,8 @@ class VaultFragment : BaseComposeFragment() { Timber.e(it, "Error while opening create chat screen from vault") } } - is Command.OpenProfileSettings -> { + + is VaultCommand.OpenProfileSettings -> { runCatching { findNavController().navigate( R.id.profileSettingsScreen, @@ -151,13 +154,15 @@ class VaultFragment : BaseComposeFragment() { Timber.e(it, "Error while opening profile settings from vault") } } - is Command.Deeplink.Invite -> { + + is VaultCommand.Deeplink.Invite -> { findNavController().navigate( R.id.requestJoinSpaceScreen, RequestJoinSpaceFragment.args(link = command.link) ) } - is Command.Deeplink.GalleryInstallation -> { + + is VaultCommand.Deeplink.GalleryInstallation -> { findNavController().navigate( R.id.galleryInstallationScreen, GalleryInstallationFragment.args( @@ -166,14 +171,16 @@ class VaultFragment : BaseComposeFragment() { ) ) } - is Command.Deeplink.MembershipScreen -> { + + is VaultCommand.Deeplink.MembershipScreen -> { findNavController().navigate( R.id.paymentsScreen, MembershipFragment.args(command.tierId), Builder().setLaunchSingleTop(true).build() ) } - is Command.Deeplink.DeepLinkToObjectNotWorking -> { + + is VaultCommand.Deeplink.DeepLinkToObjectNotWorking -> { toast( getString(R.string.multiplayer_deeplink_to_your_object_error) ) @@ -181,9 +188,9 @@ class VaultFragment : BaseComposeFragment() { } } - private fun proceed(destination: Navigation) { + private fun proceed(destination: VaultNavigation) { when (destination) { - is Navigation.OpenObject -> runCatching { + is VaultNavigation.OpenObject -> runCatching { findNavController().navigate( R.id.actionOpenSpaceFromVault, HomeScreenFragment.args( @@ -198,7 +205,8 @@ class VaultFragment : BaseComposeFragment() { }.onFailure { Timber.e(it, "Error while opening object from vault") } - is Navigation.OpenSet -> runCatching { + + is VaultNavigation.OpenSet -> runCatching { findNavController().navigate( R.id.actionOpenSpaceFromVault, HomeScreenFragment.args( @@ -214,7 +222,8 @@ class VaultFragment : BaseComposeFragment() { }.onFailure { Timber.e(it, "Error while opening set or collection from vault") } - is Navigation.OpenChat -> { + + is VaultNavigation.OpenChat -> { findNavController().navigate( R.id.actionOpenSpaceFromVault, HomeScreenFragment.args( @@ -227,7 +236,8 @@ class VaultFragment : BaseComposeFragment() { space = destination.space ) } - is Navigation.OpenDateObject -> { + + is VaultNavigation.OpenDateObject -> { runCatching { findNavController().navigate( R.id.actionOpenSpaceFromVault, @@ -245,7 +255,7 @@ class VaultFragment : BaseComposeFragment() { } } - is Navigation.OpenParticipant -> { + is VaultNavigation.OpenParticipant -> { runCatching { findNavController().navigate( R.id.actionOpenSpaceFromVault, @@ -262,9 +272,14 @@ class VaultFragment : BaseComposeFragment() { Timber.e(e, "Error while opening participant object from widgets") } } - is Navigation.OpenType -> { + + is VaultNavigation.OpenType -> { Timber.e("Illegal command: type cannot be opened from vault") } + + is VaultNavigation.ShowError -> { + toast(destination.message) + } } } @@ -304,7 +319,7 @@ class VaultFragment : BaseComposeFragment() { companion object { private const val SHOW_MNEMONIC_KEY = "arg.vault-screen.show-mnemonic" private const val DEEP_LINK_KEY = "arg.vault-screen.deep-link" - fun args(deeplink: String?) : Bundle = bundleOf( + fun args(deeplink: String?): Bundle = bundleOf( DEEP_LINK_KEY to deeplink ) } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt index 5e605785fa..45e084108c 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -24,6 +23,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -33,7 +33,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color @@ -42,41 +41,26 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.graphics.toColorInt import coil3.compose.rememberAsyncImagePainter import com.anytypeio.anytype.R import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.ObjectWrapper -import com.anytypeio.anytype.core_models.Relations -import com.anytypeio.anytype.core_models.Wallpaper -import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE -import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType import com.anytypeio.anytype.core_ui.common.DefaultPreviews -import com.anytypeio.anytype.core_ui.features.SpaceIconView -import com.anytypeio.anytype.core_ui.features.wallpaper.gradient import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.foundation.util.DraggableItem import com.anytypeio.anytype.core_ui.foundation.util.dragContainer import com.anytypeio.anytype.core_ui.foundation.util.rememberDragDropState import com.anytypeio.anytype.core_ui.views.AvatarTitle -import com.anytypeio.anytype.core_ui.views.BodySemiBold -import com.anytypeio.anytype.core_ui.views.Caption1Regular import com.anytypeio.anytype.core_ui.views.HeadlineTitle -import com.anytypeio.anytype.core_ui.views.Relations3 import com.anytypeio.anytype.core_ui.views.Title1 import com.anytypeio.anytype.core_ui.views.animations.conditionalBackground import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK -import com.anytypeio.anytype.presentation.editor.cover.CoverGradient import com.anytypeio.anytype.presentation.profile.AccountProfile import com.anytypeio.anytype.presentation.profile.ProfileIconView import com.anytypeio.anytype.presentation.spaces.SelectSpaceViewModel -import com.anytypeio.anytype.presentation.spaces.SpaceIconView -import com.anytypeio.anytype.presentation.vault.VaultViewModel.VaultSpaceView -import com.anytypeio.anytype.presentation.wallpaper.WallpaperColor +import com.anytypeio.anytype.presentation.vault.VaultSpaceView import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi @@ -114,8 +98,8 @@ fun VaultScreen( } ) - Box( - Modifier + Scaffold( + modifier = Modifier .fillMaxSize() .background( color = colorResource(id = R.color.background_primary) @@ -125,25 +109,27 @@ fun VaultScreen( Modifier.windowInsetsPadding(WindowInsets.systemBars) else Modifier + ), + topBar = { + VaultScreenToolbar( + profile = profile, + onPlusClicked = onCreateSpaceClicked, + onSettingsClicked = onSettingsClicked, + spaceCountLimitReached = spaces.size >= SelectSpaceViewModel.MAX_SPACE_COUNT, + isScrolled = isScrolled.value ) - ) { - VaultScreenToolbar( - profile = profile, - onPlusClicked = onCreateSpaceClicked, - onSettingsClicked = onSettingsClicked, - spaceCountLimitReached = spaces.size >= SelectSpaceViewModel.MAX_SPACE_COUNT, - isScrolled = isScrolled.value - ) - + } + ) { paddings -> if (spaces.isEmpty()) { VaultEmptyState( + modifier = Modifier.padding(paddings), onCreateSpaceClicked = onCreateSpaceClicked ) } else { LazyColumn( modifier = Modifier .fillMaxSize() - .padding(top = 48.dp) + .padding(paddings) .dragContainer(dragDropState), state = lazyListState, verticalArrangement = Arrangement.spacedBy(8.dp) @@ -152,36 +138,52 @@ fun VaultScreen( items = spaceList, key = { _, item -> item.space.id + }, + contentType = { _, item -> + when (item) { + is VaultSpaceView.Chat -> TYPE_CHAT + is VaultSpaceView.Space -> TYPE_SPACE + is VaultSpaceView.Loading -> TYPE_LOADING + } } ) { idx, item -> if (idx == 0) { Spacer(modifier = Modifier.height(4.dp)) } - DraggableItem(dragDropState = dragDropState, index = idx) { - if (item.space.isLoading) { - LoadingSpaceCard() - } else { - VaultSpaceCard( - title = item.space.name.orEmpty(), - subtitle = when (item.space.spaceAccessType) { - SpaceAccessType.PRIVATE -> stringResource(id = R.string.space_type_private_space) - SpaceAccessType.DEFAULT -> stringResource(id = R.string.space_type_default_space) - SpaceAccessType.SHARED -> stringResource(id = R.string.space_type_shared_space) - else -> EMPTY_STRING_VALUE - }, - wallpaper = item.wallpaper, - onCardClicked = { onSpaceClicked(item) }, - icon = item.icon, - unreadMessageCount = item.unreadMessageCount, - unreadMentionCount = item.unreadMentionCount - ) + when (item) { + is VaultSpaceView.Chat -> { + DraggableItem(dragDropState = dragDropState, index = idx) { + VaultChatCard( + title = item.space.name.orEmpty(), + onCardClicked = { + onSpaceClicked(item) + }, + icon = item.icon, + previewText = item.previewText, + creatorName = item.creatorName, + messageText = item.messageText, + messageTime = item.messageTime, + chatPreview = item.chatPreview + ) + } + } + is VaultSpaceView.Loading -> { + DraggableItem(dragDropState = dragDropState, index = idx) { + LoadingSpaceCard() + } + } + is VaultSpaceView.Space -> { + DraggableItem(dragDropState = dragDropState, index = idx) { + VaultSpaceCard( + title = item.space.name.orEmpty(), + subtitle = item.accessType, + onCardClicked = { + onSpaceClicked(item) + }, + icon = item.icon, + ) + } } - } - if (idx == spaces.lastIndex && spaces.size < SelectSpaceViewModel.MAX_SPACE_COUNT) { - VaultSpaceAddCard( - onCreateSpaceClicked = onCreateSpaceClicked - ) - Spacer(modifier = Modifier.height(40.dp)) } } } @@ -304,154 +306,6 @@ fun VaultScreenToolbar( } } -@Composable -fun VaultSpaceCard( - title: String, - subtitle: String, - onCardClicked: () -> Unit, - icon: SpaceIconView, - wallpaper: Wallpaper, - unreadMessageCount: Int = 0, - unreadMentionCount: Int = 0 -) { - Box( - modifier = Modifier - .fillMaxWidth() - .height(96.dp) - .padding(horizontal = 8.dp) - .clip(RoundedCornerShape(20.dp)) - .then( - when (wallpaper) { - is Wallpaper.Color -> { - val color = WallpaperColor.entries.find { - it.code == wallpaper.code - } - if (color != null) { - Modifier.background( - color = Color(color.hex.toColorInt()).copy(0.3f), - shape = RoundedCornerShape(20.dp) - ) - } else { - Modifier - } - } - - is Wallpaper.Gradient -> { - Modifier.background( - brush = Brush.verticalGradient( - colors = gradient( - gradient = wallpaper.code, - alpha = 0.3f - ) - ), - shape = RoundedCornerShape(20.dp) - ) - } - - is Wallpaper.Default -> { - Modifier.background( - brush = Brush.verticalGradient( - colors = gradient( - gradient = CoverGradient.SKY, - alpha = 0.3f - ) - ), - shape = RoundedCornerShape(20.dp) - ) - } - - else -> Modifier - } - ) - .clickable { - onCardClicked() - } - ) { - SpaceIconView( - icon = icon, - onSpaceIconClick = { - onCardClicked() - }, - mainSize = 64.dp, - modifier = Modifier - .padding(start = 16.dp) - .align(Alignment.CenterStart) - ) - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - top = 24.dp, - start = 96.dp, - end = 16.dp - ) - ) { - Text( - text = title.ifEmpty { stringResource(id = R.string.untitled) }, - style = BodySemiBold, - color = colorResource(id = R.color.text_primary), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = subtitle, - style = Relations3, - color = colorResource(id = R.color.text_primary), - modifier = Modifier.alpha(0.6f) - ) - } - Row( - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(end = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - if (unreadMentionCount > 0) { - Box( - modifier = Modifier - .background( - color = colorResource(R.color.glyph_active), - shape = CircleShape - ) - .size(18.dp), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(R.drawable.ic_chat_widget_mention), - contentDescription = null - ) - } - Spacer(modifier = Modifier.width(8.dp)) - } - - if (unreadMessageCount > 0) { - val shape = if (unreadMentionCount > 9) { - CircleShape - } else { - RoundedCornerShape(100.dp) - } - Box( - modifier = Modifier - .height(18.dp) - .background( - color = colorResource(R.color.glyph_active), - shape = shape - ) - .padding(horizontal = 5.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = unreadMessageCount.toString(), - style = Caption1Regular, - color = colorResource(id = R.color.text_white), - ) - } - } - } - } -} - @Composable fun VaultSpaceAddCard( onCreateSpaceClicked: () -> Unit @@ -593,43 +447,32 @@ fun VaultScreenToolbarScrolledPreview() { ) } -@Composable -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode") -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode") -fun VaultSpaceCardPreview() { - VaultSpaceCard( - title = "B&O Museum", - subtitle = "Private space", - onCardClicked = {}, - wallpaper = Wallpaper.Default, - icon = SpaceIconView.Placeholder(), - unreadMentionCount = 1, - unreadMessageCount = 9 - ) -} +//@Composable +//@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode") +//@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode") +//fun VaultScreenPreview() { +// VaultScreen( +// spaces = buildList { +// add( +// VaultSpaceView( +// space = ObjectWrapper.SpaceView( +// mapOf( +// Relations.NAME to "B&O Museum", +// Relations.SPACE_ACCESS_TYPE to SpaceAccessType.SHARED.code.toDouble() +// ) +// ), +// icon = SpaceIconView.Placeholder() +// ) +// ) +// }, +// onSpaceClicked = {}, +// onCreateSpaceClicked = {}, +// onSettingsClicked = {}, +// onOrderChanged = {}, +// profile = AccountProfile.Idle +// ) +//} -@Composable -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode") -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode") -fun VaultScreenPreview() { - VaultScreen( - spaces = buildList { - add( - VaultSpaceView( - space = ObjectWrapper.SpaceView( - mapOf( - Relations.NAME to "B&O Museum", - Relations.SPACE_ACCESS_TYPE to SpaceAccessType.SHARED.code.toDouble() - ) - ), - icon = SpaceIconView.Placeholder() - ) - ) - }, - onSpaceClicked = {}, - onCreateSpaceClicked = {}, - onSettingsClicked = {}, - onOrderChanged = {}, - profile = AccountProfile.Idle - ) -} \ No newline at end of file +const val TYPE_CHAT = "chat" +const val TYPE_SPACE = "space" +const val TYPE_LOADING = "loading" \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultSpaceCard.kt b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultSpaceCard.kt new file mode 100644 index 0000000000..208baf077a --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/vault/VaultSpaceCard.kt @@ -0,0 +1,14 @@ +package com.anytypeio.anytype.ui.vault + +import androidx.compose.runtime.Composable +import com.anytypeio.anytype.presentation.spaces.SpaceIconView + +@Composable +fun VaultSpaceCard( + title: String, + subtitle: String, + onCardClicked: () -> Unit, + icon: SpaceIconView, +) { + //todo next pr +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt index 99b9e50797..53fa1a0d20 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/views/TypographyCompose.kt @@ -348,4 +348,12 @@ val HeadlineTitleSemibold = fontSize = 28.sp, lineHeight = 32.sp, letterSpacing = (-0.017).em - ) \ No newline at end of file + ) + +val chatPreviewTextStyle = TextStyle( + fontFamily = fontInterRegular, + fontWeight = FontWeight.W500, + fontSize = 15.sp, + lineHeight = 20.sp, + letterSpacing = (-0.014).em +) \ No newline at end of file diff --git a/device/src/main/java/com/anytypeio/anytype/device/providers/DateProviderImpl.kt b/device/src/main/java/com/anytypeio/anytype/device/providers/DateProviderImpl.kt index cf84267842..954d320f4f 100644 --- a/device/src/main/java/com/anytypeio/anytype/device/providers/DateProviderImpl.kt +++ b/device/src/main/java/com/anytypeio/anytype/device/providers/DateProviderImpl.kt @@ -8,6 +8,7 @@ import com.anytypeio.anytype.core_models.TimeInSeconds import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.DateType import com.anytypeio.anytype.domain.misc.LocaleProvider +import com.anytypeio.anytype.domain.resources.StringResourceProvider import java.text.DateFormat import java.text.SimpleDateFormat import java.time.Instant @@ -24,7 +25,8 @@ import timber.log.Timber class DateProviderImpl @Inject constructor( private val defaultZoneId: ZoneId, private val localeProvider: LocaleProvider, - private val appDefaultDateFormatProvider: AppDefaultDateFormatProvider + private val appDefaultDateFormatProvider: AppDefaultDateFormatProvider, + private val stringResourceProvider: StringResourceProvider ) : DateProvider { private val defaultDateFormat get() = appDefaultDateFormatProvider.provide() @@ -148,6 +150,7 @@ class DateProviderImpl @Inject constructor( try { val locale = localeProvider.locale() val formatter = SimpleDateFormat(pattern, locale) + formatter.timeZone = java.util.TimeZone.getTimeZone(defaultZoneId) return formatter.format(Date(timestamp)) } catch (e: Exception) { Timber.e(e, "Error formatting timestamp to date string") @@ -256,6 +259,38 @@ class DateProviderImpl @Inject constructor( // Check if the year is within the desired range return year in yearRange.first()..yearRange.last() } + + override fun getChatPreviewDate( + timeInSeconds: TimeInSeconds, + timeStyle: Int + ): String { + val dateType = calculateDateType(timeInSeconds) + val timestamp = timeInSeconds * 1000 // Convert seconds to milliseconds + + return when (dateType) { + DateType.TODAY -> { + // Show time in HH:mm format + formatToDateString(timestamp, "HH:mm") + } + DateType.YESTERDAY -> { + // Show "Yesterday" localized + stringResourceProvider.getYesterday() + } + else -> { + // Check if it's current year + val currentYear = getLocalDateOfTime(System.currentTimeMillis()).year + val messageYear = getLocalDateOfTime(timestamp).year + + if (currentYear == messageYear) { + // Show "MMM d" format (e.g., "Apr 12") + formatToDateString(timestamp, "MMM d") + } else { + // Show "MMM d, yyyy" format (e.g., "Apr 12, 2024") + formatToDateString(timestamp, "MMM d, yyyy") + } + } + } + } } diff --git a/device/src/test/java/com/anytypeio/anytype/DateProviderImplTest.kt b/device/src/test/java/com/anytypeio/anytype/DateProviderImplTest.kt index 85e1303d78..7797ffcd84 100644 --- a/device/src/test/java/com/anytypeio/anytype/DateProviderImplTest.kt +++ b/device/src/test/java/com/anytypeio/anytype/DateProviderImplTest.kt @@ -5,8 +5,11 @@ import com.anytypeio.anytype.device.providers.AppDefaultDateFormatProviderImpl import com.anytypeio.anytype.device.providers.DateProviderImpl import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.LocaleProvider +import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.vault.ObserveVaultSettings import java.time.ZoneId +import java.time.LocalDateTime +import java.time.ZoneOffset import java.util.Locale import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest @@ -27,6 +30,9 @@ class DateProviderImplTest { @Mock lateinit var observeVaultSettings: ObserveVaultSettings + @Mock + lateinit var stringResourceProvider: StringResourceProvider + lateinit var dateProviderImpl: DateProvider lateinit var appDefaultDateFormatProvider: AppDefaultDateFormatProvider @@ -36,7 +42,8 @@ class DateProviderImplTest { MockitoAnnotations.openMocks(this) appDefaultDateFormatProvider = AppDefaultDateFormatProviderImpl(localeProvider) Mockito.`when`(localeProvider.locale()).thenReturn(Locale.getDefault()) - Mockito.`when`(localeProvider.language()).thenReturn("en") + Mockito.`when`(localeProvider.language()).thenReturn(Locale.getDefault().language) + Mockito.`when`(stringResourceProvider.getYesterday()).thenReturn("Yesterday") } @Test @@ -78,6 +85,7 @@ class DateProviderImplTest { defaultZoneId = zoneId, localeProvider = localeProvider, appDefaultDateFormatProvider = appDefaultDateFormatProvider, + stringResourceProvider = stringResourceProvider ) val startOfDayInLocalZone = dateProviderImpl.adjustFromStartOfDayInUserTimeZoneToUTC( @@ -127,6 +135,7 @@ class DateProviderImplTest { defaultZoneId = zoneId, localeProvider = localeProvider, appDefaultDateFormatProvider = appDefaultDateFormatProvider, + stringResourceProvider = stringResourceProvider ) val startOfDayInLocalZone = dateProviderImpl.adjustFromStartOfDayInUserTimeZoneToUTC(utcTimestamp * 1000) @@ -173,6 +182,7 @@ class DateProviderImplTest { defaultZoneId = zoneId, localeProvider = localeProvider, appDefaultDateFormatProvider = appDefaultDateFormatProvider, + stringResourceProvider = stringResourceProvider ) val startOfDayInLocalZone = dateProviderImpl.adjustFromStartOfDayInUserTimeZoneToUTC(utcTimestamp * 1000) @@ -218,6 +228,7 @@ class DateProviderImplTest { defaultZoneId = zoneId, localeProvider = localeProvider, appDefaultDateFormatProvider = appDefaultDateFormatProvider, + stringResourceProvider = stringResourceProvider ) val startOfDayInLocalZone = dateProviderImpl.adjustFromStartOfDayInUserTimeZoneToUTC(utcTimestamp * 1000) diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatPreviewContainer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatPreviewContainer.kt index df0a5ebcbe..3e4e307c3a 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatPreviewContainer.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/chats/ChatPreviewContainer.kt @@ -55,6 +55,15 @@ interface ChatPreviewContainer { .scan(initial = initial) { previews, events -> events.fold(previews) { state, event -> when (event) { + is Event.Command.Chats.Add -> { + state.map { preview -> + if (preview.chat == event.context) { + preview.copy(message = event.message) + } else { + preview + } + } + } is Event.Command.Chats.Update -> { state.map { preview -> if (preview.chat == event.context && preview.message?.id == event.id) { diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/misc/DateProvider.kt b/domain/src/main/java/com/anytypeio/anytype/domain/misc/DateProvider.kt index 767a4a47dd..d49c07ea35 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/misc/DateProvider.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/misc/DateProvider.kt @@ -30,6 +30,10 @@ interface DateProvider { fun isSameMinute(timestamp1: Long, timestamp2: Long): Boolean fun getLocalDateOfTime(epochMilli: Long): LocalDate fun isTimestampWithinYearRange(timeStampInMillis: Long, yearRange: IntRange): Boolean + fun getChatPreviewDate( + timeInSeconds: TimeInSeconds, + timeStyle: Int = DEFAULT_DATE_FORMAT_STYLE + ): String } interface DateTypeNameProvider { diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt b/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt index e5090dc3c8..641f7bcc60 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/resources/StringResourceProvider.kt @@ -2,6 +2,7 @@ package com.anytypeio.anytype.domain.resources import com.anytypeio.anytype.core_models.RelationFormat import com.anytypeio.anytype.core_models.RelativeDate +import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType interface StringResourceProvider { fun getRelativeDateName(relativeDate: RelativeDate): String @@ -11,4 +12,6 @@ interface StringResourceProvider { fun getPropertiesFormatPrettyString(format: RelationFormat): String fun getDefaultSpaceName(): String fun getAttachmentText(): String + fun getSpaceAccessTypeName(accessType: SpaceAccessType?): String + fun getYesterday(): String } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt index a70800028f..48b02e74a7 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/StringResourceProviderImpl.kt @@ -1,8 +1,11 @@ package com.anytypeio.anytype.presentation.util import android.content.Context +import androidx.compose.ui.res.stringResource import com.anytypeio.anytype.core_models.RelationFormat import com.anytypeio.anytype.core_models.RelativeDate +import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE +import com.anytypeio.anytype.core_models.multiplayer.SpaceAccessType import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.presentation.R import javax.inject.Inject @@ -59,4 +62,17 @@ class StringResourceProviderImpl @Inject constructor(private val context: Contex override fun getAttachmentText(): String { return context.getString(R.string.attachment) } + + override fun getSpaceAccessTypeName(accessType: SpaceAccessType?): String { + return when (accessType) { + SpaceAccessType.PRIVATE -> context.getString(R.string.space_type_private_space) + SpaceAccessType.DEFAULT -> context.getString(R.string.space_type_default_space) + SpaceAccessType.SHARED -> context.getString(R.string.space_type_shared_space) + null -> EMPTY_STRING_VALUE + } + } + + override fun getYesterday(): String { + return context.getString(R.string.yesterday) + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/Factory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/Factory.kt new file mode 100644 index 0000000000..9cc01cfdf8 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/Factory.kt @@ -0,0 +1,59 @@ +package com.anytypeio.anytype.presentation.vault + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.anytypeio.anytype.analytics.base.Analytics +import com.anytypeio.anytype.domain.chats.ChatPreviewContainer +import com.anytypeio.anytype.domain.deeplink.PendingIntentStore +import com.anytypeio.anytype.domain.misc.AppActionManager +import com.anytypeio.anytype.domain.misc.DateProvider +import com.anytypeio.anytype.domain.misc.UrlBuilder +import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver +import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.resources.StringResourceProvider +import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager +import com.anytypeio.anytype.domain.spaces.SaveCurrentSpace +import com.anytypeio.anytype.domain.vault.ObserveVaultSettings +import com.anytypeio.anytype.domain.vault.SetVaultSpaceOrder +import com.anytypeio.anytype.domain.workspace.SpaceManager +import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate +import javax.inject.Inject + +class VaultViewModelFactory @Inject constructor( + private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, + private val urlBuilder: UrlBuilder, + private val spaceManager: SpaceManager, + private val saveCurrentSpace: SaveCurrentSpace, + private val setVaultSpaceOrder: SetVaultSpaceOrder, + private val observeVaultSettings: ObserveVaultSettings, + private val analytics: Analytics, + private val deepLinkToObjectDelegate: DeepLinkToObjectDelegate, + private val appActionManager: AppActionManager, + private val spaceInviteResolver: SpaceInviteResolver, + private val profileContainer: ProfileSubscriptionManager, + private val chatPreviewContainer: ChatPreviewContainer, + private val pendingIntentStore: PendingIntentStore, + private val stringResourceProvider: StringResourceProvider, + private val dateProvider: DateProvider +) : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class + ) = VaultViewModel( + spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, + urlBuilder = urlBuilder, + spaceManager = spaceManager, + saveCurrentSpace = saveCurrentSpace, + setVaultSpaceOrder = setVaultSpaceOrder, + observeVaultSettings = observeVaultSettings, + analytics = analytics, + deepLinkToObjectDelegate = deepLinkToObjectDelegate, + appActionManager = appActionManager, + spaceInviteResolver = spaceInviteResolver, + profileContainer = profileContainer, + chatPreviewContainer = chatPreviewContainer, + pendingIntentStore = pendingIntentStore, + stringResourceProvider = stringResourceProvider, + dateProvider = dateProvider + ) as T +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/Models.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/Models.kt new file mode 100644 index 0000000000..755c884183 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/Models.kt @@ -0,0 +1,65 @@ +package com.anytypeio.anytype.presentation.vault + +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.ObjectWrapper +import com.anytypeio.anytype.core_models.primitives.Space +import com.anytypeio.anytype.presentation.spaces.SpaceIconView + +sealed class VaultSpaceView { + + abstract val space: ObjectWrapper.SpaceView + abstract val icon: SpaceIconView + + data class Loading( + override val space: ObjectWrapper.SpaceView, + override val icon: SpaceIconView + ) : VaultSpaceView() + + data class Space( + override val space: ObjectWrapper.SpaceView, + override val icon: SpaceIconView, + val accessType: String + ) : VaultSpaceView() + + data class Chat( + override val space: ObjectWrapper.SpaceView, + override val icon: SpaceIconView, + val unreadMessageCount: Int = 0, + val unreadMentionCount: Int = 0, + val chatMessage: com.anytypeio.anytype.core_models.chats.Chat.Message.Content? = null, + val chatPreview: com.anytypeio.anytype.core_models.chats.Chat.Preview? = null, + val previewText: String? = null, + val creatorName: String? = null, + val messageText: String? = null, + val messageTime: String? = null + ) : VaultSpaceView() +} + +sealed class VaultCommand { + data class EnterSpaceHomeScreen(val space: Space) : VaultCommand() + data class EnterSpaceLevelChat(val space: Space, val chat: Id) : VaultCommand() + data object CreateNewSpace : VaultCommand() + data object CreateChat : VaultCommand() + data object OpenProfileSettings : VaultCommand() + + sealed class Deeplink : VaultCommand() { + data object DeepLinkToObjectNotWorking : Deeplink() + data class Invite(val link: String) : Deeplink() + data class GalleryInstallation( + val deepLinkType: String, + val deepLinkSource: String + ) : Deeplink() + + data class MembershipScreen(val tierId: String?) : Deeplink() + } +} + +sealed class VaultNavigation { + data class OpenChat(val ctx: Id, val space: Id) : VaultNavigation() + data class OpenObject(val ctx: Id, val space: Id) : VaultNavigation() + data class OpenSet(val ctx: Id, val space: Id, val view: Id?) : VaultNavigation() + data class OpenDateObject(val ctx: Id, val space: Id) : VaultNavigation() + data class OpenParticipant(val ctx: Id, val space: Id) : VaultNavigation() + data class OpenType(val target: Id, val space: Id) : VaultNavigation() + data class ShowError(val message: String) : VaultNavigation() +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt index 5f82f282e1..308e2626db 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt @@ -1,7 +1,6 @@ package com.anytypeio.anytype.presentation.vault import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.analytics.base.Analytics import com.anytypeio.anytype.analytics.base.EventsDictionary @@ -10,47 +9,48 @@ import com.anytypeio.anytype.analytics.base.sendEvent import com.anytypeio.anytype.analytics.props.Props import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper -import com.anytypeio.anytype.core_models.Wallpaper +import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.chats.Chat import com.anytypeio.anytype.core_models.multiplayer.SpaceUxType import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.core_models.settings.VaultSettings import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.chats.ChatPreviewContainer import com.anytypeio.anytype.domain.deeplink.PendingIntentStore import com.anytypeio.anytype.domain.misc.AppActionManager +import com.anytypeio.anytype.domain.misc.DateProvider import com.anytypeio.anytype.domain.misc.DeepLinkResolver import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.SpaceInviteResolver import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer +import com.anytypeio.anytype.domain.resources.StringResourceProvider import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager import com.anytypeio.anytype.domain.spaces.SaveCurrentSpace -import com.anytypeio.anytype.domain.vault.GetVaultSettings import com.anytypeio.anytype.domain.vault.ObserveVaultSettings -import com.anytypeio.anytype.domain.vault.SetVaultSettings import com.anytypeio.anytype.domain.vault.SetVaultSpaceOrder -import com.anytypeio.anytype.domain.wallpaper.GetSpaceWallpapers import com.anytypeio.anytype.domain.workspace.SpaceManager import com.anytypeio.anytype.presentation.BuildConfig import com.anytypeio.anytype.presentation.confgs.ChatConfig import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.home.navigation import com.anytypeio.anytype.presentation.navigation.DeepLinkToObjectDelegate -import com.anytypeio.anytype.presentation.navigation.NavigationViewModel import com.anytypeio.anytype.presentation.profile.AccountProfile import com.anytypeio.anytype.presentation.profile.profileIcon import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider -import com.anytypeio.anytype.presentation.spaces.SpaceIconView import com.anytypeio.anytype.presentation.spaces.spaceIcon -import com.anytypeio.anytype.presentation.vault.VaultViewModel.Navigation.* -import javax.inject.Inject +import com.anytypeio.anytype.presentation.vault.VaultNavigation.OpenChat +import com.anytypeio.anytype.presentation.vault.VaultNavigation.OpenDateObject +import com.anytypeio.anytype.presentation.vault.VaultNavigation.OpenObject +import com.anytypeio.anytype.presentation.vault.VaultNavigation.OpenParticipant +import com.anytypeio.anytype.presentation.vault.VaultNavigation.OpenSet +import com.anytypeio.anytype.presentation.vault.VaultNavigation.OpenType import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.stateIn @@ -61,7 +61,6 @@ import timber.log.Timber class VaultViewModel( private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, private val urlBuilder: UrlBuilder, - private val getSpaceWallpapers: GetSpaceWallpapers, private val spaceManager: SpaceManager, private val saveCurrentSpace: SaveCurrentSpace, private val observeVaultSettings: ObserveVaultSettings, @@ -72,11 +71,15 @@ class VaultViewModel( private val spaceInviteResolver: SpaceInviteResolver, private val profileContainer: ProfileSubscriptionManager, private val chatPreviewContainer: ChatPreviewContainer, - private val pendingIntentStore: PendingIntentStore -) : NavigationViewModel(), DeepLinkToObjectDelegate by deepLinkToObjectDelegate { + private val pendingIntentStore: PendingIntentStore, + private val stringResourceProvider: StringResourceProvider, + private val dateProvider: DateProvider +) : ViewModel(), + DeepLinkToObjectDelegate by deepLinkToObjectDelegate { val spaces = MutableStateFlow>(emptyList()) - val commands = MutableSharedFlow(replay = 0) + val commands = MutableSharedFlow(replay = 0) + val navigations = MutableSharedFlow(replay = 0) val showChooseSpaceType = MutableStateFlow(false) val profileView = profileContainer.observe().map { obj -> @@ -93,57 +96,131 @@ class VaultViewModel( init { Timber.i("VaultViewModel, init") viewModelScope.launch { - val wallpapers = getSpaceWallpapers.async(Unit).getOrNull() ?: emptyMap() combine( spaceViewSubscriptionContainer .observe() .take(1) .onCompletion { emitAll( - spaceViewSubscriptionContainer - .observe() - .debounce(SPACE_VAULT_DEBOUNCE_DURATION) + spaceViewSubscriptionContainer.observe() ) }, observeVaultSettings.flow(), chatPreviewContainer.observePreviews() - ) { spaces, settings, chatPreviews -> - spaces - .filter { space -> (space.isActive || space.isLoading) } - .map { space -> - val chatPreview = space.targetSpaceId?.let { spaceId -> - chatPreviews.find { it.space.id == spaceId } - } - - VaultSpaceView( - space = space, - icon = space.spaceIcon( - builder = urlBuilder, - spaceGradientProvider = SpaceGradientProvider.Default - ), - wallpaper = wallpapers.getOrDefault( - key = space.targetSpaceId, - defaultValue = Wallpaper.Default - ), - unreadMessageCount = chatPreview?.state?.unreadMessages?.counter ?: 0, - unreadMentionCount = chatPreview?.state?.unreadMentions?.counter ?: 0 - ) - }.sortedBy { space -> - val idx = settings.orderOfSpaces.indexOf( - space.space.id - ) - if (idx == -1) { - Int.MIN_VALUE - } else { - idx - } - } - }.collect { - spaces.value = it + ) { spacesFromFlow, settings, chatPreviews -> + transformToVaultSpaceViews(spacesFromFlow, settings, chatPreviews) + }.collect { resultingSpaceViews -> + spaces.value = resultingSpaceViews } } } + private fun transformToVaultSpaceViews( + spacesFromFlow: List, + settings: VaultSettings, + chatPreviews: List + ): List { + return spacesFromFlow + .filter { space -> (space.isActive || space.isLoading) } + .map { space -> + val chatPreview = space.targetSpaceId?.let { spaceId -> + chatPreviews.find { it.space.id == spaceId } + } + mapToVaultSpaceViewItem(space, chatPreview) + }.sortedBy { spaceView -> + val idx = settings.orderOfSpaces.indexOf( + spaceView.space.id + ) + if (idx == -1) { + Int.MIN_VALUE + } else { + idx + } + } + } + + private fun mapToVaultSpaceViewItem( + space: ObjectWrapper.SpaceView, + chatPreview: Chat.Preview? + ): VaultSpaceView { + return when { + space.isLoading -> createLoadingView(space) + chatPreview != null -> createChatView(space, chatPreview) + else -> createStandardSpaceView(space) + } + } + + private fun createLoadingView( + space: ObjectWrapper.SpaceView + ): VaultSpaceView.Loading { + Timber.d("Space ${space.id} is loading") + return VaultSpaceView.Loading( + space = space, + icon = space.spaceIcon( + builder = urlBuilder, + spaceGradientProvider = SpaceGradientProvider.Default + ) + ) + } + + private fun createChatView( + space: ObjectWrapper.SpaceView, + chatPreview: Chat.Preview + ): VaultSpaceView.Chat { + val creator = chatPreview.message?.creator ?: "" + val messageText = chatPreview.message?.content?.text + + val creatorName = if (creator.isNotEmpty()) { + val creatorObj = chatPreview.dependencies.find { + it.getSingleValue( + Relations.IDENTITY + ) == creator + } + creatorObj?.name ?: "Unknown" + } else { + null + } + + val previewText = if (creatorName != null && messageText != null) { + "$creatorName: $messageText" + } else { + messageText + } + + val messageTime = chatPreview.message?.createdAt?.let { timeInSeconds -> + if (timeInSeconds > 0) { + dateProvider.getChatPreviewDate(timeInSeconds = timeInSeconds) + } else null + } + + return VaultSpaceView.Chat( + space = space, + icon = space.spaceIcon( + builder = urlBuilder, + spaceGradientProvider = SpaceGradientProvider.Default + ), + chatPreview = chatPreview, + previewText = previewText, + creatorName = creatorName, + messageText = messageText, + messageTime = messageTime + ) + } + + private fun createStandardSpaceView( + space: ObjectWrapper.SpaceView + ): VaultSpaceView.Space { + return VaultSpaceView.Space( + space = space, + icon = space.spaceIcon( + builder = urlBuilder, + spaceGradientProvider = SpaceGradientProvider.Default + ), + accessType = stringResourceProvider + .getSpaceAccessTypeName(accessType = space.spaceAccessType) + ) + } + fun onSpaceClicked(view: VaultSpaceView) { Timber.i("onSpaceClicked") viewModelScope.launch { @@ -170,7 +247,7 @@ class VaultViewModel( fun onSettingsClicked() { viewModelScope.launch { - commands.emit(Command.OpenProfileSettings) + commands.emit(VaultCommand.OpenProfileSettings) } } @@ -189,14 +266,14 @@ class VaultViewModel( fun onCreateSpaceClicked() { viewModelScope.launch { showChooseSpaceType.value = false - commands.emit(Command.CreateNewSpace) + commands.emit(VaultCommand.CreateNewSpace) } } fun onCreateChatClicked() { - viewModelScope.launch { + viewModelScope.launch { showChooseSpaceType.value = false - commands.emit(Command.CreateChat) + commands.emit(VaultCommand.CreateChat) } } @@ -222,7 +299,7 @@ class VaultViewModel( when (deeplink) { is DeepLinkResolver.Action.Import.Experience -> { commands.emit( - Command.Deeplink.GalleryInstallation( + VaultCommand.Deeplink.GalleryInstallation( deepLinkType = deeplink.type, deepLinkSource = deeplink.source ) @@ -231,25 +308,27 @@ class VaultViewModel( is DeepLinkResolver.Action.Invite -> { delay(1000) - commands.emit(Command.Deeplink.Invite(deeplink.link)) + commands.emit(VaultCommand.Deeplink.Invite(deeplink.link)) } + is DeepLinkResolver.Action.Unknown -> { if (BuildConfig.DEBUG) { - sendToast("Could not resolve deeplink") + //sendToast("Could not resolve deeplink") } } + is DeepLinkResolver.Action.DeepLinkToObject -> { onDeepLinkToObjectAwait( obj = deeplink.obj, space = deeplink.space, switchSpaceIfObjectFound = true ).collect { result -> - when(result) { + when (result) { is DeepLinkToObjectDelegate.Result.Error -> { val link = deeplink.invite if (link != null) { commands.emit( - Command.Deeplink.Invite( + VaultCommand.Deeplink.Invite( link = spaceInviteResolver.createInviteLink( contentId = link.cid, encryptionKey = link.key @@ -257,22 +336,25 @@ class VaultViewModel( ) ) } else { - commands.emit(Command.Deeplink.DeepLinkToObjectNotWorking) + commands.emit(VaultCommand.Deeplink.DeepLinkToObjectNotWorking) } } + is DeepLinkToObjectDelegate.Result.Success -> { proceedWithNavigation(result.obj.navigation()) } } } } + is DeepLinkResolver.Action.DeepLinkToMembership -> { commands.emit( - Command.Deeplink.MembershipScreen( + VaultCommand.Deeplink.MembershipScreen( tierId = deeplink.tierId ) ) } + else -> { Timber.d("No deep link") } @@ -288,7 +370,7 @@ class VaultViewModel( delay(1000) // Simulate some delay pendingIntentStore.getDeepLinkInvite()?.let { deeplink -> Timber.d("Processing pending deeplink: $deeplink") - commands.emit(Command.Deeplink.Invite(deeplink)) + commands.emit(VaultCommand.Deeplink.Invite(deeplink)) pendingIntentStore.clearDeepLinkInvite() } } @@ -306,16 +388,19 @@ class VaultViewModel( Timber.e(it, "Error while saving current space on vault screen") }, onSuccess = { - if (spaceUxType == SpaceUxType.CHAT && chat != null && ChatConfig.isChatAllowed(space = targetSpace)) { + if (spaceUxType == SpaceUxType.CHAT && chat != null && ChatConfig.isChatAllowed( + space = targetSpace + ) + ) { commands.emit( - Command.EnterSpaceLevelChat( + VaultCommand.EnterSpaceLevelChat( space = Space(targetSpace), chat = chat ) ) } else { commands.emit( - Command.EnterSpaceHomeScreen( + VaultCommand.EnterSpaceHomeScreen( space = Space(targetSpace) ) ) @@ -325,137 +410,69 @@ class VaultViewModel( } private fun proceedWithNavigation(navigation: OpenObjectNavigation) { - when(navigation) { + val nav = when (navigation) { is OpenObjectNavigation.OpenDataView -> { - navigate( - OpenSet( - ctx = navigation.target, - space = navigation.space, - view = null - ) + OpenSet( + ctx = navigation.target, + space = navigation.space, + view = null ) } + is OpenObjectNavigation.OpenEditor -> { - navigate( - OpenObject( - ctx = navigation.target, - space = navigation.space - ) + + OpenObject( + ctx = navigation.target, + space = navigation.space ) + } + is OpenObjectNavigation.OpenChat -> { - navigate( - OpenChat( - ctx = navigation.target, - space = navigation.space - ) + OpenChat( + ctx = navigation.target, + space = navigation.space ) + } + is OpenObjectNavigation.UnexpectedLayoutError -> { - sendToast("Unexpected layout: ${navigation.layout}") + VaultNavigation.ShowError("Unexpected layout: ${navigation.layout}") } + OpenObjectNavigation.NonValidObject -> { - sendToast("Object id is missing") + VaultNavigation.ShowError("Object id is missing") } + is OpenObjectNavigation.OpenDateObject -> { - navigate( - OpenDateObject( - ctx = navigation.target, - space = navigation.space - ) + OpenDateObject( + ctx = navigation.target, + space = navigation.space ) + } + is OpenObjectNavigation.OpenParticipant -> { - navigate( - OpenParticipant( - ctx = navigation.target, - space = navigation.space - ) + OpenParticipant( + ctx = navigation.target, + space = navigation.space ) + } + is OpenObjectNavigation.OpenType -> { - navigate( - OpenType( - target = navigation.target, - space = navigation.space - ) + OpenType( + target = navigation.target, + space = navigation.space ) } } - } - - class Factory @Inject constructor( - private val spaceViewSubscriptionContainer: SpaceViewSubscriptionContainer, - private val getSpaceWallpapers: GetSpaceWallpapers, - private val urlBuilder: UrlBuilder, - private val spaceManager: SpaceManager, - private val saveCurrentSpace: SaveCurrentSpace, - private val setVaultSpaceOrder: SetVaultSpaceOrder, - private val observeVaultSettings: ObserveVaultSettings, - private val analytics: Analytics, - private val deepLinkToObjectDelegate: DeepLinkToObjectDelegate, - private val appActionManager: AppActionManager, - private val spaceInviteResolver: SpaceInviteResolver, - private val profileContainer: ProfileSubscriptionManager, - private val chatPreviewContainer: ChatPreviewContainer, - private val pendingIntentStore: PendingIntentStore - ) : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun create( - modelClass: Class - ) = VaultViewModel( - spaceViewSubscriptionContainer = spaceViewSubscriptionContainer, - getSpaceWallpapers = getSpaceWallpapers, - urlBuilder = urlBuilder, - spaceManager = spaceManager, - saveCurrentSpace = saveCurrentSpace, - setVaultSpaceOrder = setVaultSpaceOrder, - observeVaultSettings = observeVaultSettings, - analytics = analytics, - deepLinkToObjectDelegate = deepLinkToObjectDelegate, - appActionManager = appActionManager, - spaceInviteResolver = spaceInviteResolver, - profileContainer = profileContainer, - chatPreviewContainer = chatPreviewContainer, - pendingIntentStore = pendingIntentStore - ) as T - } - - data class VaultSpaceView( - val space: ObjectWrapper.SpaceView, - val icon: SpaceIconView, - val wallpaper: Wallpaper = Wallpaper.Default, - val unreadMessageCount: Int = 0, - val unreadMentionCount: Int = 0, - ) - - sealed class Command { - data class EnterSpaceHomeScreen(val space: Space): Command() - data class EnterSpaceLevelChat(val space: Space, val chat: Id): Command() - data object CreateNewSpace: Command() - data object CreateChat: Command() - data object OpenProfileSettings: Command() - - sealed class Deeplink : Command() { - data object DeepLinkToObjectNotWorking: Deeplink() - data class Invite(val link: String) : Deeplink() - data class GalleryInstallation( - val deepLinkType: String, - val deepLinkSource: String - ) : Deeplink() - data class MembershipScreen(val tierId: String?) : Deeplink() + viewModelScope.launch { + Timber.d("Proceeding with navigation: $nav") + navigations.emit(nav) } } - sealed class Navigation { - data class OpenChat(val ctx: Id, val space: Id) : Navigation() - data class OpenObject(val ctx: Id, val space: Id) : Navigation() - data class OpenSet(val ctx: Id, val space: Id, val view: Id?) : Navigation() - data class OpenDateObject(val ctx: Id, val space: Id) : Navigation() - data class OpenParticipant(val ctx: Id, val space: Id) : Navigation() - data class OpenType(val target: Id, val space: Id) : Navigation() - } - companion object { const val SPACE_VAULT_DEBOUNCE_DURATION = 300L }