From c62c180a8aff6283c3723baec646ce1201630276 Mon Sep 17 00:00:00 2001 From: Mohammed Boukadir Date: Sat, 17 Dec 2022 12:20:44 +0100 Subject: [PATCH] WIP Basic toot creation screen ui layer --- app-android/src/main/AndroidManifest.xml | 1 + settings.gradle.kts | 2 +- .../common/modifiers/WindowInsets.kt | 18 + .../common/modifiers/WindowInsets.kt | 19 + .../common/modifiers/WindowInsets.kt | 22 ++ ui/compose-toot/build.gradle.kts | 27 ++ .../src/androidMain/AndroidManifest.xml | 2 + .../composetoot/ComposeTootComponent.kt | 25 ++ .../composetoot/ComposeTootContent.kt | 347 ++++++++++++++++++ .../androidev/composetoot/ComposeTootState.kt | 39 ++ .../composetoot/ComposeTootViewModel.kt | 89 +++++ .../DefaultComposeTootComponent.kt | 46 +++ .../root/navigation/DefaultRootComponent.kt | 1 + ui/signed-in/build.gradle.kts | 1 + .../composables/SignedInRootContent.kt | 9 +- .../DefaultSignedInRootComponent.kt | 29 +- .../navigation/SignedInRootComponent.kt | 3 + .../androiddev/timeline/TimelineContent.kt | 39 +- .../navigation/DefaultTimelineComponent.kt | 7 +- .../timeline/navigation/TimelineComponent.kt | 4 +- 20 files changed, 717 insertions(+), 13 deletions(-) create mode 100644 ui/common/src/androidMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt create mode 100644 ui/common/src/commonMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt create mode 100644 ui/common/src/desktopMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt create mode 100644 ui/compose-toot/build.gradle.kts create mode 100644 ui/compose-toot/src/androidMain/AndroidManifest.xml create mode 100644 ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootComponent.kt create mode 100644 ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootContent.kt create mode 100644 ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootState.kt create mode 100644 ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootViewModel.kt create mode 100644 ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/DefaultComposeTootComponent.kt diff --git a/app-android/src/main/AndroidManifest.xml b/app-android/src/main/AndroidManifest.xml index 054eec07..1e0af74a 100644 --- a/app-android/src/main/AndroidManifest.xml +++ b/app-android/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ diff --git a/settings.gradle.kts b/settings.gradle.kts index 2838232a..b387278c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -33,7 +33,7 @@ include(":ui:common") include(":ui:root") include(":ui:signed-in") include(":ui:signed-out") - +include(":ui:compose-toot") include(":domain:timeline") include(":domain:authentication") diff --git a/ui/common/src/androidMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt b/ui/common/src/androidMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt new file mode 100644 index 00000000..1ea04654 --- /dev/null +++ b/ui/common/src/androidMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt @@ -0,0 +1,18 @@ +/* + * This file is part of Dodo. + * + * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Dodo. If not, see . + */ +package social.androiddev.common.modifiers + +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.ui.Modifier + +actual fun Modifier.moveWithKeyboard(): Modifier { + return this.navigationBarsPadding().imePadding() +} diff --git a/ui/common/src/commonMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt b/ui/common/src/commonMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt new file mode 100644 index 00000000..4f242d24 --- /dev/null +++ b/ui/common/src/commonMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt @@ -0,0 +1,19 @@ +/* + * This file is part of Dodo. + * + * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Dodo. If not, see . + */ +package social.androiddev.common.modifiers + +import androidx.compose.ui.Modifier + +/** + * + * Use navigationBarsPadding() and imePadding() to move the composable above both the + * navigation bar, and on-screen keyboard (IME) + */ +expect fun Modifier.moveWithKeyboard(): Modifier diff --git a/ui/common/src/desktopMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt b/ui/common/src/desktopMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt new file mode 100644 index 00000000..d6a4af03 --- /dev/null +++ b/ui/common/src/desktopMain/kotlin/social/androiddev/common/modifiers/WindowInsets.kt @@ -0,0 +1,22 @@ +/* + * This file is part of Dodo. + * + * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Dodo. If not, see . + */ +package social.androiddev.common.modifiers + +import androidx.compose.ui.Modifier + +/** + * + * Use navigationBarsPadding() and imePadding() to move the composable above both the + * navigation bar, and on-screen keyboard (IME) + */ +actual fun Modifier.moveWithKeyboard(): Modifier { + // no-op in desktop + return this +} diff --git a/ui/compose-toot/build.gradle.kts b/ui/compose-toot/build.gradle.kts new file mode 100644 index 00000000..52dbf774 --- /dev/null +++ b/ui/compose-toot/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("social.androiddev.library.ui") + id("social.androiddev.codequality") +} + +android { + namespace = "social.androiddev.ui.composetoot" +} + +kotlin { + + sourceSets { + val commonMain by getting { + dependencies { + implementation(projects.domain.timeline) + implementation(projects.ui.common) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material) + implementation(compose.materialIconsExtended) + implementation(libs.io.insert.koin.core) + + } + } + + } +} diff --git a/ui/compose-toot/src/androidMain/AndroidManifest.xml b/ui/compose-toot/src/androidMain/AndroidManifest.xml new file mode 100644 index 00000000..4fb03756 --- /dev/null +++ b/ui/compose-toot/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootComponent.kt b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootComponent.kt new file mode 100644 index 00000000..8a5952fc --- /dev/null +++ b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootComponent.kt @@ -0,0 +1,25 @@ +/* + * This file is part of Dodo. + * + * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Dodo. If not, see . + */ +package social.androidev.composetoot + +import kotlinx.coroutines.flow.StateFlow + +/** + * The base component describing all business logic needed for the toot screen + */ +interface ComposeTootComponent { + + val state: StateFlow + fun onCloseClicked() + + fun onTootContentChange(text: String) + fun onPostClicked() + fun onActionClicked(action: Action) +} diff --git a/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootContent.kt b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootContent.kt new file mode 100644 index 00000000..14559671 --- /dev/null +++ b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootContent.kt @@ -0,0 +1,347 @@ +/* + * This file is part of Dodo. + * + * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Dodo. If not, see . + */ +package social.androidev.composetoot + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.material.AppBarDefaults +import androidx.compose.material.BottomAppBar +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.outlined.AlternateEmail +import androidx.compose.material.icons.outlined.AttachFile +import androidx.compose.material.icons.outlined.Feedback +import androidx.compose.material.icons.outlined.Language +import androidx.compose.material.icons.outlined.Mood +import androidx.compose.material.icons.outlined.Public +import androidx.compose.material.icons.outlined.Schedule +import androidx.compose.material.icons.outlined.Tag +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import social.androiddev.common.composables.buttons.DodoButton +import social.androiddev.common.theme.DodoTheme +import social.androiddev.common.utils.AsyncImage +import social.androiddev.common.utils.loadImageIntoPainter + +@Composable +fun ComposeTootContent( + modifier: Modifier = Modifier, + component: ComposeTootComponent +) { + + val state by component.state.collectAsState() + + ComposeTootContent( + modifier = modifier.fillMaxSize(), + composeTootState = state, + onCloseClicked = component::onCloseClicked, + onTootContentChange = component::onTootContentChange, + onActionClicked = component::onActionClicked, + onPostClicked = component::onPostClicked, + ) +} + +@Composable +private fun ComposeTootContent( + modifier: Modifier = Modifier, + composeTootState: ComposeTootState, + onCloseClicked: () -> Unit, + onTootContentChange: (String) -> Unit, + onActionClicked: (Action) -> Unit, + onPostClicked: () -> Unit, +) { + Surface(modifier = modifier) { + Column( + modifier = Modifier.fillMaxSize() + ) { + Header( + modifier = Modifier.fillMaxWidth(), + currentUser = composeTootState.currentUser, + onCloseClicked = onCloseClicked, + onActionClicked = onActionClicked, + ) + + ComposeTootArea( + modifier = Modifier.weight(1f), + content = composeTootState.content, + onTootContentChange = onTootContentChange + ) + + Footer( + modifier = Modifier.fillMaxWidth(), + tootTextCounter = composeTootState.tootTextCounter, + postTootEnabled = composeTootState.postTootEnabled, + selectedAction = composeTootState.selectedAction, + onPostClicked = onPostClicked, + onActionClicked = onActionClicked, + ) + } + } +} + +@Composable +private fun ComposeTootArea( + modifier: Modifier = Modifier, + content: Content, + onTootContentChange: (String) -> Unit +) { + Column( + modifier = modifier + ) { + + OutlinedTextField( + modifier = Modifier.fillMaxSize(), + value = content.toot, + onValueChange = onTootContentChange, + placeholder = { Text("What's happening?") } + ) + } +} + +@Composable +private fun Header( + currentUser: CurrentUser, + onCloseClicked: () -> Unit, + onActionClicked: (Action) -> Unit, + modifier: Modifier = Modifier +) { + TopAppBar( + modifier = modifier + ) { + + IconButton(onClick = onCloseClicked) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "Close" + ) + } + + Spacer( + modifier = Modifier.weight(1F) + ) + + ComposeTootIconButton( + onClick = { onActionClicked(Action.AddHashtag) }, + icon = Icons.Outlined.Tag, + description = "TAG", + selected = false + ) + + ComposeTootIconButton( + onClick = { onActionClicked(Action.AddMention) }, + icon = Icons.Outlined.AlternateEmail, + description = "Mention", + selected = false + ) + + ComposeTootIconButton( + onClick = { onActionClicked(Action.ChooseLanguage) }, + icon = Icons.Outlined.Language, + description = "Choose language", + selected = false + ) + + AsyncImage( + load = { loadImageIntoPainter(url = currentUser.avatarUrl) }, + painterFor = { remember { it } }, + contentDescription = currentUser.displayName, + modifier = Modifier + .size(48.dp) + .padding(2.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } +} + +@Composable +private fun Footer( + modifier: Modifier = Modifier, + tootTextCounter: String, + postTootEnabled: Boolean, + selectedAction: Action?, + onActionClicked: (Action) -> Unit, + onPostClicked: () -> Unit, +) { + + Column(modifier = modifier) { + BottomAppBar { + ComposeTootIconButton( + onClick = { onActionClicked(Action.AddMedia) }, + icon = Icons.Outlined.AttachFile, + description = "Add Media", + selected = selectedAction == Action.AddMedia + ) + + ComposeTootIconButton( + onClick = { onActionClicked(Action.ChangeVisibility) }, + icon = Icons.Outlined.Public, + description = "Toot Visibility", + selected = selectedAction == Action.ChangeVisibility + ) + + ComposeTootIconButton( + onClick = { onActionClicked(Action.AddContentWarning) }, + icon = Icons.Outlined.Feedback, + description = "Content Warning", + selected = selectedAction == Action.AddContentWarning + ) + + ComposeTootIconButton( + onClick = { onActionClicked(Action.AddEmoji) }, + icon = Icons.Outlined.Mood, + description = "Emoji", + selected = selectedAction == Action.AddEmoji + ) + + ComposeTootIconButton( + onClick = { onActionClicked(Action.Schedule) }, + icon = Icons.Outlined.Schedule, + description = "Schedule", + selected = selectedAction == Action.Schedule + ) + + Spacer( + modifier = Modifier.weight(1F) + ) + + Text( + text = tootTextCounter + ) + + Spacer( + modifier = Modifier.width(4.dp) + ) + + DodoButton( + text = "Post", + enabled = postTootEnabled, + onClick = onPostClicked + ) + } + + AnimatedVisibility(selectedAction != null) { + Surface( + elevation = AppBarDefaults.BottomAppBarElevation + ) { + Box( + modifier = Modifier.fillMaxWidth().height(100.dp), + contentAlignment = Alignment.Center + ) { + Text( + "Feature not yet Available !" + ) + } + } + } + } +} + +@Composable +private fun ComposeTootIconButton( + onClick: () -> Unit, + icon: ImageVector, + description: String, + selected: Boolean +) { + + val backgroundModifier = if (selected) { + Modifier.background( + color = LocalContentColor.current, + shape = CircleShape + ) + } else { + Modifier + } + + IconButton( + onClick = onClick, + modifier = backgroundModifier + ) { + val tint = if (selected) { + MaterialTheme.colors.primary + } else { + LocalContentColor.current + } + Icon( + icon, + tint = tint, + modifier = Modifier.padding(8.dp), + contentDescription = description + ) + } +} + +// skip preview to work with multiplatform +// https://github.com/JetBrains/compose-jb/issues/1603 +// @Preview +@Composable +private fun ComposeTootContentPreview() { + DodoTheme { + var state by remember { + mutableStateOf( + ComposeTootState( + content = Content( + toot = "", + warning = null + ), + currentUser = CurrentUser( + displayName = "Mohammed", + avatarUrl = "https://cdn.masto.host/androiddevsocial/accounts/avatars/109/412/078/242/933/246/original/dc1c7b288e98c67e.jpeg" + + ), + tootTextCounter = "500", + postTootEnabled = false, + ) + ) + } + + ComposeTootContent( + modifier = Modifier.fillMaxSize(), + composeTootState = state, + onCloseClicked = { + }, + onActionClicked = { + }, + onTootContentChange = { + state = state.copy(content = state.content.copy(toot = it)) + }, + onPostClicked = {}, + ) + } +} diff --git a/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootState.kt b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootState.kt new file mode 100644 index 00000000..5408651f --- /dev/null +++ b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootState.kt @@ -0,0 +1,39 @@ +/* + * This file is part of Dodo. + * + * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Dodo. If not, see . + */ +package social.androidev.composetoot + +data class ComposeTootState( + val content: Content, + val currentUser: CurrentUser, + val selectedAction: Action? = null, + val tootTextCounter: String, + val postTootEnabled: Boolean +) + +data class CurrentUser( + val displayName: String, + val avatarUrl: String +) + +data class Content( + val toot: String, + val warning: String? +) + +enum class Action { + AddMention, + AddHashtag, + ChooseLanguage, + AddMedia, + ChangeVisibility, + AddContentWarning, + AddEmoji, + Schedule +} diff --git a/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootViewModel.kt b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootViewModel.kt new file mode 100644 index 00000000..467fbd73 --- /dev/null +++ b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/ComposeTootViewModel.kt @@ -0,0 +1,89 @@ +/* + * This file is part of Dodo. + * + * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Dodo. If not, see . + */ +package social.androidev.composetoot + +import com.arkivanov.essenty.instancekeeper.InstanceKeeper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlin.coroutines.CoroutineContext + +internal class ComposeTootViewModel( + mainContext: CoroutineContext, + private val onTootSucceed: () -> Unit +) : InstanceKeeper.Instance { + + // The scope survives Android configuration changes + private val scope = CoroutineScope(mainContext + SupervisorJob()) + + private val _state = MutableStateFlow(createInitialState()) + + private fun createInitialState(): ComposeTootState { + + return ComposeTootState( + content = Content(toot = "", null), + currentUser = CurrentUser( + displayName = "Mohammed", + avatarUrl = "https://cdn.masto.host/androiddevsocial/accounts/avatars/109/412/078/242/933/246/original/dc1c7b288e98c67e.jpeg" + ), + tootTextCounter = "$TOOT_LENGTH", + postTootEnabled = false + ) + } + + val state: StateFlow = _state.asStateFlow() + + override fun onDestroy() { + scope.cancel() + } + + fun onActionClicked(action: Action) { + + _state.update { + // TODO implement the right logic + if (action == it.selectedAction || + action == Action.AddHashtag || + action == Action.AddMention || + action == Action.ChooseLanguage + ) { + it.copy(selectedAction = null) + } else { + it.copy(selectedAction = action) + } + } + } + + fun onTootContentChange(text: String) { + val tapedTextLength = text.length + if (tapedTextLength <= 500) { + _state.update { + val currentContent = it.content + it.copy( + content = Content(toot = text, warning = currentContent.warning), + tootTextCounter = "${500 - tapedTextLength}", + postTootEnabled = text.isNotBlank() + ) + } + } + } + + fun onPostClicked() { + // TODO("Not yet implemented") + onTootSucceed() + } + + companion object { + private const val TOOT_LENGTH = 500 + } +} diff --git a/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/DefaultComposeTootComponent.kt b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/DefaultComposeTootComponent.kt new file mode 100644 index 00000000..03c3c59e --- /dev/null +++ b/ui/compose-toot/src/commonMain/kotlin/social/androidev/composetoot/DefaultComposeTootComponent.kt @@ -0,0 +1,46 @@ +/* + * This file is part of Dodo. + * + * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + * + * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Dodo. If not, see . + */ +package social.androidev.composetoot + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.instancekeeper.getOrCreate +import kotlinx.coroutines.flow.StateFlow +import kotlin.coroutines.CoroutineContext + +class DefaultComposeTootComponent( + mainContext: CoroutineContext, + private val componentContext: ComponentContext, + private val onCloseClickedInternal: () -> Unit, +) : ComposeTootComponent, ComponentContext by componentContext { + + private val viewModel = instanceKeeper.getOrCreate { + ComposeTootViewModel( + mainContext = mainContext, + onTootSucceed = onCloseClickedInternal + ) + } + override val state: StateFlow = viewModel.state + + override fun onCloseClicked() { + onCloseClickedInternal() + } + + override fun onTootContentChange(text: String) { + viewModel.onTootContentChange(text) + } + + override fun onPostClicked() { + viewModel.onPostClicked() + } + + override fun onActionClicked(action: Action) { + viewModel.onActionClicked(action) + } +} diff --git a/ui/root/src/commonMain/kotlin/social/androiddev/root/navigation/DefaultRootComponent.kt b/ui/root/src/commonMain/kotlin/social/androiddev/root/navigation/DefaultRootComponent.kt index 4eed91a0..f3a63d20 100644 --- a/ui/root/src/commonMain/kotlin/social/androiddev/root/navigation/DefaultRootComponent.kt +++ b/ui/root/src/commonMain/kotlin/social/androiddev/root/navigation/DefaultRootComponent.kt @@ -67,6 +67,7 @@ class DefaultRootComponent( componentContext: ComponentContext, ) = DefaultSignedInRootComponent( componentContext = componentContext, + mainContext = mainContext ) private fun createSplashComponent( diff --git a/ui/signed-in/build.gradle.kts b/ui/signed-in/build.gradle.kts index e681ef59..1354e7cd 100644 --- a/ui/signed-in/build.gradle.kts +++ b/ui/signed-in/build.gradle.kts @@ -13,6 +13,7 @@ kotlin { dependencies { implementation(projects.ui.common) implementation(projects.ui.timeline) + implementation(projects.ui.composeToot) } } diff --git a/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/composables/SignedInRootContent.kt b/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/composables/SignedInRootContent.kt index 844676e4..7e34eae4 100644 --- a/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/composables/SignedInRootContent.kt +++ b/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/composables/SignedInRootContent.kt @@ -21,6 +21,7 @@ import com.arkivanov.decompose.extensions.compose.jetbrains.subscribeAsState import social.androiddev.signedin.navigation.SignedInRootComponent import social.androiddev.timeline.TimelineContent import social.androiddev.timeline.navigation.TimelineComponent +import social.androidev.composetoot.ComposeTootContent /** * The root composable for when the user launches the app and is @@ -49,6 +50,12 @@ fun SignedInRootContent( is SignedInRootComponent.Child.Timeline -> { TimelineTab(child.component) } + + is SignedInRootComponent.Child.ComposeToot -> { + ComposeTootContent( + component = child.component + ) + } } } } @@ -56,7 +63,7 @@ fun SignedInRootContent( @Composable private fun TimelineTab( - component: TimelineComponent + component: TimelineComponent, ) { TimelineContent( component = component, diff --git a/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/navigation/DefaultSignedInRootComponent.kt b/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/navigation/DefaultSignedInRootComponent.kt index 2978e449..7fe94ff4 100644 --- a/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/navigation/DefaultSignedInRootComponent.kt +++ b/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/navigation/DefaultSignedInRootComponent.kt @@ -13,10 +13,14 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.arkivanov.decompose.router.stack.push import com.arkivanov.decompose.value.Value import com.arkivanov.essenty.parcelable.Parcelable import com.arkivanov.essenty.parcelable.Parcelize import social.androiddev.timeline.navigation.DefaultTimelineComponent +import social.androidev.composetoot.DefaultComposeTootComponent +import kotlin.coroutines.CoroutineContext /** * Default impl of the [SignedInRootComponent] that manages the navigation stack for the @@ -24,7 +28,8 @@ import social.androiddev.timeline.navigation.DefaultTimelineComponent * See [Config] and [SignedInRootComponent.Child] for more details. */ class DefaultSignedInRootComponent( - private val componentContext: ComponentContext + private val componentContext: ComponentContext, + private val mainContext: CoroutineContext, ) : SignedInRootComponent, ComponentContext by componentContext { // StackNavigation accepts navigation commands and forwards them to all subscribed observers. @@ -40,17 +45,33 @@ class DefaultSignedInRootComponent( override val childStack: Value> = stack - private fun createChild(config: Config, componentContext: ComponentContext): SignedInRootComponent.Child = + private fun createChild( + config: Config, + componentContext: ComponentContext + ): SignedInRootComponent.Child = when (config) { Config.Timeline -> { SignedInRootComponent.Child.Timeline(createTimelineComponent(componentContext)) } + + Config.ComposeToot -> { + SignedInRootComponent.Child.ComposeToot(createComposeTootComponent(componentContext)) + } } private fun createTimelineComponent( componentContext: ComponentContext, ) = DefaultTimelineComponent( - componentContext = componentContext + componentContext = componentContext, + onComposeTootClickedInternal = { navigation.push(Config.ComposeToot) } + ) + + private fun createComposeTootComponent( + componentContext: ComponentContext, + ) = DefaultComposeTootComponent( + componentContext = componentContext, + mainContext = mainContext, + onCloseClickedInternal = { navigation.pop() } ) /** @@ -69,5 +90,7 @@ class DefaultSignedInRootComponent( @Parcelize object Timeline : Config + @Parcelize + object ComposeToot : Config } } diff --git a/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/navigation/SignedInRootComponent.kt b/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/navigation/SignedInRootComponent.kt index af718401..43265d81 100644 --- a/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/navigation/SignedInRootComponent.kt +++ b/ui/signed-in/src/commonMain/kotlin/social/androiddev/signedin/navigation/SignedInRootComponent.kt @@ -12,6 +12,7 @@ package social.androiddev.signedin.navigation import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.value.Value import social.androiddev.timeline.navigation.TimelineComponent +import social.androidev.composetoot.ComposeTootComponent /** * The base component describing all business logic needed for the signed-in root entry point @@ -28,5 +29,7 @@ interface SignedInRootComponent { sealed class Child { data class Timeline(val component: TimelineComponent) : Child() + + data class ComposeToot(val component: ComposeTootComponent) : Child() } } diff --git a/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/TimelineContent.kt b/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/TimelineContent.kt index 97f2e6be..0cafd058 100644 --- a/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/TimelineContent.kt +++ b/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/TimelineContent.kt @@ -10,13 +10,21 @@ package social.androiddev.timeline import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.FloatingActionButton +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme import androidx.compose.material.Surface +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Add import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @@ -35,6 +43,7 @@ fun TimelineContent( // TODO: Hook up to View Model for fetching timeline items TimelineContent( items = listOf(dummyFeedItem), + onComposeTootClicked = component::onComposeTootClicked, modifier = modifier, ) } @@ -42,6 +51,7 @@ fun TimelineContent( @Composable fun TimelineContent( items: List, + onComposeTootClicked: () -> Unit, modifier: Modifier = Modifier, ) { Surface( @@ -50,11 +60,25 @@ fun TimelineContent( .padding(8.dp) .fillMaxSize() ) { - LazyColumn { - items(items, key = { item -> item.id }) { state -> - TimelineCard( - state = state, - modifier = Modifier.wrapContentSize(), + Box { + LazyColumn { + items(items, key = { item -> item.id }) { state -> + TimelineCard( + state = state, + modifier = Modifier.wrapContentSize(), + ) + } + } + + FloatingActionButton( + modifier = Modifier.padding(32.dp).align(Alignment.BottomEnd).size(56.dp), + backgroundColor = MaterialTheme.colors.primary, + onClick = onComposeTootClicked + ) { + Icon( + imageVector = Icons.Outlined.Add, + tint = MaterialTheme.colors.onPrimary, + contentDescription = "Create toot" ) } } @@ -67,6 +91,9 @@ fun TimelineContent( @Composable private fun TimelinePreview() { DodoTheme { - TimelineContent(listOf(dummyFeedItem)) + TimelineContent( + listOf(dummyFeedItem), + onComposeTootClicked = {} + ) } } diff --git a/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/navigation/DefaultTimelineComponent.kt b/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/navigation/DefaultTimelineComponent.kt index e60ed20b..2cc3f8e1 100644 --- a/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/navigation/DefaultTimelineComponent.kt +++ b/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/navigation/DefaultTimelineComponent.kt @@ -13,4 +13,9 @@ import com.arkivanov.decompose.ComponentContext class DefaultTimelineComponent( private val componentContext: ComponentContext, -) : TimelineComponent + private val onComposeTootClickedInternal: () -> Unit, +) : TimelineComponent { + override fun onComposeTootClicked() { + onComposeTootClickedInternal() + } +} diff --git a/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/navigation/TimelineComponent.kt b/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/navigation/TimelineComponent.kt index 35236f68..ef814b6b 100644 --- a/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/navigation/TimelineComponent.kt +++ b/ui/timeline/src/commonMain/kotlin/social/androiddev/timeline/navigation/TimelineComponent.kt @@ -12,4 +12,6 @@ package social.androiddev.timeline.navigation /** * The base component describing all business logic needed for the timeline view */ -interface TimelineComponent +interface TimelineComponent { + fun onComposeTootClicked() +}