mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-2635 Chats | Enhancement | Foundation for chats (#1420)
Co-authored-by: Konstantin Ivanov <54908981+konstantiniiv@users.noreply.github.com>
This commit is contained in:
parent
b87a454bc7
commit
c149de8bce
69 changed files with 3222 additions and 153 deletions
|
@ -24,5 +24,5 @@ class DefaultFeatureToggles @Inject constructor(
|
|||
|
||||
override val isConciseLogging: Boolean = true
|
||||
|
||||
override val enableDiscussionDemo: Boolean = false
|
||||
override val enableDiscussionDemo: Boolean = true
|
||||
}
|
|
@ -50,6 +50,7 @@ import com.anytypeio.anytype.di.feature.ViewerFilterModule
|
|||
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.gallery.DaggerGalleryInstallationComponent
|
||||
import com.anytypeio.anytype.di.feature.home.DaggerHomeScreenComponent
|
||||
import com.anytypeio.anytype.di.feature.library.DaggerLibraryComponent
|
||||
|
@ -102,6 +103,7 @@ import com.anytypeio.anytype.di.feature.widgets.SelectWidgetTypeModule
|
|||
import com.anytypeio.anytype.di.main.MainComponent
|
||||
import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel
|
||||
import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel
|
||||
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
||||
import com.anytypeio.anytype.presentation.editor.EditorViewModel
|
||||
import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel
|
||||
import com.anytypeio.anytype.presentation.library.LibraryViewModel
|
||||
|
@ -1066,6 +1068,14 @@ class ComponentManager(
|
|||
.build()
|
||||
}
|
||||
|
||||
val discussionComponent = ComponentMapWithParam { params: BaseViewModel.DefaultParams ->
|
||||
DaggerDiscussionComponent
|
||||
.builder()
|
||||
.withDependencies(findComponentDependencies())
|
||||
.withParams(params)
|
||||
.build()
|
||||
}
|
||||
|
||||
val vaultComponent = Component {
|
||||
DaggerVaultComponent
|
||||
.factory()
|
||||
|
|
|
@ -0,0 +1,181 @@
|
|||
package com.anytypeio.anytype.di.feature.discussions
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import com.anytypeio.anytype.core_utils.ext.arg
|
||||
import com.anytypeio.anytype.core_utils.ext.toast
|
||||
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.presentation.DiscussionViewModelFactory
|
||||
import com.anytypeio.anytype.feature_discussions.ui.DiscussionScreenWrapper
|
||||
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
||||
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
|
||||
import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel
|
||||
import com.anytypeio.anytype.ui.editor.EditorFragment
|
||||
import com.anytypeio.anytype.ui.search.GlobalSearchScreen
|
||||
import com.anytypeio.anytype.ui.settings.typography
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
class DiscussionFragment : BaseComposeFragment() {
|
||||
|
||||
@Inject
|
||||
lateinit var factory: DiscussionViewModelFactory
|
||||
|
||||
private val vm by viewModels<DiscussionViewModel> { factory }
|
||||
|
||||
private val ctx get() = arg<Id>(CTX_KEY)
|
||||
private val space get() = arg<Id>(SPACE_KEY)
|
||||
|
||||
// Rendering
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
return ComposeView(requireContext()).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
MaterialTheme(typography = typography) {
|
||||
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
var showBottomSheet by remember { mutableStateOf(false) }
|
||||
|
||||
DiscussionScreenWrapper(
|
||||
vm = vm,
|
||||
onAttachClicked = {
|
||||
showBottomSheet = true
|
||||
}
|
||||
)
|
||||
|
||||
if (showBottomSheet) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = {
|
||||
showBottomSheet = false
|
||||
},
|
||||
sheetState = sheetState,
|
||||
containerColor = colorResource(id = R.color.background_secondary),
|
||||
shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
|
||||
dragHandle = null
|
||||
) {
|
||||
val component = componentManager().globalSearchComponent
|
||||
val searchViewModel = daggerViewModel {
|
||||
component.get(
|
||||
params = GlobalSearchViewModel.VmParams(
|
||||
space = SpaceId(space)
|
||||
)
|
||||
).getViewModel()
|
||||
}
|
||||
GlobalSearchScreen(
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
state = searchViewModel.state
|
||||
.collectAsStateWithLifecycle()
|
||||
.value
|
||||
,
|
||||
onQueryChanged = searchViewModel::onQueryChanged,
|
||||
onObjectClicked = {
|
||||
vm.onAttachObject(it)
|
||||
showBottomSheet = false
|
||||
},
|
||||
onShowRelatedClicked = {
|
||||
// Do nothing.
|
||||
},
|
||||
onClearRelatedClicked = {
|
||||
|
||||
},
|
||||
focusOnStart = false
|
||||
)
|
||||
}
|
||||
} else {
|
||||
componentManager().globalSearchComponent.release()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
vm.navigation.collect { nav ->
|
||||
when(nav) {
|
||||
is OpenObjectNavigation.OpenEditor -> {
|
||||
runCatching {
|
||||
findNavController().navigate(
|
||||
R.id.objectNavigation,
|
||||
EditorFragment.args(
|
||||
ctx = nav.target,
|
||||
space = nav.space
|
||||
)
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.w("Error while opening editor from chat.")
|
||||
}
|
||||
}
|
||||
else -> toast("TODO")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DI
|
||||
|
||||
override fun injectDependencies() {
|
||||
componentManager()
|
||||
.discussionComponent
|
||||
.get(
|
||||
key = ctx,
|
||||
param = BaseViewModel.DefaultParams(
|
||||
ctx = ctx,
|
||||
space = SpaceId(space)
|
||||
)
|
||||
)
|
||||
.inject(this)
|
||||
}
|
||||
|
||||
override fun releaseDependencies() {
|
||||
componentManager().discussionComponent.release(ctx)
|
||||
}
|
||||
|
||||
override fun onApplyWindowRootInsets(view: View) {
|
||||
// Do not apply.
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CTX_KEY = "arg.discussion.ctx"
|
||||
private const val SPACE_KEY = "arg.discussion.space"
|
||||
fun args(
|
||||
space: Id,
|
||||
ctx: Id
|
||||
) = bundleOf(
|
||||
CTX_KEY to ctx,
|
||||
SPACE_KEY to space
|
||||
)
|
||||
}
|
||||
}
|
|
@ -3,14 +3,23 @@ package com.anytypeio.anytype.di.feature.discussions
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.anytypeio.anytype.analytics.base.Analytics
|
||||
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
|
||||
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
|
||||
import com.anytypeio.anytype.di.common.ComponentDependencies
|
||||
import com.anytypeio.anytype.di.feature.EditorSubComponent.Builder
|
||||
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.chats.ChatEventChannel
|
||||
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.UserPermissionProvider
|
||||
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModelFactory
|
||||
import com.anytypeio.anytype.middleware.EventProxy
|
||||
import com.anytypeio.anytype.presentation.common.BaseViewModel
|
||||
import dagger.Binds
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import dagger.Module
|
||||
|
||||
|
@ -23,10 +32,14 @@ import dagger.Module
|
|||
)
|
||||
@PerScreen
|
||||
interface DiscussionComponent {
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(dependencies: DiscussionComponentDependencies): DiscussionComponent
|
||||
@Component.Builder
|
||||
interface Builder {
|
||||
@BindsInstance
|
||||
fun withParams(params: BaseViewModel.DefaultParams): Builder
|
||||
fun withDependencies(dependencies: DiscussionComponentDependencies): Builder
|
||||
fun build(): DiscussionComponent
|
||||
}
|
||||
fun inject(fragment: DiscussionFragment)
|
||||
}
|
||||
|
||||
@Module
|
||||
|
@ -44,9 +57,15 @@ object DiscussionModule {
|
|||
|
||||
interface DiscussionComponentDependencies : ComponentDependencies {
|
||||
fun blockRepository(): BlockRepository
|
||||
fun authRepo(): AuthRepository
|
||||
fun appCoroutineDispatchers(): AppCoroutineDispatchers
|
||||
fun analytics(): Analytics
|
||||
fun urlBuilder(): UrlBuilder
|
||||
fun userPermissionProvider(): UserPermissionProvider
|
||||
fun eventProxy(): EventProxy
|
||||
fun featureToggles(): FeatureToggles
|
||||
fun userSettings(): UserSettingsRepository
|
||||
fun chatEventChannel(): ChatEventChannel
|
||||
fun logger(): Logger
|
||||
fun members(): ActiveSpaceMemberSubscriptionContainer
|
||||
}
|
|
@ -36,6 +36,7 @@ interface GlobalSearchComponent {
|
|||
}
|
||||
|
||||
fun inject(fragment: GlobalSearchFragment)
|
||||
fun getViewModel(): GlobalSearchViewModel
|
||||
}
|
||||
|
||||
@Module
|
||||
|
|
|
@ -3,6 +3,7 @@ package com.anytypeio.anytype.di.main
|
|||
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
|
||||
import com.anytypeio.anytype.data.auth.account.AccountStatusDataChannel
|
||||
import com.anytypeio.anytype.data.auth.account.AccountStatusRemoteChannel
|
||||
import com.anytypeio.anytype.data.auth.event.ChatEventRemoteChannel
|
||||
import com.anytypeio.anytype.data.auth.event.EventDataChannel
|
||||
import com.anytypeio.anytype.data.auth.event.EventRemoteChannel
|
||||
import com.anytypeio.anytype.data.auth.event.FileLimitsDataChannel
|
||||
|
@ -12,19 +13,21 @@ import com.anytypeio.anytype.data.auth.event.SubscriptionEventRemoteChannel
|
|||
import com.anytypeio.anytype.data.auth.status.SyncAndP2PStatusEventsStore
|
||||
import com.anytypeio.anytype.di.main.ConfigModule.DEFAULT_APP_COROUTINE_SCOPE
|
||||
import com.anytypeio.anytype.domain.account.AccountStatusChannel
|
||||
import com.anytypeio.anytype.domain.chats.ChatEventChannel
|
||||
import com.anytypeio.anytype.domain.event.interactor.EventChannel
|
||||
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
|
||||
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
|
||||
import com.anytypeio.anytype.middleware.EventProxy
|
||||
import com.anytypeio.anytype.middleware.interactor.AccountStatusMiddlewareChannel
|
||||
import com.anytypeio.anytype.middleware.interactor.EventHandler
|
||||
import com.anytypeio.anytype.middleware.interactor.EventHandlerChannel
|
||||
import com.anytypeio.anytype.middleware.interactor.EventHandlerChannelImpl
|
||||
import com.anytypeio.anytype.middleware.interactor.EventHandler
|
||||
import com.anytypeio.anytype.middleware.interactor.FileLimitsMiddlewareChannel
|
||||
import com.anytypeio.anytype.middleware.interactor.MiddlewareEventChannel
|
||||
import com.anytypeio.anytype.middleware.interactor.MiddlewareProtobufLogger
|
||||
import com.anytypeio.anytype.middleware.interactor.MiddlewareSubscriptionEventChannel
|
||||
import com.anytypeio.anytype.middleware.interactor.SyncAndP2PStatusEventsStoreImpl
|
||||
import com.anytypeio.anytype.middleware.interactor.events.ChatEventMiddlewareChannel
|
||||
import com.anytypeio.anytype.presentation.common.PayloadDelegator
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
|
@ -149,6 +152,35 @@ object EventModule {
|
|||
@Singleton
|
||||
fun provideDefaultEventChannel(): EventHandlerChannel = EventHandlerChannelImpl()
|
||||
|
||||
//region Chats
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideChatEventChannel(
|
||||
channel: ChatEventRemoteChannel.Default
|
||||
): ChatEventChannel = channel
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideChatEventDataChannel(
|
||||
remote: ChatEventRemoteChannel
|
||||
): ChatEventRemoteChannel.Default = ChatEventRemoteChannel.Default(
|
||||
channel = remote
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideChatEventMWChannel(
|
||||
proxy: EventProxy
|
||||
): ChatEventRemoteChannel = ChatEventMiddlewareChannel(
|
||||
proxy
|
||||
)
|
||||
|
||||
//endregion
|
||||
|
||||
@Module
|
||||
interface Bindings {
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.anytypeio.anytype.di.feature.ObjectTypeChangeSubComponent
|
|||
import com.anytypeio.anytype.di.feature.PersonalizationSettingsSubComponent
|
||||
import com.anytypeio.anytype.di.feature.SplashDependencies
|
||||
import com.anytypeio.anytype.di.feature.auth.DeletedAccountDependencies
|
||||
import com.anytypeio.anytype.di.feature.discussions.DiscussionComponentDependencies
|
||||
import com.anytypeio.anytype.di.feature.gallery.GalleryInstallationComponentDependencies
|
||||
import com.anytypeio.anytype.di.feature.home.HomeScreenDependencies
|
||||
import com.anytypeio.anytype.di.feature.library.LibraryDependencies
|
||||
|
@ -128,7 +129,8 @@ interface MainComponent :
|
|||
GlobalSearchDependencies,
|
||||
MembershipUpdateComponentDependencies,
|
||||
VaultComponentDependencies,
|
||||
AllContentDependencies
|
||||
AllContentDependencies,
|
||||
DiscussionComponentDependencies
|
||||
{
|
||||
|
||||
fun inject(app: AndroidApplication)
|
||||
|
@ -350,6 +352,11 @@ abstract class ComponentDependenciesModule {
|
|||
@ComponentDependenciesKey(MembershipUpdateComponentDependencies::class)
|
||||
abstract fun provideMembershipUpdateComponentDependencies(component: MainComponent): ComponentDependencies
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ComponentDependenciesKey(DiscussionComponentDependencies::class)
|
||||
abstract fun provideDiscussionComponentDependencies(component: MainComponent): ComponentDependencies
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ComponentDependenciesKey(VaultComponentDependencies::class)
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.navigation.navOptions
|
|||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.di.feature.discussions.DiscussionFragment
|
||||
import com.anytypeio.anytype.presentation.navigation.AppNavigation
|
||||
import com.anytypeio.anytype.presentation.widgets.collection.Subscription
|
||||
import com.anytypeio.anytype.ui.allcontent.AllContentFragment
|
||||
|
@ -22,7 +23,6 @@ import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.TYPE_
|
|||
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.TYPE_TEMPLATE_SELECT
|
||||
import com.anytypeio.anytype.ui.templates.TemplateSelectFragment
|
||||
import com.anytypeio.anytype.ui.types.create.CreateObjectTypeFragment
|
||||
import com.anytypeio.anytype.ui.types.create.TypeCreationScreen
|
||||
import com.anytypeio.anytype.ui.types.edit.TypeEditFragment
|
||||
import com.anytypeio.anytype.ui.widgets.collection.CollectionFragment
|
||||
import timber.log.Timber
|
||||
|
@ -39,6 +39,16 @@ class Navigator : AppNavigation {
|
|||
}
|
||||
}
|
||||
|
||||
override fun openChat(target: Id, space: Id) {
|
||||
navController?.navigate(
|
||||
R.id.chatScreen,
|
||||
DiscussionFragment.args(
|
||||
ctx = target,
|
||||
space = space
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun openDocument(target: Id, space: Id) {
|
||||
navController?.navigate(
|
||||
R.id.objectNavigation,
|
||||
|
@ -49,6 +59,16 @@ class Navigator : AppNavigation {
|
|||
)
|
||||
}
|
||||
|
||||
override fun openDiscussion(target: Id, space: Id) {
|
||||
navController?.navigate(
|
||||
R.id.chatScreen,
|
||||
DiscussionFragment.args(
|
||||
ctx = target,
|
||||
space = space
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun openModalTemplateSelect(
|
||||
template: Id,
|
||||
templateTypeId: Id,
|
||||
|
|
|
@ -143,6 +143,16 @@ class AllContentFragment : BaseComposeFragment() {
|
|||
Timber.e(it, "Failed to open document from all content")
|
||||
}
|
||||
}
|
||||
is AllContentViewModel.Command.OpenChat -> {
|
||||
runCatching {
|
||||
navigation().openChat(
|
||||
target = command.target,
|
||||
space = command.space
|
||||
)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Failed to open a chat from all content")
|
||||
}
|
||||
}
|
||||
|
||||
is AllContentViewModel.Command.NavigateToSetOrCollection -> {
|
||||
runCatching {
|
||||
|
|
|
@ -17,6 +17,7 @@ class NavigationRouter(
|
|||
target = command.target,
|
||||
space = command.space
|
||||
)
|
||||
|
||||
is AppNavigation.Command.OpenModalTemplateSelect -> navigation.openModalTemplateSelect(
|
||||
template = command.template,
|
||||
templateTypeId = command.templateTypeId,
|
||||
|
@ -28,6 +29,10 @@ class NavigationRouter(
|
|||
space = command.space,
|
||||
isPopUpToDashboard = command.isPopUpToDashboard
|
||||
)
|
||||
is AppNavigation.Command.OpenChat -> navigation.openChat(
|
||||
target = command.target,
|
||||
space = command.space
|
||||
)
|
||||
is AppNavigation.Command.LaunchObjectSet -> navigation.launchObjectSet(
|
||||
target = command.target,
|
||||
space = command.space
|
||||
|
|
|
@ -55,12 +55,13 @@ class HomeScreenFragment : BaseComposeFragment() {
|
|||
get() = argOrNull<Boolean>(SHOW_MNEMONIC_KEY) ?: false
|
||||
set(value) { arguments?.putBoolean(SHOW_MNEMONIC_KEY, value) }
|
||||
|
||||
@Inject
|
||||
lateinit var factory: HomeScreenViewModel.Factory
|
||||
|
||||
@Inject
|
||||
lateinit var featureToggles: FeatureToggles
|
||||
|
||||
@Inject
|
||||
lateinit var factory: HomeScreenViewModel.Factory
|
||||
|
||||
private val vm by viewModels<HomeScreenViewModel> { factory }
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -117,10 +118,6 @@ class HomeScreenFragment : BaseComposeFragment() {
|
|||
onCreateDataViewObject = vm::onCreateDataViewObject,
|
||||
onBackLongClicked = vm::onBackLongClicked
|
||||
)
|
||||
|
||||
if (featureToggles.enableDiscussionDemo) {
|
||||
DiscussionScreenWrapper()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -347,6 +344,12 @@ class HomeScreenFragment : BaseComposeFragment() {
|
|||
view = destination.view
|
||||
)
|
||||
}
|
||||
is Navigation.OpenDiscussion -> runCatching {
|
||||
navigation().openDiscussion(
|
||||
target = destination.ctx,
|
||||
space = destination.space
|
||||
)
|
||||
}
|
||||
is Navigation.ExpandWidget -> runCatching {
|
||||
navigation().launchCollections(
|
||||
subscription = destination.subscription,
|
||||
|
@ -387,12 +390,6 @@ class HomeScreenFragment : BaseComposeFragment() {
|
|||
componentManager().homeScreenComponent.release()
|
||||
}
|
||||
|
||||
override fun onApplyWindowRootInsets(view: View) {
|
||||
if (!featureToggles.enableDiscussionDemo) {
|
||||
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"
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.anytypeio.anytype.core_utils.ext.toast
|
|||
import com.anytypeio.anytype.core_utils.insets.EDGE_TO_EDGE_MIN_SDK
|
||||
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.di.feature.discussions.DiscussionFragment
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.theme.GetTheme
|
||||
import com.anytypeio.anytype.middleware.discovery.MDNSProvider
|
||||
|
@ -222,6 +223,9 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
|
|||
Timber.e(it, "Error while editor navigation")
|
||||
}
|
||||
}
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
toast("Cannot open chat from here")
|
||||
}
|
||||
is OpenObjectNavigation.UnexpectedLayoutError -> {
|
||||
toast(getString(R.string.error_unexpected_layout))
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.anytypeio.anytype.core_utils.ext.argString
|
|||
import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior
|
||||
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.di.feature.discussions.DiscussionFragment
|
||||
import com.anytypeio.anytype.presentation.home.OpenObjectNavigation
|
||||
import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel
|
||||
import com.anytypeio.anytype.ui.editor.EditorFragment
|
||||
|
@ -64,7 +65,7 @@ class GlobalSearchFragment : BaseBottomSheetComposeFragment() {
|
|||
}
|
||||
},
|
||||
onShowRelatedClicked = vm::onShowRelatedClicked,
|
||||
onClearRelatedClicked = vm::onClearRelatedObjectClicked
|
||||
onClearRelatedClicked = vm::onClearRelatedObjectClicked,
|
||||
)
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
|
@ -79,7 +80,6 @@ class GlobalSearchFragment : BaseBottomSheetComposeFragment() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
is OpenObjectNavigation.OpenDataView -> {
|
||||
findNavController().navigate(
|
||||
R.id.dataViewNavigation,
|
||||
|
@ -89,7 +89,15 @@ class GlobalSearchFragment : BaseBottomSheetComposeFragment() {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
findNavController().navigate(
|
||||
R.id.chatScreen,
|
||||
DiscussionFragment.args(
|
||||
ctx = nav.target,
|
||||
space = nav.space
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
// Do nothing.
|
||||
}
|
||||
|
|
|
@ -101,11 +101,13 @@ import kotlinx.coroutines.launch
|
|||
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun GlobalSearchScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
state: GlobalSearchViewModel.ViewState,
|
||||
onQueryChanged: (String) -> Unit,
|
||||
onObjectClicked: (GlobalSearchItemView) -> Unit,
|
||||
onShowRelatedClicked: (GlobalSearchItemView) -> Unit,
|
||||
onClearRelatedClicked: () -> Unit
|
||||
onClearRelatedClicked: () -> Unit,
|
||||
focusOnStart: Boolean = true
|
||||
) {
|
||||
|
||||
val selectionColors = TextSelectionColors(
|
||||
|
@ -144,7 +146,7 @@ fun GlobalSearchScreen(
|
|||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(rememberNestedScrollInteropConnection())
|
||||
) {
|
||||
|
@ -159,7 +161,6 @@ fun GlobalSearchScreen(
|
|||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -364,7 +365,9 @@ fun GlobalSearchScreen(
|
|||
}
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
if (focusOnStart) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,6 +89,10 @@ class RemoteFilesManageFragment : BaseBottomSheetComposeFragment() {
|
|||
subscription = command.subscription,
|
||||
space = command.space
|
||||
)
|
||||
is CollectionViewModel.Command.OpenChat -> navigation.openChat(
|
||||
space = command.space,
|
||||
target = command.target
|
||||
)
|
||||
is CollectionViewModel.Command.ToDesktop -> navigation.exitToDesktop()
|
||||
is CollectionViewModel.Command.ToSearch -> {
|
||||
// Do nothing.
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.anytypeio.anytype.core_utils.ui.BaseFragment
|
|||
import com.anytypeio.anytype.core_utils.ui.ViewState
|
||||
import com.anytypeio.anytype.databinding.FragmentSplashBinding
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.di.feature.discussions.DiscussionFragment
|
||||
import com.anytypeio.anytype.other.DefaultDeepLinkResolver
|
||||
import com.anytypeio.anytype.presentation.splash.SplashViewModel
|
||||
import com.anytypeio.anytype.presentation.splash.SplashViewModelFactory
|
||||
|
|
|
@ -154,6 +154,13 @@ class VaultFragment : BaseComposeFragment() {
|
|||
}.onFailure {
|
||||
Timber.e(it, "Error while opening set or collection from vault")
|
||||
}
|
||||
is Navigation.OpenChat -> {
|
||||
findNavController().navigate(R.id.actionOpenSpaceFromVault)
|
||||
navigation().openChat(
|
||||
target = destination.ctx,
|
||||
space = destination.space
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -104,6 +104,10 @@ class CollectionFragment : BaseComposeFragment() {
|
|||
subscription = command.subscription,
|
||||
space = space
|
||||
)
|
||||
is Command.OpenChat -> navigation.openChat(
|
||||
target = command.target,
|
||||
space = command.space
|
||||
)
|
||||
is Command.ToDesktop -> navigation.exitToDesktop()
|
||||
is Command.ToSearch -> navigation.openGlobalSearch(
|
||||
space = command.space
|
||||
|
|
|
@ -149,6 +149,11 @@
|
|||
app:destination="@id/selectSpaceScreen"/>
|
||||
</navigation>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/chatScreen"
|
||||
android:name="com.anytypeio.anytype.di.feature.discussions.DiscussionFragment"
|
||||
android:label="Discussion" />
|
||||
|
||||
<fragment
|
||||
android:id="@+id/homeScreen"
|
||||
android:name="com.anytypeio.anytype.ui.home.HomeScreenFragment"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.anytypeio.anytype.core_models
|
||||
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod
|
||||
import com.anytypeio.anytype.core_models.membership.NameServiceNameType
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
|
@ -572,4 +573,38 @@ sealed class Command {
|
|||
val previousVersion: Id
|
||||
) : VersionHistory()
|
||||
}
|
||||
|
||||
sealed class ChatCommand {
|
||||
data class AddMessage(
|
||||
val chat: Id,
|
||||
val message: Chat.Message
|
||||
): ChatCommand()
|
||||
data class DeleteMessage(
|
||||
val chat: Id,
|
||||
val msg: Id
|
||||
): ChatCommand()
|
||||
data class EditMessage(
|
||||
val chat: Id,
|
||||
val message: Chat.Message
|
||||
): ChatCommand()
|
||||
data class GetMessages(
|
||||
val chat: Id,
|
||||
val beforeMessageId: Id,
|
||||
val limit: Int
|
||||
): ChatCommand()
|
||||
data class SubscribeLastMessages(
|
||||
val chat: Id,
|
||||
val limit: Int
|
||||
): ChatCommand() {
|
||||
data class Response(
|
||||
val messages: List<Chat.Message>,
|
||||
val messageCountBefore: Int
|
||||
)
|
||||
}
|
||||
data class ToggleMessageReaction(
|
||||
val chat: Id,
|
||||
val msg: Id,
|
||||
val emoji: String
|
||||
): ChatCommand()
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.anytypeio.anytype.core_models
|
||||
|
||||
import com.anytypeio.anytype.core_models.Block.Content.Text
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
|
||||
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
|
||||
|
||||
|
@ -332,5 +333,31 @@ sealed class Event {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Chats : Command() {
|
||||
data class Add(
|
||||
override val context: Id,
|
||||
val id: Id,
|
||||
val order: Id,
|
||||
val message: Chat.Message
|
||||
) : Chats()
|
||||
|
||||
data class Update(
|
||||
override val context: Id,
|
||||
val id: Id,
|
||||
val message: Chat.Message
|
||||
) : Chats()
|
||||
|
||||
data class Delete(
|
||||
override val context: Id,
|
||||
val id: Id
|
||||
) : Chats()
|
||||
|
||||
data class UpdateReactions(
|
||||
override val context: Id,
|
||||
val id: Id,
|
||||
val reactions: Map<String, List<Id>>
|
||||
) : Chats()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ package com.anytypeio.anytype.core_models
|
|||
object Relations {
|
||||
|
||||
const val ID = "id"
|
||||
const val CHAT_ID = "chatId"
|
||||
const val COVER = "cover"
|
||||
const val COVER_TYPE = "coverType"
|
||||
const val COVER_ID = "coverId"
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
package com.anytypeio.anytype.core_models.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Block
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
|
||||
sealed class Chat {
|
||||
|
||||
/**
|
||||
* @property [id] message id
|
||||
*/
|
||||
data class Message(
|
||||
val id: Id,
|
||||
val order: Id,
|
||||
val creator: Id,
|
||||
val createdAt: Long,
|
||||
val modifiedAt: Long,
|
||||
val content: Content?,
|
||||
val attachments: List<Attachment> = emptyList(),
|
||||
val reactions: Map<String, List<String>>,
|
||||
val replyToMessageId: Id? = null,
|
||||
) {
|
||||
data class Content(
|
||||
val text: String,
|
||||
val style: Block.Content.Text.Style,
|
||||
val marks: List<Block.Content.Text.Mark>
|
||||
)
|
||||
data class Attachment(
|
||||
val target: Id,
|
||||
val type: Type
|
||||
) {
|
||||
sealed class Type {
|
||||
data object File: Type()
|
||||
data object Image: Type()
|
||||
data object Link: Type()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* New message builder.
|
||||
*/
|
||||
fun new(
|
||||
text: String,
|
||||
attachments: List<Attachment> = emptyList()
|
||||
) : Message = Chat.Message(
|
||||
id = "",
|
||||
createdAt = 0L,
|
||||
modifiedAt = 0L,
|
||||
attachments = attachments,
|
||||
reactions = emptyMap(),
|
||||
creator = "",
|
||||
replyToMessageId = "",
|
||||
content = Chat.Message.Content(
|
||||
text = text,
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P
|
||||
),
|
||||
order = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Updated message builder.
|
||||
*/
|
||||
fun updated(
|
||||
id: Id,
|
||||
text: String
|
||||
) : Message = Chat.Message(
|
||||
id = id,
|
||||
createdAt = 0L,
|
||||
modifiedAt = 0L,
|
||||
attachments = emptyList(),
|
||||
reactions = emptyMap(),
|
||||
creator = "",
|
||||
replyToMessageId = "",
|
||||
content = Chat.Message.Content(
|
||||
text = text,
|
||||
marks = emptyList(),
|
||||
style = Block.Content.Text.Style.P
|
||||
),
|
||||
order = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -298,6 +298,13 @@ private fun getP2PCardSettings(
|
|||
)
|
||||
)
|
||||
}
|
||||
P2PStatus.RESTRICTED -> {
|
||||
CardSettings(
|
||||
icon = painterResource(R.drawable.ic_sync_p2p_error),
|
||||
mainText = stringResource(id = R.string.sync_status_p2p),
|
||||
secondaryText = stringResource(id = R.string.sync_status_p2p_disabled)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -121,6 +121,7 @@
|
|||
<color name="background_notification_primary">#000000</color>
|
||||
<color name="background_secondary">#FFFFFF</color>
|
||||
<color name="background_highlighted">#144F4F4F</color>
|
||||
<color name="background_highlighted_light">#0A4F4F4F</color>
|
||||
<color name="background_highlighted_medium">#4F4F4F</color>
|
||||
<color name="background_multiplayer_request">#252525</color>
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package com.anytypeio.anytype.data.auth.event
|
||||
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.domain.chats.ChatEventChannel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ChatEventRemoteChannel {
|
||||
fun observe(chat: Id): Flow<List<Event.Command.Chats>>
|
||||
class Default(
|
||||
private val channel: ChatEventRemoteChannel
|
||||
) : ChatEventChannel {
|
||||
override fun observe(chat: Id): Flow<List<Event.Command.Chats>> {
|
||||
return channel.observe(chat)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import com.anytypeio.anytype.core_models.DVFilter
|
|||
import com.anytypeio.anytype.core_models.DVSort
|
||||
import com.anytypeio.anytype.core_models.DVViewer
|
||||
import com.anytypeio.anytype.core_models.DVViewerType
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.ManifestInfo
|
||||
|
@ -24,6 +25,7 @@ import com.anytypeio.anytype.core_models.SearchResult
|
|||
import com.anytypeio.anytype.core_models.Struct
|
||||
import com.anytypeio.anytype.core_models.Url
|
||||
import com.anytypeio.anytype.core_models.WidgetLayout
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.history.DiffVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.ShowVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.Version
|
||||
|
@ -1041,4 +1043,34 @@ class BlockDataRepository(
|
|||
override suspend fun diffVersions(command: Command.VersionHistory.DiffVersions): DiffVersionResponse {
|
||||
return remote.diffVersions(command)
|
||||
}
|
||||
|
||||
override suspend fun addChatMessage(command: Command.ChatCommand.AddMessage): Pair<Id, List<Event.Command.Chats>> {
|
||||
return remote.addChatMessage(command)
|
||||
}
|
||||
|
||||
override suspend fun editChatMessage(command: Command.ChatCommand.EditMessage) {
|
||||
remote.editChatMessage(command)
|
||||
}
|
||||
|
||||
override suspend fun deleteChatMessage(command: Command.ChatCommand.DeleteMessage) {
|
||||
remote.deleteChatMessage(command)
|
||||
}
|
||||
|
||||
override suspend fun getChatMessages(command: Command.ChatCommand.GetMessages): List<Chat.Message> {
|
||||
return remote.getChatMessages(command)
|
||||
}
|
||||
|
||||
override suspend fun subscribeLastChatMessages(
|
||||
command: Command.ChatCommand.SubscribeLastMessages
|
||||
): Command.ChatCommand.SubscribeLastMessages.Response {
|
||||
return remote.subscribeLastChatMessages(command)
|
||||
}
|
||||
|
||||
override suspend fun toggleChatMessageReaction(command: Command.ChatCommand.ToggleMessageReaction) {
|
||||
return remote.toggleChatMessageReaction(command = command)
|
||||
}
|
||||
|
||||
override suspend fun unsubscribeChat(chat: Id) {
|
||||
return remote.unsubscribeChat(chat)
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import com.anytypeio.anytype.core_models.DVFilter
|
|||
import com.anytypeio.anytype.core_models.DVSort
|
||||
import com.anytypeio.anytype.core_models.DVViewer
|
||||
import com.anytypeio.anytype.core_models.DVViewerType
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.ManifestInfo
|
||||
|
@ -24,6 +25,7 @@ import com.anytypeio.anytype.core_models.SearchResult
|
|||
import com.anytypeio.anytype.core_models.Struct
|
||||
import com.anytypeio.anytype.core_models.Url
|
||||
import com.anytypeio.anytype.core_models.WidgetLayout
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.history.DiffVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.ShowVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.Version
|
||||
|
@ -442,4 +444,16 @@ interface BlockRemote {
|
|||
suspend fun showVersion(command: Command.VersionHistory.ShowVersion): ShowVersionResponse
|
||||
suspend fun setVersion(command: Command.VersionHistory.SetVersion)
|
||||
suspend fun diffVersions(command: Command.VersionHistory.DiffVersions): DiffVersionResponse
|
||||
|
||||
//region CHATS
|
||||
|
||||
suspend fun addChatMessage(command: Command.ChatCommand.AddMessage): Pair<Id, List<Event.Command.Chats>>
|
||||
suspend fun editChatMessage(command: Command.ChatCommand.EditMessage)
|
||||
suspend fun deleteChatMessage(command: Command.ChatCommand.DeleteMessage)
|
||||
suspend fun getChatMessages(command: Command.ChatCommand.GetMessages): List<Chat.Message>
|
||||
suspend fun subscribeLastChatMessages(command: Command.ChatCommand.SubscribeLastMessages): Command.ChatCommand.SubscribeLastMessages.Response
|
||||
suspend fun toggleChatMessageReaction(command: Command.ChatCommand.ToggleMessageReaction)
|
||||
suspend fun unsubscribeChat(chat: Id)
|
||||
|
||||
//endregion
|
||||
}
|
|
@ -9,6 +9,7 @@ import com.anytypeio.anytype.core_models.DVFilter
|
|||
import com.anytypeio.anytype.core_models.DVSort
|
||||
import com.anytypeio.anytype.core_models.DVViewer
|
||||
import com.anytypeio.anytype.core_models.DVViewerType
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.ManifestInfo
|
||||
|
@ -24,6 +25,7 @@ import com.anytypeio.anytype.core_models.SearchResult
|
|||
import com.anytypeio.anytype.core_models.Struct
|
||||
import com.anytypeio.anytype.core_models.Url
|
||||
import com.anytypeio.anytype.core_models.WidgetLayout
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.history.DiffVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.ShowVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.Version
|
||||
|
@ -485,4 +487,16 @@ interface BlockRepository {
|
|||
suspend fun showVersion(command: Command.VersionHistory.ShowVersion): ShowVersionResponse
|
||||
suspend fun setVersion(command: Command.VersionHistory.SetVersion)
|
||||
suspend fun diffVersions(command: Command.VersionHistory.DiffVersions): DiffVersionResponse
|
||||
|
||||
//region CHATS
|
||||
|
||||
suspend fun addChatMessage(command: Command.ChatCommand.AddMessage): Pair<Id, List<Event.Command.Chats>>
|
||||
suspend fun editChatMessage(command: Command.ChatCommand.EditMessage)
|
||||
suspend fun deleteChatMessage(command: Command.ChatCommand.DeleteMessage)
|
||||
suspend fun getChatMessages(command: Command.ChatCommand.GetMessages): List<Chat.Message>
|
||||
suspend fun subscribeLastChatMessages(command: Command.ChatCommand.SubscribeLastMessages): Command.ChatCommand.SubscribeLastMessages.Response
|
||||
suspend fun toggleChatMessageReaction(command: Command.ChatCommand.ToggleMessageReaction)
|
||||
suspend fun unsubscribeChat(chat: Id)
|
||||
|
||||
//endregion
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.ResultInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class AddChatMessage @Inject constructor(
|
||||
private val repo: BlockRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
) : ResultInteractor<Command.ChatCommand.AddMessage, Pair<Id, List<Event.Command.Chats>>>(dispatchers.io) {
|
||||
|
||||
override suspend fun doWork(params: Command.ChatCommand.AddMessage): Pair<Id, List<Event.Command.Chats>> {
|
||||
return repo.addChatMessage(params)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.debugging.Logger
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.scan
|
||||
|
||||
class ChatContainer @Inject constructor(
|
||||
private val repo: BlockRepository,
|
||||
private val channel: ChatEventChannel,
|
||||
private val logger: Logger
|
||||
) {
|
||||
|
||||
private val payloads = MutableSharedFlow<List<Event.Command.Chats>>()
|
||||
|
||||
fun watch(chat: Id): Flow<List<Chat.Message>> = flow {
|
||||
val initial = repo.subscribeLastChatMessages(
|
||||
command = Command.ChatCommand.SubscribeLastMessages(
|
||||
chat = chat,
|
||||
limit = DEFAULT_LAST_MESSAGE_COUNT
|
||||
)
|
||||
)
|
||||
emitAll(
|
||||
merge(
|
||||
channel.observe(chat = chat),
|
||||
payloads
|
||||
).scan(initial.messages) { state, events ->
|
||||
state.reduce(events)
|
||||
}
|
||||
)
|
||||
}.catch {
|
||||
logger.logException(it)
|
||||
emit(emptyList())
|
||||
}
|
||||
|
||||
suspend fun onPayload(events: List<Event.Command.Chats>) {
|
||||
payloads.emit(events)
|
||||
}
|
||||
|
||||
fun List<Chat.Message>.reduce(events: List<Event.Command.Chats>): List<Chat.Message> {
|
||||
// Naive implementation
|
||||
var result = this
|
||||
events.forEach { event ->
|
||||
when(event) {
|
||||
is Event.Command.Chats.Add -> {
|
||||
if (result.isNotEmpty()) {
|
||||
val last = result.last()
|
||||
result = if (last.order < event.order)
|
||||
result + listOf(event.message)
|
||||
else {
|
||||
buildList {
|
||||
addAll(result)
|
||||
add(event.message)
|
||||
}.sortedBy { it.order }
|
||||
}
|
||||
} else {
|
||||
result = listOf(event.message)
|
||||
}
|
||||
}
|
||||
is Event.Command.Chats.Delete -> {
|
||||
result = result.filter { msg ->
|
||||
msg.id != event.id
|
||||
}
|
||||
}
|
||||
is Event.Command.Chats.Update -> {
|
||||
result = result.map { msg ->
|
||||
if (msg.id == event.id)
|
||||
event.message
|
||||
else
|
||||
msg
|
||||
}
|
||||
}
|
||||
is Event.Command.Chats.UpdateReactions -> {
|
||||
result = result.map { msg ->
|
||||
if (msg.id == event.id)
|
||||
msg.copy(
|
||||
reactions = event.reactions
|
||||
)
|
||||
else
|
||||
msg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DEFAULT_LAST_MESSAGE_COUNT = 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ChatEventChannel {
|
||||
fun observe(chat: Id): Flow<List<Event.Command.Chats>>
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.ResultInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeleteChatMessage @Inject constructor(
|
||||
private val repo: BlockRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
) : ResultInteractor<Command.ChatCommand.DeleteMessage, Unit>(dispatchers.io) {
|
||||
override suspend fun doWork(params: Command.ChatCommand.DeleteMessage) {
|
||||
return repo.deleteChatMessage(command = params)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.ResultInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class EditChatMessage @Inject constructor(
|
||||
private val repo: BlockRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
) : ResultInteractor<Command.ChatCommand.EditMessage, Unit>(dispatchers.io) {
|
||||
override suspend fun doWork(params: Command.ChatCommand.EditMessage) {
|
||||
return repo.editChatMessage(command = params)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.ResultInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class GetChatMessages @Inject constructor(
|
||||
private val repo: BlockRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
): ResultInteractor<Command.ChatCommand.GetMessages, List<Chat.Message>>(dispatchers.io) {
|
||||
override suspend fun doWork(
|
||||
params: Command.ChatCommand.GetMessages
|
||||
): List<Chat.Message> {
|
||||
return repo.getChatMessages(params)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.base.ResultInteractor
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
class ToggleChatMessageReaction @Inject constructor(
|
||||
private val repo: BlockRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
) : ResultInteractor<Command.ChatCommand.ToggleMessageReaction, Unit>(dispatchers.io) {
|
||||
override suspend fun doWork(params: Command.ChatCommand.ToggleMessageReaction) {
|
||||
return repo.toggleChatMessageReaction(command = params)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,359 @@
|
|||
package com.anytypeio.anytype.domain.chats
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.anytypeio.anytype.core_models.Block
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.TextStyle
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
|
||||
import com.anytypeio.anytype.domain.block.repo.BlockRepository
|
||||
import com.anytypeio.anytype.domain.common.DefaultCoroutineTestRule
|
||||
import com.anytypeio.anytype.domain.debugging.Logger
|
||||
import com.anytypeio.anytype.test_utils.MockDataFactory
|
||||
import kotlin.test.assertEquals
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.mockito.kotlin.doReturn
|
||||
import org.mockito.kotlin.stub
|
||||
|
||||
class ChatContainerTest {
|
||||
|
||||
@get:Rule
|
||||
val rule = DefaultCoroutineTestRule()
|
||||
|
||||
val dispatchers = AppCoroutineDispatchers(
|
||||
io = rule.dispatcher,
|
||||
computation = rule.dispatcher,
|
||||
main = rule.dispatcher
|
||||
)
|
||||
|
||||
@Mock
|
||||
lateinit var channel: ChatEventChannel
|
||||
|
||||
@Mock
|
||||
lateinit var repo: BlockRepository
|
||||
|
||||
@Mock
|
||||
lateinit var logger: Logger
|
||||
|
||||
private val givenChatID = MockDataFactory.randomUuid()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.openMocks(this)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test()
|
||||
fun `should add one message to basic initial state`() = runTest {
|
||||
|
||||
val container = ChatContainer(
|
||||
repo = repo,
|
||||
channel = channel,
|
||||
logger = logger
|
||||
)
|
||||
|
||||
val msg = StubChatMessage(
|
||||
content = StubChatMessageContent(
|
||||
text = "With seemingly endless talent and versatility, Sully puts his garage hat on to produce one super-slick plate"
|
||||
)
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
subscribeLastChatMessages(
|
||||
Command.ChatCommand.SubscribeLastMessages(
|
||||
chat = givenChatID,
|
||||
limit = ChatContainer.DEFAULT_LAST_MESSAGE_COUNT
|
||||
)
|
||||
)
|
||||
} doReturn Command.ChatCommand.SubscribeLastMessages.Response(
|
||||
messages = emptyList(),
|
||||
messageCountBefore = 0
|
||||
)
|
||||
}
|
||||
|
||||
channel.stub {
|
||||
on {
|
||||
observe(chat = givenChatID)
|
||||
} doReturn flow {
|
||||
delay(300)
|
||||
emit(
|
||||
listOf(
|
||||
Event.Command.Chats.Add(
|
||||
context = givenChatID,
|
||||
message = msg,
|
||||
id = msg.id,
|
||||
order = "A"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
container.watch(givenChatID).test {
|
||||
val first = awaitItem()
|
||||
assertEquals(
|
||||
expected = emptyList(),
|
||||
actual = first
|
||||
)
|
||||
advanceUntilIdle()
|
||||
val second = awaitItem()
|
||||
assertEquals(
|
||||
expected = listOf(
|
||||
msg
|
||||
),
|
||||
actual = second
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should update existing message`() = runTest {
|
||||
|
||||
val container = ChatContainer(
|
||||
repo = repo,
|
||||
channel = channel,
|
||||
logger = logger
|
||||
)
|
||||
|
||||
val initialMsg = StubChatMessage(
|
||||
content = StubChatMessageContent(
|
||||
text = "Hello, Walter"
|
||||
)
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
subscribeLastChatMessages(
|
||||
Command.ChatCommand.SubscribeLastMessages(
|
||||
chat = givenChatID,
|
||||
limit = ChatContainer.DEFAULT_LAST_MESSAGE_COUNT
|
||||
)
|
||||
)
|
||||
} doReturn Command.ChatCommand.SubscribeLastMessages.Response(
|
||||
messages = listOf(initialMsg),
|
||||
messageCountBefore = 0
|
||||
)
|
||||
}
|
||||
|
||||
channel.stub {
|
||||
on {
|
||||
observe(chat = givenChatID)
|
||||
} doReturn flow {
|
||||
delay(300)
|
||||
emit(
|
||||
listOf(
|
||||
Event.Command.Chats.Delete(
|
||||
context = givenChatID,
|
||||
id = initialMsg.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
container.watch(givenChatID).test {
|
||||
val first = awaitItem()
|
||||
assertEquals(
|
||||
expected = listOf(
|
||||
initialMsg
|
||||
),
|
||||
actual = first
|
||||
)
|
||||
advanceUntilIdle()
|
||||
val second = awaitItem()
|
||||
assertEquals(
|
||||
expected = emptyList(),
|
||||
actual = second
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should delete existing message`() = runTest {
|
||||
|
||||
val container = ChatContainer(
|
||||
repo = repo,
|
||||
channel = channel,
|
||||
logger = logger
|
||||
)
|
||||
|
||||
val initialMsg = StubChatMessage(
|
||||
content = StubChatMessageContent(
|
||||
text = "Hello, "
|
||||
)
|
||||
)
|
||||
|
||||
val msgAfterUpdate = initialMsg.copy(
|
||||
content = initialMsg.content?.copy(
|
||||
text = "Hello, Walter"
|
||||
)
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
subscribeLastChatMessages(
|
||||
Command.ChatCommand.SubscribeLastMessages(
|
||||
chat = givenChatID,
|
||||
limit = ChatContainer.DEFAULT_LAST_MESSAGE_COUNT
|
||||
)
|
||||
)
|
||||
} doReturn Command.ChatCommand.SubscribeLastMessages.Response(
|
||||
messages = listOf(initialMsg),
|
||||
messageCountBefore = 0
|
||||
)
|
||||
}
|
||||
|
||||
channel.stub {
|
||||
on {
|
||||
observe(chat = givenChatID)
|
||||
} doReturn flow {
|
||||
delay(300)
|
||||
emit(
|
||||
listOf(
|
||||
Event.Command.Chats.Update(
|
||||
context = givenChatID,
|
||||
message = msgAfterUpdate,
|
||||
id = initialMsg.id,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
container.watch(givenChatID).test {
|
||||
val first = awaitItem()
|
||||
assertEquals(
|
||||
expected = listOf(
|
||||
initialMsg
|
||||
),
|
||||
actual = first
|
||||
)
|
||||
advanceUntilIdle()
|
||||
val second = awaitItem()
|
||||
assertEquals(
|
||||
expected = listOf(
|
||||
msgAfterUpdate
|
||||
),
|
||||
actual = second
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should insert new message before existing message according to alphabetic sorting`() = runTest {
|
||||
|
||||
val container = ChatContainer(
|
||||
repo = repo,
|
||||
channel = channel,
|
||||
logger = logger
|
||||
)
|
||||
|
||||
val initialMsg = StubChatMessage(
|
||||
order = "B",
|
||||
content = StubChatMessageContent(
|
||||
text = "Hello, "
|
||||
)
|
||||
)
|
||||
|
||||
val newMsg = StubChatMessage(
|
||||
content = StubChatMessageContent(
|
||||
text = "Hello, "
|
||||
),
|
||||
order = "A"
|
||||
)
|
||||
|
||||
repo.stub {
|
||||
onBlocking {
|
||||
subscribeLastChatMessages(
|
||||
Command.ChatCommand.SubscribeLastMessages(
|
||||
chat = givenChatID,
|
||||
limit = ChatContainer.DEFAULT_LAST_MESSAGE_COUNT
|
||||
)
|
||||
)
|
||||
} doReturn Command.ChatCommand.SubscribeLastMessages.Response(
|
||||
messages = listOf(initialMsg),
|
||||
messageCountBefore = 0
|
||||
)
|
||||
}
|
||||
|
||||
channel.stub {
|
||||
on {
|
||||
observe(chat = givenChatID)
|
||||
} doReturn flow {
|
||||
delay(300)
|
||||
emit(
|
||||
listOf(
|
||||
Event.Command.Chats.Add(
|
||||
context = givenChatID,
|
||||
message = newMsg,
|
||||
id = newMsg.id,
|
||||
order = newMsg.order
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
container.watch(givenChatID).test {
|
||||
val first = awaitItem()
|
||||
assertEquals(
|
||||
expected = listOf(
|
||||
initialMsg
|
||||
),
|
||||
actual = first
|
||||
)
|
||||
advanceUntilIdle()
|
||||
val second = awaitItem()
|
||||
assertEquals(
|
||||
expected = listOf(
|
||||
newMsg,
|
||||
initialMsg
|
||||
),
|
||||
actual = second
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO move to test-utils
|
||||
fun StubChatMessage(
|
||||
id: Id = MockDataFactory.randomUuid(),
|
||||
order: Id = MockDataFactory.randomUuid(),
|
||||
creator: Id = MockDataFactory.randomUuid(),
|
||||
timestamp: Long = MockDataFactory.randomLong(),
|
||||
modifiedAt: Long = MockDataFactory.randomLong(),
|
||||
reactions: Map<String, List<Id>> = emptyMap(),
|
||||
content: Chat.Message.Content? = null
|
||||
|
||||
): Chat.Message = Chat.Message(
|
||||
id = id,
|
||||
order = order,
|
||||
creator = creator,
|
||||
createdAt = timestamp,
|
||||
reactions = reactions,
|
||||
content = content,
|
||||
modifiedAt = modifiedAt
|
||||
)
|
||||
|
||||
// TODO move to test-utils
|
||||
fun StubChatMessageContent(
|
||||
text: String,
|
||||
style: TextStyle = TextStyle.P,
|
||||
marks: List<Block.Content.Text.Mark> = emptyList()
|
||||
): Chat.Message.Content = Chat.Message.Content(
|
||||
text = text,
|
||||
style = style,
|
||||
marks = marks
|
||||
)
|
||||
}
|
|
@ -705,6 +705,14 @@ class AllContentViewModel(
|
|||
Timber.e("Unexpected layout: ${navigation.layout}")
|
||||
commands.emit(Command.SendToast.UnexpectedLayout(navigation.layout?.name.orEmpty()))
|
||||
}
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
commands.emit(
|
||||
Command.OpenChat(
|
||||
target = navigation.target,
|
||||
space = navigation.space
|
||||
)
|
||||
)
|
||||
}
|
||||
OpenObjectNavigation.NonValidObject -> {
|
||||
Timber.e("Object id is missing")
|
||||
}
|
||||
|
@ -972,6 +980,7 @@ class AllContentViewModel(
|
|||
//endregion
|
||||
|
||||
sealed class Command {
|
||||
data class OpenChat(val target: Id, val space: Id) : Command()
|
||||
data class NavigateToEditor(val id: Id, val space: Id) : Command()
|
||||
data class NavigateToSetOrCollection(val id: Id, val space: Id) : Command()
|
||||
data class NavigateToBin(val space: Id) : Command()
|
||||
|
|
|
@ -37,6 +37,7 @@ dependencies {
|
|||
implementation libs.composeFoundation
|
||||
implementation libs.composeToolingPreview
|
||||
implementation libs.composeMaterial3
|
||||
implementation libs.composeMaterial
|
||||
|
||||
implementation libs.coilCompose
|
||||
|
||||
|
|
|
@ -1,10 +1,28 @@
|
|||
package com.anytypeio.anytype.feature_discussions.presentation
|
||||
|
||||
import com.anytypeio.anytype.core_models.Hash
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
|
||||
sealed interface DiscussionView {
|
||||
data class Message(
|
||||
val id: String,
|
||||
val msg: String,
|
||||
val content: String,
|
||||
val author: String,
|
||||
val timestamp: Long
|
||||
) : DiscussionView
|
||||
val timestamp: Long,
|
||||
val attachments: List<Chat.Message.Attachment> = emptyList(),
|
||||
val reactions: List<Reaction> = emptyList(),
|
||||
val isUserAuthor: Boolean = false,
|
||||
val isEdited: Boolean = false,
|
||||
val avatar: Avatar = Avatar.Initials()
|
||||
) : DiscussionView {
|
||||
data class Reaction(
|
||||
val emoji: String,
|
||||
val count: Int,
|
||||
val isSelected: Boolean = false
|
||||
)
|
||||
sealed class Avatar {
|
||||
data class Initials(val initial: String = ""): Avatar()
|
||||
data class Image(val hash: Hash): Avatar()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,280 @@
|
|||
package com.anytypeio.anytype.feature_discussions.presentation
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.anytypeio.anytype.core_models.Command
|
||||
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.domain.auth.interactor.GetAccount
|
||||
import com.anytypeio.anytype.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.base.onFailure
|
||||
import com.anytypeio.anytype.domain.base.onSuccess
|
||||
import com.anytypeio.anytype.domain.chats.AddChatMessage
|
||||
import com.anytypeio.anytype.domain.chats.ChatContainer
|
||||
import com.anytypeio.anytype.domain.chats.DeleteChatMessage
|
||||
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.ActiveSpaceMemberSubscriptionContainer.Store
|
||||
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 kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class DiscussionViewModel : BaseViewModel() {
|
||||
class DiscussionViewModel(
|
||||
private val params: DefaultParams,
|
||||
private val setObjectDetails: SetObjectDetails,
|
||||
private val openObject: OpenObject,
|
||||
private val chatContainer: ChatContainer,
|
||||
private val addChatMessage: AddChatMessage,
|
||||
private val editChatMessage: EditChatMessage,
|
||||
private val deleteChatMessage: DeleteChatMessage,
|
||||
private val toggleChatMessageReaction: ToggleChatMessageReaction,
|
||||
private val members: ActiveSpaceMemberSubscriptionContainer,
|
||||
private val getAccount: GetAccount,
|
||||
private val urlBuilder: UrlBuilder
|
||||
) : BaseViewModel() {
|
||||
|
||||
val name = MutableStateFlow<String?>(null)
|
||||
val messages = MutableStateFlow<List<DiscussionView.Message>>(emptyList())
|
||||
val attachments = MutableStateFlow<List<GlobalSearchItemView>>(emptyList())
|
||||
val commands = MutableSharedFlow<UXCommand>()
|
||||
val navigation = MutableSharedFlow<OpenObjectNavigation>()
|
||||
val chatBoxMode = MutableStateFlow<ChatBoxMode>(ChatBoxMode.Default)
|
||||
|
||||
// TODO naive implementation; switch to state
|
||||
private lateinit 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,
|
||||
root = root
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Error while opening chat object")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithObservingChatMessages(
|
||||
account: Id,
|
||||
root: ObjectWrapper.Basic
|
||||
) {
|
||||
val chat = root.getValue<Id>(Relations.CHAT_ID)
|
||||
if (chat != null) {
|
||||
this.chat = chat
|
||||
chatContainer
|
||||
.watch(chat)
|
||||
.onEach { Timber.d("Got new update: $it") }
|
||||
.collect {
|
||||
messages.value = it.map { msg ->
|
||||
val member = members.get().let { type ->
|
||||
when(type) {
|
||||
is Store.Data -> type.members.find { member ->
|
||||
member.identity == msg.creator
|
||||
}
|
||||
is Store.Empty -> null
|
||||
}
|
||||
}
|
||||
DiscussionView.Message(
|
||||
id = msg.id,
|
||||
timestamp = msg.createdAt * 1000,
|
||||
content = msg.content?.text.orEmpty(),
|
||||
author = member?.name ?: msg.creator.takeLast(5),
|
||||
isUserAuthor = msg.creator == account,
|
||||
isEdited = msg.modifiedAt > msg.createdAt,
|
||||
reactions = msg.reactions.map{ (emoji, ids) ->
|
||||
DiscussionView.Message.Reaction(
|
||||
emoji = emoji,
|
||||
count = ids.size,
|
||||
isSelected = ids.contains(account)
|
||||
)
|
||||
},
|
||||
attachments = msg.attachments,
|
||||
avatar = if (member != null && !member.iconImage.isNullOrEmpty()) {
|
||||
DiscussionView.Message.Avatar.Image(
|
||||
urlBuilder.thumbnail(member.iconImage!!)
|
||||
)
|
||||
} else {
|
||||
DiscussionView.Message.Avatar.Initials(member?.name.orEmpty())
|
||||
}
|
||||
)
|
||||
}.reversed()
|
||||
}
|
||||
} else {
|
||||
Timber.w("Chat ID was missing in chat smart-object details")
|
||||
}
|
||||
}
|
||||
|
||||
fun onMessageSent(msg: String) {
|
||||
Timber.d("DROID-2635 OnMessageSent: $msg")
|
||||
viewModelScope.launch {
|
||||
when(val mode = chatBoxMode.value) {
|
||||
is ChatBoxMode.Default -> {
|
||||
// TODO consider moving this use-case inside chat container
|
||||
addChatMessage.async(
|
||||
params = Command.ChatCommand.AddMessage(
|
||||
chat = chat,
|
||||
message = Chat.Message.new(
|
||||
text = msg,
|
||||
attachments = attachments.value.map { a ->
|
||||
Chat.Message.Attachment(
|
||||
target = a.id,
|
||||
type = Chat.Message.Attachment.Type.Link
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
).onSuccess { (id, payload) ->
|
||||
attachments.value = emptyList()
|
||||
chatContainer.onPayload(payload)
|
||||
delay(JUMP_TO_BOTTOM_DELAY)
|
||||
commands.emit(UXCommand.JumpToBottom)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Error while adding message")
|
||||
}
|
||||
}
|
||||
is ChatBoxMode.EditMessage -> {
|
||||
editChatMessage.async(
|
||||
params = Command.ChatCommand.EditMessage(
|
||||
chat = chat,
|
||||
message = Chat.Message.updated(
|
||||
id = mode.msg,
|
||||
text = msg
|
||||
)
|
||||
)
|
||||
).onSuccess {
|
||||
delay(JUMP_TO_BOTTOM_DELAY)
|
||||
commands.emit(UXCommand.JumpToBottom)
|
||||
}.onFailure {
|
||||
Timber.e(it, "Error while adding message")
|
||||
}.onSuccess {
|
||||
chatBoxMode.value = ChatBoxMode.Default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onRequestEditMessageClicked(msg: DiscussionView.Message) {
|
||||
Timber.d("onRequestEditMessageClicked")
|
||||
viewModelScope.launch {
|
||||
chatBoxMode.value = ChatBoxMode.EditMessage(msg.id)
|
||||
}
|
||||
}
|
||||
|
||||
fun onTitleChanged(input: String) {
|
||||
Timber.d("DROID-2635 OnTitleChanged: $input")
|
||||
viewModelScope.launch {
|
||||
name.value = input
|
||||
setObjectDetails.async(
|
||||
params = SetObjectDetails.Params(
|
||||
ctx = params.ctx,
|
||||
details = mapOf(
|
||||
Relations.NAME to input
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onAttachObject(obj: GlobalSearchItemView) {
|
||||
attachments.value = listOf(obj)
|
||||
}
|
||||
|
||||
fun onClearAttachmentClicked() {
|
||||
attachments.value = emptyList()
|
||||
}
|
||||
|
||||
fun onReacted(msg: Id, reaction: String) {
|
||||
Timber.d("onReacted")
|
||||
viewModelScope.launch {
|
||||
val message = messages.value.find { it.id == msg }
|
||||
if (message != null) {
|
||||
toggleChatMessageReaction.async(
|
||||
Command.ChatCommand.ToggleMessageReaction(
|
||||
chat = chat,
|
||||
msg = msg,
|
||||
emoji = reaction
|
||||
)
|
||||
).onFailure {
|
||||
Timber.e(it, "Error while toggling chat message reaction")
|
||||
}
|
||||
} else {
|
||||
Timber.w("Target message not found for reaction")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onDeleteMessage(msg: DiscussionView.Message) {
|
||||
Timber.d("onDeleteMessageClicked")
|
||||
viewModelScope.launch {
|
||||
deleteChatMessage.async(
|
||||
Command.ChatCommand.DeleteMessage(
|
||||
chat = chat,
|
||||
msg = msg.id
|
||||
)
|
||||
).onFailure {
|
||||
Timber.e(it, "Error while deleting chat message")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAttachmentClicked(attachment: Chat.Message.Attachment) {
|
||||
viewModelScope.launch {
|
||||
// TODO naive implementation. Currently used for debugging.
|
||||
navigation.emit(
|
||||
OpenObjectNavigation.OpenEditor(
|
||||
target = attachment.target,
|
||||
space = params.space.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onExitEditMessageMode() {
|
||||
viewModelScope.launch {
|
||||
chatBoxMode.value = ChatBoxMode.Default
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UXCommand {
|
||||
data object JumpToBottom: UXCommand()
|
||||
data class SetChatBoxInput(val input: String): UXCommand()
|
||||
}
|
||||
|
||||
sealed class ChatBoxMode {
|
||||
data object Default : ChatBoxMode()
|
||||
data class EditMessage(val msg: Id) : ChatBoxMode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Delay before jump-to-bottom after adding new message to the chat.
|
||||
*/
|
||||
const val JUMP_TO_BOTTOM_DELAY = 50L
|
||||
}
|
||||
}
|
|
@ -2,10 +2,44 @@ package com.anytypeio.anytype.feature_discussions.presentation
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
|
||||
import com.anytypeio.anytype.domain.chats.AddChatMessage
|
||||
import com.anytypeio.anytype.domain.chats.ChatContainer
|
||||
import com.anytypeio.anytype.domain.chats.DeleteChatMessage
|
||||
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.`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 setObjectDetails: SetObjectDetails,
|
||||
private val openObject: OpenObject,
|
||||
private val chatContainer: ChatContainer,
|
||||
private val addChatMessage: AddChatMessage,
|
||||
private val editChatMessage: EditChatMessage,
|
||||
private val deleteChatMessage: DeleteChatMessage,
|
||||
private val toggleChatMessageReaction: ToggleChatMessageReaction,
|
||||
private val members: ActiveSpaceMemberSubscriptionContainer,
|
||||
private val getAccount: GetAccount,
|
||||
private val urlBuilder: UrlBuilder
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = DiscussionViewModel() as T
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = DiscussionViewModel(
|
||||
params = params,
|
||||
setObjectDetails = setObjectDetails,
|
||||
openObject = openObject,
|
||||
chatContainer = chatContainer,
|
||||
addChatMessage = addChatMessage,
|
||||
toggleChatMessageReaction = toggleChatMessageReaction,
|
||||
members = members,
|
||||
getAccount = getAccount,
|
||||
deleteChatMessage = deleteChatMessage,
|
||||
urlBuilder = urlBuilder,
|
||||
editChatMessage = editChatMessage
|
||||
) as T
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
package com.anytypeio.anytype.feature_discussions.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.feature_discussions.R
|
||||
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionView
|
||||
import kotlin.time.DurationUnit
|
||||
|
@ -17,23 +19,32 @@ fun DiscussionPreview() {
|
|||
messages = listOf(
|
||||
DiscussionView.Message(
|
||||
id = "1",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
content = stringResource(id = R.string.default_text_placeholder),
|
||||
author = "Walter",
|
||||
timestamp = System.currentTimeMillis()
|
||||
),
|
||||
DiscussionView.Message(
|
||||
id = "2",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
content = stringResource(id = R.string.default_text_placeholder),
|
||||
author = "Leo",
|
||||
timestamp = System.currentTimeMillis()
|
||||
),
|
||||
DiscussionView.Message(
|
||||
id = "3",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
content = stringResource(id = R.string.default_text_placeholder),
|
||||
author = "Gilbert",
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
),
|
||||
scrollState = LazyListState(),
|
||||
title = "Conversations with friends",
|
||||
onTitleChanged = {},
|
||||
onTitleFocusChanged = {},
|
||||
onReacted = { a, b -> },
|
||||
onDeleteMessage = {},
|
||||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -48,7 +59,7 @@ fun DiscussionScreenPreview() {
|
|||
add(
|
||||
DiscussionView.Message(
|
||||
id = idx.toString(),
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
content = stringResource(id = R.string.default_text_placeholder),
|
||||
author = "User ${idx.inc()}",
|
||||
timestamp =
|
||||
System.currentTimeMillis()
|
||||
|
@ -57,7 +68,19 @@ fun DiscussionScreenPreview() {
|
|||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}.reversed(),
|
||||
onMessageSent = {},
|
||||
onTitleChanged = {},
|
||||
onAttachClicked = {},
|
||||
attachments = emptyList(),
|
||||
onClearAttachmentClicked = {},
|
||||
lazyListState = LazyListState(),
|
||||
onReacted = { a, b -> },
|
||||
onCopyMessage = {},
|
||||
onDeleteMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {},
|
||||
onExitEditMessageMode = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -68,6 +91,52 @@ fun BubblePreview() {
|
|||
Bubble(
|
||||
name = "Leo Marx",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
timestamp = System.currentTimeMillis()
|
||||
timestamp = System.currentTimeMillis(),
|
||||
onReacted = {},
|
||||
onDeleteMessage = {},
|
||||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode")
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode")
|
||||
@Composable
|
||||
fun BubbleEditedPreview() {
|
||||
Bubble(
|
||||
name = "Leo Marx",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
isEdited = true,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
onReacted = {},
|
||||
onDeleteMessage = {},
|
||||
onCopyMessage = {},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Light Mode")
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode")
|
||||
@Composable
|
||||
fun BubbleWithAttachmentPreview() {
|
||||
Bubble(
|
||||
name = "Leo Marx",
|
||||
msg = stringResource(id = R.string.default_text_placeholder),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
onReacted = {},
|
||||
onDeleteMessage = {},
|
||||
onCopyMessage = {},
|
||||
attachments = buildList {
|
||||
add(
|
||||
Chat.Message.Attachment(
|
||||
target = "Walter Benjamin",
|
||||
type = Chat.Message.Attachment.Type.Image
|
||||
)
|
||||
)
|
||||
},
|
||||
onAttachmentClicked = {},
|
||||
onEditMessage = {}
|
||||
)
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M7,7L12,12L7,17"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/glyph_selected"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M17,7L12,12L17,17"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/glyph_selected"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="18dp"
|
||||
android:height="18dp"
|
||||
android:viewportWidth="18"
|
||||
android:viewportHeight="18">
|
||||
<path
|
||||
android:pathData="M4,7L9,12L14,7"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="@color/glyph_selected"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="28"
|
||||
android:viewportHeight="28">
|
||||
<path
|
||||
android:pathData="M9.5,14C9.5,14.828 8.828,15.5 8,15.5C7.172,15.5 6.5,14.828 6.5,14C6.5,13.172 7.172,12.5 8,12.5C8.828,12.5 9.5,13.172 9.5,14ZM15.5,14C15.5,14.828 14.828,15.5 14,15.5C13.172,15.5 12.5,14.828 12.5,14C12.5,13.172 13.172,12.5 14,12.5C14.828,12.5 15.5,13.172 15.5,14ZM20,15.5C20.828,15.5 21.5,14.828 21.5,14C21.5,13.172 20.828,12.5 20,12.5C19.172,12.5 18.5,13.172 18.5,14C18.5,14.828 19.172,15.5 20,15.5Z"
|
||||
android:fillColor="@color/glyph_active"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -1814,4 +1814,10 @@ Please provide specific details of your needs here.</string>
|
|||
<string name="main_navigation_content_desc_search_button">Search objects button</string>
|
||||
<string name="main_navigation_content_desc_create_button">Create object button</string>
|
||||
|
||||
<string name="chat">Chat</string>
|
||||
<string name="copy">Copy</string>
|
||||
<string name="chats_edit_message">Edit message</string>
|
||||
<string name="chats_message_edited">edited</string>
|
||||
<string name="chat_empty_state_message">There is no messages yet.\nBe the first to start a discussion.</string>
|
||||
|
||||
</resources>
|
|
@ -10,6 +10,7 @@ import com.anytypeio.anytype.core_models.DVFilter
|
|||
import com.anytypeio.anytype.core_models.DVSort
|
||||
import com.anytypeio.anytype.core_models.DVViewer
|
||||
import com.anytypeio.anytype.core_models.DVViewerType
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.ManifestInfo
|
||||
|
@ -25,6 +26,7 @@ import com.anytypeio.anytype.core_models.SearchResult
|
|||
import com.anytypeio.anytype.core_models.Struct
|
||||
import com.anytypeio.anytype.core_models.Url
|
||||
import com.anytypeio.anytype.core_models.WidgetLayout
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.history.DiffVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.ShowVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.Version
|
||||
|
@ -1004,4 +1006,38 @@ class BlockMiddleware(
|
|||
override suspend fun diffVersions(command: Command.VersionHistory.DiffVersions): DiffVersionResponse {
|
||||
return middleware.diffVersions(command)
|
||||
}
|
||||
|
||||
override suspend fun addChatMessage(command: Command.ChatCommand.AddMessage): Pair<Id, List<Event.Command.Chats>> {
|
||||
return middleware.chatAddMessage(command)
|
||||
}
|
||||
|
||||
override suspend fun editChatMessage(command: Command.ChatCommand.EditMessage) {
|
||||
middleware.chatEditMessageContent(command)
|
||||
}
|
||||
|
||||
override suspend fun deleteChatMessage(command: Command.ChatCommand.DeleteMessage) {
|
||||
middleware.chatDeleteMessage(command)
|
||||
}
|
||||
|
||||
override suspend fun getChatMessages(
|
||||
command: Command.ChatCommand.GetMessages
|
||||
): List<Chat.Message> {
|
||||
return middleware.chatGetMessages(command)
|
||||
}
|
||||
|
||||
override suspend fun subscribeLastChatMessages(
|
||||
command: Command.ChatCommand.SubscribeLastMessages
|
||||
): Command.ChatCommand.SubscribeLastMessages.Response {
|
||||
return middleware.chatSubscribeLastMessages(command)
|
||||
}
|
||||
|
||||
override suspend fun toggleChatMessageReaction(command: Command.ChatCommand.ToggleMessageReaction) {
|
||||
middleware.chatToggleMessageReaction(
|
||||
command = command
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun unsubscribeChat(chat: Id) {
|
||||
return middleware.chatUnsubscribe(chat = chat)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import com.anytypeio.anytype.core_models.DVFilter
|
|||
import com.anytypeio.anytype.core_models.DVSort
|
||||
import com.anytypeio.anytype.core_models.DVViewer
|
||||
import com.anytypeio.anytype.core_models.DVViewerType
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.Key
|
||||
import com.anytypeio.anytype.core_models.ManifestInfo
|
||||
|
@ -31,6 +32,7 @@ import com.anytypeio.anytype.core_models.SearchResult
|
|||
import com.anytypeio.anytype.core_models.Struct
|
||||
import com.anytypeio.anytype.core_models.Url
|
||||
import com.anytypeio.anytype.core_models.WidgetLayout
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.history.DiffVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.ShowVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.Version
|
||||
|
@ -46,6 +48,7 @@ import com.anytypeio.anytype.core_utils.tools.ThreadInfo
|
|||
import com.anytypeio.anytype.middleware.BuildConfig
|
||||
import com.anytypeio.anytype.middleware.auth.toAccountSetup
|
||||
import com.anytypeio.anytype.middleware.const.Constants
|
||||
import com.anytypeio.anytype.middleware.interactor.events.payload
|
||||
import com.anytypeio.anytype.middleware.mappers.MDVFilter
|
||||
import com.anytypeio.anytype.middleware.mappers.MDetail
|
||||
import com.anytypeio.anytype.middleware.mappers.MNetworkMode
|
||||
|
@ -2695,6 +2698,98 @@ class Middleware @Inject constructor(
|
|||
return response.toCoreModel(context = command.objectId)
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatAddMessage(command: Command.ChatCommand.AddMessage) : Pair<Id, List<Event.Command.Chats>> {
|
||||
val request = Rpc.Chat.AddMessage.Request(
|
||||
chatObjectId = command.chat,
|
||||
message = command.message.mw()
|
||||
)
|
||||
logRequestIfDebug(request)
|
||||
val (response, time) = measureTimedValue { service.chatAddMessage(request) }
|
||||
logResponseIfDebug(response, time)
|
||||
val events = response
|
||||
.event
|
||||
?.messages
|
||||
?.mapNotNull { msg ->
|
||||
msg.payload(contextId = command.chat)
|
||||
}
|
||||
.orEmpty()
|
||||
return response.messageId to events
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatEditMessageContent(command: Command.ChatCommand.EditMessage) {
|
||||
val request = Rpc.Chat.EditMessageContent.Request(
|
||||
chatObjectId = command.chat,
|
||||
messageId = command.message.id,
|
||||
editedMessage = command.message.mw()
|
||||
)
|
||||
logRequestIfDebug(request)
|
||||
val (response, time) = measureTimedValue { service.chatEditMessage(request) }
|
||||
logResponseIfDebug(response, time)
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatGetMessages(command: Command.ChatCommand.GetMessages) : List<Chat.Message> {
|
||||
val request = Rpc.Chat.GetMessages.Request(
|
||||
chatObjectId = command.chat
|
||||
)
|
||||
logRequestIfDebug(request)
|
||||
val (response, time) = measureTimedValue { service.chatGetMessages(request) }
|
||||
logResponseIfDebug(response, time)
|
||||
return response.messages.map { it.core() }
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatDeleteMessage(command: Command.ChatCommand.DeleteMessage) {
|
||||
val request = Rpc.Chat.DeleteMessage.Request(
|
||||
chatObjectId = command.chat,
|
||||
messageId = command.msg
|
||||
)
|
||||
logRequestIfDebug(request)
|
||||
val (response, time) = measureTimedValue { service.chatDeleteMessage(request) }
|
||||
logResponseIfDebug(response, time)
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatSubscribeLastMessages(
|
||||
command: Command.ChatCommand.SubscribeLastMessages
|
||||
): Command.ChatCommand.SubscribeLastMessages.Response {
|
||||
val request = Rpc.Chat.SubscribeLastMessages.Request(
|
||||
chatObjectId = command.chat,
|
||||
limit = command.limit
|
||||
)
|
||||
logRequestIfDebug(request)
|
||||
val (response, time) = measureTimedValue { service.chatSubscribeLastMessages(request) }
|
||||
logResponseIfDebug(response, time)
|
||||
return Command.ChatCommand.SubscribeLastMessages.Response(
|
||||
messages = response.messages.map { it.core() },
|
||||
messageCountBefore = response.numMessagesBefore
|
||||
)
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatToggleMessageReaction(
|
||||
command: Command.ChatCommand.ToggleMessageReaction
|
||||
) {
|
||||
val request = Rpc.Chat.ToggleMessageReaction.Request(
|
||||
chatObjectId = command.chat,
|
||||
messageId = command.msg,
|
||||
emoji = command.emoji
|
||||
)
|
||||
logRequestIfDebug(request)
|
||||
val (response, time) = measureTimedValue { service.chatToggleMessageReaction(request) }
|
||||
logResponseIfDebug(response, time)
|
||||
}
|
||||
|
||||
@Throws
|
||||
fun chatUnsubscribe(chat: Id) {
|
||||
val request = Rpc.Chat.Unsubscribe.Request(chatObjectId = chat)
|
||||
logRequestIfDebug(request)
|
||||
val (response, time) = measureTimedValue { service.chatUnsubscribe(request) }
|
||||
logResponseIfDebug(response, time)
|
||||
}
|
||||
|
||||
private fun logRequestIfDebug(request: Any) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
logger.logRequest(request).also {
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.anytypeio.anytype.core_models.Block
|
|||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.middleware.BuildConfig
|
||||
import com.anytypeio.anytype.middleware.mappers.MWidgetLayout
|
||||
import com.anytypeio.anytype.middleware.mappers.core
|
||||
import com.anytypeio.anytype.middleware.mappers.toCoreModel
|
||||
import com.anytypeio.anytype.middleware.mappers.toCoreModels
|
||||
import com.anytypeio.anytype.middleware.mappers.toCoreModelsAlign
|
||||
|
@ -291,6 +292,44 @@ fun anytype.Event.Message.toCoreModels(
|
|||
isCollection = event.value_
|
||||
)
|
||||
}
|
||||
chatAdd != null -> {
|
||||
val event = chatAdd
|
||||
checkNotNull(event)
|
||||
Event.Command.Chats.Add(
|
||||
context = context,
|
||||
id = event.id,
|
||||
order = event.orderId,
|
||||
message = requireNotNull(event.message).core()
|
||||
)
|
||||
}
|
||||
chatDelete != null -> {
|
||||
val event = chatDelete
|
||||
checkNotNull(event)
|
||||
Event.Command.Chats.Delete(
|
||||
context = context,
|
||||
id = event.id
|
||||
)
|
||||
}
|
||||
chatUpdate != null -> {
|
||||
val event = chatUpdate
|
||||
checkNotNull(event)
|
||||
Event.Command.Chats.Update(
|
||||
context = context,
|
||||
id = event.id,
|
||||
message = requireNotNull(event.message).core()
|
||||
)
|
||||
}
|
||||
chatUpdateReactions != null -> {
|
||||
val event = chatUpdateReactions
|
||||
checkNotNull(event)
|
||||
Event.Command.Chats.UpdateReactions(
|
||||
context = context,
|
||||
id = event.id,
|
||||
reactions = event.reactions?.reactions.orEmpty().mapValues { (unicode, identities) ->
|
||||
identities.ids
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.w("Skipped event while mapping: $this")
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
package com.anytypeio.anytype.middleware.interactor.events
|
||||
|
||||
import com.anytypeio.anytype.core_models.Event
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.data.auth.event.ChatEventRemoteChannel
|
||||
import com.anytypeio.anytype.middleware.EventProxy
|
||||
import com.anytypeio.anytype.middleware.mappers.MEventMessage
|
||||
import com.anytypeio.anytype.middleware.mappers.core
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
|
||||
class ChatEventMiddlewareChannel(
|
||||
private val eventProxy: EventProxy
|
||||
): ChatEventRemoteChannel {
|
||||
|
||||
override fun observe(chat: Id): Flow<List<Event.Command.Chats>> {
|
||||
return eventProxy
|
||||
.flow()
|
||||
.filter { it.contextId == chat }
|
||||
.mapNotNull { item ->
|
||||
item.messages.mapNotNull { msg -> msg.payload(contextId = item.contextId) }
|
||||
}.filter { events ->
|
||||
events.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun MEventMessage.payload(contextId: Id) : Event.Command.Chats? {
|
||||
return when {
|
||||
chatAdd != null -> {
|
||||
val event = chatAdd
|
||||
checkNotNull(event)
|
||||
Event.Command.Chats.Add(
|
||||
context = contextId,
|
||||
order = event.orderId,
|
||||
id = event.id,
|
||||
message = requireNotNull(event.message?.core())
|
||||
)
|
||||
}
|
||||
|
||||
chatUpdate != null -> {
|
||||
val event = chatUpdate
|
||||
checkNotNull(event)
|
||||
Event.Command.Chats.Update(
|
||||
context = contextId,
|
||||
id = event.id,
|
||||
message = requireNotNull(event.message?.core())
|
||||
)
|
||||
}
|
||||
|
||||
chatDelete != null -> {
|
||||
val event = chatDelete
|
||||
checkNotNull(event)
|
||||
Event.Command.Chats.Delete(
|
||||
context = contextId,
|
||||
id = event.id
|
||||
)
|
||||
}
|
||||
|
||||
chatUpdateReactions != null -> {
|
||||
val event = chatUpdateReactions
|
||||
checkNotNull(event)
|
||||
Event.Command.Chats.UpdateReactions(
|
||||
context = contextId,
|
||||
id = event.id,
|
||||
reactions = event.reactions?.reactions?.mapValues { (unicode, identities) ->
|
||||
identities.ids
|
||||
} ?: emptyMap()
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,9 @@ package com.anytypeio.anytype.middleware.mappers
|
|||
import anytype.Event.P2PStatus
|
||||
import anytype.Event.Space
|
||||
|
||||
|
||||
typealias MEvent = anytype.Event
|
||||
typealias MEventMessage = anytype.Event.Message
|
||||
typealias MAccount = anytype.model.Account
|
||||
typealias MAccountStatus = anytype.model.Account.Status
|
||||
typealias MAccountStatusType = anytype.model.Account.StatusType
|
||||
|
@ -35,6 +38,13 @@ typealias MBPosition = anytype.model.Block.Position
|
|||
typealias MBSplitMode = anytype.Rpc.Block.Split.Request.Mode
|
||||
typealias MBTableOfContents = anytype.model.Block.Content.TableOfContents
|
||||
|
||||
typealias MChatMessage = anytype.model.ChatMessage
|
||||
typealias MChatMessageContent = anytype.model.ChatMessage.MessageContent
|
||||
typealias MChatMessageAttachment = anytype.model.ChatMessage.Attachment
|
||||
typealias MChatMessageAttachmentType = anytype.model.ChatMessage.Attachment.AttachmentType
|
||||
typealias MChatMessageReactions = anytype.model.ChatMessage.Reactions
|
||||
typealias MChatMessageReactionIdentity = anytype.model.ChatMessage.Reactions.IdentityList
|
||||
|
||||
typealias MDV = anytype.model.Block.Content.Dataview
|
||||
typealias MDVView = anytype.model.Block.Content.Dataview.View
|
||||
typealias MDVViewType = anytype.model.Block.Content.Dataview.View.Type
|
||||
|
|
|
@ -44,6 +44,7 @@ import com.anytypeio.anytype.core_models.Relation
|
|||
import com.anytypeio.anytype.core_models.RelationFormat
|
||||
import com.anytypeio.anytype.core_models.RelationLink
|
||||
import com.anytypeio.anytype.core_models.SpaceUsage
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.history.DiffVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.ShowVersionResponse
|
||||
import com.anytypeio.anytype.core_models.history.Version
|
||||
|
@ -1108,6 +1109,35 @@ fun MP2PStatus.toCoreModel(): P2PStatus = when (this) {
|
|||
MP2PStatus.Restricted -> P2PStatus.RESTRICTED
|
||||
}
|
||||
|
||||
fun MChatMessage.core(): Chat.Message = Chat.Message(
|
||||
id = id,
|
||||
content = message?.core(),
|
||||
creator = creator,
|
||||
createdAt = createdAt,
|
||||
modifiedAt = modifiedAt,
|
||||
replyToMessageId = replyToMessageId.ifEmpty { null },
|
||||
attachments = attachments.map { attachment ->
|
||||
Chat.Message.Attachment(
|
||||
target = attachment.target,
|
||||
type = when(attachment.type) {
|
||||
MChatMessageAttachmentType.FILE -> Chat.Message.Attachment.Type.File
|
||||
MChatMessageAttachmentType.IMAGE -> Chat.Message.Attachment.Type.Image
|
||||
MChatMessageAttachmentType.LINK -> Chat.Message.Attachment.Type.Link
|
||||
}
|
||||
)
|
||||
},
|
||||
order = orderId,
|
||||
reactions = reactions?.reactions?.mapValues { (unicode, identities) ->
|
||||
identities.ids
|
||||
} ?: emptyMap()
|
||||
)
|
||||
|
||||
fun MChatMessageContent.core(): Chat.Message.Content = Chat.Message.Content(
|
||||
text = text,
|
||||
style = style.toCoreModels(),
|
||||
marks = marks.map { it.toCoreModels() }
|
||||
)
|
||||
|
||||
fun Rpc.History.Version.toCoreModel(): Version {
|
||||
return Version(
|
||||
id = id,
|
||||
|
|
|
@ -14,6 +14,7 @@ import com.anytypeio.anytype.core_models.ObjectType
|
|||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_models.Position
|
||||
import com.anytypeio.anytype.core_models.Relation
|
||||
import com.anytypeio.anytype.core_models.chats.Chat
|
||||
import com.anytypeio.anytype.core_models.membership.MembershipPaymentMethod
|
||||
import com.anytypeio.anytype.core_models.membership.NameServiceNameType
|
||||
import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions
|
||||
|
@ -561,6 +562,40 @@ fun MembershipPaymentMethod.toMw(): MMembershipPaymentMethod = when (this) {
|
|||
MembershipPaymentMethod.METHOD_INAPP_GOOGLE -> MMembershipPaymentMethod.MethodInappGoogle
|
||||
}
|
||||
|
||||
fun Chat.Message.mw(): MChatMessage = MChatMessage(
|
||||
id = id,
|
||||
message = content?.mw(),
|
||||
orderId = order,
|
||||
attachments = attachments.map { it.mw() },
|
||||
createdAt = createdAt,
|
||||
modifiedAt = modifiedAt,
|
||||
creator = creator,
|
||||
replyToMessageId = replyToMessageId.orEmpty(),
|
||||
reactions = MChatMessageReactions(
|
||||
reactions = reactions.mapValues { (unicode, ids) ->
|
||||
MChatMessageReactionIdentity(
|
||||
ids = ids
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
fun Chat.Message.Content.mw(): MChatMessageContent = MChatMessageContent(
|
||||
text = text,
|
||||
marks = marks.map { it.toMiddlewareModel() },
|
||||
style = style.toMiddlewareModel()
|
||||
)
|
||||
|
||||
fun Chat.Message.Attachment.mw(): MChatMessageAttachment = MChatMessageAttachment(
|
||||
target = target,
|
||||
type = when(type) {
|
||||
Chat.Message.Attachment.Type.File -> MChatMessageAttachmentType.FILE
|
||||
Chat.Message.Attachment.Type.Image -> MChatMessageAttachmentType.IMAGE
|
||||
Chat.Message.Attachment.Type.Link -> MChatMessageAttachmentType.LINK
|
||||
}
|
||||
)
|
||||
|
||||
fun Rpc.Object.SearchWithMeta.Response.toCoreModelSearchResults(): List<Command.SearchWithMeta.Result> {
|
||||
return results.map { result ->
|
||||
Command.SearchWithMeta.Result(
|
||||
|
@ -593,4 +628,5 @@ fun Rpc.Object.SearchWithMeta.Response.toCoreModelSearchResults(): List<Command.
|
|||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -584,4 +584,16 @@ interface MiddlewareService {
|
|||
@Throws(Exception::class)
|
||||
fun diffVersions(request: Rpc.History.DiffVersions.Request): Rpc.History.DiffVersions.Response
|
||||
//endregion
|
||||
|
||||
//region CHATS
|
||||
|
||||
fun chatAddMessage(request: Rpc.Chat.AddMessage.Request): Rpc.Chat.AddMessage.Response
|
||||
fun chatEditMessage(request: Rpc.Chat.EditMessageContent.Request): Rpc.Chat.EditMessageContent.Response
|
||||
fun chatGetMessages(request: Rpc.Chat.GetMessages.Request): Rpc.Chat.GetMessages.Response
|
||||
fun chatDeleteMessage(request: Rpc.Chat.DeleteMessage.Request): Rpc.Chat.DeleteMessage.Response
|
||||
fun chatSubscribeLastMessages(request: Rpc.Chat.SubscribeLastMessages.Request): Rpc.Chat.SubscribeLastMessages.Response
|
||||
fun chatToggleMessageReaction(request: Rpc.Chat.ToggleMessageReaction.Request): Rpc.Chat.ToggleMessageReaction.Response
|
||||
fun chatUnsubscribe(request: Rpc.Chat.Unsubscribe.Request): Rpc.Chat.Unsubscribe.Response
|
||||
|
||||
//endregion
|
||||
}
|
|
@ -2297,4 +2297,97 @@ class MiddlewareServiceImplementation @Inject constructor(
|
|||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun chatAddMessage(request: Rpc.Chat.AddMessage.Request): Rpc.Chat.AddMessage.Response {
|
||||
val encoded = Service.chatAddMessage(
|
||||
Rpc.Chat.AddMessage.Request.ADAPTER.encode(request)
|
||||
)
|
||||
val response = Rpc.Chat.AddMessage.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Chat.AddMessage.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun chatEditMessage(request: Rpc.Chat.EditMessageContent.Request): Rpc.Chat.EditMessageContent.Response {
|
||||
val encoded = Service.chatEditMessageContent(
|
||||
Rpc.Chat.EditMessageContent.Request.ADAPTER.encode(request)
|
||||
)
|
||||
val response = Rpc.Chat.EditMessageContent.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Chat.EditMessageContent.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun chatDeleteMessage(request: Rpc.Chat.DeleteMessage.Request): Rpc.Chat.DeleteMessage.Response {
|
||||
val encoded = Service.chatDeleteMessage(
|
||||
Rpc.Chat.DeleteMessage.Request.ADAPTER.encode(request)
|
||||
)
|
||||
val response = Rpc.Chat.DeleteMessage.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Chat.DeleteMessage.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun chatGetMessages(request: Rpc.Chat.GetMessages.Request): Rpc.Chat.GetMessages.Response {
|
||||
val encoded = Service.chatGetMessages(
|
||||
Rpc.Chat.GetMessages.Request.ADAPTER.encode(request)
|
||||
)
|
||||
val response = Rpc.Chat.GetMessages.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Chat.GetMessages.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun chatSubscribeLastMessages(request: Rpc.Chat.SubscribeLastMessages.Request): Rpc.Chat.SubscribeLastMessages.Response {
|
||||
val encoded = Service.chatSubscribeLastMessages(
|
||||
Rpc.Chat.SubscribeLastMessages.Request.ADAPTER.encode(request)
|
||||
)
|
||||
val response = Rpc.Chat.SubscribeLastMessages.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Chat.SubscribeLastMessages.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun chatToggleMessageReaction(
|
||||
request: Rpc.Chat.ToggleMessageReaction.Request
|
||||
): Rpc.Chat.ToggleMessageReaction.Response {
|
||||
val encoded = Service.chatToggleMessageReaction(
|
||||
Rpc.Chat.ToggleMessageReaction.Request.ADAPTER.encode(request)
|
||||
)
|
||||
val response = Rpc.Chat.ToggleMessageReaction.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Chat.ToggleMessageReaction.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
override fun chatUnsubscribe(request: Rpc.Chat.Unsubscribe.Request): Rpc.Chat.Unsubscribe.Response {
|
||||
val encoded = Service.chatUnsubscribe(
|
||||
Rpc.Chat.Unsubscribe.Request.ADAPTER.encode(request)
|
||||
)
|
||||
val response = Rpc.Chat.Unsubscribe.Response.ADAPTER.decode(encoded)
|
||||
val error = response.error
|
||||
if (error != null && error.code != Rpc.Chat.Unsubscribe.Response.Error.Code.NULL) {
|
||||
throw Exception(error.description)
|
||||
} else {
|
||||
return response
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,8 @@ package com.anytypeio.anytype.presentation.common
|
|||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.anytypeio.anytype.core_models.Id
|
||||
import com.anytypeio.anytype.core_models.primitives.SpaceId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -14,4 +16,9 @@ open class BaseViewModel : ViewModel() {
|
|||
companion object {
|
||||
const val DEFAULT_STOP_TIMEOUT_LIMIT = 5000L
|
||||
}
|
||||
|
||||
data class DefaultParams(
|
||||
val space: SpaceId,
|
||||
val ctx: Id
|
||||
)
|
||||
}
|
|
@ -4436,6 +4436,9 @@ class EditorViewModel(
|
|||
)
|
||||
)
|
||||
}
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
sendToast("not implemented")
|
||||
}
|
||||
is OpenObjectNavigation.UnexpectedLayoutError -> {
|
||||
sendToast("Unexpected layout: ${navigation.layout}")
|
||||
}
|
||||
|
|
|
@ -1359,6 +1359,14 @@ class HomeScreenViewModel(
|
|||
)
|
||||
)
|
||||
}
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
navigate(
|
||||
Navigation.OpenDiscussion(
|
||||
ctx = navigation.target,
|
||||
space = navigation.space
|
||||
)
|
||||
)
|
||||
}
|
||||
is OpenObjectNavigation.UnexpectedLayoutError -> {
|
||||
sendToast("Unexpected layout: ${navigation.layout}")
|
||||
}
|
||||
|
@ -2073,6 +2081,7 @@ class HomeScreenViewModel(
|
|||
|
||||
sealed class Navigation {
|
||||
data class OpenObject(val ctx: Id, val space: Id) : Navigation()
|
||||
data class OpenDiscussion(val ctx: Id, val space: Id) : Navigation()
|
||||
data class OpenSet(val ctx: Id, val space: Id, val view: Id?) : Navigation()
|
||||
data class ExpandWidget(val subscription: Subscription, val space: Id) : Navigation()
|
||||
data object OpenSpaceSwitcher: Navigation()
|
||||
|
@ -2292,6 +2301,7 @@ sealed class OpenObjectNavigation {
|
|||
data class OpenDataView(val target: Id, val space: Id): OpenObjectNavigation()
|
||||
data class UnexpectedLayoutError(val layout: ObjectType.Layout?): OpenObjectNavigation()
|
||||
data object NonValidObject: OpenObjectNavigation()
|
||||
data class OpenDiscussion(val target: Id, val space: Id): OpenObjectNavigation()
|
||||
}
|
||||
|
||||
fun ObjectWrapper.Basic.navigation() : OpenObjectNavigation {
|
||||
|
@ -2334,6 +2344,12 @@ fun ObjectWrapper.Basic.navigation() : OpenObjectNavigation {
|
|||
space = requireNotNull(spaceId)
|
||||
)
|
||||
}
|
||||
ObjectType.Layout.CHAT -> {
|
||||
OpenObjectNavigation.OpenDiscussion(
|
||||
target = id,
|
||||
space = requireNotNull(spaceId)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
OpenObjectNavigation.UnexpectedLayoutError(layout)
|
||||
}
|
||||
|
@ -2374,6 +2390,12 @@ fun ObjectType.Layout.navigation(
|
|||
space = space
|
||||
)
|
||||
}
|
||||
ObjectType.Layout.CHAT -> {
|
||||
OpenObjectNavigation.OpenDiscussion(
|
||||
target = target,
|
||||
space = space
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
OpenObjectNavigation.UnexpectedLayoutError(this)
|
||||
}
|
||||
|
|
|
@ -235,6 +235,9 @@ class LibraryViewModel(
|
|||
is OpenObjectNavigation.OpenEditor -> {
|
||||
navigate(Navigation.OpenEditor(navigation.target))
|
||||
}
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
sendToast("not implemented")
|
||||
}
|
||||
is OpenObjectNavigation.UnexpectedLayoutError -> {
|
||||
sendToast("Unexpected layout: ${navigation.layout}")
|
||||
}
|
||||
|
|
|
@ -16,7 +16,9 @@ interface AppNavigation {
|
|||
view: Id? = null,
|
||||
isPopUpToDashboard: Boolean = false
|
||||
)
|
||||
fun openChat(target: Id, space: Id)
|
||||
fun openDocument(target: Id, space: Id)
|
||||
fun openDiscussion(target: Id, space: Id)
|
||||
fun openModalTemplateSelect(
|
||||
template: Id,
|
||||
templateTypeId: Id,
|
||||
|
@ -66,6 +68,7 @@ interface AppNavigation {
|
|||
data object ExitFromMigrationScreen : Command()
|
||||
|
||||
data class OpenObject(val target: Id, val space: Id) : Command()
|
||||
data class OpenChat(val target: Id, val space: Id) : Command()
|
||||
data class LaunchDocument(val target: Id, val space: Id) : Command()
|
||||
data class OpenModalTemplateSelect(
|
||||
val template: Id,
|
||||
|
|
|
@ -56,9 +56,9 @@ class ObjectTypeChangeViewModel(
|
|||
|
||||
private val pipeline = combine(searchQuery, setup) { query, setup ->
|
||||
val recommendedLayouts = if (setup.isWithFiles) {
|
||||
SupportedLayouts.editorLayouts + SupportedLayouts.fileLayouts
|
||||
SupportedLayouts.editorLayouts + SupportedLayouts.fileLayouts + listOf(ObjectType.Layout.CHAT)
|
||||
} else {
|
||||
SupportedLayouts.editorLayouts
|
||||
SupportedLayouts.editorLayouts + listOf(ObjectType.Layout.CHAT)
|
||||
}
|
||||
val myTypes = proceedWithGettingMyTypes(
|
||||
query = query,
|
||||
|
|
|
@ -16,7 +16,8 @@ object SupportedLayouts {
|
|||
ObjectType.Layout.NOTE,
|
||||
ObjectType.Layout.BOOKMARK,
|
||||
ObjectType.Layout.AUDIO,
|
||||
ObjectType.Layout.PDF
|
||||
ObjectType.Layout.PDF,
|
||||
ObjectType.Layout.CHAT
|
||||
)
|
||||
val editorLayouts = listOf(
|
||||
ObjectType.Layout.BASIC,
|
||||
|
@ -52,7 +53,8 @@ object SupportedLayouts {
|
|||
ObjectType.Layout.COLLECTION,
|
||||
ObjectType.Layout.TODO,
|
||||
ObjectType.Layout.NOTE,
|
||||
ObjectType.Layout.BOOKMARK
|
||||
ObjectType.Layout.BOOKMARK,
|
||||
ObjectType.Layout.CHAT
|
||||
)
|
||||
|
||||
val addAsLinkToLayouts = editorLayouts + listOf(
|
||||
|
|
|
@ -66,7 +66,7 @@ import kotlinx.coroutines.flow.take
|
|||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
class GlobalSearchViewModel(
|
||||
class GlobalSearchViewModel @Inject constructor(
|
||||
private val vmParams: VmParams,
|
||||
private val searchWithMeta: SearchWithMeta,
|
||||
private val storeOfObjectTypes: StoreOfObjectTypes,
|
||||
|
@ -499,7 +499,7 @@ class GlobalSearchViewModel(
|
|||
* @property [title] object title
|
||||
* @property [type] type screen name
|
||||
*/
|
||||
data class GlobalSearchItemView(
|
||||
data class GlobalSearchItemView(
|
||||
val id: Id,
|
||||
val icon: ObjectIcon,
|
||||
val space: SpaceId,
|
||||
|
|
|
@ -76,7 +76,7 @@ sealed class ObjectSetCommand {
|
|||
val selectedTypes: List<Id>
|
||||
) : Modal()
|
||||
|
||||
object OpenEmptyDataViewSelectQueryScreen: Modal()
|
||||
data object OpenEmptyDataViewSelectQueryScreen: Modal()
|
||||
|
||||
data class EditIntrinsicTextRelation(
|
||||
val ctx: Id,
|
||||
|
|
|
@ -1527,6 +1527,31 @@ class ObjectSetViewModel(
|
|||
}
|
||||
)
|
||||
}
|
||||
ObjectType.Layout.CHAT -> {
|
||||
closeBlock.async(context).fold(
|
||||
onSuccess = {
|
||||
navigate(
|
||||
EventWrapper(
|
||||
AppNavigation.Command.OpenChat(
|
||||
target = target,
|
||||
space = space
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
onFailure = {
|
||||
Timber.e(it, "Error while closing object set: $context")
|
||||
navigate(
|
||||
EventWrapper(
|
||||
AppNavigation.Command.OpenChat(
|
||||
target = target,
|
||||
space = space
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
toast("Unexpected layout: $layout")
|
||||
Timber.e("Unexpected layout: $layout")
|
||||
|
|
|
@ -259,6 +259,14 @@ class VaultViewModel(
|
|||
)
|
||||
)
|
||||
}
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
navigate(
|
||||
Navigation.OpenChat(
|
||||
ctx = navigation.target,
|
||||
space = navigation.space
|
||||
)
|
||||
)
|
||||
}
|
||||
is OpenObjectNavigation.UnexpectedLayoutError -> {
|
||||
sendToast("Unexpected layout: ${navigation.layout}")
|
||||
}
|
||||
|
@ -323,6 +331,7 @@ class VaultViewModel(
|
|||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -886,6 +886,14 @@ class CollectionViewModel(
|
|||
)
|
||||
)
|
||||
}
|
||||
is OpenObjectNavigation.OpenDiscussion -> {
|
||||
commands.emit(
|
||||
Command.OpenChat(
|
||||
target = navigation.target,
|
||||
space = navigation.space
|
||||
)
|
||||
)
|
||||
}
|
||||
is OpenObjectNavigation.UnexpectedLayoutError -> {
|
||||
toasts.emit("Unexpected layout: ${navigation.layout}")
|
||||
}
|
||||
|
@ -990,6 +998,7 @@ class CollectionViewModel(
|
|||
data class LaunchDocument(val target: Id, val space: Id) : Command()
|
||||
data class OpenCollection(val subscription: Subscription, val space: Id) : Command()
|
||||
data class LaunchObjectSet(val target: Id, val space: Id) : Command()
|
||||
data class OpenChat(val target: Id, val space: Id) : Command()
|
||||
|
||||
data object ToDesktop : Command()
|
||||
data class ToSearch(val space: Id) : Command()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue