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

DROID-2635 Discussions | Tech | New module, MVVM components, feature toggle, basic views (#1387)

This commit is contained in:
Evgenii Kozlov 2024-07-10 15:39:16 +02:00 committed by GitHub
parent 5a2b43b1dd
commit 97db09d65d
Signed by: github
GPG key ID: B5690EEEBB952194
15 changed files with 533 additions and 0 deletions

View file

@ -160,6 +160,7 @@ dependencies {
implementation project(':ui-settings')
implementation project(':crash-reporting')
implementation project(':payments')
implementation project(':feature-discussions')
implementation project(':gallery-experience')
//Compile time dependencies

View file

@ -31,4 +31,6 @@ class DefaultFeatureToggles @Inject constructor(
override val isConciseLogging: Boolean = true
override val enableSpaces: Boolean = true
override val enableDiscussionDemo: Boolean = false
}

View file

@ -0,0 +1,52 @@
package com.anytypeio.anytype.di.feature.discussions
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
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.UserPermissionProvider
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionViewModelFactory
import com.anytypeio.anytype.middleware.EventProxy
import dagger.Binds
import dagger.Component
import dagger.Module
@Component(
dependencies = [DiscussionComponentDependencies::class],
modules = [
DiscussionModule::class,
DiscussionModule.Declarations::class
]
)
@PerScreen
interface DiscussionComponent {
@Component.Factory
interface Factory {
fun create(dependencies: DiscussionComponentDependencies): DiscussionComponent
}
}
@Module
object DiscussionModule {
@Module
interface Declarations {
@PerScreen
@Binds
fun bindViewModelFactory(
factory: DiscussionViewModelFactory
): ViewModelProvider.Factory
}
}
interface DiscussionComponentDependencies : ComponentDependencies {
fun blockRepository(): BlockRepository
fun appCoroutineDispatchers(): AppCoroutineDispatchers
fun analytics(): Analytics
fun urlBuilder(): UrlBuilder
fun userPermissionProvider(): UserPermissionProvider
fun eventProxy(): EventProxy
}

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Payload
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.widgets.SelectWidgetSourceSubcomponent
import com.anytypeio.anytype.di.feature.widgets.SelectWidgetTypeSubcomponent
@ -287,4 +288,5 @@ interface HomeScreenDependencies : ComponentDependencies {
fun notificationChannel(): NotificationsChannel
fun activeSpaceMembers() : ActiveSpaceMemberSubscriptionContainer
fun analyticSpaceHelperDelegate(): AnalyticSpaceHelperDelegate
fun featureToggles(): FeatureToggles
}

View file

@ -4,9 +4,16 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.colorResource
@ -16,14 +23,21 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.extensions.throttledClick
import com.anytypeio.anytype.core_utils.ext.argOrNull
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
import com.anytypeio.anytype.core_utils.ui.BaseComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.feature_discussions.ui.DiscussionPreview
import com.anytypeio.anytype.feature_discussions.ui.DiscussionScreenPreview
import com.anytypeio.anytype.feature_discussions.ui.DiscussionScreenWrapper
import com.anytypeio.anytype.other.DefaultDeepLinkResolver
import com.anytypeio.anytype.presentation.home.Command
import com.anytypeio.anytype.presentation.home.HomeScreenViewModel
@ -38,6 +52,7 @@ import com.anytypeio.anytype.ui.settings.space.SpaceSettingsFragment
import com.anytypeio.anytype.ui.settings.typography
import com.anytypeio.anytype.ui.widgets.SelectWidgetSourceFragment
import com.anytypeio.anytype.ui.widgets.SelectWidgetTypeFragment
import com.google.accompanist.navigation.material.rememberBottomSheetNavigator
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber
@ -53,6 +68,9 @@ class HomeScreenFragment : BaseComposeFragment() {
@Inject
lateinit var factory: HomeScreenViewModel.Factory
@Inject
lateinit var featureToggles: FeatureToggles
private val vm by viewModels<HomeScreenViewModel> { factory }
override fun onCreateView(
@ -112,6 +130,10 @@ class HomeScreenFragment : BaseComposeFragment() {
onObjectCheckboxClicked = vm::onObjectCheckboxClicked,
onSpaceShareIconClicked = vm::onSpaceShareIconClicked
)
if (featureToggles.enableDiscussionDemo) {
DiscussionScreenWrapper()
}
}
}
}
@ -309,6 +331,12 @@ class HomeScreenFragment : BaseComposeFragment() {
componentManager().homeScreenComponent.release()
}
override fun onApplyWindowRootInsets(view: View) {
if (!featureToggles.enableDiscussionDemo) {
super.onApplyWindowRootInsets(view)
}
}
companion object {
const val SHOW_MNEMONIC_KEY = "arg.home-screen.show-mnemonic"
const val DEEP_LINK_KEY = "arg.home-screen.deep-link"

View file

@ -17,4 +17,6 @@ interface FeatureToggles {
val isLogEditorControlPanelMachine: Boolean
val enableSpaces: Boolean
val enableDiscussionDemo: Boolean
}

View file

@ -0,0 +1,60 @@
plugins {
id "com.android.library"
id "kotlin-android"
alias(libs.plugins.compose.compiler)
}
android {
buildFeatures {
compose true
}
namespace 'com.anytypeio.anytype.feature_discussions'
testOptions {
unitTests.returnDefaultValues = true
}
}
dependencies {
implementation project(':domain')
implementation project(':core-ui')
implementation project(':analytics')
implementation project(':core-models')
implementation project(':core-utils')
implementation project(':localization')
implementation project(':presentation')
implementation project(':library-emojifier')
implementation libs.navigationCompose
compileOnly libs.javaxInject
implementation libs.lifecycleViewModel
implementation libs.lifecycleRuntime
implementation libs.appcompat
implementation libs.compose
implementation libs.composeFoundation
implementation libs.composeToolingPreview
implementation libs.composeMaterial3
implementation libs.coilCompose
debugImplementation libs.composeTooling
implementation libs.timber
testImplementation libs.junit
testImplementation libs.kotlinTest
testImplementation libs.mockitoKotlin
testImplementation libs.coroutineTesting
testImplementation libs.liveDataTesting
testImplementation libs.archCoreTesting
testImplementation libs.androidXTestCore
testImplementation libs.robolectric
testImplementation libs.timberJUnit
testImplementation libs.turbine
testImplementation project(":test:utils")
testImplementation project(":test:core-models-stub")
}

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest/>

View file

@ -0,0 +1,10 @@
package com.anytypeio.anytype.feature_discussions.presentation
sealed interface DiscussionView {
data class Message(
val id: String,
val msg: String,
val author: String,
val timestamp: Long
) : DiscussionView
}

View file

@ -0,0 +1,7 @@
package com.anytypeio.anytype.feature_discussions.presentation
import com.anytypeio.anytype.presentation.common.BaseViewModel
class DiscussionViewModel : BaseViewModel() {
}

View file

@ -0,0 +1,11 @@
package com.anytypeio.anytype.feature_discussions.presentation
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import javax.inject.Inject
class DiscussionViewModelFactory @Inject constructor(
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = DiscussionViewModel() as T
}

View file

@ -0,0 +1,73 @@
package com.anytypeio.anytype.feature_discussions.ui
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.anytypeio.anytype.feature_discussions.R
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionView
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@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 DiscussionPreview() {
Messages(
messages = listOf(
DiscussionView.Message(
id = "1",
msg = stringResource(id = R.string.default_text_placeholder),
author = "Walter",
timestamp = System.currentTimeMillis()
),
DiscussionView.Message(
id = "2",
msg = stringResource(id = R.string.default_text_placeholder),
author = "Leo",
timestamp = System.currentTimeMillis()
),
DiscussionView.Message(
id = "3",
msg = stringResource(id = R.string.default_text_placeholder),
author = "Gilbert",
timestamp = System.currentTimeMillis()
)
)
)
}
@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 DiscussionScreenPreview() {
DiscussionScreen(
title = "Conversations with friends",
messages = buildList {
repeat(30) { idx ->
add(
DiscussionView.Message(
id = idx.toString(),
msg = stringResource(id = R.string.default_text_placeholder),
author = "User ${idx.inc()}",
timestamp =
System.currentTimeMillis()
- 30.toDuration(DurationUnit.DAYS).inWholeMilliseconds
+ idx.toDuration(DurationUnit.DAYS).inWholeMilliseconds
)
)
}
}
)
}
@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 BubblePreview() {
Bubble(
name = "Leo Marx",
msg = stringResource(id = R.string.default_text_placeholder),
timestamp = System.currentTimeMillis()
)
}

View file

@ -0,0 +1,273 @@
package com.anytypeio.anytype.feature_discussions.ui
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.Spacer
import androidx.compose.foundation.layout.WindowInsets
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
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_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.HeadlineTitle
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations2
import com.anytypeio.anytype.core_utils.const.DateConst.DEFAULT_DATE_FORMAT
import com.anytypeio.anytype.core_utils.ext.formatTimeInMillis
import com.anytypeio.anytype.feature_discussions.R
import com.anytypeio.anytype.feature_discussions.presentation.DiscussionView
@Composable
fun DiscussionScreenWrapper() {
NavHost(
navController = rememberNavController(),
startDestination = "discussions"
) {
composable(
route = "discussions"
) {
Surface(
modifier = Modifier
.fillMaxSize()
.background(color = colorResource(id = R.color.background_primary))
) {
DiscussionScreenPreview()
}
}
}
}
/**
* TODO: do date formating before rendering?
*/
@Composable
fun DiscussionScreen(
title: String,
messages: List<DiscussionView.Message>
) {
Column(
modifier = Modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.systemBars)
) {
Text(
style = HeadlineTitle,
text = title,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.padding(
top = 20.dp,
start = 20.dp,
end = 20.dp,
bottom = 8.dp
)
)
Text(
style = Relations2,
text = "Discussion",
color = colorResource(id = R.color.text_secondary),
modifier = Modifier.padding(
bottom = 8.dp,
start = 20.dp
)
)
Messages(
modifier = Modifier.weight(1.0f),
messages = messages
)
Divider(
paddingStart = 0.dp,
paddingEnd = 0.dp
)
Row(
modifier = Modifier
.imePadding()
.fillMaxWidth()
.height(56.dp)
) {
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.clip(CircleShape)
.align(Alignment.CenterVertically)
.clickable {
// TODO
}
) {
Image(
painter = painterResource(id = R.drawable.ic_plus_32),
contentDescription = "Plus button",
modifier = Modifier
.align(Alignment.Center)
.clickable {
// TODO
}
)
}
var textField by rememberSaveable { mutableStateOf("") }
BasicTextField(
value = textField,
onValueChange = { textField = it },
textStyle = BodyRegular.copy(
color = colorResource(id = R.color.text_primary)
),
modifier = Modifier
.imePadding()
.weight(1f)
.padding(
start = 4.dp,
end = 4.dp
)
.align(Alignment.CenterVertically)
,
cursorBrush = SolidColor(colorResource(id = R.color.palette_system_blue))
)
Box(
modifier = Modifier
.padding(horizontal = 4.dp)
.clip(CircleShape)
.align(Alignment.CenterVertically)
.clickable {
// TODO
}
) {
Image(
painter = painterResource(id = R.drawable.ic_send_message),
contentDescription = "Send message button",
modifier = Modifier
.padding(horizontal = 4.dp, vertical = 4.dp)
.align(Alignment.Center)
)
}
}
}
}
@Composable
fun Messages(
modifier: Modifier = Modifier,
messages: List<DiscussionView.Message>
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
itemsIndexed(
messages,
key = { _, msg -> msg.id }
) { idx, msg ->
if (idx == 0)
Spacer(modifier = Modifier.height(36.dp))
Row(
modifier = Modifier.padding(horizontal = 48.dp)
) {
Box(
modifier = Modifier
.size(32.dp)
.background(
colorResource(id = R.color.palette_system_blue),
shape = CircleShape
)
.align(Alignment.Bottom)
)
Spacer(modifier = Modifier.width(8.dp))
Bubble(
name = msg.author,
msg = msg.msg,
timestamp = msg.timestamp
)
}
if (idx == messages.lastIndex) {
Spacer(modifier = Modifier.height(36.dp))
}
}
}
}
@Composable
fun Bubble(
name: String,
msg: String,
timestamp: Long
) {
Box(
modifier = Modifier
.fillMaxWidth()
.background(
color = colorResource(id = R.color.palette_very_light_grey),
shape = RoundedCornerShape(24.dp)
)
) {
Row(
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
top = 12.dp
)
) {
Text(
text = name,
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary),
maxLines = 1,
modifier = Modifier.weight(1f)
)
Text(
text = timestamp.formatTimeInMillis(
DEFAULT_DATE_FORMAT
),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary),
maxLines = 1
)
}
Text(
modifier = Modifier.padding(
top = 32.dp,
start = 16.dp,
end = 16.dp,
bottom = 12.dp
),
text = msg,
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M5.406,25.615C4.709,24.9 5.032,24.094 5.564,23.088L8.329,17.813C8.688,17.153 8.966,16.915 9.565,16.899L25.466,16.285C25.653,16.277 25.758,16.154 25.758,16.001C25.75,15.855 25.653,15.724 25.466,15.717L9.565,15.164C8.943,15.141 8.658,14.88 8.329,14.235L5.511,8.829C5.009,7.869 4.717,7.094 5.406,6.387C5.953,5.827 6.823,5.934 7.692,6.333L25.518,14.481C25.998,14.696 26.38,14.934 26.635,15.195C27.122,15.694 27.122,16.308 26.635,16.807C26.38,17.068 25.998,17.306 25.518,17.521L7.789,25.63C6.793,26.083 5.946,26.167 5.406,25.615Z"
android:fillColor="@color/glyph_selected"/>
</vector>

View file

@ -64,3 +64,4 @@ include ':crash-reporting'
include ':localization'
include ':payments'
include ':gallery-experience'
include ':feature-discussions'