From a1aa1619e0775a94c66eb4693124b36123366037 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Wed, 15 Jan 2025 13:10:28 +0100 Subject: [PATCH] DROID-3232 App | Tech | Renamings (#1998) --- app/build.gradle | 2 +- .../anytype/di/common/ComponentManager.kt | 20 +- .../{discussions => chats}/ChatReactionDI.kt | 4 +- .../DiscussionsDI.kt => chats/ChatsDI.kt} | 46 +- .../SelectChatReactionDI.kt | 4 +- .../anytype/di/main/MainComponent.kt | 10 +- .../anytypeio/anytype/navigation/Navigator.kt | 6 +- .../chats/ChatFragment.kt} | 22 +- .../anytype/ui/chats/ChatReactionFragment.kt | 4 +- .../ui/chats/SelectChatReactionFragment.kt | 4 +- .../anytype/ui/home/HomeScreenFragment.kt | 10 +- .../anytype/ui/home/HomeScreenToolbar.kt | 5 +- .../anytypeio/anytype/ui/main/MainActivity.kt | 3 +- .../anytype/ui/search/GlobalSearchFragment.kt | 6 +- app/src/main/res/navigation/graph.xml | 2 +- .../core_ui/widgets/ObjectIconCompose.kt | 2 +- .../core_ui/widgets/ObjectIconWidget.kt | 2 +- .../presentation/AllContentViewModel.kt | 2 +- .../build.gradle | 2 +- .../src/main/AndroidManifest.xml | 0 .../presentation/ChatReactionViewModel.kt | 2 +- .../feature_chats/presentation/ChatView.kt | 9 +- .../presentation/ChatViewModel.kt | 66 +- .../presentation/ChatViewModelFactory.kt | 11 +- .../SelectChatReactionViewModel.kt | 2 +- .../anytype/feature_chats/ui/Attachments.kt | 184 ++ .../anytype/feature_chats/ui/ChatBox.kt | 470 +++++ .../anytype/feature_chats/ui/ChatBubble.kt | 421 ++++ .../anytype/feature_chats/ui/ChatPreviews.kt | 51 +- .../feature_chats}/ui/ChatReactionPicker.kt | 7 +- .../feature_chats}/ui/ChatReactionScreen.kt | 6 +- .../anytype/feature_chats/ui/ChatScreen.kt | 617 ++++++ .../anytype/feature_chats/ui/Reactions.kt | 141 ++ .../anytype/feature_chats/ui/Toolbars.kt | 111 + .../anytype/feature_chats/ui/Utils.kt | 102 + .../drawable/ic_chat_box_add_attachment.xml | 0 .../drawable/ic_chat_close_chat_box_reply.xml | 0 .../drawable/ic_clear_chatbox_attachment.xml | 0 .../res/drawable/ic_edit_message_close.xml | 0 .../res/drawable/ic_go_to_bottom_arrow.xml | 0 .../src/main/res/drawable/ic_send_message.xml | 0 .../res/drawable/ic_toolbar_three_dots.xml | 0 .../viewmodel/DateObjectViewModel.kt | 3 +- .../ui/DiscussionScreen.kt | 1877 ----------------- .../presentation/editor/EditorViewModel.kt | 2 +- .../presentation/home/HomeScreenViewModel.kt | 14 +- .../presentation/mapper/ObjectIconMapper.kt | 2 +- .../presentation/objects/ObjectIcon.kt | 2 +- .../presentation/vault/VaultViewModel.kt | 3 +- .../widgets/collection/CollectionViewModel.kt | 2 +- settings.gradle | 2 +- 51 files changed, 2208 insertions(+), 2055 deletions(-) rename app/src/main/java/com/anytypeio/anytype/di/feature/{discussions => chats}/ChatReactionDI.kt (92%) rename app/src/main/java/com/anytypeio/anytype/di/feature/{discussions/DiscussionsDI.kt => chats/ChatsDI.kt} (65%) rename app/src/main/java/com/anytypeio/anytype/di/feature/{discussions => chats}/SelectChatReactionDI.kt (93%) rename app/src/main/java/com/anytypeio/anytype/{di/feature/discussions/DiscussionFragment.kt => ui/chats/ChatFragment.kt} (91%) rename {feature-discussions => feature-chats}/build.gradle (96%) rename {feature-discussions => feature-chats}/src/main/AndroidManifest.xml (100%) rename {feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions => feature-chats/src/main/java/com/anytypeio/anytype/feature_chats}/presentation/ChatReactionViewModel.kt (98%) rename feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionView.kt => feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatView.kt (94%) rename feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionViewModel.kt => feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt (90%) rename feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionViewModelFactory.kt => feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt (87%) rename {feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions => feature-chats/src/main/java/com/anytypeio/anytype/feature_chats}/presentation/SelectChatReactionViewModel.kt (98%) create mode 100644 feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Attachments.kt create mode 100644 feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt create mode 100644 feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt rename feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/DiscussionPreviews.kt => feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatPreviews.kt (83%) rename {feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions => feature-chats/src/main/java/com/anytypeio/anytype/feature_chats}/ui/ChatReactionPicker.kt (95%) rename {feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions => feature-chats/src/main/java/com/anytypeio/anytype/feature_chats}/ui/ChatReactionScreen.kt (97%) create mode 100644 feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt create mode 100644 feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Reactions.kt create mode 100644 feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Toolbars.kt create mode 100644 feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Utils.kt rename {feature-discussions => feature-chats}/src/main/res/drawable/ic_chat_box_add_attachment.xml (100%) rename {feature-discussions => feature-chats}/src/main/res/drawable/ic_chat_close_chat_box_reply.xml (100%) rename {feature-discussions => feature-chats}/src/main/res/drawable/ic_clear_chatbox_attachment.xml (100%) rename {feature-discussions => feature-chats}/src/main/res/drawable/ic_edit_message_close.xml (100%) rename {feature-discussions => feature-chats}/src/main/res/drawable/ic_go_to_bottom_arrow.xml (100%) rename {feature-discussions => feature-chats}/src/main/res/drawable/ic_send_message.xml (100%) rename {feature-discussions => feature-chats}/src/main/res/drawable/ic_toolbar_three_dots.xml (100%) delete mode 100644 feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/DiscussionScreen.kt diff --git a/app/build.gradle b/app/build.gradle index 4b70054c3f..38a70f537f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -173,7 +173,7 @@ dependencies { implementation project(':ui-settings') implementation project(':crash-reporting') implementation project(':payments') - implementation project(':feature-discussions') + implementation project(':feature-chats') implementation project(':gallery-experience') implementation project(':feature-all-content') implementation project(':feature-date') diff --git a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt index 509396a4d3..add16e9516 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt @@ -48,11 +48,11 @@ import com.anytypeio.anytype.di.feature.TextBlockIconPickerModule 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.chats.DaggerChatComponent import com.anytypeio.anytype.di.feature.cover.UnsplashModule -import com.anytypeio.anytype.di.feature.discussions.DaggerChatReactionComponent -import com.anytypeio.anytype.di.feature.discussions.DaggerDiscussionComponent -import com.anytypeio.anytype.di.feature.discussions.DaggerSelectChatReactionComponent -import com.anytypeio.anytype.di.feature.discussions.DaggerSpaceLevelChatComponent +import com.anytypeio.anytype.di.feature.chats.DaggerChatReactionComponent +import com.anytypeio.anytype.di.feature.chats.DaggerSelectChatReactionComponent +import com.anytypeio.anytype.di.feature.chats.DaggerSpaceLevelChatComponent import com.anytypeio.anytype.di.feature.gallery.DaggerGalleryInstallationComponent import com.anytypeio.anytype.di.feature.home.DaggerHomeScreenComponent import com.anytypeio.anytype.di.feature.membership.DaggerMembershipComponent @@ -104,9 +104,9 @@ import com.anytypeio.anytype.di.feature.widgets.DaggerSelectWidgetTypeComponent import com.anytypeio.anytype.di.main.MainComponent import com.anytypeio.anytype.feature_allcontent.presentation.AllContentViewModel import com.anytypeio.anytype.feature_date.viewmodel.DateObjectVmParams -import com.anytypeio.anytype.feature_discussions.presentation.ChatReactionViewModel -import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel -import com.anytypeio.anytype.feature_discussions.presentation.SelectChatReactionViewModel +import com.anytypeio.anytype.feature_chats.presentation.ChatReactionViewModel +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel +import com.anytypeio.anytype.feature_chats.presentation.SelectChatReactionViewModel import com.anytypeio.anytype.gallery_experience.viewmodel.GalleryInstallationViewModel import com.anytypeio.anytype.presentation.editor.EditorViewModel import com.anytypeio.anytype.presentation.history.VersionHistoryViewModel @@ -1067,15 +1067,15 @@ class ComponentManager( .build() } - val discussionComponent = ComponentMapWithParam { params: DiscussionViewModel.Params -> - DaggerDiscussionComponent + val chatComponent = ComponentMapWithParam { params: ChatViewModel.Params -> + DaggerChatComponent .builder() .withDependencies(findComponentDependencies()) .withParams(params) .build() } - val spaceLevelChatComponent = ComponentMapWithParam { params: DiscussionViewModel.Params -> + val spaceLevelChatComponent = ComponentMapWithParam { params: ChatViewModel.Params -> DaggerSpaceLevelChatComponent .builder() .withDependencies(findComponentDependencies()) diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/ChatReactionDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatReactionDI.kt similarity index 92% rename from app/src/main/java/com/anytypeio/anytype/di/feature/discussions/ChatReactionDI.kt rename to app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatReactionDI.kt index eac54ffd11..5bb8654b8f 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/ChatReactionDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatReactionDI.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.di.feature.discussions +package com.anytypeio.anytype.di.feature.chats import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.core_utils.di.scope.PerScreen @@ -7,7 +7,7 @@ import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers import com.anytypeio.anytype.domain.block.repo.BlockRepository import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer -import com.anytypeio.anytype.feature_discussions.presentation.ChatReactionViewModel +import com.anytypeio.anytype.feature_chats.presentation.ChatReactionViewModel import com.anytypeio.anytype.ui.chats.ChatReactionFragment import dagger.Binds import dagger.BindsInstance diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/DiscussionsDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt similarity index 65% rename from app/src/main/java/com/anytypeio/anytype/di/feature/discussions/DiscussionsDI.kt rename to app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt index 983e7e27af..1a8893eda7 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/DiscussionsDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/ChatsDI.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.di.feature.discussions +package com.anytypeio.anytype.di.feature.chats import android.content.Context import androidx.lifecycle.ViewModelProvider @@ -6,7 +6,6 @@ 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 @@ -18,15 +17,12 @@ import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionCon import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.multiplayer.UserPermissionProvider import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes -import com.anytypeio.anytype.emojifier.data.Emoji -import com.anytypeio.anytype.emojifier.data.EmojiProvider -import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel -import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModelFactory +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModelFactory import com.anytypeio.anytype.middleware.EventProxy -import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory import com.anytypeio.anytype.presentation.util.DefaultCopyFileToCacheDirectory -import com.anytypeio.anytype.ui.home.HomeScreenFragment +import com.anytypeio.anytype.ui.chats.ChatFragment import dagger.Binds import dagger.BindsInstance import dagger.Component @@ -34,29 +30,29 @@ import dagger.Module import dagger.Provides @Component( - dependencies = [DiscussionComponentDependencies::class], + dependencies = [ChatComponentDependencies::class], modules = [ - DiscussionModule::class, - DiscussionModule.Declarations::class + ChatModule::class, + ChatModule.Declarations::class ] ) @PerScreen -interface DiscussionComponent { +interface ChatComponent { @Component.Builder interface Builder { @BindsInstance - fun withParams(params: DiscussionViewModel.Params): Builder - fun withDependencies(dependencies: DiscussionComponentDependencies): Builder - fun build(): DiscussionComponent + fun withParams(params: ChatViewModel.Params): Builder + fun withDependencies(dependencies: ChatComponentDependencies): Builder + fun build(): ChatComponent } - fun inject(fragment: DiscussionFragment) + fun inject(fragment: ChatFragment) } @Component( - dependencies = [DiscussionComponentDependencies::class], + dependencies = [ChatComponentDependencies::class], modules = [ - DiscussionModule::class, - DiscussionModule.Declarations::class + ChatModule::class, + ChatModule.Declarations::class ] ) @PerScreen @@ -64,16 +60,16 @@ interface SpaceLevelChatComponent { @Component.Builder interface Builder { @BindsInstance - fun withParams(params: DiscussionViewModel.Params): Builder - fun withDependencies(dependencies: DiscussionComponentDependencies): Builder + fun withParams(params: ChatViewModel.Params): Builder + fun withDependencies(dependencies: ChatComponentDependencies): Builder fun build(): SpaceLevelChatComponent } - fun getViewModel(): DiscussionViewModel + fun getViewModel(): ChatViewModel } @Module -object DiscussionModule { +object ChatModule { @JvmStatic @Provides @@ -87,12 +83,12 @@ object DiscussionModule { @PerScreen @Binds fun bindViewModelFactory( - factory: DiscussionViewModelFactory + factory: ChatViewModelFactory ): ViewModelProvider.Factory } } -interface DiscussionComponentDependencies : ComponentDependencies { +interface ChatComponentDependencies : ComponentDependencies { fun blockRepository(): BlockRepository fun authRepo(): AuthRepository fun appCoroutineDispatchers(): AppCoroutineDispatchers diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/SelectChatReactionDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/SelectChatReactionDI.kt similarity index 93% rename from app/src/main/java/com/anytypeio/anytype/di/feature/discussions/SelectChatReactionDI.kt rename to app/src/main/java/com/anytypeio/anytype/di/feature/chats/SelectChatReactionDI.kt index 478509a340..a469ed907d 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/SelectChatReactionDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/chats/SelectChatReactionDI.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.di.feature.discussions +package com.anytypeio.anytype.di.feature.chats import androidx.lifecycle.ViewModelProvider import com.anytypeio.anytype.core_utils.di.scope.PerScreen @@ -10,7 +10,7 @@ import com.anytypeio.anytype.domain.config.UserSettingsRepository import com.anytypeio.anytype.emojifier.data.Emoji import com.anytypeio.anytype.emojifier.data.EmojiProvider import com.anytypeio.anytype.emojifier.suggest.EmojiSuggester -import com.anytypeio.anytype.feature_discussions.presentation.SelectChatReactionViewModel +import com.anytypeio.anytype.feature_chats.presentation.SelectChatReactionViewModel import com.anytypeio.anytype.ui.chats.SelectChatReactionFragment import dagger.Binds import dagger.BindsInstance diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt b/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt index 01673c7cb3..c53f97d979 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt @@ -20,9 +20,9 @@ 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.ChatReactionDependencies -import com.anytypeio.anytype.di.feature.discussions.SelectChatReactionDependencies -import com.anytypeio.anytype.di.feature.discussions.DiscussionComponentDependencies +import com.anytypeio.anytype.di.feature.chats.ChatReactionDependencies +import com.anytypeio.anytype.di.feature.chats.SelectChatReactionDependencies +import com.anytypeio.anytype.di.feature.chats.ChatComponentDependencies import com.anytypeio.anytype.di.feature.gallery.GalleryInstallationComponentDependencies import com.anytypeio.anytype.di.feature.home.HomeScreenDependencies import com.anytypeio.anytype.di.feature.membership.MembershipComponentDependencies @@ -132,7 +132,7 @@ interface MainComponent : MembershipUpdateComponentDependencies, VaultComponentDependencies, AllContentDependencies, - DiscussionComponentDependencies, + ChatComponentDependencies, SelectWidgetSourceDependencies, SelectWidgetTypeDependencies, LinkToObjectDependencies, @@ -353,7 +353,7 @@ abstract class ComponentDependenciesModule { @Binds @IntoMap - @ComponentDependenciesKey(DiscussionComponentDependencies::class) + @ComponentDependenciesKey(ChatComponentDependencies::class) abstract fun provideDiscussionComponentDependencies(component: MainComponent): ComponentDependencies @Binds diff --git a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt index d08e661266..50c62c5ba2 100644 --- a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt +++ b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt @@ -7,7 +7,7 @@ import com.anytypeio.anytype.R import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Key import com.anytypeio.anytype.core_models.primitives.SpaceId -import com.anytypeio.anytype.di.feature.discussions.DiscussionFragment +import com.anytypeio.anytype.ui.chats.ChatFragment import com.anytypeio.anytype.presentation.navigation.AppNavigation import com.anytypeio.anytype.presentation.widgets.collection.Subscription import com.anytypeio.anytype.ui.allcontent.AllContentFragment @@ -44,7 +44,7 @@ class Navigator : AppNavigation { override fun openChat(target: Id, space: Id) { navController?.navigate( R.id.chatScreen, - DiscussionFragment.args( + ChatFragment.args( ctx = target, space = space ) @@ -64,7 +64,7 @@ class Navigator : AppNavigation { override fun openDiscussion(target: Id, space: Id) { navController?.navigate( R.id.chatScreen, - DiscussionFragment.args( + ChatFragment.args( ctx = target, space = space ) diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/DiscussionFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt similarity index 91% rename from app/src/main/java/com/anytypeio/anytype/di/feature/discussions/DiscussionFragment.kt rename to app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt index 15e6ec90e9..f25c548a12 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/discussions/DiscussionFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatFragment.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.di.feature.discussions +package com.anytypeio.anytype.ui.chats import android.os.Bundle import android.view.LayoutInflater @@ -32,9 +32,9 @@ 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.feature_chats.presentation.ChatViewModel +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModelFactory +import com.anytypeio.anytype.feature_chats.ui.ChatScreenWrapper import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel import com.anytypeio.anytype.ui.editor.EditorFragment @@ -43,12 +43,12 @@ import com.anytypeio.anytype.ui.settings.typography import javax.inject.Inject import timber.log.Timber -class DiscussionFragment : BaseComposeFragment() { +class ChatFragment : BaseComposeFragment() { @Inject - lateinit var factory: DiscussionViewModelFactory + lateinit var factory: ChatViewModelFactory - private val vm by viewModels { factory } + private val vm by viewModels { factory } private val ctx get() = arg(CTX_KEY) private val space get() = arg(SPACE_KEY) @@ -70,7 +70,7 @@ class DiscussionFragment : BaseComposeFragment() { var showBottomSheet by remember { mutableStateOf(false) } - DiscussionScreenWrapper( + ChatScreenWrapper( vm = vm, onAttachObjectClicked = { showBottomSheet = true @@ -160,10 +160,10 @@ class DiscussionFragment : BaseComposeFragment() { override fun injectDependencies() { componentManager() - .discussionComponent + .chatComponent .get( key = ctx, - param = DiscussionViewModel.Params.Default( + param = ChatViewModel.Params.Default( ctx = ctx, space = SpaceId(space) ) @@ -172,7 +172,7 @@ class DiscussionFragment : BaseComposeFragment() { } override fun releaseDependencies() { - componentManager().discussionComponent.release(ctx) + componentManager().chatComponent.release(ctx) } override fun onApplyWindowRootInsets(view: View) { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatReactionFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatReactionFragment.kt index 55af771664..3bb4ed7af5 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatReactionFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/chats/ChatReactionFragment.kt @@ -15,8 +15,8 @@ import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.ext.arg import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.feature_discussions.presentation.ChatReactionViewModel -import com.anytypeio.anytype.feature_discussions.ui.ChatReactionScreen +import com.anytypeio.anytype.feature_chats.presentation.ChatReactionViewModel +import com.anytypeio.anytype.feature_chats.ui.ChatReactionScreen import com.anytypeio.anytype.ui.settings.typography import javax.inject.Inject import kotlin.getValue diff --git a/app/src/main/java/com/anytypeio/anytype/ui/chats/SelectChatReactionFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/chats/SelectChatReactionFragment.kt index 085ca8afa3..0d4e226397 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/chats/SelectChatReactionFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/chats/SelectChatReactionFragment.kt @@ -16,8 +16,8 @@ import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.ext.arg import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment import com.anytypeio.anytype.di.common.componentManager -import com.anytypeio.anytype.feature_discussions.presentation.SelectChatReactionViewModel -import com.anytypeio.anytype.feature_discussions.ui.SelectChatReactionScreen +import com.anytypeio.anytype.feature_chats.presentation.SelectChatReactionViewModel +import com.anytypeio.anytype.feature_chats.ui.SelectChatReactionScreen import com.anytypeio.anytype.ui.settings.typography import javax.inject.Inject import kotlin.getValue diff --git a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt index 4bdc35aaf3..7b0a22e862 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenFragment.kt @@ -53,8 +53,8 @@ import com.anytypeio.anytype.core_utils.tools.FeatureToggles import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment import com.anytypeio.anytype.di.common.componentManager import com.anytypeio.anytype.ext.daggerViewModel -import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel -import com.anytypeio.anytype.feature_discussions.ui.DiscussionScreenWrapper +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel +import com.anytypeio.anytype.feature_chats.ui.ChatScreenWrapper import com.anytypeio.anytype.other.DefaultDeepLinkResolver import com.anytypeio.anytype.presentation.home.Command import com.anytypeio.anytype.presentation.home.HomeScreenViewModel @@ -140,7 +140,7 @@ class HomeScreenFragment : BaseComposeFragment(), val spaceLevelChatViewModel = daggerViewModel { component.get( key = space, - param = DiscussionViewModel.Params.SpaceLevelChat( + param = ChatViewModel.Params.SpaceLevelChat( space = Space(space) ) ).getViewModel() @@ -180,7 +180,7 @@ class HomeScreenFragment : BaseComposeFragment(), focus.clearFocus(force = true) PageWithWidgets(showSpaceWidget = false) } else if (page == 0) { - DiscussionScreenWrapper( + ChatScreenWrapper( isSpaceLevelChat = true, vm = spaceLevelChatViewModel, onAttachObjectClicked = { @@ -569,7 +569,7 @@ class HomeScreenFragment : BaseComposeFragment(), view = destination.view ) } - is Navigation.OpenDiscussion -> runCatching { + is Navigation.OpenChat -> runCatching { navigation().openDiscussion( target = destination.ctx, space = destination.space diff --git a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenToolbar.kt b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenToolbar.kt index 4de326c371..4002191915 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenToolbar.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/home/HomeScreenToolbar.kt @@ -2,7 +2,6 @@ package com.anytypeio.anytype.ui.home import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -14,7 +13,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource @@ -25,8 +23,7 @@ import com.anytypeio.anytype.core_ui.features.SpaceIconView import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium import com.anytypeio.anytype.core_ui.views.Relations2 -import com.anytypeio.anytype.core_ui.views.Relations3 -import com.anytypeio.anytype.feature_discussions.R +import com.anytypeio.anytype.feature_chats.R import com.anytypeio.anytype.presentation.spaces.SpaceIconView @Composable diff --git a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt index 4eb562e779..47d2e3438b 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/main/MainActivity.kt @@ -18,7 +18,6 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.NavOptions import androidx.navigation.NavOptions.* import androidx.navigation.findNavController -import androidx.navigation.fragment.findNavController import com.anytypeio.anytype.BuildConfig import com.anytypeio.anytype.R import com.anytypeio.anytype.analytics.base.EventsDictionary @@ -226,7 +225,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr Timber.e(it, "Error while editor navigation") } } - is OpenObjectNavigation.OpenDiscussion -> { + is OpenObjectNavigation.OpenChat -> { toast("Cannot open chat from here") } is OpenObjectNavigation.UnexpectedLayoutError -> { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/search/GlobalSearchFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/search/GlobalSearchFragment.kt index 57aea9996d..47691922ac 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/search/GlobalSearchFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/search/GlobalSearchFragment.kt @@ -22,7 +22,7 @@ import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior import com.anytypeio.anytype.core_utils.ext.toast 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.ui.chats.ChatFragment import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.search.GlobalSearchViewModel import com.anytypeio.anytype.ui.date.DateObjectFragment @@ -92,10 +92,10 @@ class GlobalSearchFragment : BaseBottomSheetComposeFragment() { ) ) } - is OpenObjectNavigation.OpenDiscussion -> { + is OpenObjectNavigation.OpenChat -> { findNavController().navigate( R.id.chatScreen, - DiscussionFragment.args( + ChatFragment.args( ctx = nav.target, space = nav.space ) diff --git a/app/src/main/res/navigation/graph.xml b/app/src/main/res/navigation/graph.xml index f7cbb4c7b3..58b49074c9 100644 --- a/app/src/main/res/navigation/graph.xml +++ b/app/src/main/res/navigation/graph.xml @@ -159,7 +159,7 @@ R.drawable.ic_empty_state_link - ObjectIcon.Empty.Discussion -> R.drawable.ic_empty_state_chat + ObjectIcon.Empty.Chat -> R.drawable.ic_empty_state_chat ObjectIcon.Empty.List -> R.drawable.ic_empty_state_list ObjectIcon.Empty.ObjectType -> R.drawable.ic_empty_state_type ObjectIcon.Empty.Page -> R.drawable.ic_empty_state_page diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconWidget.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconWidget.kt index fa82e469ae..482afdbf79 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconWidget.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/widgets/ObjectIconWidget.kt @@ -363,7 +363,7 @@ class ObjectIconWidget @JvmOverloads constructor( private fun ObjectIcon.Empty.setEmptyIcon() { val (drawable, containerBackground) = when (this) { ObjectIcon.Empty.Bookmark -> R.drawable.ic_empty_state_link to true - ObjectIcon.Empty.Discussion -> R.drawable.ic_empty_state_chat to true + ObjectIcon.Empty.Chat -> R.drawable.ic_empty_state_chat to true ObjectIcon.Empty.List -> R.drawable.ic_empty_state_list to true ObjectIcon.Empty.ObjectType -> R.drawable.ic_empty_state_type to true ObjectIcon.Empty.Page -> R.drawable.ic_empty_state_page to true diff --git a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModel.kt b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModel.kt index eb913384fd..c465846354 100644 --- a/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModel.kt +++ b/feature-all-content/src/main/java/com/anytypeio/anytype/feature_allcontent/presentation/AllContentViewModel.kt @@ -710,7 +710,7 @@ class AllContentViewModel( Timber.e("Unexpected layout: ${navigation.layout}") commands.emit(Command.SendToast.UnexpectedLayout(navigation.layout?.name.orEmpty())) } - is OpenObjectNavigation.OpenDiscussion -> { + is OpenObjectNavigation.OpenChat -> { commands.emit( Command.OpenChat( target = navigation.target, diff --git a/feature-discussions/build.gradle b/feature-chats/build.gradle similarity index 96% rename from feature-discussions/build.gradle rename to feature-chats/build.gradle index 67292eb5fc..31a760c343 100644 --- a/feature-discussions/build.gradle +++ b/feature-chats/build.gradle @@ -9,7 +9,7 @@ android { compose true } - namespace 'com.anytypeio.anytype.feature_discussions' + namespace 'com.anytypeio.anytype.feature_chats' testOptions { unitTests.returnDefaultValues = true diff --git a/feature-discussions/src/main/AndroidManifest.xml b/feature-chats/src/main/AndroidManifest.xml similarity index 100% rename from feature-discussions/src/main/AndroidManifest.xml rename to feature-chats/src/main/AndroidManifest.xml diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/ChatReactionViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatReactionViewModel.kt similarity index 98% rename from feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/ChatReactionViewModel.kt rename to feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatReactionViewModel.kt index 22ff63ff94..0a743d617f 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/ChatReactionViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatReactionViewModel.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.feature_discussions.presentation +package com.anytypeio.anytype.feature_chats.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionView.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatView.kt similarity index 94% rename from feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionView.kt rename to feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatView.kt index d5babe3542..8d88259894 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionView.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatView.kt @@ -1,19 +1,18 @@ -package com.anytypeio.anytype.feature_discussions.presentation +package com.anytypeio.anytype.feature_chats.presentation import com.anytypeio.anytype.core_models.Block import com.anytypeio.anytype.core_models.Hash import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.ObjectWrapper -import com.anytypeio.anytype.core_models.Url import com.anytypeio.anytype.presentation.objects.ObjectIcon import com.anytypeio.anytype.presentation.search.GlobalSearchItemView -sealed interface DiscussionView { +sealed interface ChatView { data class DateSection( val formattedDate: String, val timeInMillis: Long - ) : DiscussionView + ) : ChatView data class Message( val id: String, @@ -26,7 +25,7 @@ sealed interface DiscussionView { val isEdited: Boolean = false, val avatar: Avatar = Avatar.Initials(), val reply: Reply? = null - ) : DiscussionView { + ) : ChatView { data class Content(val msg: String, val parts: List) { data class Part( diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt similarity index 90% rename from feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionViewModel.kt rename to feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt index a4b96ba5ae..d89c5d0dbc 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.feature_discussions.presentation +package com.anytypeio.anytype.feature_chats.presentation import androidx.lifecycle.viewModelScope import com.anytypeio.anytype.core_models.Command @@ -46,7 +46,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -class DiscussionViewModel @Inject constructor( +class ChatViewModel @Inject constructor( private val vmParams: Params, private val setObjectDetails: SetObjectDetails, private val openObject: OpenObject, @@ -66,8 +66,8 @@ class DiscussionViewModel @Inject constructor( ) : BaseViewModel() { val name = MutableStateFlow(null) - val messages = MutableStateFlow>(emptyList()) - val chatBoxAttachments = MutableStateFlow>(emptyList()) + val messages = MutableStateFlow>(emptyList()) + val chatBoxAttachments = MutableStateFlow>(emptyList()) val commands = MutableSharedFlow() val navigation = MutableSharedFlow() val chatBoxMode = MutableStateFlow(ChatBoxMode.Default) @@ -129,8 +129,8 @@ class DiscussionViewModel @Inject constructor( chatContainer.fetchReplies(chat = chat) ) { result, dependencies, replies -> data.value = result - var previousDate: DiscussionView.DateSection? = null - buildList { + var previousDate: ChatView.DateSection? = null + buildList { result.forEach { msg -> val allMembers = members.get() val member = allMembers.let { type -> @@ -152,7 +152,7 @@ class DiscussionViewModel @Inject constructor( } else { val msg = replies[replyToId] if (msg != null) { - DiscussionView.Message.Reply( + ChatView.Message.Reply( msg = msg.id, text = msg.content?.text.orEmpty().ifEmpty { // Fallback to attachment name if empty @@ -184,16 +184,16 @@ class DiscussionViewModel @Inject constructor( } } - val view = DiscussionView.Message( + val view = ChatView.Message( id = msg.id, timestamp = msg.createdAt * 1000, - content = DiscussionView.Message.Content( + content = ChatView.Message.Content( msg = content?.text.orEmpty(), parts = content?.text .orEmpty() .splitByMarks(marks = content?.marks.orEmpty()) .map { (part, styles) -> - DiscussionView.Message.Content.Part( + ChatView.Message.Content.Part( part = part, styles = styles ) @@ -204,7 +204,7 @@ class DiscussionViewModel @Inject constructor( isUserAuthor = msg.creator == account, isEdited = msg.modifiedAt > msg.createdAt, reactions = msg.reactions.map { (emoji, ids) -> - DiscussionView.Message.Reaction( + ChatView.Message.Reaction( emoji = emoji, count = ids.size, isSelected = ids.contains(account) @@ -214,7 +214,7 @@ class DiscussionViewModel @Inject constructor( when (attachment.type) { Chat.Message.Attachment.Type.Image -> { val wrapper = dependencies[attachment.target] - DiscussionView.Message.Attachment.Image( + ChatView.Message.Attachment.Image( target = attachment.target, url = urlBuilder.medium(path = attachment.target), name = wrapper?.name.orEmpty(), @@ -224,7 +224,7 @@ class DiscussionViewModel @Inject constructor( else -> { val wrapper = dependencies[attachment.target] if (wrapper?.layout == ObjectType.Layout.IMAGE) { - DiscussionView.Message.Attachment.Image( + ChatView.Message.Attachment.Image( target = attachment.target, url = urlBuilder.large(path = attachment.target), name = wrapper.name.orEmpty(), @@ -232,7 +232,7 @@ class DiscussionViewModel @Inject constructor( ) } else { val type = wrapper?.type?.firstOrNull() - DiscussionView.Message.Attachment.Link( + ChatView.Message.Attachment.Link( target = attachment.target, wrapper = wrapper, icon = wrapper?.objectIcon(urlBuilder) ?: ObjectIcon.None, @@ -246,14 +246,14 @@ class DiscussionViewModel @Inject constructor( } }, avatar = if (member != null && !member.iconImage.isNullOrEmpty()) { - DiscussionView.Message.Avatar.Image( + ChatView.Message.Avatar.Image( urlBuilder.thumbnail(member.iconImage!!) ) } else { - DiscussionView.Message.Avatar.Initials(member?.name.orEmpty()) + ChatView.Message.Avatar.Initials(member?.name.orEmpty()) } ) - val currDate = DiscussionView.DateSection( + val currDate = ChatView.DateSection( formattedDate = dateFormatter.format(msg.createdAt * 1000), timeInMillis = msg.createdAt * 1000L ) @@ -275,7 +275,7 @@ class DiscussionViewModel @Inject constructor( val attachments = buildList { chatBoxAttachments.value.forEach { attachment -> when(attachment) { - is DiscussionView.Message.ChatBoxAttachment.Link -> { + is ChatView.Message.ChatBoxAttachment.Link -> { add( Chat.Message.Attachment( target = attachment.target, @@ -283,7 +283,7 @@ class DiscussionViewModel @Inject constructor( ) ) } - is DiscussionView.Message.ChatBoxAttachment.Media -> { + is ChatView.Message.ChatBoxAttachment.Media -> { uploadFile.async( UploadFile.Params( space = vmParams.space, @@ -298,7 +298,7 @@ class DiscussionViewModel @Inject constructor( ) } } - is DiscussionView.Message.ChatBoxAttachment.File -> { + is ChatView.Message.ChatBoxAttachment.File -> { val path = withContext(dispatchers.io) { copyFileToCacheDirectory.copy(attachment.uri) } @@ -391,7 +391,7 @@ class DiscussionViewModel @Inject constructor( } } - fun onRequestEditMessageClicked(msg: DiscussionView.Message) { + fun onRequestEditMessageClicked(msg: ChatView.Message) { Timber.d("onRequestEditMessageClicked") viewModelScope.launch { chatBoxMode.value = ChatBoxMode.EditMessage(msg.id) @@ -419,14 +419,14 @@ class DiscussionViewModel @Inject constructor( fun onAttachObject(obj: GlobalSearchItemView) { chatBoxAttachments.value = chatBoxAttachments.value + listOf( - DiscussionView.Message.ChatBoxAttachment.Link( + ChatView.Message.ChatBoxAttachment.Link( target = obj.id, wrapper = obj ) ) } - fun onClearAttachmentClicked(attachment: DiscussionView.Message.ChatBoxAttachment) { + fun onClearAttachmentClicked(attachment: ChatView.Message.ChatBoxAttachment) { chatBoxAttachments.value = chatBoxAttachments.value.filter { it != attachment } @@ -441,7 +441,7 @@ class DiscussionViewModel @Inject constructor( fun onReacted(msg: Id, reaction: String) { Timber.d("onReacted") viewModelScope.launch { - val message = messages.value.find { it is DiscussionView.Message && it.id == msg } + val message = messages.value.find { it is ChatView.Message && it.id == msg } if (message != null) { toggleChatMessageReaction.async( Command.ChatCommand.ToggleMessageReaction( @@ -458,7 +458,7 @@ class DiscussionViewModel @Inject constructor( } } - fun onReplyMessage(msg: DiscussionView.Message) { + fun onReplyMessage(msg: ChatView.Message) { viewModelScope.launch { chatBoxMode.value = ChatBoxMode.Reply( msg = msg.id, @@ -467,14 +467,14 @@ class DiscussionViewModel @Inject constructor( if (msg.attachments.isNotEmpty()) { val attachment = msg.attachments.last() when(attachment) { - is DiscussionView.Message.Attachment.Image -> { + is ChatView.Message.Attachment.Image -> { if (attachment.ext.isNotEmpty()) { "${attachment.name}.${attachment.ext}" } else { attachment.name } } - is DiscussionView.Message.Attachment.Link -> { + is ChatView.Message.Attachment.Link -> { attachment.wrapper?.name.orEmpty() } } @@ -487,7 +487,7 @@ class DiscussionViewModel @Inject constructor( } } - fun onDeleteMessage(msg: DiscussionView.Message) { + fun onDeleteMessage(msg: ChatView.Message) { Timber.d("onDeleteMessageClicked") viewModelScope.launch { deleteChatMessage.async( @@ -501,18 +501,18 @@ class DiscussionViewModel @Inject constructor( } } - fun onAttachmentClicked(attachment: DiscussionView.Message.Attachment) { + fun onAttachmentClicked(attachment: ChatView.Message.Attachment) { Timber.d("onAttachmentClicked") viewModelScope.launch { when(attachment) { - is DiscussionView.Message.Attachment.Image -> { + is ChatView.Message.Attachment.Image -> { commands.emit( UXCommand.OpenFullScreenImage( url = urlBuilder.original(attachment.target) ) ) } - is DiscussionView.Message.Attachment.Link -> { + is ChatView.Message.Attachment.Link -> { val wrapper = attachment.wrapper if (wrapper != null) { navigation.emit(wrapper.navigation()) @@ -527,7 +527,7 @@ class DiscussionViewModel @Inject constructor( fun onChatBoxMediaPicked(uris: List) { Timber.d("onChatBoxMediaPicked: $uris") chatBoxAttachments.value = chatBoxAttachments.value + uris.map { - DiscussionView.Message.ChatBoxAttachment.Media( + ChatView.Message.ChatBoxAttachment.Media( uri = it ) } @@ -536,7 +536,7 @@ class DiscussionViewModel @Inject constructor( fun onChatBoxFilePicked(infos: List) { Timber.d("onChatBoxFilePicked: $infos") chatBoxAttachments.value = chatBoxAttachments.value + infos.map { info -> - DiscussionView.Message.ChatBoxAttachment.File( + ChatView.Message.ChatBoxAttachment.File( uri = info.uri, name = info.name, size = info.size diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionViewModelFactory.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt similarity index 87% rename from feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionViewModelFactory.kt rename to feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt index b26165bd8c..b4bf8de785 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/DiscussionViewModelFactory.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModelFactory.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.feature_discussions.presentation +package com.anytypeio.anytype.feature_chats.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -9,7 +9,6 @@ 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.media.FileDrop import com.anytypeio.anytype.domain.media.UploadFile import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.multiplayer.ActiveSpaceMemberSubscriptionContainer @@ -17,13 +16,11 @@ import com.anytypeio.anytype.domain.multiplayer.SpaceViewSubscriptionContainer import com.anytypeio.anytype.domain.`object`.OpenObject import com.anytypeio.anytype.domain.`object`.SetObjectDetails import com.anytypeio.anytype.domain.objects.StoreOfObjectTypes -import com.anytypeio.anytype.emojifier.data.EmojiProvider -import com.anytypeio.anytype.presentation.common.BaseViewModel import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory import javax.inject.Inject -class DiscussionViewModelFactory @Inject constructor( - private val params: DiscussionViewModel.Params, +class ChatViewModelFactory @Inject constructor( + private val params: ChatViewModel.Params, private val setObjectDetails: SetObjectDetails, private val openObject: OpenObject, private val chatContainer: ChatContainer, @@ -41,7 +38,7 @@ class DiscussionViewModelFactory @Inject constructor( private val copyFileToCacheDirectory: CopyFileToCacheDirectory ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T = DiscussionViewModel( + override fun create(modelClass: Class): T = ChatViewModel( vmParams = params, setObjectDetails = setObjectDetails, openObject = openObject, diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/SelectChatReactionViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/SelectChatReactionViewModel.kt similarity index 98% rename from feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/SelectChatReactionViewModel.kt rename to feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/SelectChatReactionViewModel.kt index cbb50c55ef..ca616329e0 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/presentation/SelectChatReactionViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/SelectChatReactionViewModel.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.feature_discussions.presentation +package com.anytypeio.anytype.feature_chats.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Attachments.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Attachments.kt new file mode 100644 index 0000000000..bbb34ef295 --- /dev/null +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Attachments.kt @@ -0,0 +1,184 @@ +package com.anytypeio.anytype.feature_chats.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium +import com.anytypeio.anytype.core_ui.views.Relations3 +import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon +import com.anytypeio.anytype.feature_chats.R +import com.anytypeio.anytype.feature_chats.presentation.ChatView +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi +import com.bumptech.glide.integration.compose.GlideImage + +@Composable +@OptIn(ExperimentalGlideComposeApi::class) +fun BubbleAttachments( + attachments: List, + onAttachmentClicked: (ChatView.Message.Attachment) -> Unit, + isUserAuthor: Boolean +) { + attachments.forEachIndexed { idx, attachment -> + when (attachment) { + is ChatView.Message.Attachment.Image -> { + Box( + modifier = Modifier + .padding( + start = 4.dp, + end = 4.dp, + bottom = 4.dp, + top = if (idx == 0) 4.dp else 0.dp + ) + .size(300.dp) + .background( + color = colorResource(R.color.shape_tertiary), + shape = RoundedCornerShape(16.dp) + ) + ) { + CircularProgressIndicator( + modifier = Modifier + .align(alignment = Alignment.Center) + .size(64.dp), + color = colorResource(R.color.glyph_active), + trackColor = colorResource(R.color.glyph_active).copy(alpha = 0.5f), + strokeWidth = 8.dp + ) + GlideImage( + model = attachment.url, + contentDescription = "Attachment image", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(300.dp) + .clip(shape = RoundedCornerShape(16.dp)) + .clickable { + onAttachmentClicked(attachment) + } + ) + } + } + is ChatView.Message.Attachment.Link -> { + AttachedObject( + modifier = Modifier + .padding( + start = 4.dp, + end = 4.dp, + bottom = 4.dp, + top = if (idx == 0) 4.dp else 0.dp + ) + .fillMaxWidth() + , + title = attachment.wrapper?.name.orEmpty(), + type = attachment.typeName, + icon = attachment.icon, + onAttachmentClicked = { + onAttachmentClicked(attachment) + } + ) + } + } + } +} + +@Composable +fun AttachedObject( + modifier: Modifier, + title: String, + type: String, + icon: ObjectIcon, + onAttachmentClicked: () -> Unit +) { + Box( + modifier = modifier + .height(72.dp) + .clip(RoundedCornerShape(18.dp)) + .border( + width = 1.dp, + color = colorResource(id = R.color.shape_transparent_secondary), + shape = RoundedCornerShape(18.dp) + ) + .background( + color = colorResource(id = R.color.background_secondary) + ) + .clickable { + onAttachmentClicked() + } + ) { + ListWidgetObjectIcon( + icon = icon, + iconSize = 48.dp, + modifier = Modifier + .padding( + start = 12.dp + ) + .align(alignment = Alignment.CenterStart), + onTaskIconClicked = { + // Do nothing + } + ) + Text( + text = title.ifEmpty { stringResource(R.string.untitled) }, + modifier = Modifier.padding( + start = if (icon != ObjectIcon.None) + 72.dp + else + 12.dp, + top = 17.5.dp, + end = 12.dp + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = PreviewTitle2Medium, + color = colorResource(id = R.color.text_primary) + ) + Text( + text = type.ifEmpty { stringResource(R.string.unknown_type) }, + modifier = Modifier + .align(Alignment.BottomStart) + .padding( + start = if (icon != ObjectIcon.None) + 72.dp + else + 12.dp, + bottom = 17.5.dp, + end = 12.dp + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = Relations3, + color = colorResource(id = R.color.text_secondary) + ) + } +} + +@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 AttachmentPreview() { + AttachedObject( + modifier = Modifier.fillMaxWidth(), + icon = ObjectIcon.None, + type = "Project", + title = "Travel to Switzerland", + onAttachmentClicked = {} + ) +} \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt new file mode 100644 index 0000000000..9c1215ee5b --- /dev/null +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBox.kt @@ -0,0 +1,470 @@ +package com.anytypeio.anytype.feature_chats.ui + +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.DropdownMenu +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.foundation.noRippleClickable +import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.views.Caption1Medium +import com.anytypeio.anytype.core_ui.views.Caption1Regular +import com.anytypeio.anytype.feature_chats.R +import com.anytypeio.anytype.feature_chats.presentation.ChatConfig +import com.anytypeio.anytype.feature_chats.presentation.ChatView +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel.ChatBoxMode +import com.anytypeio.anytype.presentation.objects.ObjectIcon +import kotlin.collections.forEach +import kotlinx.coroutines.launch + +@Composable +fun ChatBox( + mode: ChatBoxMode = ChatBoxMode.Default, + modifier: Modifier = Modifier, + onBackButtonClicked: () -> Unit, + chatBoxFocusRequester: FocusRequester, + textState: TextFieldValue, + onMessageSent: (String) -> Unit = {}, + onAttachClicked: () -> Unit = {}, + resetScroll: () -> Unit = {}, + isTitleFocused: Boolean, + attachments: List, + clearText: () -> Unit, + updateValue: (TextFieldValue) -> Unit, + onAttachObjectClicked: () -> Unit, + onAttachMediaClicked: () -> Unit, + onAttachFileClicked: () -> Unit, + onUploadAttachmentClicked: () -> Unit, + onClearAttachmentClicked: (ChatView.Message.ChatBoxAttachment) -> Unit, + onClearReplyClicked: () -> Unit, + onChatBoxMediaPicked: (List) -> Unit, + onChatBoxFilePicked: (List) -> Unit, + onExitEditMessageMode: () -> Unit +) { + + val uploadMediaLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.PickMultipleVisualMedia(maxItems = ChatConfig.MAX_ATTACHMENT_COUNT) + ) { + onChatBoxMediaPicked(it) + } + + val uploadFileLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.OpenMultipleDocuments() + ) { uris -> + onChatBoxFilePicked(uris.take(ChatConfig.MAX_ATTACHMENT_COUNT)) + } + + var showDropdownMenu by remember { mutableStateOf(false) } + + val scope = rememberCoroutineScope() + + val focus = LocalFocusManager.current + + Column( + modifier = modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 56.dp) + .padding( + start = 12.dp, + end = 12.dp, + bottom = 20.dp + ) + .background( + color = colorResource(R.color.navigation_panel), + shape = RoundedCornerShape(16.dp) + ) + ) { + if (mode is ChatBoxMode.EditMessage) { + EditMessageToolbar( + onExitClicked = onExitEditMessageMode + ) + } + LazyRow( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + attachments.forEach { attachment -> + when(attachment) { + is ChatView.Message.ChatBoxAttachment.Link -> { + item { + Box { + AttachedObject( + modifier = Modifier + .padding( + top = 12.dp, + end = 4.dp + ) + .width(216.dp), + title = attachment.wrapper.title, + type = attachment.wrapper.type, + icon = attachment.wrapper.icon, + onAttachmentClicked = { + // TODO + } + ) + Image( + painter = painterResource(id = R.drawable.ic_clear_chatbox_attachment), + contentDescription = "Close icon", + modifier = Modifier + .align( + Alignment.TopEnd + ) + .padding(top = 6.dp) + .noRippleClickable { + onClearAttachmentClicked(attachment) + } + ) + } + } + } + is ChatView.Message.ChatBoxAttachment.Media -> { + item { + Box(modifier = Modifier.padding()) { + Image( + painter = rememberAsyncImagePainter(attachment.uri), + contentDescription = null, + modifier = Modifier + .padding( + top = 12.dp, + end = 4.dp + ) + .size(72.dp) + .clip(RoundedCornerShape(8.dp)) + + , + contentScale = ContentScale.Crop + ) + Image( + painter = painterResource(R.drawable.ic_clear_chatbox_attachment), + contentDescription = "Clear attachment icon", + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 6.dp) + .noRippleClickable { + onClearAttachmentClicked(attachment) + } + ) + } + } + } + is ChatView.Message.ChatBoxAttachment.File -> { + item { + Box { + AttachedObject( + modifier = Modifier + .padding( + top = 12.dp, + end = 4.dp + ) + .width(216.dp), + title = attachment.name, + type = stringResource(R.string.file), + icon = ObjectIcon.File( + mime = null, + fileName = null + ), + onAttachmentClicked = { + // TODO + } + ) + Image( + painter = painterResource(id = R.drawable.ic_clear_chatbox_attachment), + contentDescription = "Close icon", + modifier = Modifier + .align( + Alignment.TopEnd + ) + .padding(top = 6.dp) + .noRippleClickable { + onClearAttachmentClicked(attachment) + } + ) + } + } + } + } + } + } + when(mode) { + is ChatBoxMode.Default -> { + + } + is ChatBoxMode.EditMessage -> { + + } + is ChatBoxMode.Reply -> { + Box( + modifier = Modifier + .fillMaxWidth() + .height(54.dp) + ) { + Text( + text = "Reply to ${mode.author}", + modifier = Modifier.padding( + start = 12.dp, + top = 8.dp, + end = 44.dp + ), + style = Caption1Medium, + color = colorResource(R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = mode.text, + modifier = Modifier.padding( + start = 12.dp, + top = 28.dp, + end = 44.dp + ), + style = Caption1Regular, + color = colorResource(R.color.text_primary), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Image( + painter = painterResource(R.drawable.ic_chat_close_chat_box_reply), + contentDescription = "Clear reply to icon", + modifier = Modifier + .padding(end = 12.dp) + .align(Alignment.CenterEnd) + .clickable { + onClearReplyClicked() + } + ) + } + } + } + Row { + Box( + modifier = Modifier + .padding(horizontal = 4.dp, vertical = 8.dp) + .clip(CircleShape) + .align(Alignment.Bottom) + .clickable { + scope.launch { + focus.clearFocus(force = true) + showDropdownMenu = true + } + } + ) { + Image( + painter = painterResource(id = R.drawable.ic_chat_box_add_attachment), + contentDescription = "Plus button", + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 4.dp, vertical = 4.dp) + ) + if (attachments.size < ChatConfig.MAX_ATTACHMENT_COUNT) { + MaterialTheme( + shapes = MaterialTheme.shapes.copy( + medium = RoundedCornerShape( + 12.dp + ) + ), + colors = MaterialTheme.colors.copy( + surface = colorResource(id = R.color.background_secondary) + ) + ) { + DropdownMenu( + offset = DpOffset(8.dp, 40.dp), + expanded = showDropdownMenu, + onDismissRequest = { + showDropdownMenu = false + }, + modifier = Modifier + .align(Alignment.BottomEnd) + .defaultMinSize( + minWidth = 252.dp + ) + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.chat_attachment_object), + color = colorResource(id = R.color.text_primary) + ) + }, + onClick = { + showDropdownMenu = false + onAttachObjectClicked() + } + ) + Divider( + paddingStart = 0.dp, + paddingEnd = 0.dp + ) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.chat_attachment_media), + color = colorResource(id = R.color.text_primary) + ) + }, + onClick = { + showDropdownMenu = false + uploadMediaLauncher.launch( + PickVisualMediaRequest(mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + } + ) + Divider( + paddingStart = 0.dp, + paddingEnd = 0.dp + ) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.chat_attachment_file), + color = colorResource(id = R.color.text_primary) + ) + }, + onClick = { + showDropdownMenu = false + uploadFileLauncher.launch( + arrayOf("*/*") + ) + } + ) + } + } + } + } + ChatBoxUserInput( + textState = textState, + onMessageSent = { + onMessageSent(it) + clearText() + resetScroll() + }, + onTextChanged = { value -> + updateValue(value) + }, + modifier = Modifier + .weight(1f) + .align(Alignment.Bottom) + .focusRequester(chatBoxFocusRequester) + ) + AnimatedVisibility( + visible = attachments.isNotEmpty() || textState.text.isNotEmpty(), + exit = fadeOut() + scaleOut(), + enter = fadeIn() + scaleIn(), + modifier = Modifier.align(Alignment.Bottom) + ) { + Box( + modifier = Modifier + .padding(horizontal = 4.dp, vertical = 8.dp) + .clip(CircleShape) + .clickable { + onMessageSent(textState.text) + clearText() + resetScroll() + } + ) { + Image( + painter = painterResource(id = R.drawable.ic_send_message), + contentDescription = "Send message button", + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 4.dp, vertical = 4.dp) + ) + } + } + } + } +} + +@Composable +private fun ChatBoxUserInput( + modifier: Modifier, + textState: TextFieldValue, + onMessageSent: (String) -> Unit, + onTextChanged: (TextFieldValue) -> Unit, +) { + BasicTextField( + value = textState, + onValueChange = { onTextChanged(it) }, + textStyle = BodyRegular.copy( + color = colorResource(id = R.color.text_primary) + ), + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Send + ), + keyboardActions = KeyboardActions { + if (textState.text.isNotBlank()) { + onMessageSent(textState.text) + } + }, + modifier = modifier + .padding( + start = 4.dp, + end = 4.dp, + top = 16.dp, + bottom = 16.dp + ) + , + cursorBrush = SolidColor(colorResource(id = R.color.palette_system_blue)), + maxLines = 5, + decorationBox = @Composable { innerTextField -> + DefaultHintDecorationBox( + text = textState.text, + hint = stringResource(R.string.write_a_message), + innerTextField = innerTextField, + textStyle = BodyRegular.copy(color = colorResource(R.color.text_tertiary)) + ) + } + ) +} \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt new file mode 100644 index 0000000000..14c90c6682 --- /dev/null +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatBubble.kt @@ -0,0 +1,421 @@ +package com.anytypeio.anytype.feature_chats.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.DropdownMenu +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.anytypeio.anytype.core_ui.foundation.AlertConfig +import com.anytypeio.anytype.core_ui.foundation.BUTTON_SECONDARY +import com.anytypeio.anytype.core_ui.foundation.BUTTON_WARNING +import com.anytypeio.anytype.core_ui.foundation.Divider +import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED +import com.anytypeio.anytype.core_ui.foundation.GenericAlert +import com.anytypeio.anytype.core_ui.views.BodyRegular +import com.anytypeio.anytype.core_ui.views.Caption1Medium +import com.anytypeio.anytype.core_ui.views.Caption1Regular +import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium +import com.anytypeio.anytype.core_ui.views.fontIBM +import com.anytypeio.anytype.core_utils.const.DateConst.TIME_H24 +import com.anytypeio.anytype.core_utils.ext.formatTimeInMillis +import com.anytypeio.anytype.feature_chats.R +import com.anytypeio.anytype.feature_chats.presentation.ChatView +import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi + +@OptIn(ExperimentalGlideComposeApi::class, ExperimentalMaterial3Api::class) +@Composable +fun Bubble( + modifier: Modifier = Modifier, + name: String, + reply: ChatView.Message.Reply? = null, + content: ChatView.Message.Content, + timestamp: Long, + attachments: List = emptyList(), + isUserAuthor: Boolean = false, + isEdited: Boolean = false, + reactions: List = emptyList(), + onReacted: (String) -> Unit, + onDeleteMessage: () -> Unit, + onCopyMessage: () -> Unit, + onEditMessage: () -> Unit, + onReply: () -> Unit, + onAttachmentClicked: (ChatView.Message.Attachment) -> Unit, + onMarkupLinkClicked: (String) -> Unit, + onScrollToReplyClicked: (ChatView.Message.Reply) -> Unit, + onAddReactionClicked: () -> Unit, + onViewChatReaction: (String) -> Unit +) { + var showDropdownMenu by remember { mutableStateOf(false) } + var showDeleteMessageWarning by remember { mutableStateOf(false) } + if (showDeleteMessageWarning) { + ModalBottomSheet( + onDismissRequest = { + showDeleteMessageWarning = false + }, + containerColor = colorResource(id = R.color.background_secondary), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + dragHandle = null + ) { + GenericAlert( + config = AlertConfig.WithTwoButtons( + title = stringResource(R.string.chats_alert_delete_this_message), + description = stringResource(R.string.chats_alert_delete_this_message_description), + firstButtonText = stringResource(R.string.cancel), + secondButtonText = stringResource(R.string.delete), + secondButtonType = BUTTON_WARNING, + firstButtonType = BUTTON_SECONDARY, + icon = AlertConfig.Icon( + gradient = GRADIENT_TYPE_RED, + icon = R.drawable.ic_alert_question_warning + ) + ), + onFirstButtonClicked = { + showDeleteMessageWarning = false + }, + onSecondButtonClicked = { + onDeleteMessage() + } + ) + } + } + Column( + modifier = modifier + .width(IntrinsicSize.Max) + .background( + color = if (isUserAuthor) + colorResource(R.color.background_primary) + else + colorResource(R.color.shape_transparent_secondary), + shape = RoundedCornerShape(20.dp) + ) + .clip(RoundedCornerShape(20.dp)) + .clickable { + showDropdownMenu = !showDropdownMenu + } + ) { + if (reply != null) { + Box( + modifier = Modifier + .padding(4.dp) + .fillMaxWidth() + .height(52.dp) + .background( + color = colorResource(R.color.shape_transparent_secondary), + shape = RoundedCornerShape(16.dp) + ) + .clip(RoundedCornerShape(16.dp)) + .clickable { + onScrollToReplyClicked(reply) + } + ) { + Box( + modifier = Modifier + .width(4.dp) + .fillMaxHeight() + .background( + color = colorResource(R.color.shape_transparent_primary) + ) + ) + Text( + text = reply.author, + modifier = Modifier.padding( + start = 12.dp, + top = 8.dp, + end = 12.dp + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = colorResource(id = R.color.text_primary), + style = Caption1Medium + ) + Text( + modifier = Modifier.padding( + start = 12.dp, + top = 26.dp, + end = 12.dp + ), + text = reply.text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = colorResource(id = R.color.text_primary), + style = Caption1Regular + ) + } + } + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = 12.dp, + end = 12.dp, + top = if (reply == null) 12.dp else 0.dp + ), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = name, + style = PreviewTitle2Medium, + color = colorResource(id = R.color.text_primary), + maxLines = 1 + ) + Spacer(Modifier.width(12.dp)) + Text( + modifier = Modifier.padding(top = 1.dp), + text = timestamp.formatTimeInMillis( + TIME_H24 + ), + style = Caption1Regular, + color = colorResource(id = R.color.text_secondary), + maxLines = 1 + ) + } + if (content.msg.isNotEmpty()) { + Text( + modifier = Modifier.padding( + top = 0.dp, + start = 12.dp, + end = 12.dp, + bottom = if (reactions.isEmpty() && attachments.isEmpty()) 12.dp else 0.dp + ), + text = buildAnnotatedString { + content.parts.forEach { part -> + if (part.link != null && part.link.param != null) { + withLink( + LinkAnnotation.Clickable( + tag = "link", + styles = TextLinkStyles( + style = SpanStyle( + fontWeight = if (part.isBold) FontWeight.Bold else null, + fontStyle = if (part.isItalic) FontStyle.Italic else null, + textDecoration = TextDecoration.Underline + ) + ) + ) { + onMarkupLinkClicked(part.link.param.orEmpty()) + } + ) { + append(part.part) + } + } else { + withStyle( + style = SpanStyle( + fontWeight = if (part.isBold) FontWeight.Bold else null, + fontStyle = if (part.isItalic) FontStyle.Italic else null, + textDecoration = if (part.underline) + TextDecoration.Underline + else if (part.isStrike) + TextDecoration.LineThrough + else null, + fontFamily = if (part.isCode) fontIBM else null, + ) + ) { + append(part.part) + } + } + } + if (isEdited) { + withStyle( + style = SpanStyle(color = colorResource(id = R.color.text_tertiary)) + ) { + append( + " (${stringResource(R.string.chats_message_edited)})" + ) + } + } + }, + style = BodyRegular, + color = colorResource(id = R.color.text_primary), + ) + } + BubbleAttachments( + attachments = attachments, + isUserAuthor = isUserAuthor, + onAttachmentClicked = onAttachmentClicked + ) + if (reactions.isNotEmpty()) { + ReactionList( + reactions = reactions, + onReacted = onReacted, + onViewReaction = onViewChatReaction + ) + } + MaterialTheme( + shapes = MaterialTheme.shapes.copy( + medium = RoundedCornerShape( + 16.dp + ) + ), + colors = MaterialTheme.colors.copy( + surface = colorResource(id = R.color.background_secondary) + ) + ) { + DropdownMenu( + offset = DpOffset(0.dp, 8.dp), + expanded = showDropdownMenu, + onDismissRequest = { + showDropdownMenu = false + } + ) { + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.chats_add_reaction), + color = colorResource(id = R.color.text_primary), + modifier = Modifier.padding(end = 64.dp) + ) + }, + onClick = { + onAddReactionClicked() + showDropdownMenu = false + } + ) + Divider(paddingStart = 0.dp, paddingEnd = 0.dp) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.chats_reply), + color = colorResource(id = R.color.text_primary), + modifier = Modifier.padding(end = 64.dp) + ) + }, + onClick = { + onReply() + showDropdownMenu = false + } + ) + if (content.msg.isNotEmpty()) { + Divider(paddingStart = 0.dp, paddingEnd = 0.dp) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.copy), + color = colorResource(id = R.color.text_primary), + modifier = Modifier.padding(end = 64.dp) + ) + }, + onClick = { + onCopyMessage() + showDropdownMenu = false + } + ) + } + if (isUserAuthor) { + Divider(paddingStart = 0.dp, paddingEnd = 0.dp) + DropdownMenuItem( + text = { + Text( + text = stringResource(R.string.edit), + color = colorResource(id = R.color.text_primary), + modifier = Modifier.padding(end = 64.dp) + ) + }, + onClick = { + onEditMessage() + showDropdownMenu = false + } + ) + } + if (isUserAuthor) { + Divider(paddingStart = 0.dp, paddingEnd = 0.dp) + DropdownMenuItem( + text = { + Text( + text = stringResource(id = R.string.delete), + color = colorResource(id = R.color.palette_system_red), + modifier = Modifier.padding(end = 64.dp) + ) + }, + onClick = { + showDeleteMessageWarning = true + showDropdownMenu = false + } + ) + } + } + } + } +} + +@Composable +fun ChatUserAvatar( + msg: ChatView.Message, + avatar: ChatView.Message.Avatar, + modifier: Modifier +) { + Box( + modifier = modifier + .size(32.dp) + .background( + color = colorResource(id = R.color.text_tertiary), + shape = CircleShape + ) + ) { + Text( + text = msg.author.take(1).uppercase().ifEmpty { stringResource(id = R.string.u) }, + modifier = Modifier.align(Alignment.Center), + style = TextStyle( + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold, + color = colorResource(id = R.color.text_white) + ) + ) + if (avatar is ChatView.Message.Avatar.Image) { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(avatar.hash) + .crossfade(true) + .build(), + contentDescription = "Space member profile icon", + modifier = modifier + .size(32.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + } + } +} \ No newline at end of file diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/DiscussionPreviews.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatPreviews.kt similarity index 83% rename from feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/DiscussionPreviews.kt rename to feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatPreviews.kt index 68592afcb8..d037659428 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/DiscussionPreviews.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatPreviews.kt @@ -1,14 +1,13 @@ -package com.anytypeio.anytype.feature_discussions.ui +package com.anytypeio.anytype.feature_chats.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 com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel +import com.anytypeio.anytype.feature_chats.R +import com.anytypeio.anytype.feature_chats.presentation.ChatView +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -18,12 +17,12 @@ import kotlin.time.toDuration fun DiscussionPreview() { Messages( messages = listOf( - DiscussionView.Message( + ChatView.Message( id = "1", - content = DiscussionView.Message.Content( + content = ChatView.Message.Content( msg = stringResource(id = R.string.default_text_placeholder), parts = listOf( - DiscussionView.Message.Content.Part( + ChatView.Message.Content.Part( part = stringResource(id = R.string.default_text_placeholder) ) ) @@ -31,12 +30,12 @@ fun DiscussionPreview() { author = "Walter", timestamp = System.currentTimeMillis() ), - DiscussionView.Message( + ChatView.Message( id = "2", - content = DiscussionView.Message.Content( + content = ChatView.Message.Content( msg = stringResource(id = R.string.default_text_placeholder), parts = listOf( - DiscussionView.Message.Content.Part( + ChatView.Message.Content.Part( part = stringResource(id = R.string.default_text_placeholder) ) ) @@ -44,12 +43,12 @@ fun DiscussionPreview() { author = "Leo", timestamp = System.currentTimeMillis() ), - DiscussionView.Message( + ChatView.Message( id = "3", - content = DiscussionView.Message.Content( + content = ChatView.Message.Content( msg = stringResource(id = R.string.default_text_placeholder), parts = listOf( - DiscussionView.Message.Content.Part( + ChatView.Message.Content.Part( part = stringResource(id = R.string.default_text_placeholder) ) ) @@ -78,17 +77,17 @@ fun DiscussionPreview() { @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Dark Mode") @Composable fun DiscussionScreenPreview() { - DiscussionScreen( + ChatScreen( title = "Conversations with friends", messages = buildList { repeat(30) { idx -> add( - DiscussionView.Message( + ChatView.Message( id = idx.toString(), - content = DiscussionView.Message.Content( + content = ChatView.Message.Content( msg = stringResource(id = R.string.default_text_placeholder), parts = listOf( - DiscussionView.Message.Content.Part( + ChatView.Message.Content.Part( part = stringResource(id = R.string.default_text_placeholder) ) ) @@ -122,7 +121,7 @@ fun DiscussionScreenPreview() { onAttachMediaClicked = {}, onAttachObjectClicked = {}, onReplyMessage = {}, - chatBoxMode = DiscussionViewModel.ChatBoxMode.Default, + chatBoxMode = ChatViewModel.ChatBoxMode.Default, onClearReplyClicked = {}, onChatBoxMediaPicked = {}, onChatBoxFilePicked = {}, @@ -137,10 +136,10 @@ fun DiscussionScreenPreview() { fun BubblePreview() { Bubble( name = "Leo Marx", - content = DiscussionView.Message.Content( + content = ChatView.Message.Content( msg = stringResource(id = R.string.default_text_placeholder), parts = listOf( - DiscussionView.Message.Content.Part( + ChatView.Message.Content.Part( part = stringResource(id = R.string.default_text_placeholder) ) ) @@ -165,10 +164,10 @@ fun BubblePreview() { fun BubbleEditedPreview() { Bubble( name = "Leo Marx", - content = DiscussionView.Message.Content( + content = ChatView.Message.Content( msg = stringResource(id = R.string.default_text_placeholder), parts = listOf( - DiscussionView.Message.Content.Part( + ChatView.Message.Content.Part( part = stringResource(id = R.string.default_text_placeholder) ) ) @@ -194,10 +193,10 @@ fun BubbleEditedPreview() { fun BubbleWithAttachmentPreview() { Bubble( name = "Leo Marx", - content = DiscussionView.Message.Content( + content = ChatView.Message.Content( msg = stringResource(id = R.string.default_text_placeholder), parts = listOf( - DiscussionView.Message.Content.Part( + ChatView.Message.Content.Part( part = stringResource(id = R.string.default_text_placeholder) ) ) @@ -208,7 +207,7 @@ fun BubbleWithAttachmentPreview() { onCopyMessage = {}, attachments = buildList { add( - DiscussionView.Message.Attachment.Link( + ChatView.Message.Attachment.Link( target = "ID", wrapper = null, typeName = "Page" diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionPicker.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatReactionPicker.kt similarity index 95% rename from feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionPicker.kt rename to feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatReactionPicker.kt index ba2bfcf441..6350fed19e 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionPicker.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatReactionPicker.kt @@ -1,4 +1,4 @@ -package com.anytypeio.anytype.feature_discussions.ui +package com.anytypeio.anytype.feature_chats.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -22,13 +22,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.anytypeio.anytype.core_ui.common.DefaultPreviews -import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.views.Caption1Medium import com.anytypeio.anytype.emojifier.data.Emoji -import com.anytypeio.anytype.feature_discussions.R -import com.anytypeio.anytype.feature_discussions.presentation.SelectChatReactionViewModel.ReactionPickerView +import com.anytypeio.anytype.feature_chats.R +import com.anytypeio.anytype.feature_chats.presentation.SelectChatReactionViewModel.ReactionPickerView import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi import com.bumptech.glide.integration.compose.GlideImage diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionScreen.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatReactionScreen.kt similarity index 97% rename from feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionScreen.kt rename to feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatReactionScreen.kt index cee0a22d03..23449b1f10 100644 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/ChatReactionScreen.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatReactionScreen.kt @@ -1,6 +1,6 @@ -package com.anytypeio.anytype.feature_discussions.ui +package com.anytypeio.anytype.feature_chats.ui -import com.anytypeio.anytype.feature_discussions.R +import com.anytypeio.anytype.feature_chats.R import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -25,7 +25,7 @@ import com.anytypeio.anytype.core_ui.foundation.Dragger import com.anytypeio.anytype.core_ui.views.BodyCallout import com.anytypeio.anytype.core_ui.views.BodyRegular import com.anytypeio.anytype.core_ui.views.Relations3 -import com.anytypeio.anytype.feature_discussions.presentation.ChatReactionViewModel.ViewState +import com.anytypeio.anytype.feature_chats.presentation.ChatReactionViewModel.ViewState import com.anytypeio.anytype.presentation.objects.SpaceMemberIconView @Composable diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt new file mode 100644 index 0000000000..3c7035ccb6 --- /dev/null +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt @@ -0,0 +1,617 @@ +package com.anytypeio.anytype.feature_chats.ui + +import android.content.res.Configuration +import android.net.Uri +import android.provider.OpenableColumns +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_ui.foundation.AlertConfig +import com.anytypeio.anytype.core_ui.foundation.AlertIcon +import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_BLUE +import com.anytypeio.anytype.core_ui.views.Caption1Medium +import com.anytypeio.anytype.core_ui.views.Caption1Regular +import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular +import com.anytypeio.anytype.core_ui.views.Relations2 +import com.anytypeio.anytype.core_utils.common.DefaultFileInfo +import com.anytypeio.anytype.core_utils.ext.parseImagePath +import com.anytypeio.anytype.feature_chats.R +import com.anytypeio.anytype.feature_chats.presentation.ChatView +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel.ChatBoxMode +import com.anytypeio.anytype.feature_chats.presentation.ChatViewModel.UXCommand +import kotlinx.coroutines.launch + + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChatScreenWrapper( + isSpaceLevelChat: Boolean = false, + vm: ChatViewModel, + // TODO move to view model + onAttachObjectClicked: () -> Unit, + onBackButtonClicked: () -> Unit, + onMarkupLinkClicked: (String) -> Unit, + onRequestOpenFullScreenImage: (String) -> Unit, + onSelectChatReaction: (String) -> Unit, + onViewChatReaction: (Id, String) -> Unit +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) + var showReactionSheet by remember { mutableStateOf(false) } + val context = LocalContext.current + NavHost( + navController = rememberNavController(), + startDestination = "discussions" + ) { + composable( + route = "discussions" + ) { + Box( + modifier = Modifier + .fillMaxSize() + .then( + if (!isSpaceLevelChat) { + Modifier.background( + color = colorResource(id = R.color.background_primary) + ) + } else { + Modifier + } + ) + ) { + val clipboard = LocalClipboardManager.current + val lazyListState = rememberLazyListState() + + ChatScreen( + chatBoxMode = vm.chatBoxMode.collectAsState().value, + isSpaceLevelChat = isSpaceLevelChat, + title = vm.name.collectAsState().value, + messages = vm.messages.collectAsState().value, + attachments = vm.chatBoxAttachments.collectAsState().value, + onMessageSent = vm::onMessageSent, + onTitleChanged = vm::onTitleChanged, + onAttachClicked = onAttachObjectClicked, + onClearAttachmentClicked = vm::onClearAttachmentClicked, + lazyListState = lazyListState, + onReacted = vm::onReacted, + onCopyMessage = { msg -> + clipboard.setText(AnnotatedString(text = msg.content.msg)) + }, + onDeleteMessage = vm::onDeleteMessage, + onEditMessage = vm::onRequestEditMessageClicked, + onAttachmentClicked = vm::onAttachmentClicked, + isInEditMessageMode = vm.chatBoxMode.collectAsState().value is ChatBoxMode.EditMessage, + onExitEditMessageMode = vm::onExitEditMessageMode, + onBackButtonClicked = onBackButtonClicked, + onMarkupLinkClicked = onMarkupLinkClicked, + onAttachObjectClicked = onAttachObjectClicked, + onAttachMediaClicked = { + + }, + onAttachFileClicked = { + + }, + onUploadAttachmentClicked = { + + }, + onReplyMessage = vm::onReplyMessage, + onClearReplyClicked = vm::onClearReplyClicked, + onChatBoxMediaPicked = { uris -> + vm.onChatBoxMediaPicked(uris.map { it.parseImagePath(context = context) }) + }, + onChatBoxFilePicked = { uris -> + val infos = uris.mapNotNull { uri -> + val cursor = context.contentResolver.query( + uri, + null, + null, + null, + null + ) + if (cursor != null) { + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + DefaultFileInfo( + uri = uri.toString(), + name = cursor.getString(nameIndex), + size = cursor.getLong(sizeIndex).toInt() + ) + } else { + null + } + } + vm.onChatBoxFilePicked(infos) + }, + onAddReactionClicked = onSelectChatReaction, + onViewChatReaction = onViewChatReaction + ) + LaunchedEffect(Unit) { + vm.commands.collect { command -> + when(command) { + is UXCommand.JumpToBottom -> { + lazyListState.animateScrollToItem(0) + } + is UXCommand.SetChatBoxInput -> { + // TODO + } + is UXCommand.OpenFullScreenImage -> { + onRequestOpenFullScreenImage(command.url) + } + } + } + } + } + } + } + if (showReactionSheet) { + ModalBottomSheet( + onDismissRequest = { + showReactionSheet = false + }, + sheetState = sheetState, + containerColor = colorResource(id = R.color.background_secondary), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + dragHandle = null + ) { + SelectChatReactionScreen( + onEmojiClicked = {} + ) + } + } +} + +/** + * TODO: do date formating before rendering? + */ +@Composable +fun ChatScreen( + chatBoxMode: ChatBoxMode, + isSpaceLevelChat: Boolean, + isInEditMessageMode: Boolean = false, + lazyListState: LazyListState, + title: String?, + messages: List, + attachments: List, + onMessageSent: (String) -> Unit, + onTitleChanged: (String) -> Unit, + onAttachClicked: () -> Unit, + onBackButtonClicked: () -> Unit, + onClearAttachmentClicked: (ChatView.Message.ChatBoxAttachment) -> Unit, + onClearReplyClicked: () -> Unit, + onReacted: (Id, String) -> Unit, + onDeleteMessage: (ChatView.Message) -> Unit, + onCopyMessage: (ChatView.Message) -> Unit, + onEditMessage: (ChatView.Message) -> Unit, + onReplyMessage: (ChatView.Message) -> Unit, + onAttachmentClicked: (ChatView.Message.Attachment) -> Unit, + onExitEditMessageMode: () -> Unit, + onMarkupLinkClicked: (String) -> Unit, + onAttachObjectClicked: () -> Unit, + onAttachMediaClicked: () -> Unit, + onAttachFileClicked: () -> Unit, + onUploadAttachmentClicked: () -> Unit, + onChatBoxMediaPicked: (List) -> Unit, + onChatBoxFilePicked: (List) -> Unit, + onAddReactionClicked: (String) -> Unit, + onViewChatReaction: (Id, String) -> Unit +) { + var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue("")) + } + var isTitleFocused by remember { mutableStateOf(false) } + val chatBoxFocusRequester = FocusRequester() + val isHeaderVisible by remember { + derivedStateOf { + val layoutInfo = lazyListState.layoutInfo + val visibleItems = layoutInfo.visibleItemsInfo + if (visibleItems.isEmpty()) { + false + } else { + visibleItems.last().key == HEADER_KEY + } + } + } + + val scope = rememberCoroutineScope() + + + // Scrolling to bottom when list size changes and we are at the bottom of the list + LaunchedEffect(messages.size) { + if (lazyListState.firstVisibleItemScrollOffset == 0) { + scope.launch { + lazyListState.animateScrollToItem(0) + } + } + } + + Column( + modifier = Modifier.fillMaxSize() + ) { + if (!isSpaceLevelChat) { + TopDiscussionToolbar( + title = title, + isHeaderVisible = isHeaderVisible + ) + } + Box(modifier = Modifier.weight(1f)) { + Messages( + isSpaceLevelChat = isSpaceLevelChat, + modifier = Modifier.fillMaxSize(), + messages = messages, + scrollState = lazyListState, + onTitleChanged = onTitleChanged, + title = title, + onTitleFocusChanged = { + isTitleFocused = it + }, + onReacted = onReacted, + onCopyMessage = onCopyMessage, + onDeleteMessage = onDeleteMessage, + onAttachmentClicked = onAttachmentClicked, + onEditMessage = { msg -> + onEditMessage(msg).also { + textState = TextFieldValue( + msg.content.msg, + selection = TextRange(msg.content.msg.length) + ) + chatBoxFocusRequester.requestFocus() + } + }, + onReplyMessage = { + onReplyMessage(it) + chatBoxFocusRequester.requestFocus() + }, + onMarkupLinkClicked = onMarkupLinkClicked, + onAddReactionClicked = onAddReactionClicked, + onViewChatReaction = onViewChatReaction + ) + // Jump to bottom button shows up when user scrolls past a threshold. + // Convert to pixels: + val jumpThreshold = with(LocalDensity.current) { + JumpToBottomThreshold.toPx() + } + + // Show the button if the first visible item is not the first one or if the offset is + // greater than the threshold. + val jumpToBottomButtonEnabled by remember { + derivedStateOf { + lazyListState.firstVisibleItemIndex != 0 || + lazyListState.firstVisibleItemScrollOffset > jumpThreshold + } + } + + GoToBottomButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 12.dp), + onGoToBottomClicked = { + scope.launch { + lazyListState.animateScrollToItem(index = 0) + } + }, + enabled = jumpToBottomButtonEnabled + ) + } + ChatBox( + mode = chatBoxMode, + modifier = Modifier + .imePadding() + .navigationBarsPadding(), + chatBoxFocusRequester = chatBoxFocusRequester, + textState = textState, + onMessageSent = onMessageSent, + onAttachClicked = onAttachClicked, + resetScroll = { + if (lazyListState.firstVisibleItemScrollOffset > 0) { + scope.launch { + lazyListState.animateScrollToItem(index = 0) + } + } + }, + isTitleFocused = isTitleFocused, + attachments = attachments, + updateValue = { + textState = it + }, + clearText = { + textState = TextFieldValue() + }, + onBackButtonClicked = onBackButtonClicked, + onAttachFileClicked = onAttachFileClicked, + onAttachMediaClicked = onAttachMediaClicked, + onUploadAttachmentClicked = onUploadAttachmentClicked, + onAttachObjectClicked = onAttachObjectClicked, + onClearAttachmentClicked = onClearAttachmentClicked, + onClearReplyClicked = onClearReplyClicked, + onChatBoxMediaPicked = onChatBoxMediaPicked, + onChatBoxFilePicked = onChatBoxFilePicked, + onExitEditMessageMode = { + onExitEditMessageMode().also { + textState = TextFieldValue() + } + } + ) + } +} + +@Composable +fun Messages( + isSpaceLevelChat: Boolean = true, + title: String?, + onTitleChanged: (String) -> Unit, + modifier: Modifier = Modifier, + messages: List, + scrollState: LazyListState, + onTitleFocusChanged: (Boolean) -> Unit, + onReacted: (Id, String) -> Unit, + onDeleteMessage: (ChatView.Message) -> Unit, + onCopyMessage: (ChatView.Message) -> Unit, + onAttachmentClicked: (ChatView.Message.Attachment) -> Unit, + onEditMessage: (ChatView.Message) -> Unit, + onReplyMessage: (ChatView.Message) -> Unit, + onMarkupLinkClicked: (String) -> Unit, + onAddReactionClicked: (String) -> Unit, + onViewChatReaction: (Id, String) -> Unit +) { + val scope = rememberCoroutineScope() + LazyColumn( + modifier = modifier, + reverseLayout = true, + state = scrollState, + ) { + itemsIndexed( + messages, + key = { _, msg -> + when(msg) { + is ChatView.DateSection -> msg.timeInMillis + is ChatView.Message -> msg.id + } + } + ) { idx, msg -> + if (msg is ChatView.Message) { + if (idx == 0) + Spacer(modifier = Modifier.height(36.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 6.dp) + .animateItem(), + horizontalArrangement = if (msg.isUserAuthor) + Arrangement.End + else + Arrangement.Start + ) { + if (!msg.isUserAuthor) { + ChatUserAvatar( + msg = msg, + avatar = msg.avatar, + modifier = Modifier.align(Alignment.Bottom) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Bubble( + modifier = Modifier.padding( + start = if (msg.isUserAuthor) 32.dp else 0.dp, + end = if (msg.isUserAuthor) 0.dp else 32.dp + ), + name = msg.author, + content = msg.content, + timestamp = msg.timestamp, + attachments = msg.attachments, + isUserAuthor = msg.isUserAuthor, + isEdited = msg.isEdited, + onReacted = { emoji -> + onReacted(msg.id, emoji) + }, + reactions = msg.reactions, + onDeleteMessage = { + onDeleteMessage(msg) + }, + onCopyMessage = { + onCopyMessage(msg) + }, + onAttachmentClicked = onAttachmentClicked, + onEditMessage = { + onEditMessage(msg) + }, + onMarkupLinkClicked = onMarkupLinkClicked, + onReply = { + onReplyMessage(msg) + }, + reply = msg.reply, + onScrollToReplyClicked = { reply -> + // Naive implementation + val idx = messages.indexOfFirst { it is ChatView.Message && it.id == reply.msg } + if (idx != -1) { + scope.launch { + scrollState.animateScrollToItem(index = idx) + } + } + }, + onAddReactionClicked = { + onAddReactionClicked(msg.id) + }, + onViewChatReaction = { emoji -> + onViewChatReaction(msg.id, emoji) + } + ) + } + if (idx == messages.lastIndex) { + Spacer(modifier = Modifier.height(36.dp)) + } + } else if (msg is ChatView.DateSection) { + Text( + text = msg.formattedDate, + style = Caption1Medium, + modifier = Modifier.fillMaxWidth().padding(16.dp), + textAlign = TextAlign.Center, + color = colorResource(R.color.transparent_active) + ) + } + } + if (messages.isEmpty()) { + item { + Box( + modifier = Modifier + .fillParentMaxSize() + ) { + Column( + modifier = Modifier + .align(Alignment.CenterStart) + ) { + AlertIcon( + icon = AlertConfig.Icon( + gradient = GRADIENT_TYPE_BLUE, + icon = R.drawable.ic_alert_message + ) + ) + Text( + text = stringResource(R.string.chat_empty_state_message), + style = Caption1Regular, + color = colorResource(id = R.color.text_secondary), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding( + start = 20.dp, + end = 20.dp, + top = 12.dp + ) + ) + } + } + } + } + if (!isSpaceLevelChat) { + item(key = HEADER_KEY) { + Column { + DiscussionTitle( + title = title, + onTitleChanged = onTitleChanged, + onFocusChanged = onTitleFocusChanged + ) + Text( + style = Relations2, + text = stringResource(R.string.chat), + color = colorResource(id = R.color.text_secondary), + modifier = Modifier.padding( + start = 20.dp + ) + ) + } + } + } + } +} + +@Composable +fun TopDiscussionToolbar( + title: String? = null, + isHeaderVisible: Boolean = false +) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .width(48.dp) + ) { + Box( + modifier = Modifier + .size(10.dp) + .align(Alignment.Center) + .background(color = Color.Green, shape = CircleShape) + ) + } + Text( + text = if (isHeaderVisible) "" else title ?: stringResource(id = R.string.untitled), + style = PreviewTitle2Regular, + color = colorResource(id = R.color.text_primary), + textAlign = TextAlign.Center, + modifier = Modifier + .align(Alignment.CenterVertically) + .weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Box( + modifier = Modifier + .fillMaxHeight() + .width(48.dp) + ) { + Image( + painter = painterResource(id = R.drawable.ic_toolbar_three_dots), + contentDescription = "Three dots menu", + modifier = Modifier.align(Alignment.Center) + ) + } + } +} + +@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 TopDiscussionToolbarPreview() { + TopDiscussionToolbar() +} + +private const val HEADER_KEY = "key.discussions.item.header" +private val JumpToBottomThreshold = 200.dp \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Reactions.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Reactions.kt new file mode 100644 index 0000000000..33b211cfaa --- /dev/null +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Reactions.kt @@ -0,0 +1,141 @@ +package com.anytypeio.anytype.feature_chats.ui + +import android.content.res.Configuration +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium +import com.anytypeio.anytype.core_ui.views.Caption1Regular +import com.anytypeio.anytype.feature_chats.R +import com.anytypeio.anytype.feature_chats.presentation.ChatView + +@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) +@Composable +fun ReactionList( + reactions: List, + onReacted: (String) -> Unit, + onViewReaction: (String) -> Unit +) { + FlowRow( + modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp, top = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + reactions.forEach { reaction -> + Box( + modifier = Modifier + .height(28.dp) + .width(46.dp) + .background( + color = if (reaction.isSelected) + colorResource(id = R.color.palette_very_light_orange) + else + colorResource(id = R.color.shape_transparent_primary), + shape = RoundedCornerShape(100.dp) + ) + .clip(RoundedCornerShape(100.dp)) + .then( + if (reaction.isSelected) + Modifier.border( + width = 1.dp, + color = colorResource(id = R.color.palette_system_amber_50), + shape = RoundedCornerShape(100.dp) + ) + else + Modifier + ) + .combinedClickable( + onClick = { + onReacted(reaction.emoji) + }, + onLongClick = { + onViewReaction(reaction.emoji) + } + ) + ) { + Text( + text = reaction.emoji, + style = BodyCalloutMedium, + modifier = Modifier + .align( + alignment = Alignment.CenterStart + ) + .padding( + start = 8.dp + ) + ) + Text( + text = reaction.count.toString(), + style = Caption1Regular, + modifier = Modifier + .align( + alignment = Alignment.CenterEnd + ) + .padding( + end = 8.dp + ), + color = colorResource(id = R.color.text_primary) + ) + } + } + } +} + +@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 ReactionListPreview() { + ReactionList( + reactions = listOf( + ChatView.Message.Reaction( + emoji = "❤\uFE0F", + count = 1, + isSelected = false + ), + ChatView.Message.Reaction( + emoji = "❤\uFE0F", + count = 1, + isSelected = true + ), + ChatView.Message.Reaction( + emoji = "❤\uFE0F", + count = 1, + isSelected = false + ), + ChatView.Message.Reaction( + emoji = "❤\uFE0F", + count = 1, + isSelected = false + ), + ChatView.Message.Reaction( + emoji = "❤\uFE0F", + count = 1, + isSelected = false + ), + ChatView.Message.Reaction( + emoji = "❤\uFE0F", + count = 1, + isSelected = false + ) + ), + onReacted = {}, + onViewReaction = {} + ) +} \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Toolbars.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Toolbars.kt new file mode 100644 index 0000000000..879ac0f6f9 --- /dev/null +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Toolbars.kt @@ -0,0 +1,111 @@ +package com.anytypeio.anytype.feature_chats.ui + +import androidx.compose.animation.core.animateDp +import androidx.compose.animation.core.updateTransition +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.foundation.noRippleClickable +import com.anytypeio.anytype.core_ui.views.Caption1Medium +import com.anytypeio.anytype.feature_chats.R + +@Composable +fun EditMessageToolbar( + onExitClicked: () -> Unit +) { + Box( + modifier = Modifier + .height(40.dp) + .fillMaxWidth() + .background( + color = colorResource(id = R.color.background_highlighted_light), + shape = RoundedCornerShape( + topStart = 16.dp, + topEnd = 16.dp + ) + ) + ) { + Text( + modifier = Modifier + .padding( + start = 12.dp + ) + .align( + Alignment.CenterStart + ), + text = stringResource(R.string.chats_edit_message), + style = Caption1Medium, + color = colorResource(id = R.color.text_primary) + ) + Image( + modifier = Modifier + .padding( + end = 12.dp + ) + .align( + Alignment.CenterEnd + ) + .noRippleClickable { + onExitClicked() + } + , + painter = painterResource(id = R.drawable.ic_edit_message_close), + contentDescription = "Close edit-message mode" + ) + } +} + +@Composable +fun GoToBottomButton( + enabled: Boolean, + modifier: Modifier, + onGoToBottomClicked: () -> Unit +) { + val transition = updateTransition( + enabled, + label = "JumpToBottom visibility animation" + ) + val bottomOffset by transition.animateDp(label = "JumpToBottom offset animation") { + if (it) { + (12).dp + } else { + (-12).dp + } + } + if (bottomOffset > 0.dp) { + Box( + modifier = modifier + .offset(x = 0.dp, y = -bottomOffset) + .size(48.dp) + .clip(RoundedCornerShape(12.dp)) + .background(color = colorResource(id = R.color.navigation_panel)) + .clickable { + onGoToBottomClicked() + } + + ) { + Image( + painter = painterResource(id = R.drawable.ic_go_to_bottom_arrow), + contentDescription = "Arrow icon", + modifier = Modifier.align(Alignment.Center) + ) + } + } +} \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Utils.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Utils.kt new file mode 100644 index 0000000000..5e75b53bf1 --- /dev/null +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/Utils.kt @@ -0,0 +1,102 @@ +package com.anytypeio.anytype.feature_chats.ui + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Text +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.runtime.Composable +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.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.anytypeio.anytype.core_ui.views.HeadlineTitle +import com.anytypeio.anytype.feature_chats.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable + +fun DefaultHintDecorationBox( + text: String, + hint: String, + innerTextField: @Composable () -> Unit, + textStyle: TextStyle +) { + OutlinedTextFieldDefaults.DecorationBox( + value = text, + visualTransformation = VisualTransformation.None, + innerTextField = innerTextField, + singleLine = true, + enabled = true, + placeholder = { + Text( + text = hint, + color = colorResource(id = R.color.text_tertiary), + style = textStyle + ) + }, + interactionSource = remember { MutableInteractionSource() }, + colors = OutlinedTextFieldDefaults.colors( + disabledBorderColor = Color.Transparent, + errorBorderColor = Color.Transparent, + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent + ), + contentPadding = PaddingValues() + ) +} + +@Composable +fun DiscussionTitle( + title: String?, + onTitleChanged: (String) -> Unit = {}, + onFocusChanged: (Boolean) -> Unit = {} +) { + var lastFocusState by remember { mutableStateOf(false) } + BasicTextField( + textStyle = HeadlineTitle.copy( + color = colorResource(id = R.color.text_primary) + ), + value = title.orEmpty(), + onValueChange = { + onTitleChanged(it) + }, + modifier = Modifier + .padding( + top = 24.dp, + start = 20.dp, + end = 20.dp, + bottom = 8.dp + ) + .onFocusChanged { state -> + if (lastFocusState != state.isFocused) { + onFocusChanged(state.isFocused) + } + lastFocusState = state.isFocused + } + , + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Done + ), + decorationBox = @Composable { innerTextField -> + DefaultHintDecorationBox( + hint = stringResource(id = R.string.untitled), + text = title.orEmpty(), + innerTextField = innerTextField, + textStyle = HeadlineTitle + ) + } + ) +} \ No newline at end of file diff --git a/feature-discussions/src/main/res/drawable/ic_chat_box_add_attachment.xml b/feature-chats/src/main/res/drawable/ic_chat_box_add_attachment.xml similarity index 100% rename from feature-discussions/src/main/res/drawable/ic_chat_box_add_attachment.xml rename to feature-chats/src/main/res/drawable/ic_chat_box_add_attachment.xml diff --git a/feature-discussions/src/main/res/drawable/ic_chat_close_chat_box_reply.xml b/feature-chats/src/main/res/drawable/ic_chat_close_chat_box_reply.xml similarity index 100% rename from feature-discussions/src/main/res/drawable/ic_chat_close_chat_box_reply.xml rename to feature-chats/src/main/res/drawable/ic_chat_close_chat_box_reply.xml diff --git a/feature-discussions/src/main/res/drawable/ic_clear_chatbox_attachment.xml b/feature-chats/src/main/res/drawable/ic_clear_chatbox_attachment.xml similarity index 100% rename from feature-discussions/src/main/res/drawable/ic_clear_chatbox_attachment.xml rename to feature-chats/src/main/res/drawable/ic_clear_chatbox_attachment.xml diff --git a/feature-discussions/src/main/res/drawable/ic_edit_message_close.xml b/feature-chats/src/main/res/drawable/ic_edit_message_close.xml similarity index 100% rename from feature-discussions/src/main/res/drawable/ic_edit_message_close.xml rename to feature-chats/src/main/res/drawable/ic_edit_message_close.xml diff --git a/feature-discussions/src/main/res/drawable/ic_go_to_bottom_arrow.xml b/feature-chats/src/main/res/drawable/ic_go_to_bottom_arrow.xml similarity index 100% rename from feature-discussions/src/main/res/drawable/ic_go_to_bottom_arrow.xml rename to feature-chats/src/main/res/drawable/ic_go_to_bottom_arrow.xml diff --git a/feature-discussions/src/main/res/drawable/ic_send_message.xml b/feature-chats/src/main/res/drawable/ic_send_message.xml similarity index 100% rename from feature-discussions/src/main/res/drawable/ic_send_message.xml rename to feature-chats/src/main/res/drawable/ic_send_message.xml diff --git a/feature-discussions/src/main/res/drawable/ic_toolbar_three_dots.xml b/feature-chats/src/main/res/drawable/ic_toolbar_three_dots.xml similarity index 100% rename from feature-discussions/src/main/res/drawable/ic_toolbar_three_dots.xml rename to feature-chats/src/main/res/drawable/ic_toolbar_three_dots.xml diff --git a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt index 40fb383128..7bba68faf7 100644 --- a/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt +++ b/feature-date/src/main/java/com/anytypeio/anytype/feature_date/viewmodel/DateObjectViewModel.kt @@ -62,7 +62,6 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -646,7 +645,7 @@ class DateObjectViewModel( effects.emit(DateObjectCommand.SendToast.UnexpectedLayout(navigation.layout?.name.orEmpty())) } - is OpenObjectNavigation.OpenDiscussion -> { + is OpenObjectNavigation.OpenChat -> { effects.emit( DateObjectCommand.OpenChat( target = navigation.target, diff --git a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/DiscussionScreen.kt b/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/DiscussionScreen.kt deleted file mode 100644 index 9153a9e932..0000000000 --- a/feature-discussions/src/main/java/com/anytypeio/anytype/feature_discussions/ui/DiscussionScreen.kt +++ /dev/null @@ -1,1877 +0,0 @@ -package com.anytypeio.anytype.feature_discussions.ui - -import android.content.res.Configuration -import android.net.Uri -import android.provider.OpenableColumns -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.PickVisualMediaRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateDp -import androidx.compose.animation.core.updateTransition -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material.DropdownMenu -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.colorResource -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.LinkAnnotation -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLinkStyles -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.text.withLink -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import coil.compose.AsyncImage -import coil.compose.rememberAsyncImagePainter -import coil.request.ImageRequest -import com.anytypeio.anytype.core_models.Id -import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE -import com.anytypeio.anytype.core_ui.foundation.AlertConfig -import com.anytypeio.anytype.core_ui.foundation.AlertIcon -import com.anytypeio.anytype.core_ui.foundation.BUTTON_SECONDARY -import com.anytypeio.anytype.core_ui.foundation.BUTTON_WARNING -import com.anytypeio.anytype.core_ui.foundation.Divider -import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_BLUE -import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED -import com.anytypeio.anytype.core_ui.foundation.GenericAlert -import com.anytypeio.anytype.core_ui.foundation.Warning -import com.anytypeio.anytype.core_ui.foundation.noRippleClickable -import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium -import com.anytypeio.anytype.core_ui.views.BodyRegular -import com.anytypeio.anytype.core_ui.views.Caption1Medium -import com.anytypeio.anytype.core_ui.views.Caption1Regular -import com.anytypeio.anytype.core_ui.views.HeadlineTitle -import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium -import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular -import com.anytypeio.anytype.core_ui.views.Relations2 -import com.anytypeio.anytype.core_ui.views.Relations3 -import com.anytypeio.anytype.core_ui.views.fontIBM -import com.anytypeio.anytype.core_ui.widgets.ListWidgetObjectIcon -import com.anytypeio.anytype.core_utils.common.DefaultFileInfo -import com.anytypeio.anytype.core_utils.const.DateConst.TIME_H24 -import com.anytypeio.anytype.core_utils.ext.formatTimeInMillis -import com.anytypeio.anytype.core_utils.ext.parseImagePath -import com.anytypeio.anytype.feature_discussions.R -import com.anytypeio.anytype.feature_discussions.presentation.ChatConfig -import com.anytypeio.anytype.feature_discussions.presentation.DiscussionView -import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel -import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel.ChatBoxMode -import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModel.UXCommand -import com.anytypeio.anytype.presentation.objects.ObjectIcon -import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi -import com.bumptech.glide.integration.compose.GlideImage -import kotlinx.coroutines.launch -import org.jetbrains.annotations.Async -import timber.log.Timber - - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DiscussionScreenWrapper( - isSpaceLevelChat: Boolean = false, - vm: DiscussionViewModel, - // TODO move to view model - onAttachObjectClicked: () -> Unit, - onBackButtonClicked: () -> Unit, - onMarkupLinkClicked: (String) -> Unit, - onRequestOpenFullScreenImage: (String) -> Unit, - onSelectChatReaction: (String) -> Unit, - onViewChatReaction: (Id, String) -> Unit -) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) - var showReactionSheet by remember { mutableStateOf(false) } - val context = LocalContext.current - NavHost( - navController = rememberNavController(), - startDestination = "discussions" - ) { - composable( - route = "discussions" - ) { - Box( - modifier = Modifier - .fillMaxSize() - .then( - if (!isSpaceLevelChat) { - Modifier.background( - color = colorResource(id = R.color.background_primary) - ) - } else { - Modifier - } - ) - ) { - val clipboard = LocalClipboardManager.current - val lazyListState = rememberLazyListState() - - DiscussionScreen( - chatBoxMode = vm.chatBoxMode.collectAsState().value, - isSpaceLevelChat = isSpaceLevelChat, - title = vm.name.collectAsState().value, - messages = vm.messages.collectAsState().value, - attachments = vm.chatBoxAttachments.collectAsState().value, - onMessageSent = vm::onMessageSent, - onTitleChanged = vm::onTitleChanged, - onAttachClicked = onAttachObjectClicked, - onClearAttachmentClicked = vm::onClearAttachmentClicked, - lazyListState = lazyListState, - onReacted = vm::onReacted, - onCopyMessage = { msg -> - clipboard.setText(AnnotatedString(text = msg.content.msg)) - }, - onDeleteMessage = vm::onDeleteMessage, - onEditMessage = vm::onRequestEditMessageClicked, - onAttachmentClicked = vm::onAttachmentClicked, - isInEditMessageMode = vm.chatBoxMode.collectAsState().value is ChatBoxMode.EditMessage, - onExitEditMessageMode = vm::onExitEditMessageMode, - onBackButtonClicked = onBackButtonClicked, - onMarkupLinkClicked = onMarkupLinkClicked, - onAttachObjectClicked = onAttachObjectClicked, - onAttachMediaClicked = { - - }, - onAttachFileClicked = { - - }, - onUploadAttachmentClicked = { - - }, - onReplyMessage = vm::onReplyMessage, - onClearReplyClicked = vm::onClearReplyClicked, - onChatBoxMediaPicked = { uris -> - vm.onChatBoxMediaPicked(uris.map { it.parseImagePath(context = context) }) - }, - onChatBoxFilePicked = { uris -> - val infos = uris.mapNotNull { uri -> - val cursor = context.contentResolver.query( - uri, - null, - null, - null, - null - ) - if (cursor != null) { - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - cursor.moveToFirst() - DefaultFileInfo( - uri = uri.toString(), - name = cursor.getString(nameIndex), - size = cursor.getLong(sizeIndex).toInt() - ) - } else { - null - } - } - vm.onChatBoxFilePicked(infos) - }, - onAddReactionClicked = onSelectChatReaction, - onViewChatReaction = onViewChatReaction - ) - LaunchedEffect(Unit) { - vm.commands.collect { command -> - when(command) { - is UXCommand.JumpToBottom -> { - lazyListState.animateScrollToItem(0) - } - is UXCommand.SetChatBoxInput -> { - // TODO - } - is UXCommand.OpenFullScreenImage -> { - onRequestOpenFullScreenImage(command.url) - } - } - } - } - } - } - } - if (showReactionSheet) { - ModalBottomSheet( - onDismissRequest = { - showReactionSheet = false - }, - sheetState = sheetState, - containerColor = colorResource(id = R.color.background_secondary), - shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - dragHandle = null - ) { - SelectChatReactionScreen( - onEmojiClicked = {} - ) - } - } -} - -/** - * TODO: do date formating before rendering? - */ -@Composable -fun DiscussionScreen( - chatBoxMode: ChatBoxMode, - isSpaceLevelChat: Boolean, - isInEditMessageMode: Boolean = false, - lazyListState: LazyListState, - title: String?, - messages: List, - attachments: List, - onMessageSent: (String) -> Unit, - onTitleChanged: (String) -> Unit, - onAttachClicked: () -> Unit, - onBackButtonClicked: () -> Unit, - onClearAttachmentClicked: (DiscussionView.Message.ChatBoxAttachment) -> Unit, - onClearReplyClicked: () -> Unit, - onReacted: (Id, String) -> Unit, - onDeleteMessage: (DiscussionView.Message) -> Unit, - onCopyMessage: (DiscussionView.Message) -> Unit, - onEditMessage: (DiscussionView.Message) -> Unit, - onReplyMessage: (DiscussionView.Message) -> Unit, - onAttachmentClicked: (DiscussionView.Message.Attachment) -> Unit, - onExitEditMessageMode: () -> Unit, - onMarkupLinkClicked: (String) -> Unit, - onAttachObjectClicked: () -> Unit, - onAttachMediaClicked: () -> Unit, - onAttachFileClicked: () -> Unit, - onUploadAttachmentClicked: () -> Unit, - onChatBoxMediaPicked: (List) -> Unit, - onChatBoxFilePicked: (List) -> Unit, - onAddReactionClicked: (String) -> Unit, - onViewChatReaction: (Id, String) -> Unit -) { - var textState by rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue("")) - } - var isTitleFocused by remember { mutableStateOf(false) } - val chatBoxFocusRequester = FocusRequester() - val isHeaderVisible by remember { - derivedStateOf { - val layoutInfo = lazyListState.layoutInfo - val visibleItems = layoutInfo.visibleItemsInfo - if (visibleItems.isEmpty()) { - false - } else { - visibleItems.last().key == HEADER_KEY - } - } - } - - val scope = rememberCoroutineScope() - - - // Scrolling to bottom when list size changes and we are at the bottom of the list - LaunchedEffect(messages.size) { - if (lazyListState.firstVisibleItemScrollOffset == 0) { - scope.launch { - lazyListState.animateScrollToItem(0) - } - } - } - - Column( - modifier = Modifier.fillMaxSize() - ) { - if (!isSpaceLevelChat) { - TopDiscussionToolbar( - title = title, - isHeaderVisible = isHeaderVisible - ) - } - Box(modifier = Modifier.weight(1f)) { - Messages( - isSpaceLevelChat = isSpaceLevelChat, - modifier = Modifier.fillMaxSize(), - messages = messages, - scrollState = lazyListState, - onTitleChanged = onTitleChanged, - title = title, - onTitleFocusChanged = { - isTitleFocused = it - }, - onReacted = onReacted, - onCopyMessage = onCopyMessage, - onDeleteMessage = onDeleteMessage, - onAttachmentClicked = onAttachmentClicked, - onEditMessage = { msg -> - onEditMessage(msg).also { - textState = TextFieldValue( - msg.content.msg, - selection = TextRange(msg.content.msg.length) - ) - chatBoxFocusRequester.requestFocus() - } - }, - onReplyMessage = { - onReplyMessage(it) - chatBoxFocusRequester.requestFocus() - }, - onMarkupLinkClicked = onMarkupLinkClicked, - onAddReactionClicked = onAddReactionClicked, - onViewChatReaction = onViewChatReaction - ) - // Jump to bottom button shows up when user scrolls past a threshold. - // Convert to pixels: - val jumpThreshold = with(LocalDensity.current) { - JumpToBottomThreshold.toPx() - } - - // Show the button if the first visible item is not the first one or if the offset is - // greater than the threshold. - val jumpToBottomButtonEnabled by remember { - derivedStateOf { - lazyListState.firstVisibleItemIndex != 0 || - lazyListState.firstVisibleItemScrollOffset > jumpThreshold - } - } - - GoToBottomButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 12.dp), - onGoToBottomClicked = { - scope.launch { - lazyListState.animateScrollToItem(index = 0) - } - }, - enabled = jumpToBottomButtonEnabled - ) - } - ChatBox( - mode = chatBoxMode, - modifier = Modifier - .imePadding() - .navigationBarsPadding(), - chatBoxFocusRequester = chatBoxFocusRequester, - textState = textState, - onMessageSent = onMessageSent, - onAttachClicked = onAttachClicked, - resetScroll = { - if (lazyListState.firstVisibleItemScrollOffset > 0) { - scope.launch { - lazyListState.animateScrollToItem(index = 0) - } - } - }, - isTitleFocused = isTitleFocused, - attachments = attachments, - updateValue = { - textState = it - }, - clearText = { - textState = TextFieldValue() - }, - onBackButtonClicked = onBackButtonClicked, - onAttachFileClicked = onAttachFileClicked, - onAttachMediaClicked = onAttachMediaClicked, - onUploadAttachmentClicked = onUploadAttachmentClicked, - onAttachObjectClicked = onAttachObjectClicked, - onClearAttachmentClicked = onClearAttachmentClicked, - onClearReplyClicked = onClearReplyClicked, - onChatBoxMediaPicked = onChatBoxMediaPicked, - onChatBoxFilePicked = onChatBoxFilePicked, - onExitEditMessageMode = { - onExitEditMessageMode().also { - textState = TextFieldValue() - } - } - ) - } -} - -@Composable -private fun DiscussionTitle( - title: String?, - onTitleChanged: (String) -> Unit = {}, - onFocusChanged: (Boolean) -> Unit = {} -) { - var lastFocusState by remember { mutableStateOf(false) } - BasicTextField( - textStyle = HeadlineTitle.copy( - color = colorResource(id = R.color.text_primary) - ), - value = title.orEmpty(), - onValueChange = { - onTitleChanged(it) - }, - modifier = Modifier - .padding( - top = 24.dp, - start = 20.dp, - end = 20.dp, - bottom = 8.dp - ) - .onFocusChanged { state -> - if (lastFocusState != state.isFocused) { - onFocusChanged(state.isFocused) - } - lastFocusState = state.isFocused - } - , - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Done - ), - decorationBox = @Composable { innerTextField -> - DefaultHintDecorationBox( - hint = stringResource(id = R.string.untitled), - text = title.orEmpty(), - innerTextField = innerTextField, - textStyle = HeadlineTitle - ) - } - ) -} - -@Composable -private fun ChatBox( - mode: ChatBoxMode = ChatBoxMode.Default, - modifier: Modifier = Modifier, - onBackButtonClicked: () -> Unit, - chatBoxFocusRequester: FocusRequester, - textState: TextFieldValue, - onMessageSent: (String) -> Unit = {}, - onAttachClicked: () -> Unit = {}, - resetScroll: () -> Unit = {}, - isTitleFocused: Boolean, - attachments: List, - clearText: () -> Unit, - updateValue: (TextFieldValue) -> Unit, - onAttachObjectClicked: () -> Unit, - onAttachMediaClicked: () -> Unit, - onAttachFileClicked: () -> Unit, - onUploadAttachmentClicked: () -> Unit, - onClearAttachmentClicked: (DiscussionView.Message.ChatBoxAttachment) -> Unit, - onClearReplyClicked: () -> Unit, - onChatBoxMediaPicked: (List) -> Unit, - onChatBoxFilePicked: (List) -> Unit, - onExitEditMessageMode: () -> Unit -) { - - val uploadMediaLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.PickMultipleVisualMedia(maxItems = ChatConfig.MAX_ATTACHMENT_COUNT) - ) { - onChatBoxMediaPicked(it) - } - - val uploadFileLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.OpenMultipleDocuments() - ) { uris -> - onChatBoxFilePicked(uris.take(ChatConfig.MAX_ATTACHMENT_COUNT)) - } - - var showDropdownMenu by remember { mutableStateOf(false) } - - val scope = rememberCoroutineScope() - - val focus = LocalFocusManager.current - - Column( - modifier = modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 56.dp) - .padding( - start = 12.dp, - end = 12.dp, - bottom = 20.dp - ) - .background( - color = colorResource(R.color.navigation_panel), - shape = RoundedCornerShape(16.dp) - ) - ) { - if (mode is ChatBoxMode.EditMessage) { - EditMessageToolbar( - onExitClicked = onExitEditMessageMode - ) - } - LazyRow( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - attachments.forEach { attachment -> - when(attachment) { - is DiscussionView.Message.ChatBoxAttachment.Link -> { - item { - Box { - AttachedObject( - modifier = Modifier - .padding( - top = 12.dp, - end = 4.dp - ) - .width(216.dp), - title = attachment.wrapper.title, - type = attachment.wrapper.type, - icon = attachment.wrapper.icon, - onAttachmentClicked = { - // TODO - } - ) - Image( - painter = painterResource(id = R.drawable.ic_clear_chatbox_attachment), - contentDescription = "Close icon", - modifier = Modifier - .align( - Alignment.TopEnd - ) - .padding(top = 6.dp) - .noRippleClickable { - onClearAttachmentClicked(attachment) - } - ) - } - } - } - is DiscussionView.Message.ChatBoxAttachment.Media -> { - item { - Box(modifier = Modifier.padding()) { - Image( - painter = rememberAsyncImagePainter(attachment.uri), - contentDescription = null, - modifier = Modifier - .padding( - top = 12.dp, - end = 4.dp - ) - .size(72.dp) - .clip(RoundedCornerShape(8.dp)) - - , - contentScale = ContentScale.Crop - ) - Image( - painter = painterResource(R.drawable.ic_clear_chatbox_attachment), - contentDescription = "Clear attachment icon", - modifier = Modifier - .align(Alignment.TopEnd) - .padding(top = 6.dp) - .noRippleClickable { - onClearAttachmentClicked(attachment) - } - ) - } - } - } - is DiscussionView.Message.ChatBoxAttachment.File -> { - item { - Box { - AttachedObject( - modifier = Modifier - .padding( - top = 12.dp, - end = 4.dp - ) - .width(216.dp), - title = attachment.name, - type = stringResource(R.string.file), - icon = ObjectIcon.File( - mime = null, - fileName = null - ), - onAttachmentClicked = { - // TODO - } - ) - Image( - painter = painterResource(id = R.drawable.ic_clear_chatbox_attachment), - contentDescription = "Close icon", - modifier = Modifier - .align( - Alignment.TopEnd - ) - .padding(top = 6.dp) - .noRippleClickable { - onClearAttachmentClicked(attachment) - } - ) - } - } - } - } - } - } - when(mode) { - is ChatBoxMode.Default -> { - - } - is ChatBoxMode.EditMessage -> { - - } - is ChatBoxMode.Reply -> { - Box( - modifier = Modifier - .fillMaxWidth() - .height(54.dp) - ) { - Text( - text = "Reply to ${mode.author}", - modifier = Modifier.padding( - start = 12.dp, - top = 8.dp, - end = 44.dp - ), - style = Caption1Medium, - color = colorResource(R.color.text_primary), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = mode.text, - modifier = Modifier.padding( - start = 12.dp, - top = 28.dp, - end = 44.dp - ), - style = Caption1Regular, - color = colorResource(R.color.text_primary), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Image( - painter = painterResource(R.drawable.ic_chat_close_chat_box_reply), - contentDescription = "Clear reply to icon", - modifier = Modifier - .padding(end = 12.dp) - .align(Alignment.CenterEnd) - .clickable { - onClearReplyClicked() - } - ) - } - } - } - Row { - Box( - modifier = Modifier - .padding(horizontal = 4.dp, vertical = 8.dp) - .clip(CircleShape) - .align(Alignment.Bottom) - .clickable { - scope.launch { - focus.clearFocus(force = true) - showDropdownMenu = true - } - } - ) { - Image( - painter = painterResource(id = R.drawable.ic_chat_box_add_attachment), - contentDescription = "Plus button", - modifier = Modifier - .align(Alignment.Center) - .padding(horizontal = 4.dp, vertical = 4.dp) - ) - if (attachments.size < ChatConfig.MAX_ATTACHMENT_COUNT) { - MaterialTheme( - shapes = MaterialTheme.shapes.copy( - medium = RoundedCornerShape( - 12.dp - ) - ), - colors = MaterialTheme.colors.copy( - surface = colorResource(id = R.color.background_secondary) - ) - ) { - DropdownMenu( - offset = DpOffset(8.dp, 40.dp), - expanded = showDropdownMenu, - onDismissRequest = { - showDropdownMenu = false - }, - modifier = Modifier - .align(Alignment.BottomEnd) - .defaultMinSize( - minWidth = 252.dp - ) - ) { - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.chat_attachment_object), - color = colorResource(id = R.color.text_primary) - ) - }, - onClick = { - showDropdownMenu = false - onAttachObjectClicked() - } - ) - Divider( - paddingStart = 0.dp, - paddingEnd = 0.dp - ) - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.chat_attachment_media), - color = colorResource(id = R.color.text_primary) - ) - }, - onClick = { - showDropdownMenu = false - uploadMediaLauncher.launch( - PickVisualMediaRequest(mediaType = ActivityResultContracts.PickVisualMedia.ImageOnly) - ) - } - ) - Divider( - paddingStart = 0.dp, - paddingEnd = 0.dp - ) - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.chat_attachment_file), - color = colorResource(id = R.color.text_primary) - ) - }, - onClick = { - showDropdownMenu = false - uploadFileLauncher.launch( - arrayOf("*/*") - ) - } - ) - } - } - } - } - ChatBoxUserInput( - textState = textState, - onMessageSent = { - onMessageSent(it) - clearText() - resetScroll() - }, - onTextChanged = { value -> - updateValue(value) - }, - modifier = Modifier - .weight(1f) - .align(Alignment.Bottom) - .focusRequester(chatBoxFocusRequester) - ) - AnimatedVisibility( - visible = attachments.isNotEmpty() || textState.text.isNotEmpty(), - exit = fadeOut() + scaleOut(), - enter = fadeIn() + scaleIn(), - modifier = Modifier.align(Alignment.Bottom) - ) { - Box( - modifier = Modifier - .padding(horizontal = 4.dp, vertical = 8.dp) - .clip(CircleShape) - .clickable { - onMessageSent(textState.text) - clearText() - resetScroll() - } - ) { - Image( - painter = painterResource(id = R.drawable.ic_send_message), - contentDescription = "Send message button", - modifier = Modifier - .align(Alignment.Center) - .padding(horizontal = 4.dp, vertical = 4.dp) - ) - } - } - } - } -} - -@Composable -fun EditMessageToolbar( - onExitClicked: () -> Unit -) { - Box( - modifier = Modifier - .height(40.dp) - .fillMaxWidth() - .background( - color = colorResource(id = R.color.background_highlighted_light), - shape = RoundedCornerShape( - topStart = 16.dp, - topEnd = 16.dp - ) - ) - ) { - Text( - modifier = Modifier - .padding( - start = 12.dp - ) - .align( - Alignment.CenterStart - ), - text = stringResource(R.string.chats_edit_message), - style = Caption1Medium, - color = colorResource(id = R.color.text_primary) - ) - Image( - modifier = Modifier - .padding( - end = 12.dp - ) - .align( - Alignment.CenterEnd - ) - .noRippleClickable { - onExitClicked() - } - , - painter = painterResource(id = R.drawable.ic_edit_message_close), - contentDescription = "Close edit-message mode" - ) - } -} - -@Composable -private fun ChatBoxUserInput( - modifier: Modifier, - textState: TextFieldValue, - onMessageSent: (String) -> Unit, - onTextChanged: (TextFieldValue) -> Unit, -) { - BasicTextField( - value = textState, - onValueChange = { onTextChanged(it) }, - textStyle = BodyRegular.copy( - color = colorResource(id = R.color.text_primary) - ), - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.Send - ), - keyboardActions = KeyboardActions { - if (textState.text.isNotBlank()) { - onMessageSent(textState.text) - } - }, - modifier = modifier - .padding( - start = 4.dp, - end = 4.dp, - top = 16.dp, - bottom = 16.dp - ) - , - cursorBrush = SolidColor(colorResource(id = R.color.palette_system_blue)), - maxLines = 5, - decorationBox = @Composable { innerTextField -> - DefaultHintDecorationBox( - text = textState.text, - hint = stringResource(R.string.write_a_message), - innerTextField = innerTextField, - textStyle = BodyRegular.copy(color = colorResource(R.color.text_tertiary)) - ) - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable - -private fun DefaultHintDecorationBox( - text: String, - hint: String, - innerTextField: @Composable () -> Unit, - textStyle: TextStyle -) { - OutlinedTextFieldDefaults.DecorationBox( - value = text, - visualTransformation = VisualTransformation.None, - innerTextField = innerTextField, - singleLine = true, - enabled = true, - placeholder = { - Text( - text = hint, - color = colorResource(id = R.color.text_tertiary), - style = textStyle - ) - }, - interactionSource = remember { MutableInteractionSource() }, - colors = OutlinedTextFieldDefaults.colors( - disabledBorderColor = Color.Transparent, - errorBorderColor = Color.Transparent, - focusedBorderColor = Color.Transparent, - unfocusedBorderColor = Color.Transparent - ), - contentPadding = PaddingValues() - ) -} - - -@Composable -fun Messages( - isSpaceLevelChat: Boolean = true, - title: String?, - onTitleChanged: (String) -> Unit, - modifier: Modifier = Modifier, - messages: List, - scrollState: LazyListState, - onTitleFocusChanged: (Boolean) -> Unit, - onReacted: (Id, String) -> Unit, - onDeleteMessage: (DiscussionView.Message) -> Unit, - onCopyMessage: (DiscussionView.Message) -> Unit, - onAttachmentClicked: (DiscussionView.Message.Attachment) -> Unit, - onEditMessage: (DiscussionView.Message) -> Unit, - onReplyMessage: (DiscussionView.Message) -> Unit, - onMarkupLinkClicked: (String) -> Unit, - onAddReactionClicked: (String) -> Unit, - onViewChatReaction: (Id, String) -> Unit -) { - val scope = rememberCoroutineScope() - LazyColumn( - modifier = modifier, - reverseLayout = true, - state = scrollState, - ) { - itemsIndexed( - messages, - key = { _, msg -> - when(msg) { - is DiscussionView.DateSection -> msg.timeInMillis - is DiscussionView.Message -> msg.id - } - } - ) { idx, msg -> - if (msg is DiscussionView.Message) { - if (idx == 0) - Spacer(modifier = Modifier.height(36.dp)) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 6.dp) - .animateItem(), - horizontalArrangement = if (msg.isUserAuthor) - Arrangement.End - else - Arrangement.Start - ) { - if (!msg.isUserAuthor) { - ChatUserAvatar( - msg = msg, - avatar = msg.avatar, - modifier = Modifier.align(Alignment.Bottom) - ) - Spacer(modifier = Modifier.width(8.dp)) - } - Bubble( - modifier = Modifier.padding( - start = if (msg.isUserAuthor) 32.dp else 0.dp, - end = if (msg.isUserAuthor) 0.dp else 32.dp - ), - name = msg.author, - content = msg.content, - timestamp = msg.timestamp, - attachments = msg.attachments, - isUserAuthor = msg.isUserAuthor, - isEdited = msg.isEdited, - onReacted = { emoji -> - onReacted(msg.id, emoji) - }, - reactions = msg.reactions, - onDeleteMessage = { - onDeleteMessage(msg) - }, - onCopyMessage = { - onCopyMessage(msg) - }, - onAttachmentClicked = onAttachmentClicked, - onEditMessage = { - onEditMessage(msg) - }, - onMarkupLinkClicked = onMarkupLinkClicked, - onReply = { - onReplyMessage(msg) - }, - reply = msg.reply, - onScrollToReplyClicked = { reply -> - // Naive implementation - val idx = messages.indexOfFirst { it is DiscussionView.Message && it.id == reply.msg } - if (idx != -1) { - scope.launch { - scrollState.animateScrollToItem(index = idx) - } - } - }, - onAddReactionClicked = { - onAddReactionClicked(msg.id) - }, - onViewChatReaction = { emoji -> - onViewChatReaction(msg.id, emoji) - } - ) - } - if (idx == messages.lastIndex) { - Spacer(modifier = Modifier.height(36.dp)) - } - } else if (msg is DiscussionView.DateSection) { - Text( - text = msg.formattedDate, - style = Caption1Medium, - modifier = Modifier.fillMaxWidth().padding(16.dp), - textAlign = TextAlign.Center, - color = colorResource(R.color.transparent_active) - ) - } - } - if (messages.isEmpty()) { - item { - Box( - modifier = Modifier - .fillParentMaxSize() - ) { - Column( - modifier = Modifier - .align(Alignment.CenterStart) - ) { - AlertIcon( - icon = AlertConfig.Icon( - gradient = GRADIENT_TYPE_BLUE, - icon = R.drawable.ic_alert_message - ) - ) - Text( - text = stringResource(R.string.chat_empty_state_message), - style = Caption1Regular, - color = colorResource(id = R.color.text_secondary), - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding( - start = 20.dp, - end = 20.dp, - top = 12.dp - ) - ) - } - } - } - } - if (!isSpaceLevelChat) { - item(key = HEADER_KEY) { - Column { - DiscussionTitle( - title = title, - onTitleChanged = onTitleChanged, - onFocusChanged = onTitleFocusChanged - ) - Text( - style = Relations2, - text = stringResource(R.string.chat), - color = colorResource(id = R.color.text_secondary), - modifier = Modifier.padding( - start = 20.dp - ) - ) - } - } - } - } -} - -@Composable -private fun ChatUserAvatar( - msg: DiscussionView.Message, - avatar: DiscussionView.Message.Avatar, - modifier: Modifier -) { - Box( - modifier = modifier - .size(32.dp) - .background( - color = colorResource(id = R.color.text_tertiary), - shape = CircleShape - ) - ) { - Text( - text = msg.author.take(1).uppercase().ifEmpty { stringResource(id = R.string.u) }, - modifier = Modifier.align(Alignment.Center), - style = TextStyle( - fontSize = 20.sp, - fontWeight = FontWeight.SemiBold, - color = colorResource(id = R.color.text_white) - ) - ) - if (avatar is DiscussionView.Message.Avatar.Image) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(avatar.hash) - .crossfade(true) - .build(), - contentDescription = "Space member profile icon", - modifier = modifier - .size(32.dp) - .clip(CircleShape), - contentScale = ContentScale.Crop - ) - } - } -} - -@OptIn(ExperimentalGlideComposeApi::class, ExperimentalMaterial3Api::class) -@Composable -fun Bubble( - modifier: Modifier = Modifier, - name: String, - reply: DiscussionView.Message.Reply? = null, - content: DiscussionView.Message.Content, - timestamp: Long, - attachments: List = emptyList(), - isUserAuthor: Boolean = false, - isEdited: Boolean = false, - reactions: List = emptyList(), - onReacted: (String) -> Unit, - onDeleteMessage: () -> Unit, - onCopyMessage: () -> Unit, - onEditMessage: () -> Unit, - onReply: () -> Unit, - onAttachmentClicked: (DiscussionView.Message.Attachment) -> Unit, - onMarkupLinkClicked: (String) -> Unit, - onScrollToReplyClicked: (DiscussionView.Message.Reply) -> Unit, - onAddReactionClicked: () -> Unit, - onViewChatReaction: (String) -> Unit -) { - var showDropdownMenu by remember { mutableStateOf(false) } - var showDeleteMessageWarning by remember { mutableStateOf(false) } - if (showDeleteMessageWarning) { - ModalBottomSheet( - onDismissRequest = { - showDeleteMessageWarning = false - }, - containerColor = colorResource(id = R.color.background_secondary), - shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - dragHandle = null - ) { - GenericAlert( - config = AlertConfig.WithTwoButtons( - title = stringResource(R.string.chats_alert_delete_this_message), - description = stringResource(R.string.chats_alert_delete_this_message_description), - firstButtonText = stringResource(R.string.cancel), - secondButtonText = stringResource(R.string.delete), - secondButtonType = BUTTON_WARNING, - firstButtonType = BUTTON_SECONDARY, - icon = AlertConfig.Icon( - gradient = GRADIENT_TYPE_RED, - icon = R.drawable.ic_alert_question_warning - ) - ), - onFirstButtonClicked = { - showDeleteMessageWarning = false - }, - onSecondButtonClicked = { - onDeleteMessage() - } - ) - } - } - Column( - modifier = modifier - .width(IntrinsicSize.Max) - .background( - color = if (isUserAuthor) - colorResource(R.color.background_primary) - else - colorResource(R.color.shape_transparent_secondary), - shape = RoundedCornerShape(20.dp) - ) - .clip(RoundedCornerShape(20.dp)) - .clickable { - showDropdownMenu = !showDropdownMenu - } - ) { - if (reply != null) { - Box( - modifier = Modifier - .padding(4.dp) - .fillMaxWidth() - .height(52.dp) - .background( - color = colorResource(R.color.shape_transparent_secondary), - shape = RoundedCornerShape(16.dp) - ) - .clip(RoundedCornerShape(16.dp)) - .clickable { - onScrollToReplyClicked(reply) - } - ) { - Box( - modifier = Modifier - .width(4.dp) - .fillMaxHeight() - .background( - color = colorResource(R.color.shape_transparent_primary) - ) - ) - Text( - text = reply.author, - modifier = Modifier.padding( - start = 12.dp, - top = 8.dp, - end = 12.dp - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = colorResource(id = R.color.text_primary), - style = Caption1Medium - ) - Text( - modifier = Modifier.padding( - start = 12.dp, - top = 26.dp, - end = 12.dp - ), - text = reply.text, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = colorResource(id = R.color.text_primary), - style = Caption1Regular - ) - } - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding( - start = 12.dp, - end = 12.dp, - top = if (reply == null) 12.dp else 0.dp - ), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = name, - style = PreviewTitle2Medium, - color = colorResource(id = R.color.text_primary), - maxLines = 1 - ) - Spacer(Modifier.width(12.dp)) - Text( - modifier = Modifier.padding(top = 1.dp), - text = timestamp.formatTimeInMillis( - TIME_H24 - ), - style = Caption1Regular, - color = colorResource(id = R.color.text_secondary), - maxLines = 1 - ) - } - if (content.msg.isNotEmpty()) { - Text( - modifier = Modifier.padding( - top = 0.dp, - start = 12.dp, - end = 12.dp, - bottom = if (reactions.isEmpty() && attachments.isEmpty()) 12.dp else 0.dp - ), - text = buildAnnotatedString { - content.parts.forEach { part -> - if (part.link != null && part.link.param != null) { - withLink( - LinkAnnotation.Clickable( - tag = "link", - styles = TextLinkStyles( - style = SpanStyle( - fontWeight = if (part.isBold) FontWeight.Bold else null, - fontStyle = if (part.isItalic) FontStyle.Italic else null, - textDecoration = TextDecoration.Underline - ) - ) - ) { - onMarkupLinkClicked(part.link.param.orEmpty()) - } - ) { - append(part.part) - } - } else { - withStyle( - style = SpanStyle( - fontWeight = if (part.isBold) FontWeight.Bold else null, - fontStyle = if (part.isItalic) FontStyle.Italic else null, - textDecoration = if (part.underline) - TextDecoration.Underline - else if (part.isStrike) - TextDecoration.LineThrough - else null, - fontFamily = if (part.isCode) fontIBM else null, - ) - ) { - append(part.part) - } - } - } - if (isEdited) { - withStyle( - style = SpanStyle(color = colorResource(id = R.color.text_tertiary)) - ) { - append( - " (${stringResource(R.string.chats_message_edited)})" - ) - } - } - }, - style = BodyRegular, - color = colorResource(id = R.color.text_primary), - ) - } - BubbleAttachments( - attachments = attachments, - isUserAuthor = isUserAuthor, - onAttachmentClicked = onAttachmentClicked - ) - if (reactions.isNotEmpty()) { - ReactionList( - reactions = reactions, - onReacted = onReacted, - onViewReaction = onViewChatReaction - ) - } - MaterialTheme( - shapes = MaterialTheme.shapes.copy( - medium = RoundedCornerShape( - 16.dp - ) - ), - colors = MaterialTheme.colors.copy( - surface = colorResource(id = R.color.background_secondary) - ) - ) { - DropdownMenu( - offset = DpOffset(0.dp, 8.dp), - expanded = showDropdownMenu, - onDismissRequest = { - showDropdownMenu = false - } - ) { - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.chats_add_reaction), - color = colorResource(id = R.color.text_primary), - modifier = Modifier.padding(end = 64.dp) - ) - }, - onClick = { - onAddReactionClicked() - showDropdownMenu = false - } - ) - Divider(paddingStart = 0.dp, paddingEnd = 0.dp) - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.chats_reply), - color = colorResource(id = R.color.text_primary), - modifier = Modifier.padding(end = 64.dp) - ) - }, - onClick = { - onReply() - showDropdownMenu = false - } - ) - if (content.msg.isNotEmpty()) { - Divider(paddingStart = 0.dp, paddingEnd = 0.dp) - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.copy), - color = colorResource(id = R.color.text_primary), - modifier = Modifier.padding(end = 64.dp) - ) - }, - onClick = { - onCopyMessage() - showDropdownMenu = false - } - ) - } - if (isUserAuthor) { - Divider(paddingStart = 0.dp, paddingEnd = 0.dp) - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.edit), - color = colorResource(id = R.color.text_primary), - modifier = Modifier.padding(end = 64.dp) - ) - }, - onClick = { - onEditMessage() - showDropdownMenu = false - } - ) - } - if (isUserAuthor) { - Divider(paddingStart = 0.dp, paddingEnd = 0.dp) - DropdownMenuItem( - text = { - Text( - text = stringResource(id = R.string.delete), - color = colorResource(id = R.color.palette_system_red), - modifier = Modifier.padding(end = 64.dp) - ) - }, - onClick = { - showDeleteMessageWarning = true - showDropdownMenu = false - } - ) - } - } - } - } -} - -@Composable -@OptIn(ExperimentalGlideComposeApi::class) -private fun BubbleAttachments( - attachments: List, - onAttachmentClicked: (DiscussionView.Message.Attachment) -> Unit, - isUserAuthor: Boolean -) { - attachments.forEachIndexed { idx, attachment -> - when (attachment) { - is DiscussionView.Message.Attachment.Image -> { - Box( - modifier = Modifier - .padding( - start = 4.dp, - end = 4.dp, - bottom = 4.dp, - top = if (idx == 0) 4.dp else 0.dp - ) - .size(300.dp) - .background( - color = colorResource(R.color.shape_tertiary), - shape = RoundedCornerShape(16.dp) - ) - ) { - CircularProgressIndicator( - modifier = Modifier - .align(alignment = Alignment.Center) - .size(64.dp), - color = colorResource(R.color.glyph_active), - trackColor = colorResource(R.color.glyph_active).copy(alpha = 0.5f), - strokeWidth = 8.dp - ) - GlideImage( - model = attachment.url, - contentDescription = "Attachment image", - contentScale = ContentScale.Crop, - modifier = Modifier - .size(300.dp) - .clip(shape = RoundedCornerShape(16.dp)) - .clickable { - onAttachmentClicked(attachment) - } - ) - } - } - is DiscussionView.Message.Attachment.Link -> { - AttachedObject( - modifier = Modifier - .padding( - start = 4.dp, - end = 4.dp, - bottom = 4.dp, - top = if (idx == 0) 4.dp else 0.dp - ) - .fillMaxWidth() - , - title = attachment.wrapper?.name.orEmpty(), - type = attachment.typeName, - icon = attachment.icon, - onAttachmentClicked = { - onAttachmentClicked(attachment) - } - ) - } - } - } -} - -@Composable -fun TopDiscussionToolbar( - title: String? = null, - isHeaderVisible: Boolean = false -) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - ) { - Box( - modifier = Modifier - .fillMaxHeight() - .width(48.dp) - ) { - Box( - modifier = Modifier - .size(10.dp) - .align(Alignment.Center) - .background(color = Color.Green, shape = CircleShape) - ) - } - Text( - text = if (isHeaderVisible) "" else title ?: stringResource(id = R.string.untitled), - style = PreviewTitle2Regular, - color = colorResource(id = R.color.text_primary), - textAlign = TextAlign.Center, - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Box( - modifier = Modifier - .fillMaxHeight() - .width(48.dp) - ) { - Image( - painter = painterResource(id = R.drawable.ic_toolbar_three_dots), - contentDescription = "Three dots menu", - modifier = Modifier.align(Alignment.Center) - ) - } - } -} - -@Composable -fun AttachedObject( - modifier: Modifier, - title: String, - type: String, - icon: ObjectIcon, - onAttachmentClicked: () -> Unit -) { - Box( - modifier = modifier - .height(72.dp) - .clip(RoundedCornerShape(18.dp)) - .border( - width = 1.dp, - color = colorResource(id = R.color.shape_transparent_secondary), - shape = RoundedCornerShape(18.dp) - ) - .background( - color = colorResource(id = R.color.background_secondary) - ) - .clickable { - onAttachmentClicked() - } - ) { - ListWidgetObjectIcon( - icon = icon, - iconSize = 48.dp, - modifier = Modifier - .padding( - start = 12.dp - ) - .align(alignment = Alignment.CenterStart), - onTaskIconClicked = { - // Do nothing - } - ) - Text( - text = title.ifEmpty { stringResource(R.string.untitled) }, - modifier = Modifier.padding( - start = if (icon != ObjectIcon.None) - 72.dp - else - 12.dp, - top = 17.5.dp, - end = 12.dp - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = PreviewTitle2Medium, - color = colorResource(id = R.color.text_primary) - ) - Text( - text = type.ifEmpty { stringResource(R.string.unknown_type) }, - modifier = Modifier - .align(Alignment.BottomStart) - .padding( - start = if (icon != ObjectIcon.None) - 72.dp - else - 12.dp, - bottom = 17.5.dp, - end = 12.dp - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = Relations3, - color = colorResource(id = R.color.text_secondary) - ) - } -} - -@Composable -fun GoToBottomButton( - enabled: Boolean, - modifier: Modifier, - onGoToBottomClicked: () -> Unit -) { - val transition = updateTransition( - enabled, - label = "JumpToBottom visibility animation" - ) - val bottomOffset by transition.animateDp(label = "JumpToBottom offset animation") { - if (it) { - (12).dp - } else { - (-12).dp - } - } - if (bottomOffset > 0.dp) { - Box( - modifier = modifier - .offset(x = 0.dp, y = -bottomOffset) - .size(48.dp) - .clip(RoundedCornerShape(12.dp)) - .background(color = colorResource(id = R.color.navigation_panel)) - .clickable { - onGoToBottomClicked() - } - - ) { - Image( - painter = painterResource(id = R.drawable.ic_go_to_bottom_arrow), - contentDescription = "Arrow icon", - modifier = Modifier.align(Alignment.Center) - ) - } - } -} - -@OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) -@Composable -fun ReactionList( - reactions: List, - onReacted: (String) -> Unit, - onViewReaction: (String) -> Unit -) { - FlowRow( - modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp, top = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - reactions.forEach { reaction -> - Box( - modifier = Modifier - .height(28.dp) - .width(46.dp) - .background( - color = if (reaction.isSelected) - colorResource(id = R.color.palette_very_light_orange) - else - colorResource(id = R.color.shape_transparent_primary), - shape = RoundedCornerShape(100.dp) - ) - .clip(RoundedCornerShape(100.dp)) - .then( - if (reaction.isSelected) - Modifier.border( - width = 1.dp, - color = colorResource(id = R.color.palette_system_amber_50), - shape = RoundedCornerShape(100.dp) - ) - else - Modifier - ) - .combinedClickable( - onClick = { - onReacted(reaction.emoji) - }, - onLongClick = { - onViewReaction(reaction.emoji) - } - ) - ) { - Text( - text = reaction.emoji, - style = BodyCalloutMedium, - modifier = Modifier - .align( - alignment = Alignment.CenterStart - ) - .padding( - start = 8.dp - ) - ) - Text( - text = reaction.count.toString(), - style = Caption1Regular, - modifier = Modifier - .align( - alignment = Alignment.CenterEnd - ) - .padding( - end = 8.dp - ), - color = colorResource(id = R.color.text_primary) - ) - } - } - } -} - -@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 ReactionListPreview() { - ReactionList( - reactions = listOf( - DiscussionView.Message.Reaction( - emoji = "❤\uFE0F", - count = 1, - isSelected = false - ), - DiscussionView.Message.Reaction( - emoji = "❤\uFE0F", - count = 1, - isSelected = true - ), - DiscussionView.Message.Reaction( - emoji = "❤\uFE0F", - count = 1, - isSelected = false - ), - DiscussionView.Message.Reaction( - emoji = "❤\uFE0F", - count = 1, - isSelected = false - ), - DiscussionView.Message.Reaction( - emoji = "❤\uFE0F", - count = 1, - isSelected = false - ), - DiscussionView.Message.Reaction( - emoji = "❤\uFE0F", - count = 1, - isSelected = false - ) - ), - onReacted = {}, - onViewReaction = {} - ) -} - -@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 TopDiscussionToolbarPreview() { - TopDiscussionToolbar() -} - -@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 AttachmentPreview() { - AttachedObject( - modifier = Modifier.fillMaxWidth(), - icon = ObjectIcon.None, - type = "Project", - title = "Travel to Switzerland", - onAttachmentClicked = {} - ) -} - -private const val HEADER_KEY = "key.discussions.item.header" -private val JumpToBottomThreshold = 200.dp \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt index ec82297c6c..ec6eecea03 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt @@ -4456,7 +4456,7 @@ class EditorViewModel( ) ) } - is OpenObjectNavigation.OpenDiscussion -> { + is OpenObjectNavigation.OpenChat -> { sendToast("not implemented") } is OpenObjectNavigation.UnexpectedLayoutError -> { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt index 3bf91eb615..0a78e9e593 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/home/HomeScreenViewModel.kt @@ -1097,7 +1097,7 @@ class HomeScreenViewModel( val space = spaceView.space.targetSpaceId if (chat != null && space != null) { navigation( - Navigation.OpenDiscussion( + Navigation.OpenChat( space = space, ctx = chat ) @@ -1422,9 +1422,9 @@ class HomeScreenViewModel( ) ) } - is OpenObjectNavigation.OpenDiscussion -> { + is OpenObjectNavigation.OpenChat -> { navigate( - Navigation.OpenDiscussion( + Navigation.OpenChat( ctx = navigation.target, space = navigation.space ) @@ -2168,7 +2168,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 OpenChat(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() @@ -2403,7 +2403,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() + data class OpenChat(val target: Id, val space: Id): OpenObjectNavigation() data class OpenDataObject(val target: Id, val space: Id): OpenObjectNavigation() } @@ -2448,7 +2448,7 @@ fun ObjectWrapper.Basic.navigation() : OpenObjectNavigation { ) } ObjectType.Layout.CHAT_DERIVED -> { - OpenObjectNavigation.OpenDiscussion( + OpenObjectNavigation.OpenChat( target = id, space = requireNotNull(spaceId) ) @@ -2500,7 +2500,7 @@ fun ObjectType.Layout.navigation( ) } ObjectType.Layout.CHAT_DERIVED -> { - OpenObjectNavigation.OpenDiscussion( + OpenObjectNavigation.OpenChat( target = target, space = space ) diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt index ec4792e5de..455054192f 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/mapper/ObjectIconMapper.kt @@ -47,7 +47,7 @@ fun ObjectType.Layout?.emptyType(): ObjectIcon.Empty { ObjectType.Layout.SET, ObjectType.Layout.COLLECTION -> ObjectIcon.Empty.List ObjectType.Layout.OBJECT_TYPE -> ObjectIcon.Empty.ObjectType ObjectType.Layout.BOOKMARK -> ObjectIcon.Empty.Bookmark - ObjectType.Layout.CHAT, ObjectType.Layout.CHAT_DERIVED -> ObjectIcon.Empty.Discussion + ObjectType.Layout.CHAT, ObjectType.Layout.CHAT_DERIVED -> ObjectIcon.Empty.Chat ObjectType.Layout.DATE -> ObjectIcon.Empty.Date else -> ObjectIcon.Empty.Page } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectIcon.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectIcon.kt index 7ca9f10552..d0694146dd 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectIcon.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/objects/ObjectIcon.kt @@ -13,7 +13,7 @@ sealed class ObjectIcon { data object Page : Empty() data object List : Empty() data object Bookmark : Empty() - data object Discussion : Empty() + data object Chat : Empty() data object ObjectType : Empty() data object Date : Empty() } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt index e39ac133c6..c3c7b5416b 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/vault/VaultViewModel.kt @@ -13,7 +13,6 @@ import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Wallpaper import com.anytypeio.anytype.core_models.primitives.Space import com.anytypeio.anytype.core_models.primitives.SpaceId -import com.anytypeio.anytype.core_models.restrictions.SpaceStatus import com.anytypeio.anytype.domain.base.fold import com.anytypeio.anytype.domain.base.onFailure import com.anytypeio.anytype.domain.base.onSuccess @@ -281,7 +280,7 @@ class VaultViewModel( ) ) } - is OpenObjectNavigation.OpenDiscussion -> { + is OpenObjectNavigation.OpenChat -> { navigate( Navigation.OpenChat( ctx = navigation.target, diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/collection/CollectionViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/collection/CollectionViewModel.kt index 5d05d87aaf..53e88e6888 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/collection/CollectionViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/widgets/collection/CollectionViewModel.kt @@ -920,7 +920,7 @@ class CollectionViewModel( ) ) } - is OpenObjectNavigation.OpenDiscussion -> { + is OpenObjectNavigation.OpenChat -> { commands.emit( Command.OpenChat( target = navigation.target, diff --git a/settings.gradle b/settings.gradle index afc75da1cf..44d38f0269 100644 --- a/settings.gradle +++ b/settings.gradle @@ -65,6 +65,6 @@ include ':crash-reporting' include ':localization' include ':payments' include ':gallery-experience' -include ':feature-discussions' +include ':feature-chats' include ':feature-all-content' include ':feature-date'