diff --git a/experimental/sample/octonaut/android/app/build.gradle.kts b/experimental/sample/octonaut/android/app/build.gradle.kts index 1223ec115..d579af162 100644 --- a/experimental/sample/octonaut/android/app/build.gradle.kts +++ b/experimental/sample/octonaut/android/app/build.gradle.kts @@ -44,13 +44,16 @@ dependencies { implementation(project(":experimental:sample:octonaut:xplat:foundation:di:api")) implementation(project(":experimental:sample:octonaut:xplat:foundation:networking:impl")) + implementation(project(":experimental:sample:octonaut:xplat:foundation:webview")) implementation(project(":experimental:sample:octonaut:xplat:common:market")) implementation(project(":experimental:sample:octonaut:xplat:domain:user:impl")) + implementation(project(":experimental:sample:octonaut:xplat:domain:feed:impl")) implementation(project(":experimental:sample:octonaut:xplat:domain:notifications:impl")) implementation(project(":experimental:sample:octonaut:xplat:feat:homeTab:impl")) implementation(project(":experimental:sample:octonaut:xplat:feat:notificationsTab:impl")) implementation(project(":experimental:sample:octonaut:xplat:feat:exploreTab:impl")) implementation(project(":experimental:sample:octonaut:xplat:feat:profileTab:impl")) + implementation(libs.compose.webview.multiplatform) } ksp { diff --git a/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/MainActivity.kt b/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/MainActivity.kt index 9d3d2b49b..f62a44ba3 100644 --- a/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/MainActivity.kt +++ b/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/MainActivity.kt @@ -5,23 +5,27 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import coil3.annotation.ExperimentalCoilApi -import com.slack.circuit.foundation.Circuit -import com.slack.circuit.foundation.CircuitCompositionLocals -import com.slack.circuit.foundation.CircuitContent -import com.slack.circuit.runtime.screen.Screen +import com.multiplatform.webview.web.WebView +import com.multiplatform.webview.web.WebViewNavigator +import com.multiplatform.webview.web.rememberWebViewState +import com.slack.circuit.backstack.rememberSaveableBackStack +import com.slack.circuit.foundation.* import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.sample.octonaut.android.app.di.CoreComponent import org.mobilenativefoundation.sample.octonaut.android.app.di.create @@ -31,7 +35,7 @@ import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.ap import kotlin.time.Duration.Companion.minutes -@OptIn(ExperimentalCoilApi::class) +@OptIn(ExperimentalCoilApi::class, ExperimentalMaterial3Api::class) @Inject class MainActivity : ComponentActivity() { @@ -51,27 +55,33 @@ class MainActivity : ComponentActivity() { setContent { + val homeTab = remember { coreComponent.screenFactory.homeTab() } + val exploreTab = remember { coreComponent.screenFactory.exploreTab() } + val notificationsTab = remember { coreComponent.screenFactory.notificationsTab() } + + val webViewUrlState = coreComponent.webViewUrlStateHolder.url.collectAsState() + + val webViewState = webViewUrlState.value?.let { rememberWebViewState(it) } + + + val backStack = rememberSaveableBackStack(root = homeTab) + val navigator = rememberCircuitNavigator(backStack) + CircuitCompositionLocals(circuit) { - val homeTab = remember { coreComponent.screenFactory.homeTab() } - val exploreTab = remember { coreComponent.screenFactory.exploreTab() } - val notificationsTab = remember { coreComponent.screenFactory.notificationsTab()} - val activeScreen = remember { mutableStateOf(homeTab) } OctonautTheme { Box( modifier = Modifier.fillMaxSize() ) { - LaunchedEffect(Unit) { try { - - coreComponent.userSupplier.supply(GetUserQuery("matt-ramotar")) - + coreComponent.currentUserSupplier.supply(GetUserQuery("matt-ramotar")) coreComponent.scheduledNotificationsSupplier.supply( ListNotificationsQueryParams(), 5.minutes ) + coreComponent.feedSupplier.supply(Unit) } catch (error: Throwable) { println("Error: ${error.stackTraceToString()}") @@ -84,7 +94,7 @@ class MainActivity : ComponentActivity() { BottomAppBar(containerColor = MaterialTheme.colorScheme.surfaceContainer) { IconButton( onClick = { - activeScreen.value = homeTab + navigator.goTo(homeTab) } ) { Icon(Icons.Default.Home, "") @@ -92,7 +102,7 @@ class MainActivity : ComponentActivity() { IconButton( onClick = { - activeScreen.value = notificationsTab + navigator.goTo(notificationsTab) } ) { Icon(Icons.Default.Notifications, "") @@ -100,7 +110,7 @@ class MainActivity : ComponentActivity() { IconButton( onClick = { - activeScreen.value = exploreTab + navigator.goTo(exploreTab) } ) { Icon(Icons.Default.Search, "") @@ -115,12 +125,49 @@ class MainActivity : ComponentActivity() { } ) { innerPadding -> - CircuitContent( - screen = activeScreen.value, + NavigableCircuitContent( + navigator, + backStack, modifier = Modifier.padding(innerPadding) - .background(MaterialTheme.colorScheme.background) + .background(MaterialTheme.colorScheme.background), + decoration = NavigatorDefaults.EmptyDecoration ) } + + webViewState?.let { + val webViewNavigator = WebViewNavigator(rememberCoroutineScope()) + + Box(modifier = Modifier.fillMaxSize()) { + Column { + TopAppBar( + title = { + Text(webViewState.pageTitle?.let { "${it}..." } ?: "", + style = MaterialTheme.typography.labelLarge, + maxLines = 1) + }, + navigationIcon = { + IconButton(onClick = { + if (webViewNavigator.canGoBack) { + webViewNavigator.navigateBack() + } else { + coreComponent.webViewUrlStateHolder.url.value = null + } + + }) { + Icon(Icons.AutoMirrored.Default.ArrowBack, "") + } + } + ) + WebView( + it, + modifier = Modifier.fillMaxSize(), + captureBackPresses = true, + navigator = webViewNavigator + ) + } + + } + } } } } diff --git a/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/di/CoreComponent.kt b/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/di/CoreComponent.kt index 6603faf4f..386d36cd9 100644 --- a/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/di/CoreComponent.kt +++ b/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/di/CoreComponent.kt @@ -6,6 +6,7 @@ import com.slack.circuit.runtime.ui.Ui import io.ktor.client.* import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import me.tatarka.inject.annotations.Component import me.tatarka.inject.annotations.Provides import org.mobilenativefoundation.market.impl.MarketActionFactory @@ -19,9 +20,13 @@ import org.mobilenativefoundation.sample.octonaut.android.app.market.MutableOcto import org.mobilenativefoundation.sample.octonaut.android.app.market.RealMutableOctonautMarket import org.mobilenativefoundation.sample.octonaut.android.app.market.RealOctonautMarketDispatcher import org.mobilenativefoundation.sample.octonaut.xplat.common.market.* +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api.FeedStore +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api.FeedSupplier +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.impl.FeedStoreFactory import org.mobilenativefoundation.sample.octonaut.xplat.domain.notifications.api.NotificationsStore import org.mobilenativefoundation.sample.octonaut.xplat.domain.notifications.api.NotificationsSupplier import org.mobilenativefoundation.sample.octonaut.xplat.domain.notifications.impl.NotificationsStoreFactory +import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.CurrentUserSupplier import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.UserStore import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.UserSupplier import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.impl.UserStoreFactory @@ -32,13 +37,11 @@ import org.mobilenativefoundation.sample.octonaut.xplat.feat.homeTab.api.HomeTab import org.mobilenativefoundation.sample.octonaut.xplat.feat.homeTab.impl.* import org.mobilenativefoundation.sample.octonaut.xplat.feat.notificationsTab.api.NotificationsTab import org.mobilenativefoundation.sample.octonaut.xplat.foundation.di.api.UserScope -import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.GetUserQuery -import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.ListNotificationsResponse -import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.NetworkingClient -import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.NetworkingComponent +import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.* import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.impl.Env import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.impl.RealNetworkingClient import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.impl.httpClient +import org.mobilenativefoundation.sample.octonaut.xplat.foundation.webview.WebViewUrlStateHolder @UserScope @Component @@ -51,8 +54,10 @@ abstract class CoreComponent : NetworkingComponent { abstract val mutableMarket: MutableOctonautMarket abstract val marketDispatcher: OctonautMarketDispatcher abstract val coroutineDispatcher: CoroutineDispatcher - abstract val userSupplier: UserSupplier + abstract val currentUserSupplier: CurrentUserSupplier abstract val scheduledNotificationsSupplier: NotificationsSupplier + abstract val feedSupplier: FeedSupplier + abstract val webViewUrlStateHolder: WebViewUrlStateHolder @UserScope @Provides @@ -113,6 +118,17 @@ abstract class CoreComponent : NetworkingComponent { return notificationsStoreFactory.create() } + @Provides + fun provideFeedStoreFactory(networkingClient: NetworkingClient): FeedStoreFactory { + return FeedStoreFactory(networkingClient) + } + + @Provides + @UserScope + fun provideFeedStore(notificationsStoreFactory: FeedStoreFactory): FeedStore { + return notificationsStoreFactory.create() + } + @Provides @UserScope @@ -123,7 +139,48 @@ abstract class CoreComponent : NetworkingComponent { ): UserSupplier { val marketActionFactory = MarketActionFactory { storeOutput -> - OctonautMarketAction.UpdateUser(storeOutput) + OctonautMarketAction.AddUser(storeOutput) + } + + return RealMarketSupplier( + coroutineDispatcher, + userStore, + marketDispatcher, + marketActionFactory + ) + } + + @Provides + @UserScope + fun provideCurrentUserSupplier( + coroutineDispatcher: CoroutineDispatcher, + userStore: UserStore, + marketDispatcher: OctonautMarketDispatcher, + ): CurrentUserSupplier { + + val marketActionFactory = MarketActionFactory { storeOutput -> + OctonautMarketAction.UpdateCurrentUser(storeOutput) + } + + return RealMarketSupplier( + coroutineDispatcher, + userStore, + marketDispatcher, + marketActionFactory + ) + } + + @Provides + @UserScope + fun provideFeedSupplier( + coroutineDispatcher: CoroutineDispatcher, + userStore: FeedStore, + marketDispatcher: OctonautMarketDispatcher, + ): FeedSupplier { + + val marketActionFactory = MarketActionFactory { storeOutput -> + println("STORE OUTPUT = $storeOutput") + OctonautMarketAction.UpdateFeed(storeOutput) } return RealMarketSupplier( @@ -182,9 +239,10 @@ abstract class CoreComponent : NetworkingComponent { @Provides fun provideHomeTabPresenter( - warehouse: HomeTabWarehouse + warehouse: HomeTabWarehouse, + webViewUrlStateHolder: WebViewUrlStateHolder ): HomeTabPresenter { - return HomeTabPresenter(warehouse) + return HomeTabPresenter(warehouse, webViewUrlStateHolder) } @Provides @@ -253,6 +311,14 @@ abstract class CoreComponent : NetworkingComponent { @Provides fun provideNotificationsTab(): NotificationsTab = RealNotificationsTab + @Provides + @UserScope + fun provideWebViewUrlStateHolder(): WebViewUrlStateHolder { + return object : WebViewUrlStateHolder { + override val url = MutableStateFlow(null) + } + } + companion object } diff --git a/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/market/RealOctonautMarketDispatcher.kt b/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/market/RealOctonautMarketDispatcher.kt index db9a8dd42..7d3d40f7e 100644 --- a/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/market/RealOctonautMarketDispatcher.kt +++ b/experimental/sample/octonaut/android/app/src/main/kotlin/org/mobilenativefoundation/sample/octonaut/android/app/market/RealOctonautMarketDispatcher.kt @@ -3,8 +3,11 @@ package org.mobilenativefoundation.sample.octonaut.android.app.market import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.sample.octonaut.xplat.common.market.OctonautMarketAction import org.mobilenativefoundation.sample.octonaut.xplat.common.market.OctonautMarketDispatcher +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api.* import org.mobilenativefoundation.sample.octonaut.xplat.domain.notifications.api.Notification +import org.mobilenativefoundation.sample.octonaut.xplat.domain.notifications.api.NotificationsState import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.User +import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.UsersState @Inject class RealOctonautMarketDispatcher( @@ -13,9 +16,9 @@ class RealOctonautMarketDispatcher( override fun dispatch(action: OctonautMarketAction) { val prevState = mutableMarket.state.value val nextState = when (action) { - is OctonautMarketAction.UpdateUser -> { + is OctonautMarketAction.UpdateCurrentUser -> { prevState.copy( - user = action.user?.let { + currentUser = action.user?.let { User( id = it.id, email = it.email, @@ -41,21 +44,173 @@ class RealOctonautMarketDispatcher( } is OctonautMarketAction.UpdateNotifications -> { + + val notifications = action.notifications.notifications.map { + Notification( + it.id, + it.unread, + it.updatedAt, + it.lastReadAt, + it.url, + it.subscriptionUrl + ) + } + + val byId = notifications.associateBy { it.id } + val allIds = notifications.map { it.id } + + val notificationsState = NotificationsState( + byId = byId, + allIds = allIds + ) + prevState.copy( - notifications = action.notifications.notifications.map { - Notification( - it.id, - it.unread, - it.updatedAt, - it.lastReadAt, - it.url, - it.subscriptionUrl - ) + notifications = notificationsState + ) + } + + is OctonautMarketAction.UpdateFeed -> { + + val id = action.feed.id + + val entries = action.feed.entry.map { + Entry( + it.id, + it.published, + it.updated, + it.link.href, + it.title, + it.author.uri, + it.thumbnail.url + ) + } + + val entriesState = EntriesState( + byId = entries.associateBy { it.id }, + allIds = entries.map { it.id } + ) + + val links = action.feed.link.map { + Link( + it.type, + it.rel, + it.href + ) + } + + val linksState = LinksState( + byHref = links.associateBy { it.href }, + allHrefs = links.map { it.href } + ) + + val thumbnails = action.feed.entry.map { + Thumbnail( + it.thumbnail.height, + it.thumbnail.width, + it.thumbnail.url + ) + } + + val thumbnailsState = ThumbnailsState( + byUrl = thumbnails.associateBy { it.url }, + allUrls = thumbnails.map { it.url } + ) + + val feedState = FeedState( + id = id, + entries = entriesState, + links = linksState, + thumbnails = thumbnailsState + ) + + + prevState.copy( + feed = feedState + ) + } + + is OctonautMarketAction.AddUser -> { + val user = User( + id = action.user.id, + email = action.user.email, + name = action.user.name ?: "", + login = action.user.login, + avatarUrl = action.user.avatarUrl.toString(), + repositories = action.user.repositories.nodes?.mapNotNull { repo -> repo?.id } ?: emptyList(), + starredRepositories = action.user.starredRepositories.nodes?.mapNotNull { repo -> repo?.id } + ?: emptyList(), + organizations = action.user.organizations.nodes?.mapNotNull { org -> org?.id } ?: emptyList(), + pinnedItems = action.user.pinnedItems.let { pinnedItems -> List(pinnedItems.totalCount) { "" } }, + socialAccounts = action.user.socialAccounts.nodes?.mapNotNull { socialAccount -> + socialAccount?.let { + User.SocialAccount( + socialAccount.displayName, + socialAccount.provider.name + ) + } + } ?: emptyList() + ) + + val allIds = if (user.id !in prevState.users.byId) { + val copy = prevState.users.allIds.toMutableList() + copy.add(action.user.id) + copy + } else { + prevState.users.allIds + } + + val byId = prevState.users.byId.toMutableMap() + byId[user.id] = user + + val usersState = UsersState( + byId = byId, + allIds = allIds + ) + + prevState.copy( + users = usersState + ) + } + + is OctonautMarketAction.AddNotifications -> { + + + val notifications = action.notifications.notifications.map { + Notification( + it.id, + it.unread, + it.updatedAt, + it.lastReadAt, + it.url, + it.subscriptionUrl + ) + } + + val byId = prevState.notifications.byId.toMutableMap() + val allIds = prevState.notifications.allIds.toMutableList() + + notifications.forEach { + if (it.id !in byId) { + allIds.add(it.id) } + + byId[it.id] = it + } + + val notificationsState = NotificationsState( + byId = byId, + allIds = allIds + ) + + prevState.copy( + notifications = notificationsState ) + } } + println("UPDATING STATE: $prevState to $nextState") + mutableMarket.updateState(nextState) } } \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/common/market/build.gradle.kts b/experimental/sample/octonaut/xplat/common/market/build.gradle.kts index 29ce549db..443244772 100644 --- a/experimental/sample/octonaut/xplat/common/market/build.gradle.kts +++ b/experimental/sample/octonaut/xplat/common/market/build.gradle.kts @@ -18,6 +18,7 @@ kotlin { api(project(":experimental:market:warehouse")) api(project(":experimental:sample:octonaut:xplat:domain:user:api")) api(project(":experimental:sample:octonaut:xplat:domain:notifications:api")) + api(project(":experimental:sample:octonaut:xplat:domain:feed:api")) } } } diff --git a/experimental/sample/octonaut/xplat/common/market/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/common/market/OctonautMarketAction.kt b/experimental/sample/octonaut/xplat/common/market/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/common/market/OctonautMarketAction.kt index 7fedf1102..69665c599 100644 --- a/experimental/sample/octonaut/xplat/common/market/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/common/market/OctonautMarketAction.kt +++ b/experimental/sample/octonaut/xplat/common/market/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/common/market/OctonautMarketAction.kt @@ -1,16 +1,29 @@ package org.mobilenativefoundation.sample.octonaut.xplat.common.market import org.mobilenativefoundation.market.Market -import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.User +import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.Feed import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.GetUserQuery import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.ListNotificationsResponse -sealed interface OctonautMarketAction: Market.Action { - data class UpdateUser( +sealed interface OctonautMarketAction : Market.Action { + + data class UpdateCurrentUser( val user: GetUserQuery.User? ): OctonautMarketAction + data class AddUser( + val user: GetUserQuery.User + ) : OctonautMarketAction + data class UpdateNotifications( val notifications: ListNotificationsResponse + ) : OctonautMarketAction + + data class AddNotifications( + val notifications: ListNotificationsResponse + ): OctonautMarketAction + + data class UpdateFeed( + val feed: Feed ): OctonautMarketAction } \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/common/market/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/common/market/OctonautMarketState.kt b/experimental/sample/octonaut/xplat/common/market/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/common/market/OctonautMarketState.kt index e544c38a4..5cec72ac4 100644 --- a/experimental/sample/octonaut/xplat/common/market/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/common/market/OctonautMarketState.kt +++ b/experimental/sample/octonaut/xplat/common/market/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/common/market/OctonautMarketState.kt @@ -1,11 +1,16 @@ package org.mobilenativefoundation.sample.octonaut.xplat.common.market import org.mobilenativefoundation.market.Market -import org.mobilenativefoundation.sample.octonaut.xplat.domain.notifications.api.Notification +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api.FeedState +import org.mobilenativefoundation.sample.octonaut.xplat.domain.notifications.api.NotificationsState import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.User +import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.UsersState data class OctonautMarketState( - val repositories: List = emptyList(), - val user: User? = null, - val notifications: List = emptyList() -) : Market.State \ No newline at end of file + val currentUser: User? = null, + val users: UsersState = UsersState(), + val notifications: NotificationsState = NotificationsState(), + val feed: FeedState = FeedState() +) : Market.State + + diff --git a/experimental/sample/octonaut/xplat/domain/feed/api/build.gradle.kts b/experimental/sample/octonaut/xplat/domain/feed/api/build.gradle.kts new file mode 100644 index 000000000..5aa9109b8 --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/feed/api/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("plugin.octonaut.android.library") + id("plugin.octonaut.kotlin.multiplatform") + alias(libs.plugins.serialization) + alias(libs.plugins.compose) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(compose.runtime) + api(compose.components.resources) + api(libs.circuit.foundation) + api(libs.kotlinInject.runtime) + api(project(":experimental:market")) + api(project(":experimental:sample:octonaut:xplat:foundation:networking:api")) + api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) + api(project(":store")) + } + } + } +} + +android { + namespace = "org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api" +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/Feed.kt b/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/Feed.kt new file mode 100644 index 000000000..b6e6e1167 --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/Feed.kt @@ -0,0 +1,36 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api + + +data class Feed( + val id: String, + val linkHrefs: List, + val entryIds: List, +) + +data class Link( + val type: String, + val rel: String, + val href: String +) + +data class Entry( + val id: String, + val published: String, + val updated: String, + val linkHref: String, + val title: String, + val authorUri: String, + val thumbnailUrl: String +) + +data class Author( + val name: String, + val uri: String, + val email: String, +) + +data class Thumbnail( + val height: Int, + val width: Int, + val url: String +) \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedState.kt b/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedState.kt new file mode 100644 index 000000000..ba0ff57b8 --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedState.kt @@ -0,0 +1,23 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api + +data class FeedState( + val id: String? = null, + val entries: EntriesState = EntriesState(), + val links: LinksState = LinksState(), + val thumbnails: ThumbnailsState = ThumbnailsState() +) + +data class EntriesState( + val byId: Map = mapOf(), + val allIds: List = emptyList() +) + +data class LinksState( + val byHref: Map = mapOf(), + val allHrefs: List = emptyList() +) + +data class ThumbnailsState( + val byUrl: Map = mapOf(), + val allUrls: List = emptyList() +) \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedStore.kt b/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedStore.kt new file mode 100644 index 000000000..ba6ba38f7 --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedStore.kt @@ -0,0 +1,6 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api + +import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.Feed +import org.mobilenativefoundation.store.store5.Store + +typealias FeedStore = Store \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedSupplier.kt b/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedSupplier.kt new file mode 100644 index 000000000..4376598b2 --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/feed/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/api/FeedSupplier.kt @@ -0,0 +1,5 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api + +import org.mobilenativefoundation.market.MarketSupplier + +typealias FeedSupplier = MarketSupplier \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/domain/feed/impl/build.gradle.kts b/experimental/sample/octonaut/xplat/domain/feed/impl/build.gradle.kts new file mode 100644 index 000000000..c9bcdcd5b --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/feed/impl/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("plugin.octonaut.android.library") + id("plugin.octonaut.kotlin.multiplatform") + alias(libs.plugins.serialization) + alias(libs.plugins.compose) + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.plugin.parcelize) +} + +kotlin { + androidTarget { + plugins.apply(libs.plugins.paparazzi.get().pluginId) + } + + sourceSets { + + commonMain { + dependencies { + implementation(compose.material3) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinInject.runtime) + + api(project(":experimental:sample:octonaut:xplat:domain:feed:api")) + api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) + api(project(":experimental:sample:octonaut:xplat:common:market")) + } + } + + androidUnitTest { + dependencies { + implementation(kotlin("test")) + } + } + } +} + +dependencies { + add("kspAndroid", libs.kotlinInject.compiler) + add("kspIosX64", libs.kotlinInject.compiler) + add("kspIosArm64", libs.kotlinInject.compiler) +} + +android { + namespace = "org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.impl" +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/domain/feed/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/impl/FeedStoreFactory.kt b/experimental/sample/octonaut/xplat/domain/feed/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/impl/FeedStoreFactory.kt new file mode 100644 index 000000000..3796c88a2 --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/feed/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/feed/impl/FeedStoreFactory.kt @@ -0,0 +1,22 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.impl + +import me.tatarka.inject.annotations.Inject +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api.FeedStore +import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.NetworkingClient +import org.mobilenativefoundation.store.store5.Fetcher +import org.mobilenativefoundation.store.store5.StoreBuilder + +@Inject +class FeedStoreFactory( + private val networkingClient: NetworkingClient +) { + fun create(): FeedStore { + val storeBuilder = StoreBuilder.from( + fetcher = Fetcher.of { _: Unit -> + networkingClient.getFeed() + } + ) + + return storeBuilder.build() + } +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/domain/notifications/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/notifications/api/NotificationsState.kt b/experimental/sample/octonaut/xplat/domain/notifications/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/notifications/api/NotificationsState.kt new file mode 100644 index 000000000..af39037db --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/notifications/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/notifications/api/NotificationsState.kt @@ -0,0 +1,6 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.domain.notifications.api + +data class NotificationsState( + val byId: Map = mapOf(), + val allIds: List = emptyList() +) diff --git a/experimental/sample/octonaut/xplat/domain/user/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/user/api/CurrentUserSupplier.kt b/experimental/sample/octonaut/xplat/domain/user/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/user/api/CurrentUserSupplier.kt new file mode 100644 index 000000000..dd1bd1e8c --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/user/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/user/api/CurrentUserSupplier.kt @@ -0,0 +1,6 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api + +import org.mobilenativefoundation.market.MarketSupplier +import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.GetUserQuery + +typealias CurrentUserSupplier = MarketSupplier \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/domain/user/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/user/api/UsersState.kt b/experimental/sample/octonaut/xplat/domain/user/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/user/api/UsersState.kt new file mode 100644 index 000000000..4de274b59 --- /dev/null +++ b/experimental/sample/octonaut/xplat/domain/user/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/domain/user/api/UsersState.kt @@ -0,0 +1,6 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api + +data class UsersState( + val byId: Map = mapOf(), + val allIds: List = emptyList() +) \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/exploreTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/exploreTab/impl/ExploreTabWarehouseFactory.kt b/experimental/sample/octonaut/xplat/feat/exploreTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/exploreTab/impl/ExploreTabWarehouseFactory.kt index 806b186dc..ac6606dd9 100644 --- a/experimental/sample/octonaut/xplat/feat/exploreTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/exploreTab/impl/ExploreTabWarehouseFactory.kt +++ b/experimental/sample/octonaut/xplat/feat/exploreTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/exploreTab/impl/ExploreTabWarehouseFactory.kt @@ -13,7 +13,7 @@ class ExploreTabWarehouseFactory( return warehouseBuilder.extractor { marketState -> ExploreTabWarehouseState( - user = marketState.user, + user = marketState.currentUser, searchResults = emptyList() // TODO ) }.actionHandler { action, marketState -> diff --git a/experimental/sample/octonaut/xplat/feat/homeTab/api/build.gradle.kts b/experimental/sample/octonaut/xplat/feat/homeTab/api/build.gradle.kts index 39746f0d2..15d44df14 100644 --- a/experimental/sample/octonaut/xplat/feat/homeTab/api/build.gradle.kts +++ b/experimental/sample/octonaut/xplat/feat/homeTab/api/build.gradle.kts @@ -17,6 +17,7 @@ kotlin { api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) api(project(":store")) api(project(":experimental:sample:octonaut:xplat:domain:user:api")) + api(project(":experimental:sample:octonaut:xplat:domain:feed:api")) } } } diff --git a/experimental/sample/octonaut/xplat/feat/homeTab/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/api/HomeTab.kt b/experimental/sample/octonaut/xplat/feat/homeTab/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/api/HomeTab.kt index b6351b03b..cdeef22de 100644 --- a/experimental/sample/octonaut/xplat/feat/homeTab/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/api/HomeTab.kt +++ b/experimental/sample/octonaut/xplat/feat/homeTab/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/api/HomeTab.kt @@ -3,6 +3,7 @@ package org.mobilenativefoundation.sample.octonaut.xplat.feat.homeTab.api import com.slack.circuit.runtime.CircuitUiEvent import com.slack.circuit.runtime.CircuitUiState import com.slack.circuit.runtime.screen.Screen +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api.FeedState import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.User import com.slack.circuit.runtime.presenter.Presenter as CircuitPresenter import com.slack.circuit.runtime.ui.Ui as CircuitUi @@ -12,17 +13,27 @@ interface HomeTab : Screen { data object Initial : State data class Loading( val eventSink: (Event) -> Unit - ): State + ) : State + data class Loaded( val user: User, - val eventSink: (Event) -> Unit + val feed: FeedState, + val eventSink: (Event) -> Unit, ) : State } sealed interface Event : CircuitUiEvent { - data object Refresh: Event + data object Refresh : Event + data class OpenWebView(val url: String) : Event + sealed interface OpenDetailedView : Event { + val id: String + + data class User(override val id: String) : OpenDetailedView + data class Repository(override val id: String) : OpenDetailedView + } } interface Ui : CircuitUi interface Presenter : CircuitPresenter + } \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/homeTab/impl/build.gradle.kts b/experimental/sample/octonaut/xplat/feat/homeTab/impl/build.gradle.kts index a2d42f680..992fff3b7 100644 --- a/experimental/sample/octonaut/xplat/feat/homeTab/impl/build.gradle.kts +++ b/experimental/sample/octonaut/xplat/feat/homeTab/impl/build.gradle.kts @@ -24,9 +24,12 @@ kotlin { api(project(":experimental:sample:octonaut:xplat:common:market")) api(project(":experimental:sample:octonaut:xplat:feat:homeTab:api")) api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) + api(project(":experimental:sample:octonaut:xplat:foundation:webview")) + api(project(":experimental:sample:octonaut:xplat:feat:exploreTab:api")) implementation(libs.coil.compose) implementation(libs.coil.network) implementation(libs.ktor.core) + api(libs.compose.webview.multiplatform) } } diff --git a/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabPresenter.kt b/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabPresenter.kt index a995600f1..1c748f39c 100644 --- a/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabPresenter.kt +++ b/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabPresenter.kt @@ -4,13 +4,23 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import me.tatarka.inject.annotations.Inject import org.mobilenativefoundation.sample.octonaut.xplat.feat.homeTab.api.HomeTab +import org.mobilenativefoundation.sample.octonaut.xplat.foundation.webview.WebViewUrlStateHolder @Inject -class HomeTabPresenter(private val warehouse: HomeTabWarehouse) : HomeTab.Presenter { +class HomeTabPresenter( + private val warehouse: HomeTabWarehouse, + private val webViewUrlStateHolder: WebViewUrlStateHolder +) : HomeTab.Presenter { private fun on(event: HomeTab.Event) { when (event) { HomeTab.Event.Refresh -> warehouse.dispatch(HomeTabWarehouseAction.Refresh) + is HomeTab.Event.OpenWebView -> { + webViewUrlStateHolder.url.value = event.url + } + + is HomeTab.Event.OpenDetailedView.Repository -> TODO() + is HomeTab.Event.OpenDetailedView.User -> TODO() } } @@ -18,9 +28,12 @@ class HomeTabPresenter(private val warehouse: HomeTabWarehouse) : HomeTab.Presen override fun present(): HomeTab.State { val warehouseState = warehouse.state.collectAsState() - return warehouseState.value.user?.let { user -> - HomeTab.State.Loaded(user, ::on) - } ?: HomeTab.State.Loading(::on) + return warehouseState.value.let { state -> + state.user?.let { user -> + state.feed?.let { feed -> + HomeTab.State.Loaded(user, feed, ::on) + } + } ?: HomeTab.State.Loading(::on) + } } - } \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabUi.kt b/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabUi.kt index 481575793..40929e6de 100644 --- a/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabUi.kt +++ b/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabUi.kt @@ -1,6 +1,7 @@ package org.mobilenativefoundation.sample.octonaut.xplat.feat.homeTab.impl import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -9,9 +10,9 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp -import coil3.compose.AsyncImagePainter import coil3.compose.rememberAsyncImagePainter import me.tatarka.inject.annotations.Inject +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api.Entry import org.mobilenativefoundation.sample.octonaut.xplat.feat.homeTab.api.HomeTab @Inject @@ -35,9 +36,24 @@ class HomeTabUi : HomeTab.Ui { Image(painter = painter, contentDescription = "", modifier = Modifier.size(60.dp).clip(CircleShape)) - state.user.repositories.forEach { - Text(it) + state.feed.entries.allIds.forEach { entryId -> + state.feed.entries.byId[entryId]?.let { + FeedItem(it) { + state.eventSink(HomeTab.Event.OpenWebView(it.linkHref)) + } + } + } } -} \ No newline at end of file + @Composable + private fun FeedItem(entry: Entry, onClick: () -> Unit) { + + Column(modifier = Modifier.clickable { + onClick() + }) { + Text(entry.title) + } + + } +} diff --git a/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabWarehouse.kt b/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabWarehouse.kt index 22a73a98e..c16e03971 100644 --- a/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabWarehouse.kt +++ b/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabWarehouse.kt @@ -1,10 +1,12 @@ package org.mobilenativefoundation.sample.octonaut.xplat.feat.homeTab.impl import org.mobilenativefoundation.market.warehouse.Warehouse +import org.mobilenativefoundation.sample.octonaut.xplat.domain.feed.api.FeedState import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.User data class HomeTabWarehouseState( - val user: User? = null + val user: User? = null, + val feed: FeedState? = null ) : Warehouse.State sealed interface HomeTabWarehouseAction : Warehouse.Action { diff --git a/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabWarehouseFactory.kt b/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabWarehouseFactory.kt index 13b4b6946..120a2fae7 100644 --- a/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabWarehouseFactory.kt +++ b/experimental/sample/octonaut/xplat/feat/homeTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/homeTab/impl/HomeTabWarehouseFactory.kt @@ -17,13 +17,13 @@ class HomeTabWarehouseFactory( return warehouseBuilder .extractor { marketState -> - HomeTabWarehouseState(marketState.user) + HomeTabWarehouseState(marketState.currentUser, marketState.feed) } .actionHandler { action, marketState -> when (action) { HomeTabWarehouseAction.Refresh -> { // Refresh user - val currentUser = marketState.user!! + val currentUser = marketState.currentUser!! userSupplier.supply(GetUserQuery(currentUser.login)) // Refresh repositories diff --git a/experimental/sample/octonaut/xplat/feat/notificationsTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/NotificationsTabWarehouseFactory.kt b/experimental/sample/octonaut/xplat/feat/notificationsTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/NotificationsTabWarehouseFactory.kt index 29d1d8a43..eccd49c86 100644 --- a/experimental/sample/octonaut/xplat/feat/notificationsTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/NotificationsTabWarehouseFactory.kt +++ b/experimental/sample/octonaut/xplat/feat/notificationsTab/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/NotificationsTabWarehouseFactory.kt @@ -14,7 +14,7 @@ class NotificationsTabWarehouseFactory( warehouseBuilderFactory.create() return warehouseBuilder.extractor { marketState -> - NotificationsTabWarehouseState(marketState.notifications) + NotificationsTabWarehouseState(marketState.notifications.allIds.mapNotNull { marketState.notifications.byId[it] }) } .actionHandler { action, marketState -> when (action) { diff --git a/experimental/sample/octonaut/xplat/feat/userProfile/api/build.gradle.kts b/experimental/sample/octonaut/xplat/feat/userProfile/api/build.gradle.kts new file mode 100644 index 000000000..72a7e3c9b --- /dev/null +++ b/experimental/sample/octonaut/xplat/feat/userProfile/api/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + id("plugin.octonaut.android.library") + id("plugin.octonaut.kotlin.multiplatform") + alias(libs.plugins.serialization) + alias(libs.plugins.compose) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(compose.runtime) + api(compose.components.resources) + api(libs.circuit.foundation) + api(libs.kotlinInject.runtime) + api(project(":experimental:sample:octonaut:xplat:foundation:networking:api")) + api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) + api(project(":store")) + api(project(":experimental:sample:octonaut:xplat:domain:user:api")) + api(project(":experimental:sample:octonaut:xplat:domain:feed:api")) + } + } + } +} + +android { + namespace = "org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.api" +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/userProfile/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/api/UserProfileScreen.kt b/experimental/sample/octonaut/xplat/feat/userProfile/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/api/UserProfileScreen.kt new file mode 100644 index 000000000..15e1b5a10 --- /dev/null +++ b/experimental/sample/octonaut/xplat/feat/userProfile/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/api/UserProfileScreen.kt @@ -0,0 +1,32 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.api + +import com.slack.circuit.runtime.CircuitUiEvent +import com.slack.circuit.runtime.CircuitUiState +import com.slack.circuit.runtime.screen.Screen +import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.User +import com.slack.circuit.runtime.presenter.Presenter as CircuitPresenter +import com.slack.circuit.runtime.ui.Ui as CircuitUi + +interface UserProfileScreen : Screen { + val login: String + + sealed interface State : CircuitUiState { + data object Initial : State + data class Loaded( + val user: User, + val eventSink: (Event) -> Unit, + ) : State + + data class Loading( + val eventSink: (Event) -> Unit + ) : State + } + + sealed interface Event : CircuitUiEvent { + data object Follow : Event + data object Unfollow : Event + } + + interface Ui : CircuitUi + interface Presenter : CircuitPresenter +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/userProfile/impl/build.gradle.kts b/experimental/sample/octonaut/xplat/feat/userProfile/impl/build.gradle.kts new file mode 100644 index 000000000..50e91235b --- /dev/null +++ b/experimental/sample/octonaut/xplat/feat/userProfile/impl/build.gradle.kts @@ -0,0 +1,52 @@ +plugins { + id("plugin.octonaut.android.library") + id("plugin.octonaut.kotlin.multiplatform") + alias(libs.plugins.serialization) + alias(libs.plugins.compose) + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.plugin.parcelize) +} + +kotlin { + androidTarget { + plugins.apply(libs.plugins.paparazzi.get().pluginId) + } + + sourceSets { + + commonMain { + dependencies { + implementation(compose.material3) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinInject.runtime) + + api(project(":experimental:market:warehouse")) + api(project(":experimental:sample:octonaut:xplat:common:market")) + api(project(":experimental:sample:octonaut:xplat:feat:homeTab:api")) + api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) + api(project(":experimental:sample:octonaut:xplat:foundation:webview")) + api(project(":experimental:sample:octonaut:xplat:feat:userProfile:api")) + implementation(libs.coil.compose) + implementation(libs.coil.network) + implementation(libs.ktor.core) + api(libs.compose.webview.multiplatform) + } + } + + androidUnitTest { + dependencies { + implementation(kotlin("test")) + } + } + } +} + +dependencies { + add("kspAndroid", libs.kotlinInject.compiler) + add("kspIosX64", libs.kotlinInject.compiler) + add("kspIosArm64", libs.kotlinInject.compiler) +} + +android { + namespace = "org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.impl" +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/RealUserProfileScreen.kt b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/RealUserProfileScreen.kt new file mode 100644 index 000000000..f9a2e29c9 --- /dev/null +++ b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/RealUserProfileScreen.kt @@ -0,0 +1,7 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.impl + +import org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.api.UserProfileScreen + +data class RealUserProfileScreen( + override val login: String +) : UserProfileScreen \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenPresenter.kt b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenPresenter.kt new file mode 100644 index 000000000..a597adfd4 --- /dev/null +++ b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenPresenter.kt @@ -0,0 +1,36 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import me.tatarka.inject.annotations.Inject +import org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.api.UserProfileScreen + +@Inject +class UserProfileScreenPresenter( + private val screen: UserProfileScreen, + private val warehouse: UserProfileScreenWarehouse +) : UserProfileScreen.Presenter { + + init { + warehouse.dispatch(UserProfileScreenWarehouseAction.LoadUser(screen.login)) + } + + private fun on(event: UserProfileScreen.Event) { + when (event) { + UserProfileScreen.Event.Follow -> { + // TODO + } + + UserProfileScreen.Event.Unfollow -> { + // TODO + } + } + } + + @Composable + override fun present(): UserProfileScreen.State { + val warehouseState = warehouse.state.collectAsState() + + return UserProfileScreen.State.Loading(::on) + } +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenUi.kt b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenUi.kt new file mode 100644 index 000000000..b6b08be81 --- /dev/null +++ b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenUi.kt @@ -0,0 +1,29 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.impl + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import me.tatarka.inject.annotations.Inject +import org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.api.UserProfileScreen + + +@Inject +class UserProfileScreenUi : UserProfileScreen.Ui { + @Composable + override fun Content(state: UserProfileScreen.State, modifier: Modifier) { + Column { + when (state) { + UserProfileScreen.State.Initial -> Text("Initial") + is UserProfileScreen.State.Loaded -> LoadedContent(state) + is UserProfileScreen.State.Loading -> Text("Loading") + } + } + } + + @Composable + private fun LoadedContent(state: UserProfileScreen.State.Loaded) { + + + } +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenWarehouse.kt b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenWarehouse.kt new file mode 100644 index 000000000..c20b71d68 --- /dev/null +++ b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenWarehouse.kt @@ -0,0 +1,15 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.impl + +import org.mobilenativefoundation.market.warehouse.Warehouse + +data class UserProfileScreenWarehouseState( + val user: Any +) : Warehouse.State + +sealed interface UserProfileScreenWarehouseAction : Warehouse.Action { + data class LoadUser( + val login: String + ) : UserProfileScreenWarehouseAction +} + +typealias UserProfileScreenWarehouse = Warehouse \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenWarehouseFactory.kt b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenWarehouseFactory.kt new file mode 100644 index 000000000..158efa78d --- /dev/null +++ b/experimental/sample/octonaut/xplat/feat/userProfile/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/feat/userProfile/impl/UserProfileScreenWarehouseFactory.kt @@ -0,0 +1,27 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.feat.userProfile.impl + +import me.tatarka.inject.annotations.Inject +import org.mobilenativefoundation.sample.octonaut.xplat.common.market.WarehouseBuilderFactory +import org.mobilenativefoundation.sample.octonaut.xplat.domain.user.api.UserSupplier +import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.GetUserQuery + +@Inject +class UserProfileScreenWarehouseFactory( + private val warehouseBuilderFactory: WarehouseBuilderFactory, + private val userSupplier: UserSupplier +) { + fun create(): UserProfileScreenWarehouse { + val warehouseBuilder = + warehouseBuilderFactory.create() + + return warehouseBuilder.extractor { marketState -> + UserProfileScreenWarehouseState(marketState.notifications) + } + .actionHandler { action, marketState -> + when (action) { + is UserProfileScreenWarehouseAction.LoadUser -> userSupplier.supply(GetUserQuery(action.login)) + } + } + .build() + } +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/foundation/networking/api/build.gradle.kts b/experimental/sample/octonaut/xplat/foundation/networking/api/build.gradle.kts index 1ed4cff11..178459148 100644 --- a/experimental/sample/octonaut/xplat/foundation/networking/api/build.gradle.kts +++ b/experimental/sample/octonaut/xplat/foundation/networking/api/build.gradle.kts @@ -15,7 +15,10 @@ kotlin { api(libs.circuit.foundation) api(libs.apollo.runtime) api(libs.kotlinx.serialization.core) + implementation(libs.ktor.serialization.xml) api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) + api("io.github.pdvrieze.xmlutil:serialization:0.86.3") + } } } diff --git a/experimental/sample/octonaut/xplat/foundation/networking/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/api/GetFeedResponse.kt b/experimental/sample/octonaut/xplat/foundation/networking/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/api/GetFeedResponse.kt new file mode 100644 index 000000000..144583586 --- /dev/null +++ b/experimental/sample/octonaut/xplat/foundation/networking/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/api/GetFeedResponse.kt @@ -0,0 +1,134 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import nl.adaptivity.xmlutil.serialization.XmlElement +import nl.adaptivity.xmlutil.serialization.XmlSerialName + +@Serializable +data class GetFeedResponse( + @SerialName("timeline_url") + val timelineUrl: String, + @SerialName("user_url") + val userUrl: String, + @SerialName("current_user_public_url") + val currentUserPublicUrl: String, + @SerialName("current_user_url") + val currentUserUrl: String, + @SerialName("current_user_actor_url") + val currentUserActorUrl: String, + @SerialName("current_user_organization_url") + val currentUserOrganizationUrl: String, + @SerialName("current_user_organization_urls") + val currentUserOrganizationUrls: List, + @SerialName("security_advisories_url") + val securityAdvisoriesUrl: String, + @SerialName("repository_discussions_url") + val repositoryDiscussionsUrl: String, + @SerialName("repository_discussions_category_url") + val repositoryDiscussionsCategoryUrl: String, +) + +@Serializable +data class Links( + val timeline: LinkWithType, + val user: LinkWithType, + @SerialName("security_advisories") + val securityAdvisories: LinkWithType?, + @SerialName("current_user") + val currentUser: LinkWithType?, + @SerialName("current_user_public") + val currentUserPublic: LinkWithType?, + @SerialName("current_user_actor") + val currentUserActor: LinkWithType?, + @SerialName("current_user_organization") + val currentUserOrganization: LinkWithType?, + @SerialName("current_user_organizations") + val currentUserOrganizations: List?, + @SerialName("repository_discussions") + val repositoryDiscussions: LinkWithType?, + @SerialName("repository_discussions_category") + val repositoryDiscussionsCategory: LinkWithType? +) + +@Serializable +data class LinkWithType( + val href: String, + val type: String +) + +@Serializable +@XmlSerialName("feed", namespace = "http://www.w3.org/2005/Atom", prefix = "") +data class Feed( + @XmlSerialName("id", namespace = "http://www.w3.org/2005/Atom") + @XmlElement(true) + val id: String, + @XmlElement(true) + val link: List, + @XmlElement(true) + val title: String, + @XmlElement(true) + val updated: String, + @XmlElement(true) + val entry: List +) + +@Serializable +@XmlSerialName("link", namespace = "http://www.w3.org/2005/Atom", prefix = "") +data class Link( + @XmlElement(false) + @XmlSerialName("type", namespace = "http://www.w3.org/2005/Atom") + val type: String, + @XmlElement(false) + @XmlSerialName("rel") + val rel: String, + @XmlElement(false) + @XmlSerialName("href") + val href: String +) + +@Serializable +@XmlSerialName("entry", namespace = "http://www.w3.org/2005/Atom", prefix = "") +data class Entry( + @XmlElement(true) + val id: String, + @XmlElement(true) + val published: String, + @XmlElement(true) + val updated: String, + @XmlElement(true) + val link: Link, + @XmlElement(true) + val title: String, + @XmlElement(true) + val author: Author, + @XmlElement(true) + val thumbnail: Thumbnail, + @XmlElement(true) + val content: String +) + +@Serializable +@XmlSerialName("author", namespace = "http://www.w3.org/2005/Atom", prefix = "") +data class Author( + @XmlElement(true) + @XmlSerialName("name", namespace = "http://www.w3.org/2005/Atom") + val name: String, + @XmlElement(true) + @XmlSerialName("uri", namespace = "http://www.w3.org/2005/Atom") + val uri: String, + @XmlElement(true) + @XmlSerialName("email", namespace = "http://www.w3.org/2005/Atom") + val email: String? = null +) + +@Serializable +@XmlSerialName("thumbnail", namespace = "http://search.yahoo.com/mrss/", prefix = "media") +data class Thumbnail( + @XmlElement(false) + val height: Int, + @XmlElement(false) + val width: Int, + @XmlElement(false) + val url: String +) \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/foundation/networking/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/api/NetworkingClient.kt b/experimental/sample/octonaut/xplat/foundation/networking/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/api/NetworkingClient.kt index e309e582d..3caeb70c4 100644 --- a/experimental/sample/octonaut/xplat/foundation/networking/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/api/NetworkingClient.kt +++ b/experimental/sample/octonaut/xplat/foundation/networking/api/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/api/NetworkingClient.kt @@ -4,4 +4,5 @@ interface NetworkingClient { suspend fun getUser(query: GetUserQuery): GetUserQuery.Data? suspend fun listNotifications(queryParams: ListNotificationsQueryParams): ListNotificationsResponse + suspend fun getFeed(): Feed } \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/foundation/networking/impl/build.gradle.kts b/experimental/sample/octonaut/xplat/foundation/networking/impl/build.gradle.kts index a0c0e0572..a9b225d47 100644 --- a/experimental/sample/octonaut/xplat/foundation/networking/impl/build.gradle.kts +++ b/experimental/sample/octonaut/xplat/foundation/networking/impl/build.gradle.kts @@ -22,8 +22,11 @@ kotlin { implementation(libs.ktor.core) implementation(libs.ktor.negotiation) implementation(libs.ktor.serialization.json) + implementation(libs.ktor.serialization.xml) + implementation(libs.kotlinx.serialization.core) api(project(":experimental:sample:octonaut:xplat:foundation:networking:api")) api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) + implementation("io.github.pdvrieze.xmlutil:serialization:0.86.3") } } diff --git a/experimental/sample/octonaut/xplat/foundation/networking/impl/src/androidMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/impl/httpClient.android.kt b/experimental/sample/octonaut/xplat/foundation/networking/impl/src/androidMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/impl/httpClient.android.kt index 3f17995fd..bb7501d4c 100644 --- a/experimental/sample/octonaut/xplat/foundation/networking/impl/src/androidMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/impl/httpClient.android.kt +++ b/experimental/sample/octonaut/xplat/foundation/networking/impl/src/androidMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/impl/httpClient.android.kt @@ -4,6 +4,7 @@ import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* +import io.ktor.serialization.kotlinx.xml.* import kotlinx.serialization.json.Json import java.util.concurrent.TimeUnit @@ -15,6 +16,7 @@ actual fun httpClient(config: HttpClientConfig<*>.() -> Unit) = HttpClient(OkHtt isLenient = true ignoreUnknownKeys = true }) + xml() } engine { diff --git a/experimental/sample/octonaut/xplat/foundation/networking/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/impl/RealNetworkingClient.kt b/experimental/sample/octonaut/xplat/foundation/networking/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/impl/RealNetworkingClient.kt index af6d7d2ae..c8c337fd4 100644 --- a/experimental/sample/octonaut/xplat/foundation/networking/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/impl/RealNetworkingClient.kt +++ b/experimental/sample/octonaut/xplat/foundation/networking/impl/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/networking/impl/RealNetworkingClient.kt @@ -6,7 +6,10 @@ import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.client.statement.* import io.ktor.http.* +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.serializer import me.tatarka.inject.annotations.Inject +import nl.adaptivity.xmlutil.serialization.XML import org.mobilenativefoundation.sample.octonaut.xplat.foundation.networking.api.* @Inject @@ -52,4 +55,31 @@ class RealNetworkingClient( val notifications: List = httpResponse.body() return ListNotificationsResponse(notifications) } + + @OptIn(InternalSerializationApi::class) + override suspend fun getFeed(): Feed { + val getFeedHttpResponse = httpClient.get("https://api.github.com/feeds") { + method = HttpMethod.Get + headers { + append(HttpHeaders.Authorization, "Bearer ${Env.X_PAT_CLASSIC}") + append(HttpHeaders.Accept, "application/vnd.github+json") + append("X-GitHub-Api-Version", "2022-11-28") + } + } + + val getFeedResponse = getFeedHttpResponse.body() + val currentUserUrl = getFeedResponse.currentUserUrl + val userFeedHttpResponse = httpClient.get(currentUserUrl) { + method = HttpMethod.Get + headers { + append(HttpHeaders.Authorization, "Bearer ${Env.X_PAT_CLASSIC}") + append(HttpHeaders.Accept, ContentType.Application.Atom) + append("X-GitHub-Api-Version", "2022-11-28") + } + + contentType(ContentType.Application.Atom) + } + + return XML.decodeFromString(Feed::class.serializer(), userFeedHttpResponse.bodyAsText()) + } } \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/foundation/webview/build.gradle.kts b/experimental/sample/octonaut/xplat/foundation/webview/build.gradle.kts new file mode 100644 index 000000000..be701bb62 --- /dev/null +++ b/experimental/sample/octonaut/xplat/foundation/webview/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("plugin.octonaut.android.library") + id("plugin.octonaut.kotlin.multiplatform") + alias(libs.plugins.serialization) + alias(libs.plugins.compose) +} + +kotlin { + sourceSets { + commonMain { + dependencies { + api(compose.runtime) + api(compose.components.resources) + api(libs.circuit.foundation) + api(libs.kotlinx.serialization.core) + implementation(libs.ktor.serialization.xml) + api(project(":experimental:sample:octonaut:xplat:foundation:di:api")) + implementation(libs.compose.webview.multiplatform) + } + } + } +} + +android { + namespace = "org.mobilenativefoundation.sample.octonaut.xplat.foundation.webview" +} \ No newline at end of file diff --git a/experimental/sample/octonaut/xplat/foundation/webview/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/webview/WebViewUrlStateHolder.kt b/experimental/sample/octonaut/xplat/foundation/webview/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/webview/WebViewUrlStateHolder.kt new file mode 100644 index 000000000..c13781d94 --- /dev/null +++ b/experimental/sample/octonaut/xplat/foundation/webview/src/commonMain/kotlin/org/mobilenativefoundation/sample/octonaut/xplat/foundation/webview/WebViewUrlStateHolder.kt @@ -0,0 +1,7 @@ +package org.mobilenativefoundation.sample.octonaut.xplat.foundation.webview + +import kotlinx.coroutines.flow.MutableStateFlow + +interface WebViewUrlStateHolder { + val url: MutableStateFlow +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3343422ec..2ac6089e6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,6 +42,7 @@ circuit = "0.20.0" compose-bom = "2024.02.02" lint = "1.2.0" apolloVerson = "3.8.2" +compose-webview = "1.9.4" [libraries] android-gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" } @@ -95,6 +96,9 @@ ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-serialization = { module = "io.ktor:ktor-client-serialization", version.ref = "ktor" } ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-serialization-xml = {module = "io.ktor:ktor-serialization-kotlinx-xml", version.ref = "ktor"} +compose-webview-multiplatform = {module = "io.github.kevinnzou:compose-webview-multiplatform", version.ref = "compose-webview"} + # Gradle Plugins android-desugarJdkLibs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" } buildkonfig-gradlePlugin = { module = "com.codingfeline.buildkonfig:buildkonfig-gradle-plugin", version.ref = "buildkonfig" } diff --git a/settings.gradle b/settings.gradle index 2ff63f0ef..708d1f5a4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,13 +21,18 @@ include ':experimental:sample:octonaut:android:app' include ':experimental:sample:octonaut:xplat:foundation:di:api' include ':experimental:sample:octonaut:xplat:foundation:networking:api' include ':experimental:sample:octonaut:xplat:foundation:networking:impl' +include ':experimental:sample:octonaut:xplat:foundation:webview' include ':experimental:sample:octonaut:xplat:common:market' include ':experimental:sample:octonaut:xplat:domain:user:api' include ':experimental:sample:octonaut:xplat:domain:user:impl' include ':experimental:sample:octonaut:xplat:domain:notifications:api' include ':experimental:sample:octonaut:xplat:domain:notifications:impl' +include ':experimental:sample:octonaut:xplat:domain:feed:api' +include ':experimental:sample:octonaut:xplat:domain:feed:impl' include ':experimental:sample:octonaut:xplat:feat:homeTab:api' include ':experimental:sample:octonaut:xplat:feat:homeTab:impl' +include ':experimental:sample:octonaut:xplat:feat:userProfile:api' +include ':experimental:sample:octonaut:xplat:feat:userProfile:impl' include ':experimental:sample:octonaut:xplat:feat:notificationsTab:api' include ':experimental:sample:octonaut:xplat:feat:notificationsTab:impl' include ':experimental:sample:octonaut:xplat:feat:exploreTab:api'