chore(identity): add identity module

This commit is contained in:
Diego Beraldin 2023-07-28 23:59:08 +02:00
parent 49706c603d
commit e941dc0f21
42 changed files with 464 additions and 71 deletions

View File

@ -36,11 +36,12 @@ kotlin {
val commonMain by getting { val commonMain by getting {
dependencies { dependencies {
implementation(libs.koin.core) implementation(libs.koin.core)
implementation(libs.ktorfit.lib) api(libs.ktorfit.lib)
implementation(libs.ktor.serialization) implementation(libs.ktor.serialization)
implementation(libs.ktor.contentnegotiation) implementation(libs.ktor.contentnegotiation)
implementation(libs.ktor.json) implementation(libs.ktor.json)
implementation(libs.ktor.logging) implementation(libs.ktor.logging)
implementation(projects.coreUtils)
} }
} }
val commonTest by getting { val commonTest by getting {

View File

@ -1,10 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.provider
import android.util.Log
import io.ktor.client.plugins.logging.Logger
internal actual val defaultLogger: Logger = object : Logger {
override fun log(message: String) {
Log.d("com.github.diegoberaldin.raccoonforlemmy", message)
}
}

View File

@ -16,4 +16,8 @@ val coreApiModule = module {
val provider: ServiceProvider = get() val provider: ServiceProvider = get()
provider.communityService provider.communityService
} }
single {
val provider: ServiceProvider = get()
provider.authService
}
} }

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class LoginForm(
@SerialName("username_or_email") val username: String,
@SerialName("password") val password: String,
@SerialName("totp_2fa_token") val totp2faToken: String? = null,
)

View File

@ -0,0 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class LoginResponse(
@SerialName("jwt") val token: String? = null,
@SerialName("registration_created") val registrationCreated: Boolean,
@SerialName("verify_email_sent") val verifyEmailSent: Boolean,
)

View File

@ -1,5 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.provider package com.github.diegoberaldin.raccoonforlemmy.core_api.provider
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.AuthService
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.CommunityService import com.github.diegoberaldin.raccoonforlemmy.core_api.service.CommunityService
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.PostService import com.github.diegoberaldin.raccoonforlemmy.core_api.service.PostService
import de.jensklingenberg.ktorfit.Ktorfit import de.jensklingenberg.ktorfit.Ktorfit
@ -26,6 +27,9 @@ internal class DefaultServiceProvider : ServiceProvider {
override lateinit var communityService: CommunityService override lateinit var communityService: CommunityService
private set private set
override lateinit var authService: AuthService
private set
private val baseUrl: String get() = "https://$currentInstance/api/$VERSION/" private val baseUrl: String get() = "https://$currentInstance/api/$VERSION/"
private val client = HttpClient { private val client = HttpClient {
install(Logging) { install(Logging) {
@ -54,5 +58,6 @@ internal class DefaultServiceProvider : ServiceProvider {
.build() .build()
postService = ktorfit.create() postService = ktorfit.create()
communityService = ktorfit.create() communityService = ktorfit.create()
authService = ktorfit.create()
} }
} }

View File

@ -1,5 +1,10 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.provider package com.github.diegoberaldin.raccoonforlemmy.core_api.provider
import com.github.diegoberaldin.racconforlemmy.core_utils.Log
import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logger
internal expect val defaultLogger: Logger internal val defaultLogger = object : Logger {
override fun log(message: String) {
Log.d(message)
}
}

View File

@ -1,5 +1,6 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.provider package com.github.diegoberaldin.raccoonforlemmy.core_api.provider
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.AuthService
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.CommunityService import com.github.diegoberaldin.raccoonforlemmy.core_api.service.CommunityService
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.PostService import com.github.diegoberaldin.raccoonforlemmy.core_api.service.PostService
@ -8,6 +9,7 @@ interface ServiceProvider {
val currentInstance: String val currentInstance: String
val postService: PostService val postService: PostService
val communityService: CommunityService val communityService: CommunityService
val authService: AuthService
fun changeInstance(value: String) fun changeInstance(value: String)
} }

View File

@ -0,0 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.service
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.LoginForm
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.LoginResponse
import de.jensklingenberg.ktorfit.Response
import de.jensklingenberg.ktorfit.http.Body
import de.jensklingenberg.ktorfit.http.POST
interface AuthService {
@POST("user/login")
suspend fun login(@Body form: LoginForm): Response<LoginResponse>
}

View File

@ -1,10 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.core_api.provider
import io.ktor.client.plugins.logging.Logger
import platform.Foundation.NSLog
internal actual val defaultLogger: Logger = object : Logger {
override fun log(message: String) {
NSLog(message)
}
}

View File

@ -8,35 +8,35 @@ internal class DefaultTemporaryKeyStore(
private val settings: Settings, private val settings: Settings,
) : TemporaryKeyStore { ) : TemporaryKeyStore {
override suspend fun containsKey(key: String): Boolean = settings.keys.contains(key) override fun containsKey(key: String): Boolean = settings.keys.contains(key)
override suspend fun save(key: String, value: Boolean) { override fun save(key: String, value: Boolean) {
settings[key] = value settings[key] = value
} }
override suspend fun get(key: String, default: Boolean): Boolean = settings[key, default] override fun get(key: String, default: Boolean): Boolean = settings[key, default]
override suspend fun save(key: String, value: String) { override fun save(key: String, value: String) {
settings[key] = value settings[key] = value
} }
override suspend fun get(key: String, default: String): String = settings[key, default] override fun get(key: String, default: String): String = settings[key, default]
override suspend fun save(key: String, value: Int) { override fun save(key: String, value: Int) {
settings[key] = value settings[key] = value
} }
override suspend fun get(key: String, default: Int): Int = settings[key, default] override fun get(key: String, default: Int): Int = settings[key, default]
override suspend fun save(key: String, value: Float) { override fun save(key: String, value: Float) {
settings[key] = value settings[key] = value
} }
override suspend fun get(key: String, default: Float): Float = settings[key, default] override fun get(key: String, default: Float): Float = settings[key, default]
override suspend fun save(key: String, value: Double) { override fun save(key: String, value: Double) {
settings[key] = value settings[key] = value
} }
override suspend fun get(key: String, default: Double): Double = settings[key, default] override fun get(key: String, default: Double): Double = settings[key, default]
} }

View File

@ -3,4 +3,5 @@ package com.github.diegoberaldin.raccoonforlemmy.core_preferences
object KeyStoreKeys { object KeyStoreKeys {
const val UITheme = "uiTheme" const val UITheme = "uiTheme"
const val Locale = "locale" const val Locale = "locale"
const val AuthToken = "auth"
} }

View File

@ -10,7 +10,7 @@ interface TemporaryKeyStore {
* @param key Key to check * @param key Key to check
* @return true if the key store contains the key, false otherwise * @return true if the key store contains the key, false otherwise
*/ */
suspend fun containsKey(key: String): Boolean fun containsKey(key: String): Boolean
/** /**
* Save a boolean value in the keystore under a given key. * Save a boolean value in the keystore under a given key.
@ -18,7 +18,7 @@ interface TemporaryKeyStore {
* @param key Key * @param key Key
* @param value Value * @param value Value
*/ */
suspend fun save(key: String, value: Boolean) fun save(key: String, value: Boolean)
/** /**
* Retrieve a boolean value from the key store given its key. * Retrieve a boolean value from the key store given its key.
@ -27,7 +27,7 @@ interface TemporaryKeyStore {
* @param default Default value * @param default Default value
* @return value saved in the keystore or the default one * @return value saved in the keystore or the default one
*/ */
suspend fun get(key: String, default: Boolean): Boolean fun get(key: String, default: Boolean): Boolean
/** /**
* Save a string value in the keystore under a given key. * Save a string value in the keystore under a given key.
@ -35,7 +35,7 @@ interface TemporaryKeyStore {
* @param key Key * @param key Key
* @param value Value * @param value Value
*/ */
suspend fun save(key: String, value: String) fun save(key: String, value: String)
/** /**
* Retrieve a string value from the key store given its key. * Retrieve a string value from the key store given its key.
@ -44,7 +44,7 @@ interface TemporaryKeyStore {
* @param default Default value * @param default Default value
* @return value saved in the keystore or the default one * @return value saved in the keystore or the default one
*/ */
suspend fun get(key: String, default: String): String operator fun get(key: String, default: String): String
/** /**
* Save an integer value in the keystore under a given key. * Save an integer value in the keystore under a given key.
@ -52,7 +52,7 @@ interface TemporaryKeyStore {
* @param key Key * @param key Key
* @param value Value * @param value Value
*/ */
suspend fun save(key: String, value: Int) fun save(key: String, value: Int)
/** /**
* Retrieve an integer value from the key store given its key. * Retrieve an integer value from the key store given its key.
@ -61,7 +61,7 @@ interface TemporaryKeyStore {
* @param default Default value * @param default Default value
* @return value saved in the keystore or the default one * @return value saved in the keystore or the default one
*/ */
suspend fun get(key: String, default: Int): Int fun get(key: String, default: Int): Int
/** /**
* Save a floating point value in the keystore under a given key. * Save a floating point value in the keystore under a given key.
@ -69,7 +69,7 @@ interface TemporaryKeyStore {
* @param key Key * @param key Key
* @param value Value * @param value Value
*/ */
suspend fun save(key: String, value: Float) fun save(key: String, value: Float)
/** /**
* Retrieve a floating point value from the key store given its key. * Retrieve a floating point value from the key store given its key.
@ -78,7 +78,7 @@ interface TemporaryKeyStore {
* @param default Default value * @param default Default value
* @return value saved in the keystore or the default one * @return value saved in the keystore or the default one
*/ */
suspend fun get(key: String, default: Float): Float fun get(key: String, default: Float): Float
/** /**
* Save a floating point (double precision) value in the keystore under a given key. * Save a floating point (double precision) value in the keystore under a given key.
@ -86,7 +86,7 @@ interface TemporaryKeyStore {
* @param key Key * @param key Key
* @param value Value * @param value Value
*/ */
suspend fun save(key: String, value: Double) fun save(key: String, value: Double)
/** /**
* Retrieve a floating point (double precision) value from the key store given its key. * Retrieve a floating point (double precision) value from the key store given its key.
@ -95,5 +95,5 @@ interface TemporaryKeyStore {
* @param default Default value * @param default Default value
* @return value saved in the keystore or the default one * @return value saved in the keystore or the default one
*/ */
suspend fun get(key: String, default: Double): Double fun get(key: String, default: Double): Double
} }

View File

@ -0,0 +1,9 @@
package com.github.diegoberaldin.racconforlemmy.core_utils
actual object Log {
private const val TAG = "com.github.diegoberaldin.racconforlemmy"
actual fun d(message: String) {
android.util.Log.d(TAG, message)
}
}

View File

@ -0,0 +1,5 @@
package com.github.diegoberaldin.racconforlemmy.core_utils
expect object Log {
fun d(message: String)
}

View File

@ -0,0 +1,9 @@
package com.github.diegoberaldin.racconforlemmy.core_utils
import platform.Foundation.NSLog
actual object Log {
actual fun d(message: String) {
NSLog(message)
}
}

View File

@ -0,0 +1,57 @@
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.library)
alias(libs.plugins.compose)
alias(libs.plugins.native.cocoapods)
}
@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class)
kotlin {
targetHierarchy.default()
android {
compilations.all {
kotlinOptions {
jvmTarget = "1.8"
}
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
cocoapods {
summary = "Some description for the Shared Module"
homepage = "Link to the Shared Module homepage"
version = "1.0"
ios.deploymentTarget = "14.1"
framework {
baseName = "domain-identity"
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation(libs.koin.core)
implementation(compose.foundation)
implementation(projects.corePreferences)
implementation(projects.coreApi)
implementation(projects.coreUtils)
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
}
}
android {
namespace = "com.github.diegoberaldin.raccoonforlemmy.domain_identity"
compileSdk = 33
defaultConfig {
minSdk = 26
}
}

View File

@ -0,0 +1,42 @@
Pod::Spec.new do |spec|
spec.name = 'domain-identity'
spec.version = '1.0'
spec.homepage = 'Link to the Shared Module homepage'
spec.source = { :git => "Not Published", :tag => "Cocoapods/#{spec.name}/#{spec.version}" }
spec.authors = ''
spec.license = ''
spec.summary = 'Some description for the Shared Module'
spec.vendored_frameworks = "build/cocoapods/framework/domain-identity.framework"
spec.libraries = "c++"
spec.module_name = "#{spec.name}_umbrella"
spec.ios.deployment_target = '14.1'
spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':domain-identity',
'PRODUCT_MODULE_NAME' => 'domain-identity',
}
spec.script_phases = [
{
:name => 'Build domain-identity',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$COCOAPODS_SKIP_KOTLIN_BUILD" ]; then
echo "Skipping Gradle build task invocation due to COCOAPODS_SKIP_KOTLIN_BUILD environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
"$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration=$CONFIGURATION
SCRIPT
}
]
end

View File

@ -0,0 +1,39 @@
Pod::Spec.new do |spec|
spec.name = 'domain_identity'
spec.version = '1.0'
spec.homepage = 'Link to the Shared Module homepage'
spec.source = { :http=> ''}
spec.authors = ''
spec.license = ''
spec.summary = 'Some description for the Shared Module'
spec.vendored_frameworks = 'build/cocoapods/framework/domain-identity.framework'
spec.libraries = 'c++'
spec.ios.deployment_target = '14.1'
spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':domain-identity',
'PRODUCT_MODULE_NAME' => 'domain-identity',
}
spec.script_phases = [
{
:name => 'Build domain_identity',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
"$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
SCRIPT
}
]
end

View File

@ -0,0 +1,30 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.di
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.usecase.DefaultLoginUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.usecase.LoginUseCase
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.ApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.AuthRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.DefaultApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.DefaultAuthRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.DefaultIdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.IdentityRepository
import org.koin.dsl.module
val coreIdentityModule = module {
single<ApiConfigurationRepository> {
DefaultApiConfigurationRepository(get())
}
single<IdentityRepository> {
DefaultIdentityRepository(get())
}
single<AuthRepository> {
DefaultAuthRepository(get())
}
single<LoginUseCase> {
DefaultLoginUseCase(
apiConfigurationRepository = get(),
authRepository = get(),
identityRepository = get(),
)
}
}

View File

@ -0,0 +1,8 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository
interface ApiConfigurationRepository {
fun getInstance(): String
fun changeInstance(value: String)
}

View File

@ -0,0 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.LoginResponse
interface AuthRepository {
suspend fun login(
username: String,
password: String,
totp2faToken: String? = null,
): Result<LoginResponse>
}

View File

@ -0,0 +1,14 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository
import com.github.diegoberaldin.raccoonforlemmy.core_api.provider.ServiceProvider
internal class DefaultApiConfigurationRepository(
private val serviceProvider: ServiceProvider,
) : ApiConfigurationRepository {
override fun getInstance() = serviceProvider.currentInstance
override fun changeInstance(value: String) {
serviceProvider.changeInstance(value)
}
}

View File

@ -0,0 +1,28 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.LoginForm
import com.github.diegoberaldin.raccoonforlemmy.core_api.dto.LoginResponse
import com.github.diegoberaldin.raccoonforlemmy.core_api.service.AuthService
internal class DefaultAuthRepository(
private val authService: AuthService,
) : AuthRepository {
override suspend fun login(
username: String,
password: String,
totp2faToken: String?,
): Result<LoginResponse> = runCatching {
val data = LoginForm(
username = username,
password = password,
totp2faToken = totp2faToken,
)
val response = authService.login(data)
if (!response.isSuccessful) {
// TODO: better API error handling
val error = response.errorBody().toString()
throw Exception(error)
}
response.body() ?: throw Exception("No reponse from login endpoint")
}
}

View File

@ -0,0 +1,28 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.KeyStoreKeys
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.TemporaryKeyStore
import kotlinx.coroutines.flow.MutableStateFlow
internal class DefaultIdentityRepository(
private val keyStore: TemporaryKeyStore,
) : IdentityRepository {
override val authToken = MutableStateFlow<String?>(null)
init {
val previousToken = keyStore[KeyStoreKeys.AuthToken, ""]
.takeIf { it.isNotEmpty() }
authToken.value = previousToken
}
override fun storeToken(value: String) {
authToken.value = value
keyStore.save(KeyStoreKeys.AuthToken, value)
}
override fun clearToken() {
authToken.value = null
keyStore.save(KeyStoreKeys.AuthToken, "")
}
}

View File

@ -0,0 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository
import kotlinx.coroutines.flow.StateFlow
interface IdentityRepository {
val authToken: StateFlow<String?>
fun storeToken(value: String)
fun clearToken()
}

View File

@ -0,0 +1,43 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.usecase
import com.github.diegoberaldin.racconforlemmy.core_utils.Log
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.ApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.AuthRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.IdentityRepository
internal class DefaultLoginUseCase(
private val authRepository: AuthRepository,
private val apiConfigurationRepository: ApiConfigurationRepository,
private val identityRepository: IdentityRepository,
) : LoginUseCase {
override suspend fun login(
instance: String,
username: String,
password: String,
totp2faToken: String?,
): Result<Unit> {
val oldInstance = apiConfigurationRepository.getInstance()
apiConfigurationRepository.changeInstance(instance)
val response = authRepository.login(
username = username,
password = password,
totp2faToken = totp2faToken
)
return response.onFailure {
Log.d("Login failure: ${it.message}")
}.map {
val auth = it.token
if (auth == null) {
apiConfigurationRepository.changeInstance(oldInstance)
} else {
identityRepository.storeToken(auth)
}
}
}
override suspend fun logout() {
identityRepository.clearToken()
}
}

View File

@ -0,0 +1,12 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_identity.usecase
interface LoginUseCase {
suspend fun login(
instance: String,
username: String,
password: String,
totp2faToken: String? = null,
): Result<Unit>
suspend fun logout()
}

View File

@ -1,14 +0,0 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_post.repository
import com.github.diegoberaldin.raccoonforlemmy.core_api.provider.ServiceProvider
class ApiConfigurationRepository(
private val serviceProvider: ServiceProvider,
) {
fun getInstance() = serviceProvider.currentInstance
fun changeInstance(value: String) {
serviceProvider.changeInstance(value)
}
}

View File

@ -1,13 +1,11 @@
package com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.di package com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.di
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.ApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.CommunityRepository import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.CommunityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.PostsRepository import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.PostsRepository
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
val postsRepositoryModule = module { val postsRepositoryModule = module {
singleOf(::ApiConfigurationRepository)
singleOf(::PostsRepository) singleOf(::PostsRepository)
singleOf(::CommunityRepository) singleOf(::CommunityRepository)
} }

View File

@ -52,6 +52,7 @@ kotlin {
implementation(projects.coreArchitecture) implementation(projects.coreArchitecture)
implementation(projects.coreUtils) implementation(projects.coreUtils)
implementation(projects.coreMd) implementation(projects.coreMd)
implementation(projects.domainIdentity)
implementation(projects.domainPost.data) implementation(projects.domainPost.data)
implementation(projects.domainPost.repository) implementation(projects.domainPost.repository)
} }

View File

@ -14,6 +14,7 @@ actual val homeTabModule = module {
mvi = DefaultMviModel(HomeScreenMviModel.UiState()), mvi = DefaultMviModel(HomeScreenMviModel.UiState()),
postsRepository = get(), postsRepository = get(),
apiConfigRepository = get(), apiConfigRepository = get(),
identityRepository = get(),
) )
} }
} }

View File

@ -25,6 +25,7 @@ import dev.icerock.moko.resources.compose.stringResource
@Composable @Composable
fun ListingTypeBottomSheet( fun ListingTypeBottomSheet(
isLogged: Boolean = false,
onDismiss: (ListingType) -> Unit, onDismiss: (ListingType) -> Unit,
) { ) {
Column( Column(
@ -44,11 +45,13 @@ fun ListingTypeBottomSheet(
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
) )
val values = listOf( val values = buildList {
ListingType.Subscribed, if (isLogged) {
ListingType.Local, this += ListingType.Subscribed
ListingType.All, }
) this += ListingType.All
this += ListingType.Local
}
Column( Column(
modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()), modifier = Modifier.fillMaxWidth().verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(Spacing.xxxs) verticalArrangement = Arrangement.spacedBy(Spacing.xxxs)

View File

@ -79,7 +79,9 @@ object HomeTab : Tab {
sortType = uiState.sortType, sortType = uiState.sortType,
onSelectListingType = { onSelectListingType = {
bottomSheetChannel.trySend @Composable { bottomSheetChannel.trySend @Composable {
ListingTypeBottomSheet { type -> ListingTypeBottomSheet(
isLogged = uiState.isLogged
) { type ->
model.reduce(HomeScreenMviModel.Intent.ChangeListing(type)) model.reduce(HomeScreenMviModel.Intent.ChangeListing(type))
bottomSheetChannel.trySend(null) bottomSheetChannel.trySend(null)
} }

View File

@ -12,8 +12,8 @@ import com.seiko.imageloader.rememberImagePainter
@Composable @Composable
internal fun PostCardImage(post: PostModel) { internal fun PostCardImage(post: PostModel) {
val imageUrl = post.thumbnailUrl val imageUrl = post.thumbnailUrl.orEmpty()
if (!imageUrl.isNullOrEmpty()) { if (imageUrl.isNotEmpty()) {
val painter = rememberImagePainter(imageUrl) val painter = rememberImagePainter(imageUrl)
Image( Image(
modifier = Modifier.fillMaxWidth().heightIn(min = 200.dp), modifier = Modifier.fillMaxWidth().heightIn(min = 200.dp),

View File

@ -5,16 +5,21 @@ import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviMode
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel import com.github.diegoberaldin.raccoonforlemmy.core_architecture.MviModel
import com.github.diegoberaldin.raccoonforlemmy.data.ListingType import com.github.diegoberaldin.raccoonforlemmy.data.ListingType
import com.github.diegoberaldin.raccoonforlemmy.data.SortType import com.github.diegoberaldin.raccoonforlemmy.data.SortType
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.ApiConfigurationRepository import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.ApiConfigurationRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.repository.IdentityRepository
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.PostsRepository import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.PostsRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class HomeScreenModel( class HomeScreenModel(
private val mvi: DefaultMviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect>, private val mvi: DefaultMviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect>,
private val postsRepository: PostsRepository, private val postsRepository: PostsRepository,
private val apiConfigRepository: ApiConfigurationRepository, private val apiConfigRepository: ApiConfigurationRepository,
private val identityRepository: IdentityRepository,
) : ScreenModel, ) : ScreenModel,
MviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect> by mvi { MviModel<HomeScreenMviModel.Intent, HomeScreenMviModel.UiState, HomeScreenMviModel.Effect> by mvi {
@ -31,7 +36,16 @@ class HomeScreenModel(
override fun onStarted() { override fun onStarted() {
mvi.onStarted() mvi.onStarted()
mvi.updateState { it.copy(instance = apiConfigRepository.getInstance()) } mvi.updateState {
it.copy(
instance = apiConfigRepository.getInstance()
)
}
identityRepository.authToken.map { !it.isNullOrEmpty() }.onEach { isLogged ->
mvi.updateState {
it.copy(isLogged = isLogged)
}
}.launchIn(mvi.scope)
refresh() refresh()
} }

View File

@ -20,6 +20,7 @@ interface HomeScreenMviModel :
val loading: Boolean = false, val loading: Boolean = false,
val canFetchMore: Boolean = true, val canFetchMore: Boolean = true,
val instance: String = "", val instance: String = "",
val isLogged: Boolean = false,
val listingType: ListingType = ListingType.Local, val listingType: ListingType = ListingType.Local,
val sortType: SortType = SortType.Active, val sortType: SortType = SortType.Active,
val posts: List<PostModel> = emptyList(), val posts: List<PostModel> = emptyList(),

View File

@ -1,7 +1,7 @@
package com.github.diegoberaldin.raccoonforlemmy.feature_home.di package com.github.diegoberaldin.raccoonforlemmy.feature_home.di
import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel import com.github.diegoberaldin.raccoonforlemmy.core_architecture.DefaultMviModel
import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.postsRepositoryModule import com.github.diegoberaldin.raccoonforlemmy.domain_post.repository.di.postsRepositoryModule
import com.github.diegoberaldin.raccoonforlemmy.feature_home.viewmodel.HomeScreenModel import com.github.diegoberaldin.raccoonforlemmy.feature_home.viewmodel.HomeScreenModel
import com.github.diegoberaldin.raccoonforlemmy.feature_home.viewmodel.HomeScreenMviModel import com.github.diegoberaldin.raccoonforlemmy.feature_home.viewmodel.HomeScreenMviModel
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -15,6 +15,7 @@ actual val homeTabModule = module {
mvi = DefaultMviModel(HomeScreenMviModel.UiState()), mvi = DefaultMviModel(HomeScreenMviModel.UiState()),
postsRepository = get(), postsRepository = get(),
apiConfigRepository = get(), apiConfigRepository = get(),
identityRepository = get(),
) )
} }
} }

View File

@ -36,4 +36,5 @@ include(":core-api")
include(":domain-post") include(":domain-post")
include(":domain-post:repository") include(":domain-post:repository")
include(":domain-post:data") include(":domain-post:data")
include(":domain-identity")
include(":core-md") include(":core-md")

View File

@ -58,6 +58,7 @@ kotlin {
implementation(projects.coreAppearance) implementation(projects.coreAppearance)
implementation(projects.corePreferences) implementation(projects.corePreferences)
implementation(projects.coreApi) implementation(projects.coreApi)
implementation(projects.domainIdentity)
api(projects.resources) api(projects.resources)
api(projects.featureHome) api(projects.featureHome)

View File

@ -2,6 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy
import com.github.diegoberaldin.raccoonforlemmy.core_api.di.coreApiModule import com.github.diegoberaldin.raccoonforlemmy.core_api.di.coreApiModule
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.di.coreAppearanceModule import com.github.diegoberaldin.raccoonforlemmy.core_appearance.di.coreAppearanceModule
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.di.coreIdentityModule
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.di.corePreferencesModule import com.github.diegoberaldin.raccoonforlemmy.core_preferences.di.corePreferencesModule
import com.github.diegoberaldin.raccoonforlemmy.feature_inbox.inboxTabModule import com.github.diegoberaldin.raccoonforlemmy.feature_inbox.inboxTabModule
import com.github.diegoberaldin.raccoonforlemmy.feature_profile.profileTabModule import com.github.diegoberaldin.raccoonforlemmy.feature_profile.profileTabModule
@ -16,6 +17,7 @@ val sharedHelperModule = module {
coreAppearanceModule, coreAppearanceModule,
corePreferencesModule, corePreferencesModule,
coreApiModule, coreApiModule,
coreIdentityModule,
localizationModule, localizationModule,
homeTabModule, homeTabModule,
inboxTabModule, inboxTabModule,

View File

@ -2,6 +2,7 @@ package com.github.diegoberaldin.raccoonforlemmy
import com.github.diegoberaldin.raccoonforlemmy.core_api.di.coreApiModule import com.github.diegoberaldin.raccoonforlemmy.core_api.di.coreApiModule
import com.github.diegoberaldin.raccoonforlemmy.core_appearance.di.coreAppearanceModule import com.github.diegoberaldin.raccoonforlemmy.core_appearance.di.coreAppearanceModule
import com.github.diegoberaldin.raccoonforlemmy.domain_identity.di.coreIdentityModule
import com.github.diegoberaldin.raccoonforlemmy.core_preferences.di.corePreferencesModule import com.github.diegoberaldin.raccoonforlemmy.core_preferences.di.corePreferencesModule
import com.github.diegoberaldin.raccoonforlemmy.feature_home.di.homeTabModule import com.github.diegoberaldin.raccoonforlemmy.feature_home.di.homeTabModule
import com.github.diegoberaldin.raccoonforlemmy.feature_inbox.inboxTabModule import com.github.diegoberaldin.raccoonforlemmy.feature_inbox.inboxTabModule
@ -17,6 +18,7 @@ fun initKoin() {
coreAppearanceModule, coreAppearanceModule,
corePreferencesModule, corePreferencesModule,
coreApiModule, coreApiModule,
coreIdentityModule,
localizationModule, localizationModule,
homeTabModule, homeTabModule,
inboxTabModule, inboxTabModule,