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

DROID-3590 Vault 2.0 | Ui + logic, part 1 (#2494)

This commit is contained in:
Konstantin Ivanov 2025-06-05 15:17:08 +02:00 committed by GitHub
parent 86e20dfadd
commit bda6de3208
Signed by: github
GPG key ID: B5690EEEBB952194
17 changed files with 558 additions and 447 deletions

View file

@ -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() {

View file

@ -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
}

View file

@ -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

View file

@ -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,

View file

@ -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<VaultViewModel> { 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
)
}

View file

@ -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
)
}
const val TYPE_CHAT = "chat"
const val TYPE_SPACE = "space"
const val TYPE_LOADING = "loading"

View file

@ -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
}

View file

@ -348,4 +348,12 @@ val HeadlineTitleSemibold =
fontSize = 28.sp,
lineHeight = 32.sp,
letterSpacing = (-0.017).em
)
)
val chatPreviewTextStyle = TextStyle(
fontFamily = fontInterRegular,
fontWeight = FontWeight.W500,
fontSize = 15.sp,
lineHeight = 20.sp,
letterSpacing = (-0.014).em
)

View file

@ -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")
}
}
}
}
}

View file

@ -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)

View file

@ -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) {

View file

@ -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 {

View file

@ -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
}

View file

@ -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)
}
}

View file

@ -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 <T : ViewModel> create(
modelClass: Class<T>
) = 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
}

View file

@ -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()
}

View file

@ -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<VaultViewModel.Navigation>(), DeepLinkToObjectDelegate by deepLinkToObjectDelegate {
private val pendingIntentStore: PendingIntentStore,
private val stringResourceProvider: StringResourceProvider,
private val dateProvider: DateProvider
) : ViewModel(),
DeepLinkToObjectDelegate by deepLinkToObjectDelegate {
val spaces = MutableStateFlow<List<VaultSpaceView>>(emptyList())
val commands = MutableSharedFlow<Command>(replay = 0)
val commands = MutableSharedFlow<VaultCommand>(replay = 0)
val navigations = MutableSharedFlow<VaultNavigation>(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<ObjectWrapper.SpaceView>,
settings: VaultSettings,
chatPreviews: List<Chat.Preview>
): List<VaultSpaceView> {
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<String>(
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 <T : ViewModel> create(
modelClass: Class<T>
) = 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
}