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

DROID-3044 Chats | Enhancement | Space-level chat basics (#1794)

This commit is contained in:
Evgenii Kozlov 2024-11-14 00:33:08 +01:00 committed by GitHub
parent ee565a57f0
commit 047a071f85
Signed by: github
GPG key ID: B5690EEEBB952194
21 changed files with 438 additions and 131 deletions

View file

@ -28,4 +28,7 @@ class DefaultFeatureToggles @Inject constructor(
override val isSpaceLevelChatEnabled: Boolean
get() = true
override val isNewSpaceHomeEnabled: Boolean
get() = true
}

View file

@ -49,6 +49,7 @@ import com.anytypeio.anytype.di.feature.ViewerSortModule
import com.anytypeio.anytype.di.feature.auth.DaggerDeletedAccountComponent
import com.anytypeio.anytype.di.feature.cover.UnsplashModule
import com.anytypeio.anytype.di.feature.discussions.DaggerDiscussionComponent
import com.anytypeio.anytype.di.feature.discussions.DaggerSpaceLevelChatComponent
import com.anytypeio.anytype.di.feature.gallery.DaggerGalleryInstallationComponent
import com.anytypeio.anytype.di.feature.home.DaggerHomeScreenComponent
import com.anytypeio.anytype.di.feature.membership.DaggerMembershipComponent
@ -99,6 +100,7 @@ import com.anytypeio.anytype.di.feature.widgets.DaggerSelectWidgetSourceComponen
import com.anytypeio.anytype.di.feature.widgets.DaggerSelectWidgetTypeComponent
import com.anytypeio.anytype.di.main.MainComponent
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.editor.EditorViewModel
@ -1051,7 +1053,7 @@ class ComponentManager(
.build()
}
val discussionComponent = ComponentMapWithParam { params: BaseViewModel.DefaultParams ->
val discussionComponent = ComponentMapWithParam { params: DiscussionViewModel.Params ->
DaggerDiscussionComponent
.builder()
.withDependencies(findComponentDependencies())
@ -1059,6 +1061,14 @@ class ComponentManager(
.build()
}
val spaceLevelChatComponent = ComponentMapWithParam { params: DiscussionViewModel.Params ->
DaggerSpaceLevelChatComponent
.builder()
.withDependencies(findComponentDependencies())
.withParams(params)
.build()
}
val vaultComponent = Component {
DaggerVaultComponent
.factory()

View file

@ -151,7 +151,7 @@ class DiscussionFragment : BaseComposeFragment() {
.discussionComponent
.get(
key = ctx,
param = BaseViewModel.DefaultParams(
param = DiscussionViewModel.Params.Default(
ctx = ctx,
space = SpaceId(space)
)

View file

@ -14,10 +14,13 @@ import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.debugging.Logger
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModelFactory
import com.anytypeio.anytype.middleware.EventProxy
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.ui.home.HomeScreenFragment
import dagger.Binds
import dagger.BindsInstance
import dagger.Component
@ -35,13 +38,33 @@ interface DiscussionComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun withParams(params: BaseViewModel.DefaultParams): Builder
fun withParams(params: DiscussionViewModel.Params): Builder
fun withDependencies(dependencies: DiscussionComponentDependencies): Builder
fun build(): DiscussionComponent
}
fun inject(fragment: DiscussionFragment)
}
@Component(
dependencies = [DiscussionComponentDependencies::class],
modules = [
DiscussionModule::class,
DiscussionModule.Declarations::class
]
)
@PerScreen
interface SpaceLevelChatComponent {
@Component.Builder
interface Builder {
@BindsInstance
fun withParams(params: DiscussionViewModel.Params): Builder
fun withDependencies(dependencies: DiscussionComponentDependencies): Builder
fun build(): SpaceLevelChatComponent
}
fun getViewModel(): DiscussionViewModel
}
@Module
object DiscussionModule {
@Module
@ -51,7 +74,6 @@ object DiscussionModule {
fun bindViewModelFactory(
factory: DiscussionViewModelFactory
): ViewModelProvider.Factory
}
}
@ -68,4 +90,5 @@ interface DiscussionComponentDependencies : ComponentDependencies {
fun chatEventChannel(): ChatEventChannel
fun logger(): Logger
fun members(): ActiveSpaceMemberSubscriptionContainer
fun spaceViewSubscriptionContainer(): SpaceViewSubscriptionContainer
}

View file

@ -67,7 +67,6 @@ import kotlinx.coroutines.Dispatchers
)
@PerScreen
interface HomeScreenComponent {
@Component.Factory
interface Factory {
fun create(dependencies: HomeScreenDependencies): HomeScreenComponent

View file

@ -72,6 +72,7 @@ import org.burnoutcrew.reorderable.reorderable
@Composable
fun HomeScreen(
modifier: Modifier,
mode: InteractionMode,
widgets: List<WidgetView>,
onExpand: (TreePath) -> Unit,
@ -98,7 +99,7 @@ fun HomeScreen(
onBackLongClicked: () -> Unit
) {
Box(modifier = Modifier.fillMaxSize()) {
Box(modifier = modifier.fillMaxSize()) {
WidgetList(
widgets = widgets,
onExpand = onExpand,

View file

@ -4,10 +4,18 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.dp
@ -21,12 +29,16 @@ import androidx.navigation.fragment.findNavController
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.primitives.Space
import com.anytypeio.anytype.core_ui.extensions.throttledClick
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.argOrNull
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.ext.daggerViewModel
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel
import com.anytypeio.anytype.feature_discussions.ui.DiscussionScreenWrapper
import com.anytypeio.anytype.other.DefaultDeepLinkResolver
import com.anytypeio.anytype.presentation.home.Command
@ -40,9 +52,9 @@ import com.anytypeio.anytype.ui.multiplayer.ShareSpaceFragment
import com.anytypeio.anytype.ui.objects.creation.ObjectTypeSelectionFragment
import com.anytypeio.anytype.ui.objects.creation.WidgetObjectTypeFragment
import com.anytypeio.anytype.ui.objects.creation.WidgetSourceTypeFragment
import com.anytypeio.anytype.ui.objects.types.pickers.ObjectTypeSelectionListener
import com.anytypeio.anytype.ui.objects.types.pickers.WidgetObjectTypeListener
import com.anytypeio.anytype.ui.objects.types.pickers.WidgetSourceTypeListener
import com.anytypeio.anytype.ui.objects.types.pickers.ObjectTypeSelectionListener
import com.anytypeio.anytype.ui.payments.MembershipFragment
import com.anytypeio.anytype.ui.settings.space.SpaceSettingsFragment
import com.anytypeio.anytype.ui.settings.typography
@ -59,8 +71,10 @@ class HomeScreenFragment : BaseComposeFragment(),
private val deepLink: String? get() = argOrNull(DEEP_LINK_KEY)
private val space: Id get() = arg<Id>(SPACE_ID_KEY)
private var isMnemonicReminderDialogNeeded: Boolean
get() = argOrNull<Boolean>(SHOW_MNEMONIC_KEY) ?: false
get() = argOrNull<Boolean>(SHOW_MNEMONIC_KEY) == true
set(value) { arguments?.putBoolean(SHOW_MNEMONIC_KEY, value) }
@ -86,42 +100,88 @@ class HomeScreenFragment : BaseComposeFragment(),
surface = colorResource(id = R.color.background_secondary)
)
) {
HomeScreen(
widgets = vm.views.collectAsState().value,
mode = vm.mode.collectAsState().value,
onExpand = { path -> vm.onExpand(path) },
onCreateWidget = vm::onCreateWidgetClicked,
onEditWidgets = vm::onEditWidgets,
onExitEditMode = vm::onExitEditMode,
onWidgetMenuAction = { widget: Id, action: DropDownMenuAction ->
vm.onDropDownMenuAction(widget, action)
},
onWidgetObjectClicked = vm::onWidgetObjectClicked,
onWidgetSourceClicked = vm::onWidgetSourceClicked,
onChangeWidgetView = vm::onChangeCurrentWidgetView,
onToggleExpandedWidgetState = vm::onToggleCollapsedWidgetState,
onSearchClicked = vm::onSearchIconClicked,
onCreateNewObjectClicked = throttledClick(
onClick = { vm.onCreateNewObjectClicked() }
),
onCreateNewObjectLongClicked = throttledClick(
onClick = { vm.onCreateNewObjectLongClicked() }
),
onBackClicked = throttledClick(
onClick = vm::onBackClicked
),
onSpaceWidgetClicked = throttledClick(
onClick = vm::onSpaceSettingsClicked
),
onBundledWidgetClicked = vm::onBundledWidgetClicked,
onMove = vm::onMove,
onObjectCheckboxClicked = vm::onObjectCheckboxClicked,
onSpaceShareIconClicked = vm::onSpaceShareIconClicked,
onSeeAllObjectsClicked = vm::onSeeAllObjectsClicked,
onCreateObjectInsideWidget = vm::onCreateObjectInsideWidget,
onCreateDataViewObject = vm::onCreateDataViewObject,
onBackLongClicked = vm::onBackLongClicked
)
val focus = LocalFocusManager.current
val component = componentManager().spaceLevelChatComponent
val spaceLevelChatViewModel = daggerViewModel {
component.get(
key = space,
param = DiscussionViewModel.Params.SpaceLevelChat(
space = Space(space)
)
).getViewModel()
}
val pagerState = rememberPagerState { 2 }
val coroutineScope = rememberCoroutineScope()
Box(
Modifier.fillMaxSize()
) {
HomeScreenToolbar(
onWidgetTabClicked = {
coroutineScope.launch {
pagerState.animateScrollToPage(0)
}
},
onChatTabClicked = {
coroutineScope.launch {
pagerState.animateScrollToPage(1)
}
}
)
HorizontalPager(
modifier = Modifier.padding(top = 64.dp),
state = pagerState,
userScrollEnabled = false
) { page ->
if (page == 0) {
focus.clearFocus(force = true)
HomeScreen(
modifier = Modifier,
widgets = vm.views.collectAsState().value,
mode = vm.mode.collectAsState().value,
onExpand = { path -> vm.onExpand(path) },
onCreateWidget = vm::onCreateWidgetClicked,
onEditWidgets = vm::onEditWidgets,
onExitEditMode = vm::onExitEditMode,
onWidgetMenuAction = { widget: Id, action: DropDownMenuAction ->
vm.onDropDownMenuAction(widget, action)
},
onWidgetObjectClicked = vm::onWidgetObjectClicked,
onWidgetSourceClicked = vm::onWidgetSourceClicked,
onChangeWidgetView = vm::onChangeCurrentWidgetView,
onToggleExpandedWidgetState = vm::onToggleCollapsedWidgetState,
onSearchClicked = vm::onSearchIconClicked,
onCreateNewObjectClicked = throttledClick(
onClick = { vm.onCreateNewObjectClicked() }
),
onCreateNewObjectLongClicked = throttledClick(
onClick = { vm.onCreateNewObjectLongClicked() }
),
onBackClicked = throttledClick(
onClick = vm::onBackClicked
),
onSpaceWidgetClicked = throttledClick(
onClick = vm::onSpaceSettingsClicked
),
onBundledWidgetClicked = vm::onBundledWidgetClicked,
onMove = vm::onMove,
onObjectCheckboxClicked = vm::onObjectCheckboxClicked,
onSpaceShareIconClicked = vm::onSpaceShareIconClicked,
onSeeAllObjectsClicked = vm::onSeeAllObjectsClicked,
onCreateObjectInsideWidget = vm::onCreateObjectInsideWidget,
onCreateDataViewObject = vm::onCreateDataViewObject,
onBackLongClicked = vm::onBackLongClicked
)
} else {
DiscussionScreenWrapper(
isSpaceLevelChat = true,
vm = spaceLevelChatViewModel,
onAttachClicked = {
// TODO
}
)
}
}
}
}
}
}
@ -377,17 +437,28 @@ class HomeScreenFragment : BaseComposeFragment(),
override fun injectDependencies() {
componentManager().homeScreenComponent.get().inject(this)
}
override fun releaseDependencies() {
componentManager().homeScreenComponent.release()
}
override fun onApplyWindowRootInsets(view: View) {
super.onApplyWindowRootInsets(view)
}
companion object {
const val SHOW_MNEMONIC_KEY = "arg.home-screen.show-mnemonic"
const val DEEP_LINK_KEY = "arg.home-screen.deep-link"
fun args(deeplink: String?) : Bundle = bundleOf(
DEEP_LINK_KEY to deeplink
const val SPACE_ID_KEY = "arg.home-screen.space-id"
fun args(
space: Id,
deeplink: String?
) : Bundle = bundleOf(
DEEP_LINK_KEY to deeplink,
SPACE_ID_KEY to space
)
}
}

View file

@ -0,0 +1,64 @@
package com.anytypeio.anytype.ui.home
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.feature_discussions.R
@Composable
fun HomeScreenToolbar(
onWidgetTabClicked: () -> Unit,
onChatTabClicked: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(64.dp)
.padding(horizontal = 20.dp)
) {
Image(
painter = painterResource(id = R.drawable.ic_home_toolbar_widgets),
modifier = Modifier
.size(32.dp)
.align(Alignment.CenterStart)
.noRippleClickable {
onWidgetTabClicked()
},
contentDescription = "Widgets button"
)
Image(
painter = painterResource(id = R.drawable.ic_home_toolbar_chat),
modifier = Modifier
.size(32.dp)
.align(Alignment.CenterEnd)
.noRippleClickable {
onChatTabClicked()
},
contentDescription = "Chats button"
)
}
}
@DefaultPreviews
@Composable
fun HomeScreenToolbarPreview() {
HomeScreenToolbar(
onWidgetTabClicked = {},
onChatTabClicked = {}
)
}

View file

@ -20,6 +20,7 @@ import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.spaces.Command
import com.anytypeio.anytype.presentation.spaces.SelectSpaceViewModel
import com.anytypeio.anytype.ui.home.HomeScreenFragment
import com.anytypeio.anytype.ui.settings.typography
import javax.inject.Inject
@ -84,7 +85,13 @@ class SelectSpaceFragment : BaseBottomSheetComposeFragment() {
is Command.SwitchToNewSpace -> {
runCatching {
findNavController().popBackStack(R.id.vaultScreen, false)
findNavController().navigate(R.id.actionOpenSpaceFromVault)
findNavController().navigate(
R.id.actionOpenSpaceFromVault,
HomeScreenFragment.args(
space = command.space.id,
deeplink = null
)
)
}
}
}

View file

@ -109,7 +109,10 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>(R.layout.fragment_spl
)
findNavController().navigate(
R.id.actionOpenSpaceFromVault,
args = HomeScreenFragment.args(deeplink = command.deeplink)
args = HomeScreenFragment.args(
deeplink = command.deeplink,
space = command.space
)
)
}.onFailure {
Timber.e(it, "Error while navigating to widgets from splash")
@ -119,7 +122,7 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>(R.layout.fragment_spl
try {
findNavController().navigate(
resId = R.id.actionOpenVaultFromSplash,
args = HomeScreenFragment.args(deeplink = command.deeplink)
args = VaultFragment.args(deeplink = command.deeplink)
)
} catch (e: Exception) {
Timber.e(e, "Error while opening dashboard from splash screen")
@ -129,7 +132,13 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>(R.layout.fragment_spl
is SplashViewModel.Command.NavigateToObject -> {
runCatching {
findNavController().navigate(R.id.actionOpenVaultFromSplash)
findNavController().navigate(R.id.actionOpenSpaceFromVault)
findNavController().navigate(
R.id.actionOpenSpaceFromVault,
args = HomeScreenFragment.args(
space = command.space,
deeplink = null
)
)
findNavController().navigate(
resId = R.id.objectNavigation,
args = EditorFragment.args(
@ -144,7 +153,13 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>(R.layout.fragment_spl
is SplashViewModel.Command.NavigateToObjectSet -> {
runCatching {
findNavController().navigate(R.id.actionOpenVaultFromSplash)
findNavController().navigate(R.id.actionOpenSpaceFromVault)
findNavController().navigate(
R.id.actionOpenSpaceFromVault,
args = HomeScreenFragment.args(
space = command.space,
deeplink = null
)
)
findNavController().navigate(
resId = R.id.dataViewNavigation,
args = ObjectSetFragment.args(

View file

@ -27,6 +27,7 @@ import com.anytypeio.anytype.presentation.vault.VaultViewModel.Navigation
import com.anytypeio.anytype.presentation.vault.VaultViewModel.Command
import com.anytypeio.anytype.ui.base.navigation
import com.anytypeio.anytype.ui.gallery.GalleryInstallationFragment
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.ProfileSettingsFragment
@ -72,7 +73,13 @@ class VaultFragment : BaseComposeFragment() {
when (command) {
is Command.EnterSpaceHomeScreen -> {
runCatching {
findNavController().navigate(R.id.actionOpenSpaceFromVault)
findNavController().navigate(
R.id.actionOpenSpaceFromVault,
HomeScreenFragment.args(
space = command.space.id,
deeplink = null
)
)
}.onFailure {
Timber.e(it, "Error while opening space from vault")
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M9.841,26C8.094,26 8.881,25.364 9.259,24.859C9.628,24.354 9.598,24.296 10.141,23.367C10.219,23.216 10.17,23.064 10.025,22.983C6.911,21.247 5,18.409 5,15.178C5,10.099 9.889,6 16,6C22.111,6 27,10.099 27,15.178C27,20.207 22.363,24.347 15.37,24.347C15.243,24.347 15.117,24.337 14.991,24.326C14.855,24.326 14.72,24.377 14.555,24.498C12.906,25.474 11.005,26 9.841,26Z"
android:fillColor="@color/glyph_button" />
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M5,8C5,6.895 5.895,6 7,6H25C26.105,6 27,6.895 27,8C27,9.105 26.105,10 25,10H7C5.895,10 5,9.105 5,8ZM5,24C5,22.895 5.895,22 7,22H25C26.105,22 27,22.895 27,24C27,25.105 26.105,26 25,26H7C5.895,26 5,25.105 5,24ZM7,12C5.895,12 5,12.895 5,14V18C5,19.105 5.895,20 7,20H25C26.105,20 27,19.105 27,18V14C27,12.895 26.105,12 25,12H7Z"
android:fillColor="@color/glyph_button"
android:fillType="evenOdd"/>
</vector>

View file

@ -17,4 +17,6 @@ interface FeatureToggles {
val enableDiscussionDemo: Boolean
val isSpaceLevelChatEnabled: Boolean
val isNewSpaceHomeEnabled: Boolean
}

View file

@ -6,6 +6,8 @@ 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.chats.Chat
import com.anytypeio.anytype.core_models.primitives.Space
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.base.onFailure
@ -18,11 +20,13 @@ import com.anytypeio.anytype.domain.chats.ToggleChatMessageReaction
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer.Store
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.`object`.OpenObject
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
import com.anytypeio.anytype.presentation.search.GlobalSearchItemView
import javax.inject.Inject
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
@ -30,8 +34,8 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
class DiscussionViewModel(
private val params: DefaultParams,
class DiscussionViewModel @Inject constructor(
private val vmParams: Params,
private val setObjectDetails: SetObjectDetails,
private val openObject: OpenObject,
private val chatContainer: ChatContainer,
@ -41,7 +45,8 @@ class DiscussionViewModel(
private val toggleChatMessageReaction: ToggleChatMessageReaction,
private val members: ActiveSpaceMemberSubscriptionContainer,
private val getAccount: GetAccount,
private val urlBuilder: UrlBuilder
private val urlBuilder: UrlBuilder,
private val spaceViews: SpaceViewSubscriptionContainer
) : BaseViewModel() {
val name = MutableStateFlow<String?>(null)
@ -51,43 +56,65 @@ class DiscussionViewModel(
val navigation = MutableSharedFlow<OpenObjectNavigation>()
val chatBoxMode = MutableStateFlow<ChatBoxMode>(ChatBoxMode.Default)
var chat: Id = ""
init {
viewModelScope.launch {
val account = requireNotNull(getAccount.async(Unit).getOrNull())
openObject.async(
OpenObject.Params(
spaceId = params.space,
obj = params.ctx,
saveAsLastOpened = false
)
).fold(
onSuccess = { obj ->
val root = ObjectWrapper.Basic(obj.details[params.ctx].orEmpty())
name.value = root.name
proceedWithObservingChatMessages(
account = account.id
when (vmParams) {
is Params.Default -> {
chat = vmParams.ctx
openObject.async(
OpenObject.Params(
spaceId = vmParams.space,
obj = vmParams.ctx,
saveAsLastOpened = false
)
).fold(
onSuccess = { obj ->
val root = ObjectWrapper.Basic(obj.details[vmParams.ctx].orEmpty())
name.value = root.name
proceedWithObservingChatMessages(
account = account.id,
chat = vmParams.ctx
)
},
onFailure = {
Timber.e(it, "Error while opening chat object")
}
)
},
onFailure = {
Timber.e(it, "Error while opening chat object")
}
)
is Params.SpaceLevelChat -> {
val targetSpaceView = spaceViews.get(vmParams.space)
val spaceLevelChat = targetSpaceView?.getValue<Id>(Relations.CHAT_ID)
if (spaceLevelChat != null) {
chat = spaceLevelChat
proceedWithObservingChatMessages(
account = account.id,
chat = spaceLevelChat
)
}
}
}
}
}
private suspend fun proceedWithObservingChatMessages(
account: Id
account: Id,
chat: Id
) {
chatContainer
.watch(params.ctx)
.watch(chat = chat)
.onEach { Timber.d("Got new update: $it") }
.collect {
messages.value = it.map { msg ->
val member = members.get().let { type ->
when(type) {
when (type) {
is Store.Data -> type.members.find { member ->
member.identity == msg.creator
}
is Store.Empty -> null
}
}
@ -98,7 +125,7 @@ class DiscussionViewModel(
author = member?.name ?: msg.creator.takeLast(5),
isUserAuthor = msg.creator == account,
isEdited = msg.modifiedAt > msg.createdAt,
reactions = msg.reactions.map{ (emoji, ids) ->
reactions = msg.reactions.map { (emoji, ids) ->
DiscussionView.Message.Reaction(
emoji = emoji,
count = ids.size,
@ -121,12 +148,12 @@ class DiscussionViewModel(
fun onMessageSent(msg: String) {
Timber.d("DROID-2635 OnMessageSent: $msg")
viewModelScope.launch {
when(val mode = chatBoxMode.value) {
when (val mode = chatBoxMode.value) {
is ChatBoxMode.Default -> {
// TODO consider moving this use-case inside chat container
addChatMessage.async(
params = Command.ChatCommand.AddMessage(
chat = params.ctx,
chat = chat,
message = Chat.Message.new(
text = msg,
attachments = attachments.value.map { a ->
@ -146,10 +173,11 @@ class DiscussionViewModel(
Timber.e(it, "Error while adding message")
}
}
is ChatBoxMode.EditMessage -> {
editChatMessage.async(
params = Command.ChatCommand.EditMessage(
chat = params.ctx,
chat = chat,
message = Chat.Message.updated(
id = mode.msg,
text = msg
@ -181,7 +209,7 @@ class DiscussionViewModel(
name.value = input
setObjectDetails.async(
params = SetObjectDetails.Params(
ctx = params.ctx,
ctx = chat,
details = mapOf(
Relations.NAME to input
)
@ -209,7 +237,7 @@ class DiscussionViewModel(
if (message != null) {
toggleChatMessageReaction.async(
Command.ChatCommand.ToggleMessageReaction(
chat = params.ctx,
chat = chat,
msg = msg,
emoji = reaction
)
@ -227,7 +255,7 @@ class DiscussionViewModel(
viewModelScope.launch {
deleteChatMessage.async(
Command.ChatCommand.DeleteMessage(
chat = params.ctx,
chat = chat,
msg = msg.id
)
).onFailure {
@ -242,7 +270,7 @@ class DiscussionViewModel(
navigation.emit(
OpenObjectNavigation.OpenEditor(
target = attachment.target,
space = params.space.id
space = vmParams.space.id
)
)
}
@ -255,8 +283,8 @@ class DiscussionViewModel(
}
sealed class UXCommand {
data object JumpToBottom: UXCommand()
data class SetChatBoxInput(val input: String): UXCommand()
data object JumpToBottom : UXCommand()
data class SetChatBoxInput(val input: String) : UXCommand()
}
sealed class ChatBoxMode {
@ -264,6 +292,20 @@ class DiscussionViewModel(
data class EditMessage(val msg: Id) : ChatBoxMode()
}
sealed class Params {
abstract val space: Space
data class Default(
val ctx: Id,
override val space: Space
) : Params()
data class SpaceLevelChat(
override val space: Space
) : Params()
}
companion object {
/**
* Delay before jump-to-bottom after adding new message to the chat.

View file

@ -10,13 +10,14 @@ import com.anytypeio.anytype.domain.chats.EditChatMessage
import com.anytypeio.anytype.domain.chats.ToggleChatMessageReaction
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer
import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer
import com.anytypeio.anytype.domain.`object`.OpenObject
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.presentation.common.BaseViewModel
import javax.inject.Inject
class DiscussionViewModelFactory @Inject constructor(
private val params: BaseViewModel.DefaultParams,
private val params: DiscussionViewModel.Params,
private val setObjectDetails: SetObjectDetails,
private val openObject: OpenObject,
private val chatContainer: ChatContainer,
@ -26,11 +27,12 @@ class DiscussionViewModelFactory @Inject constructor(
private val toggleChatMessageReaction: ToggleChatMessageReaction,
private val members: ActiveSpaceMemberSubscriptionContainer,
private val getAccount: GetAccount,
private val urlBuilder: UrlBuilder
private val urlBuilder: UrlBuilder,
private val spaceViews: SpaceViewSubscriptionContainer
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = DiscussionViewModel(
params = params,
vmParams = params,
setObjectDetails = setObjectDetails,
openObject = openObject,
chatContainer = chatContainer,
@ -40,6 +42,7 @@ class DiscussionViewModelFactory @Inject constructor(
getAccount = getAccount,
deleteChatMessage = deleteChatMessage,
urlBuilder = urlBuilder,
editChatMessage = editChatMessage
editChatMessage = editChatMessage,
spaceViews = spaceViews
) as T
}

View file

@ -80,7 +80,8 @@ fun DiscussionScreenPreview() {
onDeleteMessage = {},
onAttachmentClicked = {},
onEditMessage = {},
onExitEditMessageMode = {}
onExitEditMessageMode = {},
isSpaceLevelChat = true
)
}

View file

@ -22,7 +22,6 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -122,6 +121,7 @@ import kotlinx.coroutines.launch
@Composable
fun DiscussionScreenWrapper(
isSpaceLevelChat: Boolean = false,
vm: DiscussionViewModel,
// TODO move to view model
onAttachClicked: () -> Unit
@ -136,13 +136,20 @@ fun DiscussionScreenWrapper(
Box(
modifier = Modifier
.fillMaxSize()
.background(
color = colorResource(id = R.color.background_primary)
.then(
if (!isSpaceLevelChat) {
Modifier.background(
color = colorResource(id = R.color.background_primary)
)
} else {
Modifier
}
)
) {
val clipboard = LocalClipboardManager.current
val lazyListState = rememberLazyListState()
DiscussionScreen(
isSpaceLevelChat = isSpaceLevelChat,
title = vm.name.collectAsState().value,
messages = vm.messages.collectAsState().value,
attachments = vm.attachments.collectAsState().value,
@ -185,6 +192,7 @@ fun DiscussionScreenWrapper(
*/
@Composable
fun DiscussionScreen(
isSpaceLevelChat: Boolean,
isInEditMessageMode: Boolean = false,
lazyListState: LazyListState,
title: String?,
@ -221,14 +229,22 @@ fun DiscussionScreen(
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
.then(
if (isSpaceLevelChat)
Modifier
else
Modifier.windowInsetsPadding(WindowInsets.systemBars)
)
) {
TopDiscussionToolbar(
title = title,
isHeaderVisible = isHeaderVisible
)
if (!isSpaceLevelChat) {
TopDiscussionToolbar(
title = title,
isHeaderVisible = isHeaderVisible
)
}
Box(modifier = Modifier.weight(1.0f)) {
Messages(
isSpaceLevelChat = isSpaceLevelChat,
modifier = Modifier.fillMaxSize(),
messages = messages,
scrollState = lazyListState,
@ -408,12 +424,6 @@ private fun ChatBox(
Row(
modifier = Modifier
.then(
if (isTitleFocused)
Modifier
else
Modifier.imePadding()
)
.fillMaxWidth()
.defaultMinSize(minHeight = 56.dp)
) {
@ -598,6 +608,7 @@ private fun DefaultHintDecorationBox(
@Composable
fun Messages(
isSpaceLevelChat: Boolean = true,
title: String?,
onTitleChanged: (String) -> Unit,
modifier: Modifier = Modifier,
@ -704,21 +715,23 @@ fun Messages(
}
}
}
item(key = HEADER_KEY) {
Column {
DiscussionTitle(
title = title,
onTitleChanged = onTitleChanged,
onFocusChanged = onTitleFocusChanged
)
Text(
style = Relations2,
text = stringResource(R.string.chat),
color = colorResource(id = R.color.text_secondary),
modifier = Modifier.padding(
start = 20.dp
if (!isSpaceLevelChat) {
item(key = HEADER_KEY) {
Column {
DiscussionTitle(
title = title,
onTitleChanged = onTitleChanged,
onFocusChanged = onTitleFocusChanged
)
)
Text(
style = Relations2,
text = stringResource(R.string.chat),
color = colorResource(id = R.color.text_secondary),
modifier = Modifier.padding(
start = 20.dp
)
)
}
}
}
}
@ -763,6 +776,9 @@ private fun ChatUserAvatar(
}
}
val defaultBubbleColor = Color(0x99FFFFFF)
val userMessageBubbleColor = Color(0x66000000)
@Composable
fun Bubble(
modifier: Modifier = Modifier,
@ -785,9 +801,9 @@ fun Bubble(
.fillMaxWidth()
.background(
color = if (isUserAuthor)
colorResource(id = R.color.palette_very_light_lime)
userMessageBubbleColor
else
colorResource(id = R.color.palette_very_light_grey),
defaultBubbleColor,
shape = RoundedCornerShape(24.dp)
)
.clip(RoundedCornerShape(24.dp))
@ -805,7 +821,10 @@ fun Bubble(
Text(
text = name,
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary),
color = if (isUserAuthor)
colorResource(id = R.color.text_white)
else
colorResource(id = R.color.text_primary),
maxLines = 1,
modifier = Modifier.weight(1f)
)
@ -814,7 +833,10 @@ fun Bubble(
TIME_H24
),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary),
color = if (isUserAuthor)
colorResource(id = R.color.text_white)
else
colorResource(id = R.color.text_secondary),
maxLines = 1
)
}
@ -829,7 +851,12 @@ fun Bubble(
text = buildAnnotatedString {
append(msg)
withStyle(
style = SpanStyle(color = colorResource(id = R.color.text_secondary))
style = SpanStyle(
color = if (isUserAuthor)
colorResource(id = R.color.text_white)
else
colorResource(id = R.color.text_primary),
)
) {
append(
" (${stringResource(R.string.chats_message_edited)})"
@ -849,7 +876,10 @@ fun Bubble(
),
text = msg,
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
color = if (isUserAuthor)
colorResource(id = R.color.text_white)
else
colorResource(id = R.color.text_primary),
)
}
attachments.forEach { attachment ->

View file

@ -12,6 +12,7 @@ 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.multiplayer.SpaceAccessType
import com.anytypeio.anytype.core_models.primitives.Space
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.restrictions.SpaceStatus
import com.anytypeio.anytype.core_utils.ext.allUniqueBy
@ -141,14 +142,15 @@ class SelectSpaceViewModel(
Timber.d("Setting space: $view")
if (!view.isSelected) {
analytics.sendEvent(eventName = EventsDictionary.switchSpace)
spaceManager.set(view.space).fold(
val space = SpaceId(view.space)
spaceManager.set(space.id).fold(
onSuccess = {
saveCurrentSpace.async(SaveCurrentSpace.Params(SpaceId(view.space))).fold(
saveCurrentSpace.async(SaveCurrentSpace.Params(space)).fold(
onFailure = {
Timber.e(it, "Error while saving current space in user settings")
},
onSuccess = {
commands.emit(Command.SwitchToNewSpace)
commands.emit(Command.SwitchToNewSpace(space))
}
)
},
@ -239,5 +241,5 @@ sealed class SelectSpaceView {
sealed class Command {
data object CreateSpace : Command()
data object Dismiss : Command()
data object SwitchToNewSpace: Command()
data class SwitchToNewSpace(val space: Space): Command()
}

View file

@ -274,7 +274,10 @@ class SplashViewModel(
private suspend fun proceedWithVaultNavigation(deeplink: String? = null) {
val space = getLastOpenedSpace.async(Unit).getOrNull()
if (space != null) {
commands.emit(Command.NavigateToWidgets(deeplink))
commands.emit(Command.NavigateToWidgets(
space = space.id,
deeplink = deeplink
))
} else {
commands.emit(Command.NavigateToVault(deeplink))
}
@ -305,7 +308,7 @@ class SplashViewModel(
}
sealed class Command {
data class NavigateToWidgets(val deeplink: String? = null) : Command()
data class NavigateToWidgets(val space: Id, val deeplink: String? = null) : Command()
data class NavigateToVault(val deeplink: String? = null) : Command()
data object NavigateToAuthStart : Command()
data object NavigateToMigration: Command()

View file

@ -11,6 +11,7 @@ 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.primitives.Space
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.restrictions.SpaceStatus
import com.anytypeio.anytype.domain.base.fold
@ -240,7 +241,11 @@ class VaultViewModel(
Timber.e(it, "Error while saving current space on vault screen")
},
onSuccess = {
commands.emit(Command.EnterSpaceHomeScreen)
commands.emit(
Command.EnterSpaceHomeScreen(
space = Space(targetSpace)
)
)
}
)
}
@ -321,7 +326,7 @@ class VaultViewModel(
)
sealed class Command {
data object EnterSpaceHomeScreen: Command()
data class EnterSpaceHomeScreen(val space: Space): Command()
data object CreateNewSpace: Command()
data object OpenProfileSettings: Command()
data object ShowIntroduceVault : Command()