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

DROID-3232 App | Tech | Renamings (#1998)

This commit is contained in:
Evgenii Kozlov 2025-01-15 13:10:28 +01:00 committed by GitHub
parent 691fc84b23
commit a1aa1619e0
Signed by: github
GPG key ID: B5690EEEBB952194
51 changed files with 2208 additions and 2055 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<DiscussionViewModel> { factory }
private val vm by viewModels<ChatViewModel> { factory }
private val ctx get() = arg<Id>(CTX_KEY)
private val space get() = arg<Id>(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) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -159,7 +159,7 @@
<fragment
android:id="@+id/chatScreen"
android:name="com.anytypeio.anytype.di.feature.discussions.DiscussionFragment"
android:name="com.anytypeio.anytype.ui.chats.ChatFragment"
android:label="Discussion" />
<dialog

View file

@ -197,7 +197,7 @@ fun cornerRadius(size: Dp): Dp {
fun imageAsset(emptyType: ObjectIcon.Empty): Int {
return when (emptyType) {
ObjectIcon.Empty.Bookmark -> 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

View file

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

View file

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

View file

@ -9,7 +9,7 @@ android {
compose true
}
namespace 'com.anytypeio.anytype.feature_discussions'
namespace 'com.anytypeio.anytype.feature_chats'
testOptions {
unitTests.returnDefaultValues = true

View file

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

View file

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

View file

@ -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<String?>(null)
val messages = MutableStateFlow<List<DiscussionView>>(emptyList())
val chatBoxAttachments = MutableStateFlow<List<DiscussionView.Message.ChatBoxAttachment>>(emptyList())
val messages = MutableStateFlow<List<ChatView>>(emptyList())
val chatBoxAttachments = MutableStateFlow<List<ChatView.Message.ChatBoxAttachment>>(emptyList())
val commands = MutableSharedFlow<UXCommand>()
val navigation = MutableSharedFlow<OpenObjectNavigation>()
val chatBoxMode = MutableStateFlow<ChatBoxMode>(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<DiscussionView> {
var previousDate: ChatView.DateSection? = null
buildList<ChatView> {
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<String>) {
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<DefaultFileInfo>) {
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

View file

@ -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 <T : ViewModel> create(modelClass: Class<T>): T = DiscussionViewModel(
override fun <T : ViewModel> create(modelClass: Class<T>): T = ChatViewModel(
vmParams = params,
setObjectDetails = setObjectDetails,
openObject = openObject,

View file

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

View file

@ -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<ChatView.Message.Attachment>,
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 = {}
)
}

View file

@ -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<ChatView.Message.ChatBoxAttachment>,
clearText: () -> Unit,
updateValue: (TextFieldValue) -> Unit,
onAttachObjectClicked: () -> Unit,
onAttachMediaClicked: () -> Unit,
onAttachFileClicked: () -> Unit,
onUploadAttachmentClicked: () -> Unit,
onClearAttachmentClicked: (ChatView.Message.ChatBoxAttachment) -> Unit,
onClearReplyClicked: () -> Unit,
onChatBoxMediaPicked: (List<Uri>) -> Unit,
onChatBoxFilePicked: (List<Uri>) -> 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))
)
}
)
}

View file

@ -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<ChatView.Message.Attachment> = emptyList(),
isUserAuthor: Boolean = false,
isEdited: Boolean = false,
reactions: List<ChatView.Message.Reaction> = 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
)
}
}
}

View file

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

View file

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

View file

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

View file

@ -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<ChatView>,
attachments: List<ChatView.Message.ChatBoxAttachment>,
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<Uri>) -> Unit,
onChatBoxFilePicked: (List<Uri>) -> 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<ChatView>,
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

View file

@ -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<ChatView.Message.Reaction>,
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 = {}
)
}

View file

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

View file

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

View file

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

View file

@ -4456,7 +4456,7 @@ class EditorViewModel(
)
)
}
is OpenObjectNavigation.OpenDiscussion -> {
is OpenObjectNavigation.OpenChat -> {
sendToast("not implemented")
}
is OpenObjectNavigation.UnexpectedLayoutError -> {

View file

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

View file

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

View file

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

View file

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

View file

@ -920,7 +920,7 @@ class CollectionViewModel(
)
)
}
is OpenObjectNavigation.OpenDiscussion -> {
is OpenObjectNavigation.OpenChat -> {
commands.emit(
Command.OpenChat(
target = navigation.target,

View file

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