From 6ff64f54f26a3926fc482ac1560954477da81ee6 Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Wed, 13 Nov 2024 00:56:08 +0900 Subject: [PATCH 01/30] feature: Add firebase messaging setting --- app/build.gradle.kts | 3 +++ app/src/main/AndroidManifest.xml | 8 ++++++- .../offroad.app/OffRoadMessagingService.kt | 17 ++++++++++++++ build.gradle.kts | 1 + gradle/libs.versions.toml | 23 +++++++++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d01ef5cf..a48f310a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -3,6 +3,7 @@ import com.teamoffroad.app.setNamespace plugins { id("offroad.android.application") + alias(libs.plugins.google.services) } android { @@ -54,4 +55,6 @@ dependencies { implementation(project(":feature:explore")) implementation(project(":feature:mypage")) implementation(libs.kakao.user) + implementation(libs.bundles.firebase) + implementation(platform(libs.firebase.bom)) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fd09daa4..7b2578f2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,13 @@ android:theme="@style/Theme.Offroad" android:usesCleartextTraffic="true" tools:targetApi="31"> - + + + + + diff --git a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt new file mode 100644 index 00000000..731624ff --- /dev/null +++ b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt @@ -0,0 +1,17 @@ +package com.teamoffroad.offroad.app + +import com.google.firebase.messaging.FirebaseMessagingService +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@AndroidEntryPoint +class OffRoadMessagingService : FirebaseMessagingService() { + + override fun onNewToken(token: String) { + super.onNewToken(token) + + } +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index db622bad..3ef32e69 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,4 +6,5 @@ plugins { alias(libs.plugins.android.library) apply false alias(libs.plugins.daggers.hilt) apply false alias(libs.plugins.ksp) apply false + alias(libs.plugins.google.services) apply false } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8909954a..97c81079 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -73,6 +73,10 @@ naverMapSdk = "3.18.0" googlePlayService = "21.1.0" googleAccompanistPermissions = "0.32.0" accompanistInsets = "0.31.5-beta" +googleServices = "4.4.2" + +# Firebase +firebase_bom = "33.5.1" # Kakao kakao = "2.20.6" @@ -175,6 +179,14 @@ google-accompanist-permissions = { group = "com.google.accompanist", name = "acc google-play-services-auth = { group = "com.google.android.gms", name = "play-services-auth", version.ref = "googlePlayService" } accompanist-insets = { module = "com.google.accompanist:accompanist-insets", version.ref = "accompanistInsets" } +# Firebase +firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase_bom" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging-ktx" } +firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics-ktx" } +firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics-ktx" } +firebase-remoteConfig = { group = "com.google.firebase", name = "firebase-config-ktx" } +firebase-database = { group = "com.google.firebase", name = "firebase-database-ktx" } + # Kakao kakao-user = { module = "com.kakao.sdk:v2-user", version.ref = "kakao" } @@ -210,6 +222,9 @@ android-library = { id = "com.android.library", version.ref = "androidGradlePlug # Kotlin Symbol Processing Plugin ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +# Google +google-services = { id = "com.google.gms.google-services", version.ref = "googleServices" } + [bundles] # Offroad Map Libraries offroad-map = [ @@ -225,3 +240,11 @@ offroad-camera = [ "camera-lifecycle", "camera-view", ] + +firebase = [ + "firebase-analytics", + "firebase-database", + "firebase-messaging", + "firebase-remoteConfig" +] + From 0747a3c4e984ac7e1f7632a21268153d50e9e45d Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Wed, 13 Nov 2024 01:37:54 +0900 Subject: [PATCH 02/30] feature: Add device token local save --- .../offroad.app/OffRoadMessagingService.kt | 6 +++- .../AutoSignInPreferencesDataSource.kt | 1 - ...DefaultDeviceTokenPreferencesDataSource.kt | 28 +++++++++++++++++++ .../DeviceTokenPreferencesDataSource.kt | 8 ++++++ .../core/common/data/di/DataModule.kt | 7 +++++ .../core/common/data/di/DataStoreModule.kt | 7 +++++ .../core/common/data/di/RepositoryModule.kt | 8 ++++++ .../repository/AutoSignInRepositoryImpl.kt | 2 -- .../repository/DeviceTokenRepositoryImpl.kt | 17 +++++++++++ .../repository/DeviceTokenRepository.kt | 8 ++++++ 10 files changed, 88 insertions(+), 4 deletions(-) create mode 100644 core/common/src/main/java/com/teamoffroad/core/common/data/datasource/DefaultDeviceTokenPreferencesDataSource.kt create mode 100644 core/common/src/main/java/com/teamoffroad/core/common/data/datasource/DeviceTokenPreferencesDataSource.kt create mode 100644 core/common/src/main/java/com/teamoffroad/core/common/data/repository/DeviceTokenRepositoryImpl.kt create mode 100644 core/common/src/main/java/com/teamoffroad/core/common/domain/repository/DeviceTokenRepository.kt diff --git a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt index 731624ff..1f461afe 100644 --- a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt +++ b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt @@ -1,6 +1,7 @@ package com.teamoffroad.offroad.app import com.google.firebase.messaging.FirebaseMessagingService +import com.teamoffroad.core.common.domain.repository.DeviceTokenRepository import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -9,9 +10,12 @@ import javax.inject.Inject @AndroidEntryPoint class OffRoadMessagingService : FirebaseMessagingService() { - + @Inject + lateinit var dataStore: DeviceTokenRepository + override fun onNewToken(token: String) { super.onNewToken(token) + CoroutineScope(Dispatchers.IO).launch { dataStore.updateDeviceTokenEnabled(token) } } } \ No newline at end of file diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/AutoSignInPreferencesDataSource.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/AutoSignInPreferencesDataSource.kt index d76280d6..a324a567 100644 --- a/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/AutoSignInPreferencesDataSource.kt +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/AutoSignInPreferencesDataSource.kt @@ -4,6 +4,5 @@ import kotlinx.coroutines.flow.Flow interface AutoSignInPreferencesDataSource { val autoLogin: Flow - suspend fun setAutoLogin(autoLogin: Boolean) } diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/DefaultDeviceTokenPreferencesDataSource.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/DefaultDeviceTokenPreferencesDataSource.kt new file mode 100644 index 00000000..ec7137ba --- /dev/null +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/DefaultDeviceTokenPreferencesDataSource.kt @@ -0,0 +1,28 @@ +package com.teamoffroad.core.common.data.datasource + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +class DefaultDeviceTokenPreferencesDataSource @Inject constructor( + private val dataStore: DataStore, +) : DeviceTokenPreferencesDataSource { + + object PreferencesKey { + val DEVICE_TOKEN_KEY = stringPreferencesKey("DEVICE_TOKEN_KEY") + } + + override val deviceToken: Flow = dataStore.data.map { preferences -> + preferences[PreferencesKey.DEVICE_TOKEN_KEY].orEmpty() + } + + override suspend fun setDeviceToken(deviceToken: String) { + dataStore.edit { preferences -> + preferences[PreferencesKey.DEVICE_TOKEN_KEY] = deviceToken + } + } +} \ No newline at end of file diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/DeviceTokenPreferencesDataSource.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/DeviceTokenPreferencesDataSource.kt new file mode 100644 index 00000000..0125b0ee --- /dev/null +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/datasource/DeviceTokenPreferencesDataSource.kt @@ -0,0 +1,8 @@ +package com.teamoffroad.core.common.data.datasource + +import kotlinx.coroutines.flow.Flow + +interface DeviceTokenPreferencesDataSource { + val deviceToken: Flow + suspend fun setDeviceToken(deviceToken: String) +} \ No newline at end of file diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/di/DataModule.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/di/DataModule.kt index 39cbde71..5d7f7b9f 100644 --- a/core/common/src/main/java/com/teamoffroad/core/common/data/di/DataModule.kt +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/di/DataModule.kt @@ -2,7 +2,9 @@ package com.teamoffroad.core.common.data.di import com.teamoffroad.core.common.data.datasource.AutoSignInPreferencesDataSource import com.teamoffroad.core.common.data.datasource.DefaultAutoSignInPreferencesDataSource +import com.teamoffroad.core.common.data.datasource.DefaultDeviceTokenPreferencesDataSource import com.teamoffroad.core.common.data.datasource.DefaultTokenPreferencesDataSource +import com.teamoffroad.core.common.data.datasource.DeviceTokenPreferencesDataSource import com.teamoffroad.core.common.data.datasource.TokenPreferencesDataSource import dagger.Binds import dagger.Module @@ -22,4 +24,9 @@ internal abstract class DataModule { abstract fun bindsAutoSignInLocalDataSource( dataSource: DefaultAutoSignInPreferencesDataSource, ): AutoSignInPreferencesDataSource + + @Binds + abstract fun bindsDeviceTokenLocalDataSource( + dataSource: DefaultDeviceTokenPreferencesDataSource, + ): DeviceTokenPreferencesDataSource } diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/di/DataStoreModule.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/di/DataStoreModule.kt index d66b748b..897c0e1d 100644 --- a/core/common/src/main/java/com/teamoffroad/core/common/data/di/DataStoreModule.kt +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/di/DataStoreModule.kt @@ -44,6 +44,13 @@ object DataStoreModule { return context.createDataStore(AUTH_PREFERENCES) } + @Provides + @Singleton + fun provideDeviceTokenDataStore(@ApplicationContext context: Context): DataStore { + return context.createDataStore(DEVICE_TOKEN_PREFERENCES) + } + private const val TOKEN_PREFERENCES = "com.teamoffroad.token_preferences" private const val AUTH_PREFERENCES = "com.teamoffroad.auth_preferences" + private const val DEVICE_TOKEN_PREFERENCES = "com.teamoffroad.device_token_preferences" } diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/di/RepositoryModule.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/di/RepositoryModule.kt index 92c49565..ba5a091c 100644 --- a/core/common/src/main/java/com/teamoffroad/core/common/data/di/RepositoryModule.kt +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/di/RepositoryModule.kt @@ -1,8 +1,10 @@ package com.teamoffroad.core.common.data.di import com.teamoffroad.core.common.data.repository.AutoSignInRepositoryImpl +import com.teamoffroad.core.common.data.repository.DeviceTokenRepositoryImpl import com.teamoffroad.core.common.data.repository.TokenRepositoryImpl import com.teamoffroad.core.common.domain.repository.AutoSignInRepository +import com.teamoffroad.core.common.domain.repository.DeviceTokenRepository import com.teamoffroad.core.common.domain.repository.TokenRepository import dagger.Binds import dagger.Module @@ -24,4 +26,10 @@ abstract class RepositoryModule { abstract fun bindAutoSignInRepository( authRepositoryImpl: AutoSignInRepositoryImpl, ): AutoSignInRepository + + @Binds + @Singleton + abstract fun bindDeviceTokenRepository( + deviceTokenRepositoryImpl: DeviceTokenRepositoryImpl + ): DeviceTokenRepository } diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/repository/AutoSignInRepositoryImpl.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/repository/AutoSignInRepositoryImpl.kt index 61f5fe7c..9232e87d 100644 --- a/core/common/src/main/java/com/teamoffroad/core/common/data/repository/AutoSignInRepositoryImpl.kt +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/repository/AutoSignInRepositoryImpl.kt @@ -14,6 +14,4 @@ class AutoSignInRepositoryImpl @Inject constructor( override suspend fun updateAutoSignInEnabled(enabled: Boolean) { autoSignInPreferencesDataSource.setAutoLogin(enabled) } - - } \ No newline at end of file diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/repository/DeviceTokenRepositoryImpl.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/repository/DeviceTokenRepositoryImpl.kt new file mode 100644 index 00000000..220dc3d7 --- /dev/null +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/repository/DeviceTokenRepositoryImpl.kt @@ -0,0 +1,17 @@ +package com.teamoffroad.core.common.data.repository + +import com.teamoffroad.core.common.data.datasource.DeviceTokenPreferencesDataSource +import com.teamoffroad.core.common.domain.repository.DeviceTokenRepository +import kotlinx.coroutines.flow.Flow +import javax.inject.Inject + +class DeviceTokenRepositoryImpl @Inject constructor( + private val deviceTokenPreferencesDataSource: DeviceTokenPreferencesDataSource, +) : DeviceTokenRepository { + + override val isDeviceTokenEnabled: Flow = deviceTokenPreferencesDataSource.deviceToken + + override suspend fun updateDeviceTokenEnabled(deviceToken: String) { + deviceTokenPreferencesDataSource.setDeviceToken(deviceToken) + } +} \ No newline at end of file diff --git a/core/common/src/main/java/com/teamoffroad/core/common/domain/repository/DeviceTokenRepository.kt b/core/common/src/main/java/com/teamoffroad/core/common/domain/repository/DeviceTokenRepository.kt new file mode 100644 index 00000000..56fa8ef4 --- /dev/null +++ b/core/common/src/main/java/com/teamoffroad/core/common/domain/repository/DeviceTokenRepository.kt @@ -0,0 +1,8 @@ +package com.teamoffroad.core.common.domain.repository + +import kotlinx.coroutines.flow.Flow + +interface DeviceTokenRepository { + val isDeviceTokenEnabled: Flow + suspend fun updateDeviceTokenEnabled(deviceToken: String) +} \ No newline at end of file From 80aabc115b3fff4d562f3b57cbcdd1382ef30ef0 Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:05:05 +0900 Subject: [PATCH 03/30] refactor: Edit emblem scroll --- .../AgreeTermsAndConditionsDialog.kt | 2 +- .../presentation/GainedEmblemsScreen.kt | 8 ++- .../presentation/GainedEmblemsViewModel.kt | 30 ++++++++++- .../component/GainedEmblemsItems.kt | 54 ++++++++++++++++++- 4 files changed, 90 insertions(+), 4 deletions(-) diff --git a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/component/AgreeTermsAndConditionsDialog.kt b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/component/AgreeTermsAndConditionsDialog.kt index a8c1c1fd..52e8a665 100644 --- a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/component/AgreeTermsAndConditionsDialog.kt +++ b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/component/AgreeTermsAndConditionsDialog.kt @@ -44,7 +44,7 @@ fun AgreeTermsAndConditionsDialog( ) { Dialog( onDismissRequest = { onClickCancel() }, - properties = DialogProperties(dismissOnClickOutside = true, dismissOnBackPress = true) + properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = true) ) { Box( modifier = modifier diff --git a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/GainedEmblemsScreen.kt b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/GainedEmblemsScreen.kt index b572729d..8c4e4844 100644 --- a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/GainedEmblemsScreen.kt +++ b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/GainedEmblemsScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.airbnb.lottie.compose.LottieAnimation import com.airbnb.lottie.compose.LottieCompositionSpec import com.airbnb.lottie.compose.rememberLottieComposition @@ -36,6 +37,7 @@ internal fun GainedEmblemsScreen( viewModel: GainedEmblemsViewModel = hiltViewModel(), ) { val isEmblemState by viewModel.emblemsUiState.collectAsState() + val isLoadMoreEmblemsUiState by viewModel.loadMoreEmblemsUiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.getEmblems() @@ -62,7 +64,11 @@ internal fun GainedEmblemsScreen( ) when (isEmblemState.gainedEmblemsValidateResult) { GainedEmblemsResult.Success -> { - GainedEmblemsItems(isEmblemState = isEmblemState) + GainedEmblemsItems( + isEmblemState = isEmblemState, + onLoadMore = { viewModel.loadMoreEmblems() }, + isLoading = isLoadMoreEmblemsUiState + ) } GainedEmblemsResult.Error -> { diff --git a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/GainedEmblemsViewModel.kt b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/GainedEmblemsViewModel.kt index 976e59bc..6a80d9c3 100644 --- a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/GainedEmblemsViewModel.kt +++ b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/GainedEmblemsViewModel.kt @@ -23,12 +23,19 @@ class GainedEmblemsViewModel @Inject constructor( MutableStateFlow(GainedEmblemsUiState()) val emblemsUiState: StateFlow = _emblemsUiState.asStateFlow() + private val _loadMoreEmblemsUiState: MutableStateFlow = + MutableStateFlow(false) + val loadMoreEmblemsUiState: StateFlow = _loadMoreEmblemsUiState.asStateFlow() + + private var currentCount = 18 + fun getEmblems() { viewModelScope.launch { runCatching { getUserEmblemListUseCase() }.onSuccess { result -> - val emblems = result.getOrNull()?.toImmutableList() ?: persistentListOf() + val emblems = + result.getOrNull()?.take(currentCount)?.toImmutableList() ?: persistentListOf() _emblemsUiState.value = _emblemsUiState.value.copy( emblemList = emblems, gainedEmblemsValidateResult = GainedEmblemsResult.Success, @@ -39,4 +46,25 @@ class GainedEmblemsViewModel @Inject constructor( } } } + + fun loadMoreEmblems() { + viewModelScope.launch { + if (_loadMoreEmblemsUiState.value) return@launch + _loadMoreEmblemsUiState.value = true + runCatching { + getUserEmblemListUseCase() + }.onSuccess { result -> + val allEmblems = result.getOrNull() ?: persistentListOf() + val newItems = allEmblems.drop(currentCount).take(10).toImmutableList() + + _emblemsUiState.value = _emblemsUiState.value.copy( + emblemList = (_emblemsUiState.value.emblemList + newItems).toImmutableList() + ) + currentCount += 10 + }.onFailure { + }.also { + _loadMoreEmblemsUiState.value = false + } + } + } } diff --git a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/component/GainedEmblemsItems.kt b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/component/GainedEmblemsItems.kt index 034bbcc4..77d586d1 100644 --- a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/component/GainedEmblemsItems.kt +++ b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/component/GainedEmblemsItems.kt @@ -2,26 +2,55 @@ package com.teamoffroad.feature.mypage.presentation.component import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.rememberLottieComposition import com.teamoffroad.core.designsystem.theme.ListBg import com.teamoffroad.feature.mypage.presentation.model.GainedEmblemsUiState +import kotlinx.coroutines.flow.collectLatest @Composable fun GainedEmblemsItems( modifier: Modifier = Modifier, isEmblemState: GainedEmblemsUiState, + onLoadMore: () -> Unit, + isLoading: Boolean, ) { + val listState = rememberLazyListState() + + LaunchedEffect(listState) { + snapshotFlow { + val isAtEnd = + listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1 + isAtEnd + }.collectLatest { isAtEnd -> + if (isAtEnd) { + onLoadMore() + } + } + } + LazyColumn( modifier = modifier .fillMaxSize() .background(ListBg), + state = listState, verticalArrangement = Arrangement.spacedBy(16.dp) ) { item { @@ -35,8 +64,31 @@ fun GainedEmblemsItems( isLock = it.isLock ) } + if (isLoading) { + item { + OnMoreEmblemsLoading() + } + } item { - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(100.dp)) } } +} + +@Composable +fun OnMoreEmblemsLoading() { + Column( + modifier = Modifier + .background(ListBg) + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(com.teamoffroad.offroad.core.designsystem.R.raw.loading_circle)) + LottieAnimation( + modifier = Modifier + .size(50.dp), + composition = composition, + ) + } } \ No newline at end of file From 47842530436d9f02804e10b8fa5f97f5ed82dfe8 Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Thu, 14 Nov 2024 01:06:09 +0900 Subject: [PATCH 04/30] feature: Add fcm --- .../common/data/local/AuthAuthenticator.kt | 40 ++++++++----------- .../repository/DeviceTokenRepositoryImpl.kt | 2 +- .../repository/DeviceTokenRepository.kt | 2 +- .../feature/home/data/di/NetworkModule.kt | 7 ++++ .../feature/home/data/di/RepositoryModule.kt | 8 ++++ .../feature/home/data/di/UseCaseModule.kt | 10 +++++ .../data/remote/request/FcmTokenRequestDto.kt | 10 +++++ .../feature/home/data/remote/request/gitkeep | 0 .../data/remote/service/FcmTokenService.kt | 14 +++++++ .../data/repository/FcmTokenRepositoryImpl.kt | 14 +++++++ .../domain/repository/FcmTokenRepository.kt | 5 +++ .../domain/usecase/PostFcmTokenUseCase.kt | 11 +++++ .../feature/home/presentation/HomeScreen.kt | 1 + .../home/presentation/HomeViewModel.kt | 17 +++++++- 14 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/request/FcmTokenRequestDto.kt delete mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/request/gitkeep create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/service/FcmTokenService.kt create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/data/repository/FcmTokenRepositoryImpl.kt create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/domain/repository/FcmTokenRepository.kt create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/domain/usecase/PostFcmTokenUseCase.kt diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/local/AuthAuthenticator.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/local/AuthAuthenticator.kt index 9042c196..ec9cfbf5 100644 --- a/core/common/src/main/java/com/teamoffroad/core/common/data/local/AuthAuthenticator.kt +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/local/AuthAuthenticator.kt @@ -1,7 +1,6 @@ package com.teamoffroad.core.common.data.local import android.content.Context -import android.util.Log import com.jakewharton.processphoenix.ProcessPhoenix import com.teamoffroad.core.common.data.datasource.TokenPreferencesDataSource import com.teamoffroad.core.common.data.remote.service.TokenService @@ -26,33 +25,26 @@ class AuthAuthenticator @Inject constructor( override fun authenticate(route: Route?, response: Response): Request? { val tokenResponse = runCatching { - runBlocking { - refreshTokenUseCase.refreshAccessToken("Bearer ${tokenPreferencesDataSource.refreshToken.first()}") + runBlocking { + refreshTokenUseCase.refreshAccessToken("Bearer ${tokenPreferencesDataSource.refreshToken.first()}") + } + }.onSuccess { + runBlocking { + tokenPreferencesDataSource.apply { + setAccessToken(it.data?.accessToken ?: return@runBlocking) + setRefreshToken(it.data.refreshToken ?: return@runBlocking) } - }.onSuccess { - - Log.d("asdasd", "재발급성공") - runBlocking { - tokenPreferencesDataSource.apply { - if (it != null) { - setAccessToken(it.data?.accessToken ?: return@runBlocking) - setRefreshToken(it.data?.refreshToken ?: return@runBlocking) - } - } - } - }.onFailure { - - Log.d("asdasd", "재발급실패") - runBlocking { - setAutoSignInUseCase.invoke(false) - } - ProcessPhoenix.triggerRebirth(context, intentProvider.getIntent()) - }.getOrThrow() + } + }.onFailure { + runBlocking { + setAutoSignInUseCase.invoke(false) + } + ProcessPhoenix.triggerRebirth(context, intentProvider.getIntent()) + }.getOrThrow() return response.request.newBuilder() - .header(AUTHORIZATION, "Bearer ${tokenResponse?.data?.accessToken}") + .header(AUTHORIZATION, "Bearer ${tokenResponse.data?.accessToken}") .build() - Log.d("asdasd", response.message) } companion object { diff --git a/core/common/src/main/java/com/teamoffroad/core/common/data/repository/DeviceTokenRepositoryImpl.kt b/core/common/src/main/java/com/teamoffroad/core/common/data/repository/DeviceTokenRepositoryImpl.kt index 220dc3d7..61b6c7f4 100644 --- a/core/common/src/main/java/com/teamoffroad/core/common/data/repository/DeviceTokenRepositoryImpl.kt +++ b/core/common/src/main/java/com/teamoffroad/core/common/data/repository/DeviceTokenRepositoryImpl.kt @@ -9,7 +9,7 @@ class DeviceTokenRepositoryImpl @Inject constructor( private val deviceTokenPreferencesDataSource: DeviceTokenPreferencesDataSource, ) : DeviceTokenRepository { - override val isDeviceTokenEnabled: Flow = deviceTokenPreferencesDataSource.deviceToken + override val deviceToken: Flow = deviceTokenPreferencesDataSource.deviceToken override suspend fun updateDeviceTokenEnabled(deviceToken: String) { deviceTokenPreferencesDataSource.setDeviceToken(deviceToken) diff --git a/core/common/src/main/java/com/teamoffroad/core/common/domain/repository/DeviceTokenRepository.kt b/core/common/src/main/java/com/teamoffroad/core/common/domain/repository/DeviceTokenRepository.kt index 56fa8ef4..f52e8959 100644 --- a/core/common/src/main/java/com/teamoffroad/core/common/domain/repository/DeviceTokenRepository.kt +++ b/core/common/src/main/java/com/teamoffroad/core/common/domain/repository/DeviceTokenRepository.kt @@ -3,6 +3,6 @@ package com.teamoffroad.core.common.domain.repository import kotlinx.coroutines.flow.Flow interface DeviceTokenRepository { - val isDeviceTokenEnabled: Flow + val deviceToken: Flow suspend fun updateDeviceTokenEnabled(deviceToken: String) } \ No newline at end of file diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/NetworkModule.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/NetworkModule.kt index 39ca121e..3b7ddd77 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/NetworkModule.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/NetworkModule.kt @@ -2,6 +2,7 @@ package com.teamoffroad.feature.home.data.di import com.teamoffroad.core.common.data.di.qualifier.Auth import com.teamoffroad.feature.home.data.remote.service.DummyUserService +import com.teamoffroad.feature.home.data.remote.service.FcmTokenService import com.teamoffroad.feature.home.data.remote.service.UserService import dagger.Module import dagger.Provides @@ -25,4 +26,10 @@ object NetworkModule { fun provideEmblemService(@Auth retrofit: Retrofit): UserService { return retrofit.create(UserService::class.java) } + + @Provides + @Singleton + fun provideFcmTokenService(@Auth retrofit: Retrofit): FcmTokenService { + return retrofit.create(FcmTokenService::class.java) + } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/RepositoryModule.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/RepositoryModule.kt index 745fe7c8..fc8b1121 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/RepositoryModule.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/RepositoryModule.kt @@ -1,8 +1,10 @@ package com.teamoffroad.feature.home.data.di import com.teamoffroad.feature.home.data.repository.DummyDummyUserRepositoryImpl +import com.teamoffroad.feature.home.data.repository.FcmTokenRepositoryImpl import com.teamoffroad.feature.home.data.repository.UserRepositoryImpl import com.teamoffroad.feature.home.domain.repository.DummyUserRepository +import com.teamoffroad.feature.home.domain.repository.FcmTokenRepository import com.teamoffroad.feature.home.domain.repository.UserRepository import dagger.Binds import dagger.Module @@ -25,4 +27,10 @@ abstract class RepositoryModule { abstract fun bindUserRepository( userRepositoryImpl: UserRepositoryImpl ): UserRepository + + @Binds + @Singleton + abstract fun bindFcmRepository( + fcmTokenRepositoryImpl: FcmTokenRepositoryImpl + ): FcmTokenRepository } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/UseCaseModule.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/UseCaseModule.kt index 23264d6e..2b410087 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/UseCaseModule.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/data/di/UseCaseModule.kt @@ -1,8 +1,10 @@ package com.teamoffroad.feature.home.data.di import com.teamoffroad.feature.home.domain.repository.DummyUserRepository +import com.teamoffroad.feature.home.domain.repository.FcmTokenRepository import com.teamoffroad.feature.home.domain.repository.UserRepository import com.teamoffroad.feature.home.domain.usecase.GetDummyUserListUseCase +import com.teamoffroad.feature.home.domain.usecase.PostFcmTokenUseCase import com.teamoffroad.feature.home.domain.usecase.UserUseCase import dagger.Module import dagger.Provides @@ -29,4 +31,12 @@ class UseCaseModule { ): UserUseCase { return UserUseCase(userRepository) } + + @Provides + @Singleton + fun providePostFcmTokenUseCase( + fcmTokenRepository: FcmTokenRepository, + ): PostFcmTokenUseCase { + return PostFcmTokenUseCase(fcmTokenRepository) + } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/request/FcmTokenRequestDto.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/request/FcmTokenRequestDto.kt new file mode 100644 index 00000000..7c7bfbcb --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/request/FcmTokenRequestDto.kt @@ -0,0 +1,10 @@ +package com.teamoffroad.feature.home.data.remote.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class FcmTokenRequestDto( + @SerialName("token") + val token: String, +) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/request/gitkeep b/feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/request/gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/service/FcmTokenService.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/service/FcmTokenService.kt new file mode 100644 index 00000000..82159d8a --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/data/remote/service/FcmTokenService.kt @@ -0,0 +1,14 @@ +package com.teamoffroad.feature.home.data.remote.service + +import com.teamoffroad.core.common.data.remote.response.BaseResponse +import com.teamoffroad.feature.home.data.remote.request.FcmTokenRequestDto +import retrofit2.http.Body +import retrofit2.http.POST + +interface FcmTokenService { + + @POST("fcm/token") + suspend fun postFcmToken( + @Body request: FcmTokenRequestDto, + ): BaseResponse +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/data/repository/FcmTokenRepositoryImpl.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/data/repository/FcmTokenRepositoryImpl.kt new file mode 100644 index 00000000..b4efc98d --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/data/repository/FcmTokenRepositoryImpl.kt @@ -0,0 +1,14 @@ +package com.teamoffroad.feature.home.data.repository + +import com.teamoffroad.feature.home.data.remote.request.FcmTokenRequestDto +import com.teamoffroad.feature.home.data.remote.service.FcmTokenService +import com.teamoffroad.feature.home.domain.repository.FcmTokenRepository +import javax.inject.Inject + +class FcmTokenRepositoryImpl @Inject constructor( + private val fcmTokenService: FcmTokenService +) : FcmTokenRepository { + override suspend fun postFcmToken(fcmToken: String) { + fcmTokenService.postFcmToken(FcmTokenRequestDto(fcmToken)) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/domain/repository/FcmTokenRepository.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/domain/repository/FcmTokenRepository.kt new file mode 100644 index 00000000..7298ea70 --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/domain/repository/FcmTokenRepository.kt @@ -0,0 +1,5 @@ +package com.teamoffroad.feature.home.domain.repository + +interface FcmTokenRepository { + suspend fun postFcmToken(fcmToken: String) +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/domain/usecase/PostFcmTokenUseCase.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/domain/usecase/PostFcmTokenUseCase.kt new file mode 100644 index 00000000..661c8378 --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/domain/usecase/PostFcmTokenUseCase.kt @@ -0,0 +1,11 @@ +package com.teamoffroad.feature.home.domain.usecase + +import com.teamoffroad.feature.home.domain.repository.FcmTokenRepository + +class PostFcmTokenUseCase( + private val fcmTokenRepository: FcmTokenRepository +) { + suspend operator fun invoke(fcmToken: String) { + return fcmTokenRepository.postFcmToken(fcmToken) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index b22cda22..41357691 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -67,6 +67,7 @@ fun HomeScreen( LaunchedEffect(Unit) { viewModel.updateAutoSignIn() + viewModel.updateFcmToken() viewModel.updateCategory(if (category.isNullOrEmpty()) "NONE" else category) viewModel.getUsersAdventuresInformation(viewModel.category.value) viewModel.getUserQuests() diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index e38fb4c8..38fd6f99 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -2,16 +2,19 @@ package com.teamoffroad.feature.home.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.teamoffroad.core.common.domain.repository.DeviceTokenRepository import com.teamoffroad.core.common.domain.usecase.SetAutoSignInUseCase import com.teamoffroad.feature.home.domain.model.Emblem import com.teamoffroad.feature.home.domain.model.UserQuests import com.teamoffroad.feature.home.domain.model.UsersAdventuresInformation import com.teamoffroad.feature.home.domain.repository.UserRepository +import com.teamoffroad.feature.home.domain.usecase.PostFcmTokenUseCase import com.teamoffroad.feature.home.presentation.component.UiState import com.teamoffroad.feature.home.presentation.component.getErrorMessage import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import javax.inject.Inject @@ -19,6 +22,8 @@ import javax.inject.Inject class HomeViewModel @Inject constructor( private val userRepository: UserRepository, private val setAutoSignInUseCase: SetAutoSignInUseCase, + private val deviceTokenRepository: DeviceTokenRepository, + private val fcmTokenUseCase: PostFcmTokenUseCase, ) : ViewModel() { private val _getUsersAdventuresInformationState = @@ -134,10 +139,20 @@ class HomeViewModel @Inject constructor( } } - fun updateAutoSignIn(){ + fun updateAutoSignIn() { viewModelScope.launch { setAutoSignInUseCase.invoke(true) } } + fun updateFcmToken() { + viewModelScope.launch { + val deviceToken = deviceTokenRepository.deviceToken.first() + if (deviceToken.isBlank()) return@launch + runCatching { + fcmTokenUseCase.invoke(deviceToken) + }.onSuccess { } + .onFailure {} + } + } } \ No newline at end of file From 0278b98f1bd9e17f7abb9841d5b2b570ce0109e1 Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Fri, 15 Nov 2024 22:29:49 +0900 Subject: [PATCH 05/30] feature: Add fcm to main --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 1 + .../offroad.app/OffRoadMessagingService.kt | 144 ++++++++++++++++++ .../offroad.app/OffroadApplication.kt | 2 + .../common/util/ActivityLifecycleHandler.kt | 34 +++++ .../teamoffroad/feature/main/MainActivity.kt | 8 + 6 files changed, 190 insertions(+) create mode 100644 core/common/src/main/java/com/teamoffroad/core/common/util/ActivityLifecycleHandler.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9ad1db64..f3c1785f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { implementation(project(":feature:mypage")) implementation(project(":feature:characterchat")) implementation(libs.kakao.user) + implementation(libs.coil) implementation(libs.bundles.firebase) implementation(platform(libs.firebase.bom)) } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7b2578f2..06b5af1e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ + Unit + ): NotificationCompat.Builder { + + val notificationBuilder = NotificationCompat.Builder(this, "channelId") + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(remoteMessage.data[KEY_TITLE]) + .setContentText(remoteMessage.data[KEY_BODY]) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .setPriority(NotificationCompat.PRIORITY_MAX) + + val imageUrl = remoteMessage.data[KEY_IMAGE] + imageUrl?.let { + val request = ImageRequest.Builder(this) + .data(it) + .target { drawable -> + val bitmap = (drawable as BitmapDrawable).bitmap + notificationBuilder.setLargeIcon(bitmap) + onLargeIconReady(notificationBuilder) + } + .build() + + Coil.imageLoader(this).enqueue(request) + } ?: run { + onLargeIconReady(notificationBuilder) + } + return notificationBuilder + } + + private fun sendNotification(remoteMessage: RemoteMessage, isForeGround: Boolean) { + val uniqueIdentifier = generateUniqueIdentifier() + val intent = createNotificationIntent(remoteMessage) + val pendingIntent = createPendingIntent(intent, uniqueIdentifier) + createNotificationBuilder(remoteMessage, pendingIntent) { notificationBuilder -> + showNotification(notificationBuilder, uniqueIdentifier) + } + } + + private fun showNotification( + notificationBuilder: NotificationCompat.Builder, + uniqueIdentifier: Int + ) { + val notificationManager = + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel("channelId", "Notice", NotificationManager.IMPORTANCE_HIGH) + notificationManager.createNotificationChannel(channel) + } + + notificationManager.notify(uniqueIdentifier, notificationBuilder.build()) + } + + companion object { + //TODO. 나중에 정리 해야됨 + private const val KEY_TITLE = "title" + private const val KEY_BODY = "body" + private const val KEY_TYPE = "type" + private const val KEY_IMAGE = "image" + private const val KEY_ID = "additionalProp1" + private const val TYPE_CHARACTER_CHAT = "CHARACTER_CHAT" + } } \ No newline at end of file diff --git a/app/src/main/java/com/teamoffroad/offroad.app/OffroadApplication.kt b/app/src/main/java/com/teamoffroad/offroad.app/OffroadApplication.kt index 2f1cea24..913debef 100644 --- a/app/src/main/java/com/teamoffroad/offroad.app/OffroadApplication.kt +++ b/app/src/main/java/com/teamoffroad/offroad.app/OffroadApplication.kt @@ -2,6 +2,7 @@ package com.teamoffroad.offroad.app import android.app.Application import com.kakao.sdk.common.KakaoSdk +import com.teamoffroad.core.common.util.ActivityLifecycleHandler import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp @@ -9,6 +10,7 @@ class OffroadApplication : Application(){ override fun onCreate() { super.onCreate() setKakaoSdk() + registerActivityLifecycleCallbacks(ActivityLifecycleHandler()) } private fun setKakaoSdk() { diff --git a/core/common/src/main/java/com/teamoffroad/core/common/util/ActivityLifecycleHandler.kt b/core/common/src/main/java/com/teamoffroad/core/common/util/ActivityLifecycleHandler.kt new file mode 100644 index 00000000..69d07070 --- /dev/null +++ b/core/common/src/main/java/com/teamoffroad/core/common/util/ActivityLifecycleHandler.kt @@ -0,0 +1,34 @@ +package com.teamoffroad.core.common.util + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +class ActivityLifecycleHandler : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(p0: Activity, p1: Bundle?) { + } + + override fun onActivityStarted(p0: Activity) { + } + + override fun onActivityResumed(p0: Activity) { + isAppInForeground = true + } + + override fun onActivityPaused(p0: Activity) { + isAppInForeground = false + } + + override fun onActivityStopped(p0: Activity) { + } + + override fun onActivitySaveInstanceState(p0: Activity, p1: Bundle) { + } + + override fun onActivityDestroyed(p0: Activity) { + } + + companion object { + var isAppInForeground = false + } +} \ No newline at end of file diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt index d0e92c88..34965a8e 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.RequiresApi @@ -20,6 +21,13 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + val a = intent.getStringExtra("type") + if (a != "CHARACTER_CHAT") { + val b = intent.getStringExtra("additionalProp1") + Log.d("fcmCHARACTER_CHAT", a.toString()) + Log.d("fcmadditionalProp1", b.toString()) + } + setContent { val navigator: MainNavigator = rememberMainNavigator() MainTransparentActionBar(window) From 02a385d88b9910e116dfd20cfbef4e4e89ca59c5 Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Sat, 16 Nov 2024 14:53:37 +0900 Subject: [PATCH 06/30] feature: Add fcm background click --- .../offroad.app/OffRoadMessagingService.kt | 57 ++++++++++++++++--- .../teamoffroad/core/navigation/RouteModel.kt | 7 +-- .../feature/auth/presentation/AuthScreen.kt | 4 ++ .../auth/presentation/AuthViewModel.kt | 12 ++++ .../auth/presentation/SetGenderScreen.kt | 18 +++++- .../auth/presentation/model/AuthUiState.kt | 1 + .../teamoffroad/feature/main/MainActivity.kt | 36 +++++++----- .../teamoffroad/feature/main/MainNavigator.kt | 8 +-- .../teamoffroad/feature/main/MainScreen.kt | 12 ++++ .../feature/main/{splash => }/SplashScreen.kt | 23 +------- .../feature/main/component/MainNavHost.kt | 9 +-- .../feature/main/splash/SplashUiState.kt | 6 -- .../feature/main/splash/SplashViewModel.kt | 39 ------------- .../splash/navigation/SplashNavigation.kt | 23 -------- .../mypage/navigation/MyPageNavigation.kt | 16 ++++-- .../mypage/presentation/AnnouncementScreen.kt | 1 + 16 files changed, 138 insertions(+), 134 deletions(-) rename feature/main/src/main/java/com/teamoffroad/feature/main/{splash => }/SplashScreen.kt (79%) delete mode 100644 feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashUiState.kt delete mode 100644 feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashViewModel.kt delete mode 100644 feature/main/src/main/java/com/teamoffroad/feature/main/splash/navigation/SplashNavigation.kt diff --git a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt index 119c136a..51e8c4d7 100644 --- a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt +++ b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt @@ -7,6 +7,7 @@ import android.content.Context import android.content.Intent import android.graphics.drawable.BitmapDrawable import android.os.Build +import android.util.Log import androidx.core.app.NotificationCompat import coil.Coil import coil.request.ImageRequest @@ -39,9 +40,9 @@ class OffRoadMessagingService : FirebaseMessagingService() { if (ActivityLifecycleHandler.isAppInForeground) { //TODO. 키 밸류값 나오면 바꾸기 if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) - sendNotification(remoteMessage, true) + sendForeGroundNotification(remoteMessage, true) } else { - sendNotification(remoteMessage, false) + sendBackGroundNotification(remoteMessage, false) } } } @@ -84,8 +85,7 @@ class OffRoadMessagingService : FirebaseMessagingService() { private fun createNotificationIntent(remoteMessage: RemoteMessage): Intent { return Intent(this, MainActivity::class.java).apply { if (ActivityLifecycleHandler.isAppInForeground) { - action = Intent.ACTION_MAIN - addCategory(Intent.CATEGORY_LAUNCHER) + Log.d("asdasd", "forground") } else { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK } @@ -96,7 +96,37 @@ class OffRoadMessagingService : FirebaseMessagingService() { } } - private fun createNotificationBuilder( + private fun createForeGroundNotificationBuilder( + remoteMessage: RemoteMessage, + onLargeIconReady: (NotificationCompat.Builder) -> Unit + ): NotificationCompat.Builder { + + val notificationBuilder = NotificationCompat.Builder(this, "channelId") + .setSmallIcon(R.mipmap.ic_launcher) + .setContentTitle(remoteMessage.data[KEY_TITLE]) + .setContentText(remoteMessage.data[KEY_BODY]) + .setAutoCancel(true) + .setPriority(NotificationCompat.PRIORITY_MAX) + + val imageUrl = remoteMessage.data[KEY_IMAGE] + imageUrl?.let { + val request = ImageRequest.Builder(this) + .data(it) + .target { drawable -> + val bitmap = (drawable as BitmapDrawable).bitmap + notificationBuilder.setLargeIcon(bitmap) + onLargeIconReady(notificationBuilder) + } + .build() + + Coil.imageLoader(this).enqueue(request) + } ?: run { + onLargeIconReady(notificationBuilder) + } + return notificationBuilder + } + + private fun createBackGroundNotificationBuilder( remoteMessage: RemoteMessage, pendingIntent: PendingIntent, onLargeIconReady: (NotificationCompat.Builder) -> Unit @@ -128,11 +158,22 @@ class OffRoadMessagingService : FirebaseMessagingService() { return notificationBuilder } - private fun sendNotification(remoteMessage: RemoteMessage, isForeGround: Boolean) { + private fun sendForeGroundNotification(remoteMessage: RemoteMessage, isForeGround: Boolean) { + val uniqueIdentifier = generateUniqueIdentifier() + createForeGroundNotificationBuilder(remoteMessage) { notificationBuilder -> + showNotification(notificationBuilder, uniqueIdentifier) + + if(isForeGround){ + + } + } + } + + private fun sendBackGroundNotification(remoteMessage: RemoteMessage, isForeGround: Boolean) { val uniqueIdentifier = generateUniqueIdentifier() val intent = createNotificationIntent(remoteMessage) val pendingIntent = createPendingIntent(intent, uniqueIdentifier) - createNotificationBuilder(remoteMessage, pendingIntent) { notificationBuilder -> + createBackGroundNotificationBuilder(remoteMessage, pendingIntent) { notificationBuilder -> showNotification(notificationBuilder, uniqueIdentifier) } } @@ -161,5 +202,7 @@ class OffRoadMessagingService : FirebaseMessagingService() { private const val KEY_IMAGE = "image" private const val KEY_ID = "additionalProp1" private const val TYPE_CHARACTER_CHAT = "CHARACTER_CHAT" + private const val TYPE_ANNOUNCEMENT = "ANNOUNCEMENT_REDIRECT" + } } \ No newline at end of file diff --git a/core/navigation/src/main/java/com/teamoffroad/core/navigation/RouteModel.kt b/core/navigation/src/main/java/com/teamoffroad/core/navigation/RouteModel.kt index 182aa803..82ad7132 100644 --- a/core/navigation/src/main/java/com/teamoffroad/core/navigation/RouteModel.kt +++ b/core/navigation/src/main/java/com/teamoffroad/core/navigation/RouteModel.kt @@ -3,9 +3,6 @@ package com.teamoffroad.core.navigation import kotlinx.serialization.Serializable sealed interface Route { - @Serializable - data object Splash : Route - @Serializable data object Auth : Route } @@ -76,7 +73,9 @@ sealed interface MyPageRoute : Route { data object Setting : MyPageRoute @Serializable - data object Announcement : MyPageRoute + data class Announcement( + val announcementId: String?, + ) : MyPageRoute @Serializable data class AnnouncementDetail( diff --git a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/AuthScreen.kt b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/AuthScreen.kt index 83eec220..7ff6c750 100644 --- a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/AuthScreen.kt +++ b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/AuthScreen.kt @@ -63,8 +63,12 @@ internal fun AuthScreen( EntryPointAccessors.fromActivity(context) val oAuthInteractor = entryPoint.getOAuthInteractor() + LaunchedEffect(Unit) { + viewModel.checkAutoSignIn() + } LaunchedEffect(isAuthUiState) { when { + isAuthUiState.isAutoSignIn -> navigateToHome() isAuthUiState.signInSuccess && !isAuthUiState.alreadyExist -> navigateToAgreeTermsAndConditions() isAuthUiState.signInSuccess && isAuthUiState.alreadyExist -> navigateToHome() isAuthUiState.kakaoSignIn -> { diff --git a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/AuthViewModel.kt index 13f63d50..15c413a1 100644 --- a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/AuthViewModel.kt @@ -6,6 +6,7 @@ import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient import com.google.android.gms.common.api.ApiException import com.google.android.gms.tasks.Task +import com.teamoffroad.core.common.domain.usecase.GetAutoSignInUseCase import com.teamoffroad.core.common.domain.usecase.SaveAccessTokenUseCase import com.teamoffroad.core.common.domain.usecase.SaveRefreshTokenUseCase import com.teamoffroad.feature.auth.domain.model.SocialSignInPlatform @@ -24,6 +25,7 @@ class AuthViewModel @Inject constructor( private val authUseCase: AuthUseCase, private val saveAccessTokenUseCase: SaveAccessTokenUseCase, private val saveRefreshTokenUseCase: SaveRefreshTokenUseCase, + private val getAutoSignInUseCase: GetAutoSignInUseCase, ) : ViewModel() { private val _authUiState: MutableStateFlow = MutableStateFlow(AuthUiState(empty = true)) @@ -77,4 +79,14 @@ class AuthViewModel @Inject constructor( } } } + + fun checkAutoSignIn() { + viewModelScope.launch { + getAutoSignInUseCase().collect { isAutoSignIn -> + _authUiState.value = _authUiState.value.copy( + isAutoSignIn = isAutoSignIn + ) + } + } + } } diff --git a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/SetGenderScreen.kt b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/SetGenderScreen.kt index 2ea82851..58fc341a 100644 --- a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/SetGenderScreen.kt +++ b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/SetGenderScreen.kt @@ -141,20 +141,32 @@ fun SetGenderButton( GenderHintButton( modifier = Modifier .padding(bottom = 12.dp) - .clickableWithoutRipple(interactionSource = interactionSource) { viewModel.updateCheckedGender("MALE") }, + .clickableWithoutRipple(interactionSource = interactionSource) { + viewModel.updateCheckedGender( + "MALE" + ) + }, value = stringResource(R.string.auth_set_gender_male), isActive = male ) GenderHintButton( modifier = Modifier .padding(bottom = 12.dp) - .clickableWithoutRipple(interactionSource =interactionSource) { viewModel.updateCheckedGender("FEMALE") }, + .clickableWithoutRipple(interactionSource = interactionSource) { + viewModel.updateCheckedGender( + "FEMALE" + ) + }, value = stringResource(R.string.auth_set_gender_female), isActive = female ) GenderHintButton( modifier = Modifier - .clickableWithoutRipple(interactionSource =interactionSource) { viewModel.updateCheckedGender("OTHER") }, + .clickableWithoutRipple(interactionSource = interactionSource) { + viewModel.updateCheckedGender( + "OTHER" + ) + }, value = stringResource(R.string.auth_set_gender_other), isActive = other ) diff --git a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/model/AuthUiState.kt b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/model/AuthUiState.kt index 73af2c56..2fc198f9 100644 --- a/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/model/AuthUiState.kt +++ b/feature/auth/src/main/java/com/teamoffroad/feature/auth/presentation/model/AuthUiState.kt @@ -6,5 +6,6 @@ data class AuthUiState( val signInSuccess: Boolean = false, val alreadyExist: Boolean = false, val kakaoSignIn: Boolean = false, + val isAutoSignIn: Boolean = false, ) diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt index 34965a8e..f1f395a7 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt @@ -4,38 +4,46 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.teamoffroad.core.designsystem.theme.OffroadTheme import com.teamoffroad.feature.main.component.MainTransparentActionBar import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.delay @AndroidEntryPoint @RequiresApi(Build.VERSION_CODES.TIRAMISU) class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - val a = intent.getStringExtra("type") - if (a != "CHARACTER_CHAT") { - val b = intent.getStringExtra("additionalProp1") - Log.d("fcmCHARACTER_CHAT", a.toString()) - Log.d("fcmadditionalProp1", b.toString()) - } - + val notificationType = intent.getStringExtra("type") + val notificationId = intent.getStringExtra("additionalProp1") setContent { val navigator: MainNavigator = rememberMainNavigator() + val showSplash = remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + delay(1550) + showSplash.value = false + } MainTransparentActionBar(window) OffroadTheme { - MainScreen( - navigator = navigator, - modifier = Modifier - ) + when (showSplash.value) { + true -> SplashScreen() + false -> MainScreen( + navigator = navigator, + modifier = Modifier, + notificationType = if (!notificationType.isNullOrBlank()) notificationType else null, + notificationId = if (!notificationId.isNullOrBlank()) notificationId else null, + ) + } } } } @@ -53,7 +61,7 @@ class MainActivity : ComponentActivity() { @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun GreetingPreview() { OffroadTheme { - MainScreen() + MainScreen(notificationType = "", notificationId = "") } } diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainNavigator.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainNavigator.kt index b1b3a732..219f9e22 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainNavigator.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainNavigator.kt @@ -21,9 +21,9 @@ import com.teamoffroad.feature.explore.navigation.navigateToExplore import com.teamoffroad.feature.explore.navigation.navigateToPlace import com.teamoffroad.feature.explore.navigation.navigateToQuest import com.teamoffroad.feature.home.navigation.navigateToHome -import com.teamoffroad.feature.main.splash.navigation.navigateToAuth import com.teamoffroad.feature.mypage.navigation.navigateToAnnouncement import com.teamoffroad.feature.mypage.navigation.navigateToAnnouncementDetail +import com.teamoffroad.feature.mypage.navigation.navigateToAuth import com.teamoffroad.feature.mypage.navigation.navigateToAvailableCouponDetail import com.teamoffroad.feature.mypage.navigation.navigateToCharacterDetail import com.teamoffroad.feature.mypage.navigation.navigateToGainedCharacter @@ -39,7 +39,7 @@ internal class MainNavigator( @Composable get() = navController .currentBackStackEntryAsState().value?.destination - val startDestination = Route.Splash + val startDestination = Route.Auth val currentTab: MainNavTab? @Composable get() = MainNavTab.find { tab -> @@ -169,8 +169,8 @@ internal class MainNavigator( navController.navigateToSetting() } - fun navigateToAnnouncement() { - navController.navigateToAnnouncement() + fun navigateToAnnouncement(announcementId: String?) { + navController.navigateToAnnouncement(announcementId) } fun navigateToAnnouncementDetail( diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt index 81bf51f9..9be38aaf 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt @@ -5,6 +5,7 @@ import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import com.teamoffroad.core.common.util.OnBackButtonListener import com.teamoffroad.core.designsystem.component.navigationPadding @@ -17,7 +18,18 @@ import kotlinx.collections.immutable.toPersistentList internal fun MainScreen( modifier: Modifier = Modifier, navigator: MainNavigator = rememberMainNavigator(), + notificationType: String?, + notificationId: String?, ) { + LaunchedEffect(Unit) { + if (!notificationType.isNullOrBlank()) { + if (notificationType == "ANNOUNCEMENT_REDIRECT") + notificationId?.let { navigator.navigateToAnnouncement(it) } + else if (notificationType == "CHARACTER_CHAT") { + navigator.navigateToHome() + } + } + } MainScreenContent( navigator = navigator, modifier = modifier, diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashScreen.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/SplashScreen.kt similarity index 79% rename from feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashScreen.kt rename to feature/main/src/main/java/com/teamoffroad/feature/main/SplashScreen.kt index 961ae735..9c1d344b 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashScreen.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/SplashScreen.kt @@ -1,4 +1,4 @@ -package com.teamoffroad.feature.main.splash +package com.teamoffroad.feature.main import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition @@ -23,9 +23,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.res.painterResource -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.LocalLifecycleOwner -import androidx.lifecycle.flowWithLifecycle import com.teamoffroad.core.designsystem.component.ChangeBottomBarColor import com.teamoffroad.core.designsystem.theme.Main2 import kotlinx.coroutines.delay @@ -33,25 +30,7 @@ import kotlinx.coroutines.launch @Composable fun SplashScreen( - navigateToAuth: () -> Unit, - navigateToHome: () -> Unit, - viewModel: SplashViewModel = hiltViewModel(), ) { - val lifecycleOwner = LocalLifecycleOwner.current - - LaunchedEffect(Unit) { - viewModel.showSplash() - } - LaunchedEffect(viewModel.splashUiState, lifecycleOwner) { - viewModel.splashUiState.flowWithLifecycle(lifecycle = lifecycleOwner.lifecycle) - .collect { splashUiState -> - when (splashUiState) { - is SplashUiState.NavigateHome -> navigateToHome() - is SplashUiState.NavigateLogin -> navigateToAuth() - } - } - } - ChangeBottomBarColor(Main2) var backgroundVisibility by remember { mutableStateOf(true) } val scale = remember { Animatable(1f) } diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/component/MainNavHost.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/component/MainNavHost.kt index a1454bf8..7935d496 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/component/MainNavHost.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/component/MainNavHost.kt @@ -16,7 +16,6 @@ import com.teamoffroad.feature.auth.navigation.authNavGraph import com.teamoffroad.feature.explore.navigation.exploreNavGraph import com.teamoffroad.feature.home.navigation.homeNavGraph import com.teamoffroad.feature.main.MainNavigator -import com.teamoffroad.feature.main.splash.navigation.splashNavGraph import com.teamoffroad.feature.mypage.navigation.myPageNavGraph @RequiresApi(Build.VERSION_CODES.TIRAMISU) @@ -39,10 +38,6 @@ internal fun MainNavHost( exitTransition = { ExitTransition.None }, popExitTransition = { ExitTransition.None }, ) { - splashNavGraph( - navigateToAuth = { navigator.navigateToAuth() }, - navigateToHome = { navigator.navigateToHome() } - ) homeNavGraph( navigateToBack = navigator::popBackStackIfNotMainTabRoute, navigateToGainedCharacter = { @@ -81,7 +76,9 @@ internal fun MainNavHost( }, navigateToGainedEmblems = navigator::navigateToGainedEmblems, navigateToSetting = navigator::navigateToSetting, - navigateToAnnouncement = navigator::navigateToAnnouncement, + navigateToAnnouncement = { announcementId -> + navigator.navigateToAnnouncement(announcementId) + }, navigateToAnnouncementDetail = navigator::navigateToAnnouncementDetail, navigateToSignIn = navigator::navigateToAuth, navigateToCharacterDetail = navigator::navigateToCharacterDetail, diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashUiState.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashUiState.kt deleted file mode 100644 index a300ec35..00000000 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashUiState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.teamoffroad.feature.main.splash - -sealed class SplashUiState { - data object NavigateHome: SplashUiState() - data object NavigateLogin: SplashUiState() -} diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashViewModel.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashViewModel.kt deleted file mode 100644 index 6c02d952..00000000 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/splash/SplashViewModel.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.teamoffroad.feature.main.splash - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.teamoffroad.core.common.domain.usecase.GetAutoSignInUseCase -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import javax.inject.Inject - -@HiltViewModel -class SplashViewModel @Inject constructor( - private val getAutoSignInUseCase: GetAutoSignInUseCase, -) : ViewModel() { - private val _splashUiState = MutableSharedFlow() - val splashUiState: SharedFlow get() = _splashUiState.asSharedFlow() - - fun showSplash() { - viewModelScope.launch { - delay(1550L) - checkAutoSignIn() - } - } - - private fun checkAutoSignIn() { - viewModelScope.launch { - val isAuthSignIn = getAutoSignInUseCase() - if (isAuthSignIn.first()) { - _splashUiState.emit(SplashUiState.NavigateHome) - } else { - _splashUiState.emit(SplashUiState.NavigateLogin) - } - } - } -} \ No newline at end of file diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/splash/navigation/SplashNavigation.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/splash/navigation/SplashNavigation.kt deleted file mode 100644 index fff773be..00000000 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/splash/navigation/SplashNavigation.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.teamoffroad.feature.main.splash.navigation - -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.compose.composable -import com.teamoffroad.core.navigation.Route -import com.teamoffroad.feature.main.splash.SplashScreen - -fun NavController.navigateToAuth() { - navigate(Route.Auth) -} - -fun NavGraphBuilder.splashNavGraph( - navigateToAuth: () -> Unit, - navigateToHome: () -> Unit, -) { - composable { - SplashScreen( - navigateToAuth, - navigateToHome, - ) - } -} \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/navigation/MyPageNavigation.kt b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/navigation/MyPageNavigation.kt index 994b3c16..51ff17df 100644 --- a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/navigation/MyPageNavigation.kt +++ b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/navigation/MyPageNavigation.kt @@ -48,8 +48,10 @@ fun NavController.navigateToSetting() { navigate(MyPageRoute.Setting) } -fun NavController.navigateToAnnouncement() { - navigate(MyPageRoute.Announcement) +fun NavController.navigateToAnnouncement(announcementId: String?) { + navigate( + MyPageRoute.Announcement(announcementId) + ) } fun NavController.navigateToAnnouncementDetail( @@ -74,7 +76,7 @@ fun NavController.navigateToAnnouncementDetail( ) } -fun NavController.navigateToSignIn() { +fun NavController.navigateToAuth() { navigate(Route.Auth) } @@ -88,7 +90,7 @@ fun NavGraphBuilder.myPageNavGraph( navigateToAvailableCouponDetail: (Int, String, String, String, Int) -> Unit, navigateToGainedEmblems: () -> Unit, navigateToSetting: () -> Unit, - navigateToAnnouncement: () -> Unit, + navigateToAnnouncement: (String?) -> Unit, navigateToAnnouncementDetail: (String, String, Boolean, String, Boolean, List, List) -> Unit, navigateToSignIn: () -> Unit, navigateToCharacterDetail: (Int, Boolean) -> Unit, @@ -127,14 +129,16 @@ fun NavGraphBuilder.myPageNavGraph( composable { SettingScreen( - navigateToAnnouncement = navigateToAnnouncement, + navigateToAnnouncement = { navigateToAnnouncement(null) }, navigateToSignIn = navigateToSignIn, navigateToBack = navigateToBack ) } - composable { + composable { backStackEntry -> + val announcementId = backStackEntry.toRoute().announcementId AnnouncementScreen( + announcementId, navigateToAnnouncementDetail, navigateToBack ) diff --git a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt index 380e639b..5c2d8bb7 100644 --- a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt +++ b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt @@ -34,6 +34,7 @@ import com.teamoffroad.offroad.feature.mypage.R @Composable internal fun AnnouncementScreen( + announcementId: String?, navigateToAnnouncementDetail: (String, String, Boolean, String, Boolean, List, List) -> Unit, navigateToBack: () -> Unit, viewModel: AnnouncementViewModel = hiltViewModel() From 7a398ea9ba2df3cd8caf7aa2b7e45aedcff7c63c Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sat, 16 Nov 2024 20:31:18 +0900 Subject: [PATCH 07/30] feature: Add chat exist --- .../home/presentation/component/HomeIcons.kt | 86 +++++++++++-------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt index cc6d37cb..b42abc81 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt @@ -7,20 +7,31 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi +import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.teamoffroad.core.designsystem.component.clickableWithoutRipple +import com.teamoffroad.core.designsystem.theme.ErrorNew import com.teamoffroad.offroad.feature.home.R @RequiresApi(Build.VERSION_CODES.TIRAMISU) @@ -30,43 +41,50 @@ fun HomeIcons( imageUrl: String, navigateToGainedCharacter: () -> Unit, ) { - val scope = rememberCoroutineScope() - val permission = Manifest.permission.WRITE_EXTERNAL_STORAGE - - val launcher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) { - Toast.makeText(context, "권한이 허용되었습니다.", Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(context, "권한이 허용되지 않았습니다.", Toast.LENGTH_SHORT).show() - } - } + val showTextField = remember { mutableStateOf(false) } + val textState = remember { mutableStateOf(TextFieldValue()) } + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current Box( - contentAlignment = Alignment.TopEnd, - modifier = Modifier.aspectRatio(48f / 144f).padding(top = 80.dp, end = 20.dp) + modifier = Modifier + .fillMaxSize() + .clickableWithoutRipple { + focusManager.clearFocus() + showTextField.value = false + } ) { - Column { - val downloadInteractionSource = remember { MutableInteractionSource() } - Image( - painter = painterResource(id = R.drawable.ic_home_chat), - contentDescription = "chat", - modifier = Modifier -// modifier = Modifier -// .clickableWithoutRipple(downloadInteractionSource) { -// if (ContextCompat.checkSelfPermission( -// context, -// permission -// ) == PackageManager.PERMISSION_GRANTED -// ) { -// downloadImage(context, imageUrl, scope) -// Toast.makeText(context, "이미지 다운 완료", Toast.LENGTH_SHORT).show() -// } else { -// launcher.launch(permission) -// } -// } - ) + Column( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 80.dp, end = 20.dp) + .width(48.dp) + ) { + Box { + Image( + painter = painterResource(id = R.drawable.ic_home_chat), + contentDescription = "chat", + modifier = Modifier.clickableWithoutRipple { + showTextField.value = true + } + ) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.TopEnd + ) { + Canvas( + modifier = Modifier + .padding(top = 6.dp, end = 6.dp) + .size(8.dp) + ) { + drawCircle( + color = ErrorNew, + style = Fill + ) + } + } + } + Image( painter = painterResource(id = R.drawable.ic_home_upload), From d90884d35d3af2e7bdf4cd9ee6a7afcf80caec0e Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sat, 16 Nov 2024 21:31:57 +0900 Subject: [PATCH 08/30] feature: Add ChatTextField --- .../home/presentation/ChatTextField.kt | 150 ++++++++++++++++++ .../feature/home/presentation/HomeScreen.kt | 53 ++++--- .../res/drawable/ic_character_chat_send.xml | 12 ++ 3 files changed, 193 insertions(+), 22 deletions(-) create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt create mode 100644 feature/home/src/main/res/drawable/ic_character_chat_send.xml diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt new file mode 100644 index 00000000..8c1435e6 --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt @@ -0,0 +1,150 @@ +package com.teamoffroad.feature.home.presentation + +import android.graphics.Rect +import android.view.ViewTreeObserver +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.teamoffroad.core.designsystem.component.clickableWithoutRipple +import com.teamoffroad.core.designsystem.theme.BtnInactive +import com.teamoffroad.core.designsystem.theme.Main2 +import com.teamoffroad.core.designsystem.theme.OffroadTheme +import com.teamoffroad.core.designsystem.theme.Transparent +import com.teamoffroad.core.designsystem.theme.White +import com.teamoffroad.offroad.feature.home.R + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ChatTextField( + modifier: Modifier = Modifier, + text: String = "", + isChatting: Boolean = false, + onValueChange: (String) -> Unit = {}, + onFocusChange: (Boolean) -> Unit = {}, + onSendClick: () -> Unit = {}, +) { + val scrollState = rememberScrollState() + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + val contextView = LocalView.current + + var keyboardVisible by remember { mutableStateOf(false) } + + LaunchedEffect(isChatting) { + if (isChatting) { + focusRequester.requestFocus() + } + } + + LaunchedEffect(keyboardVisible) { + if (!keyboardVisible) { + focusManager.clearFocus() + } + } + + DisposableEffect(contextView) { + val rect = Rect() + val listener = ViewTreeObserver.OnGlobalLayoutListener { + contextView.getWindowVisibleDisplayFrame(rect) + val screenHeight = contextView.rootView.height + val keypadHeight = screenHeight - rect.bottom + keyboardVisible = keypadHeight > screenHeight * 0.15 + } + contextView.viewTreeObserver.addOnGlobalLayoutListener(listener) + onDispose { + contextView.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + } + + AnimatedVisibility( + visible = isChatting, + ) { + Box( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .background(color = White, shape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)) + .padding(horizontal = 22.dp, vertical = 4.dp), + ) { + val textFieldHeight = remember { mutableIntStateOf(0) } + + Box( + modifier = Modifier + .fillMaxWidth() + .height(with(LocalDensity.current) { textFieldHeight.intValue.toDp() }) + .align(Alignment.Center) + .padding(vertical = 10.dp) + .padding(end = 44.dp) + .background( + color = BtnInactive, + shape = RoundedCornerShape(10.dp), + ), + ) + TextField( + value = text, + onValueChange = { onValueChange(it) }, + textStyle = OffroadTheme.typography.textRegular, + modifier = Modifier + .verticalScroll(scrollState) + .padding(end = 44.dp) + .padding(horizontal = 2.dp) + .fillMaxWidth() + .focusRequester(focusRequester) + .onGloballyPositioned { layoutCoordinates -> + textFieldHeight.intValue = layoutCoordinates.size.height + } + .onFocusChanged { focusState -> + onFocusChange(focusState.isFocused) + }, + maxLines = 2, + colors = TextFieldDefaults.textFieldColors( + containerColor = Transparent, + focusedIndicatorColor = Transparent, + unfocusedIndicatorColor = Transparent, + focusedTextColor = Main2, + ), + shape = RoundedCornerShape(12.dp), + ) + Image( + painter = painterResource(id = R.drawable.ic_character_chat_send), + contentDescription = null, + modifier = Modifier + .padding(end = 2.dp) + .size(36.dp) + .align(Alignment.CenterEnd) + .clickableWithoutRipple { onSendClick() }, + ) + } + } +} diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index b22cda22..df15802b 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -10,10 +10,11 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -22,7 +23,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -73,28 +73,37 @@ fun HomeScreen( if (completeQuests.isNotEmpty()) isCompleteQuestDialogShown.value = true } - StaticAnimationWrapper { - Surface( + Box( + modifier = Modifier + .background(homeGradientBackground) + .fillMaxSize() + .padding(bottom = 140.dp) + //.navigationBarsPadding(), + ) { + Column(modifier = Modifier.fillMaxSize()) { + UsersAdventuresInformation( + context = context, + modifier = Modifier + .weight(1f) + .actionBarPadding(), + viewModel = viewModel, + navigateToGainedCharacter = navigateToGainedCharacter, + ) + Spacer(modifier = Modifier.padding(top = 12.dp)) + UsersQuestInformation(context, viewModel) + } + + Box( modifier = Modifier - .background(homeGradientBackground) - .padding(bottom = 140.dp) - .navigationBarsPadding(), - color = Color.Transparent + .fillMaxSize(), + contentAlignment = Alignment.BottomCenter ) { - StaticAnimationWrapper { - Column(modifier = Modifier.fillMaxWidth()) { - UsersAdventuresInformation( - context = context, - modifier = Modifier - .weight(1f) - .actionBarPadding(), - viewModel = viewModel, - navigateToGainedCharacter = navigateToGainedCharacter, - ) - Spacer(modifier = Modifier.padding(top = 12.dp)) - UsersQuestInformation(context, viewModel) - } - } + ChatTextField( + modifier = Modifier + //.imePadding() + .align(Alignment.BottomCenter), + isChatting = true + ) } } diff --git a/feature/home/src/main/res/drawable/ic_character_chat_send.xml b/feature/home/src/main/res/drawable/ic_character_chat_send.xml new file mode 100644 index 00000000..d3108306 --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_character_chat_send.xml @@ -0,0 +1,12 @@ + + + + From ecbd62f76f8c4fd53f2c77b19d3c2d727b763a0f Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Sat, 16 Nov 2024 23:17:47 +0900 Subject: [PATCH 09/30] feature: Add fcm navigate to mypage --- .../offroad.app/OffRoadMessagingService.kt | 46 +++++++------------ .../common/domain/model/FcmNotificationKey.kt | 13 ++++++ feature/main/build.gradle.kts | 1 + .../teamoffroad/feature/main/MainActivity.kt | 6 ++- .../teamoffroad/feature/main/MainScreen.kt | 11 +++-- 5 files changed, 42 insertions(+), 35 deletions(-) create mode 100644 core/common/src/main/java/com/teamoffroad/core/common/domain/model/FcmNotificationKey.kt diff --git a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt index 51e8c4d7..7fd03c61 100644 --- a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt +++ b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt @@ -7,13 +7,20 @@ import android.content.Context import android.content.Intent import android.graphics.drawable.BitmapDrawable import android.os.Build -import android.util.Log import androidx.core.app.NotificationCompat import coil.Coil import coil.request.ImageRequest import com.google.firebase.messaging.Constants.MessageNotificationKeys import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.CHANNEL_ID +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_BODY +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_ID +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_IMAGE +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TITLE +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TYPE +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.NOTICE +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.TYPE_CHARACTER_CHAT import com.teamoffroad.core.common.domain.repository.DeviceTokenRepository import com.teamoffroad.core.common.util.ActivityLifecycleHandler import com.teamoffroad.feature.main.MainActivity @@ -38,11 +45,10 @@ class OffRoadMessagingService : FirebaseMessagingService() { super.onMessageReceived(remoteMessage) if (remoteMessage.data.isNotEmpty()) { if (ActivityLifecycleHandler.isAppInForeground) { - //TODO. 키 밸류값 나오면 바꾸기 if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) - sendForeGroundNotification(remoteMessage, true) + sendForeGroundNotification(remoteMessage) } else { - sendBackGroundNotification(remoteMessage, false) + sendBackGroundNotification(remoteMessage) } } } @@ -84,11 +90,7 @@ class OffRoadMessagingService : FirebaseMessagingService() { private fun createNotificationIntent(remoteMessage: RemoteMessage): Intent { return Intent(this, MainActivity::class.java).apply { - if (ActivityLifecycleHandler.isAppInForeground) { - Log.d("asdasd", "forground") - } else { - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK - } + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) { putExtra(KEY_ID, remoteMessage.data[KEY_ID]) @@ -101,7 +103,7 @@ class OffRoadMessagingService : FirebaseMessagingService() { onLargeIconReady: (NotificationCompat.Builder) -> Unit ): NotificationCompat.Builder { - val notificationBuilder = NotificationCompat.Builder(this, "channelId") + val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle(remoteMessage.data[KEY_TITLE]) .setContentText(remoteMessage.data[KEY_BODY]) @@ -132,7 +134,7 @@ class OffRoadMessagingService : FirebaseMessagingService() { onLargeIconReady: (NotificationCompat.Builder) -> Unit ): NotificationCompat.Builder { - val notificationBuilder = NotificationCompat.Builder(this, "channelId") + val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) .setContentTitle(remoteMessage.data[KEY_TITLE]) .setContentText(remoteMessage.data[KEY_BODY]) @@ -158,18 +160,14 @@ class OffRoadMessagingService : FirebaseMessagingService() { return notificationBuilder } - private fun sendForeGroundNotification(remoteMessage: RemoteMessage, isForeGround: Boolean) { + private fun sendForeGroundNotification(remoteMessage: RemoteMessage) { val uniqueIdentifier = generateUniqueIdentifier() createForeGroundNotificationBuilder(remoteMessage) { notificationBuilder -> showNotification(notificationBuilder, uniqueIdentifier) - - if(isForeGround){ - - } } } - private fun sendBackGroundNotification(remoteMessage: RemoteMessage, isForeGround: Boolean) { + private fun sendBackGroundNotification(remoteMessage: RemoteMessage) { val uniqueIdentifier = generateUniqueIdentifier() val intent = createNotificationIntent(remoteMessage) val pendingIntent = createPendingIntent(intent, uniqueIdentifier) @@ -187,22 +185,10 @@ class OffRoadMessagingService : FirebaseMessagingService() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = - NotificationChannel("channelId", "Notice", NotificationManager.IMPORTANCE_HIGH) + NotificationChannel(CHANNEL_ID, NOTICE, NotificationManager.IMPORTANCE_HIGH) notificationManager.createNotificationChannel(channel) } notificationManager.notify(uniqueIdentifier, notificationBuilder.build()) } - - companion object { - //TODO. 나중에 정리 해야됨 - private const val KEY_TITLE = "title" - private const val KEY_BODY = "body" - private const val KEY_TYPE = "type" - private const val KEY_IMAGE = "image" - private const val KEY_ID = "additionalProp1" - private const val TYPE_CHARACTER_CHAT = "CHARACTER_CHAT" - private const val TYPE_ANNOUNCEMENT = "ANNOUNCEMENT_REDIRECT" - - } } \ No newline at end of file diff --git a/core/common/src/main/java/com/teamoffroad/core/common/domain/model/FcmNotificationKey.kt b/core/common/src/main/java/com/teamoffroad/core/common/domain/model/FcmNotificationKey.kt new file mode 100644 index 00000000..19031878 --- /dev/null +++ b/core/common/src/main/java/com/teamoffroad/core/common/domain/model/FcmNotificationKey.kt @@ -0,0 +1,13 @@ +package com.teamoffroad.core.common.domain.model + +object FcmNotificationKey { + const val CHANNEL_ID = "channelId" + const val NOTICE = "Notice" + const val KEY_TITLE = "title" + const val KEY_BODY = "body" + const val KEY_TYPE = "type" + const val KEY_IMAGE = "image" + const val KEY_ID = "additionalProp1" + const val TYPE_CHARACTER_CHAT = "CHARACTER_CHAT" + const val TYPE_ANNOUNCEMENT = "ANNOUNCEMENT_REDIRECT" +} \ No newline at end of file diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index bf84b91f..a25fadcc 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -13,6 +13,7 @@ android { } dependencies { + implementation(project(":core:common")) implementation(project(":feature:auth")) implementation(project(":feature:home")) implementation(project(":feature:explore")) diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt index f1f395a7..0bc6f00f 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt @@ -13,6 +13,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_ID +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TYPE import com.teamoffroad.core.designsystem.theme.OffroadTheme import com.teamoffroad.feature.main.component.MainTransparentActionBar import dagger.hilt.android.AndroidEntryPoint @@ -23,8 +25,8 @@ import kotlinx.coroutines.delay class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val notificationType = intent.getStringExtra("type") - val notificationId = intent.getStringExtra("additionalProp1") + val notificationType = intent.getStringExtra(KEY_TYPE) + val notificationId = intent.getStringExtra(KEY_ID) setContent { val navigator: MainNavigator = rememberMainNavigator() val showSplash = remember { mutableStateOf(true) } diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt index 9be38aaf..817a6f81 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.TYPE_ANNOUNCEMENT import com.teamoffroad.core.common.util.OnBackButtonListener import com.teamoffroad.core.designsystem.component.navigationPadding import com.teamoffroad.feature.main.component.MainBottomBar @@ -23,9 +24,13 @@ internal fun MainScreen( ) { LaunchedEffect(Unit) { if (!notificationType.isNullOrBlank()) { - if (notificationType == "ANNOUNCEMENT_REDIRECT") - notificationId?.let { navigator.navigateToAnnouncement(it) } - else if (notificationType == "CHARACTER_CHAT") { + if (notificationType == TYPE_ANNOUNCEMENT) + notificationId?.let { + navigator.navigateToMyPage() + navigator.navigateToSetting() + navigator.navigateToAnnouncement(it) + } + else { navigator.navigateToHome() } } From afb049afda91f6b39058350d58d395e0cfcd5210 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sat, 16 Nov 2024 23:40:21 +0900 Subject: [PATCH 10/30] feature: Edit ChatTextField --- .../home/presentation/ChatTextField.kt | 4 +- .../feature/home/presentation/HomeScreen.kt | 38 ++++++++++++------- .../home/presentation/component/HomeIcons.kt | 15 ++++---- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt index 8c1435e6..376f1105 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -50,6 +51,7 @@ fun ChatTextField( modifier: Modifier = Modifier, text: String = "", isChatting: Boolean = false, + keyboard: Boolean, onValueChange: (String) -> Unit = {}, onFocusChange: (Boolean) -> Unit = {}, onSendClick: () -> Unit = {}, @@ -59,7 +61,7 @@ fun ChatTextField( val focusManager = LocalFocusManager.current val contextView = LocalView.current - var keyboardVisible by remember { mutableStateOf(false) } + var keyboardVisible by remember { mutableStateOf(keyboard) } LaunchedEffect(isChatting) { if (isChatting) { diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index df15802b..7268cc6f 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -8,15 +8,18 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -24,12 +27,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import com.teamoffroad.core.designsystem.component.StaticAnimationWrapper import com.teamoffroad.core.designsystem.component.actionBarPadding import com.teamoffroad.core.designsystem.theme.HomeGradi1 import com.teamoffroad.core.designsystem.theme.HomeGradi2 @@ -53,6 +56,7 @@ val homeGradientBackground = Brush.verticalGradient( colors = listOf(HomeGradi1, HomeGradi2, HomeGradi3, HomeGradi4, HomeGradi5, HomeGradi6) ) +@OptIn(ExperimentalLayoutApi::class) @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable fun HomeScreen( @@ -63,7 +67,8 @@ fun HomeScreen( val context = LocalContext.current val viewModel: HomeViewModel = hiltViewModel() val isCompleteQuestDialogShown = remember { mutableStateOf(false) } - + val imeHeight = WindowInsets.ime.getBottom(LocalDensity.current) + val isChatting = remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.updateAutoSignIn() @@ -78,10 +83,10 @@ fun HomeScreen( .background(homeGradientBackground) .fillMaxSize() .padding(bottom = 140.dp) - //.navigationBarsPadding(), ) { Column(modifier = Modifier.fillMaxSize()) { UsersAdventuresInformation( + isChatting = isChatting, context = context, modifier = Modifier .weight(1f) @@ -93,17 +98,20 @@ fun HomeScreen( UsersQuestInformation(context, viewModel) } - Box( - modifier = Modifier - .fillMaxSize(), - contentAlignment = Alignment.BottomCenter - ) { - ChatTextField( + if (isChatting.value) { + Box( modifier = Modifier - //.imePadding() - .align(Alignment.BottomCenter), - isChatting = true - ) + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 230.dp) + ) { + ChatTextField( + modifier = Modifier + .imePadding(), + isChatting = isChatting.value, + keyboard = true + ) + } } } @@ -121,6 +129,7 @@ fun HomeScreen( @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable private fun UsersAdventuresInformation( + isChatting: MutableState, context: Context, modifier: Modifier = Modifier, viewModel: HomeViewModel, @@ -150,6 +159,7 @@ private fun UsersAdventuresInformation( contentAlignment = Alignment.TopEnd ) { HomeIcons( + isChatting = isChatting, context = context, imageUrl = imageUrl, navigateToGainedCharacter = navigateToGainedCharacter, diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt index b42abc81..3ca76717 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -37,21 +38,21 @@ import com.teamoffroad.offroad.feature.home.R @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable fun HomeIcons( + isChatting: MutableState, context: Context, imageUrl: String, navigateToGainedCharacter: () -> Unit, ) { - val showTextField = remember { mutableStateOf(false) } - val textState = remember { mutableStateOf(TextFieldValue()) } - val focusRequester = remember { FocusRequester() } - val focusManager = LocalFocusManager.current +// val showTextField = remember { mutableStateOf(false) } +// val textState = remember { mutableStateOf(TextFieldValue()) } +// val focusRequester = remember { FocusRequester() } +// val focusManager = LocalFocusManager.current Box( modifier = Modifier .fillMaxSize() .clickableWithoutRipple { - focusManager.clearFocus() - showTextField.value = false + isChatting.value = false } ) { Column( @@ -65,7 +66,7 @@ fun HomeIcons( painter = painterResource(id = R.drawable.ic_home_chat), contentDescription = "chat", modifier = Modifier.clickableWithoutRipple { - showTextField.value = true + isChatting.value = true } ) Box( From 160232ef8300e8ac9ae538641cd3c09e30dd0847 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sat, 16 Nov 2024 23:46:21 +0900 Subject: [PATCH 11/30] refactor: Edit home ui --- .../presentation/{ChatTextField.kt => HomeChatTextField.kt} | 3 +-- .../com/teamoffroad/feature/home/presentation/HomeScreen.kt | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) rename feature/home/src/main/java/com/teamoffroad/feature/home/presentation/{ChatTextField.kt => HomeChatTextField.kt} (98%) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt similarity index 98% rename from feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt rename to feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt index 376f1105..d47ca2f7 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/ChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -47,7 +46,7 @@ import com.teamoffroad.offroad.feature.home.R @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ChatTextField( +fun HomeChatTextField( modifier: Modifier = Modifier, text: String = "", isChatting: Boolean = false, diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 7268cc6f..54ecc04d 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -82,7 +82,7 @@ fun HomeScreen( modifier = Modifier .background(homeGradientBackground) .fillMaxSize() - .padding(bottom = 140.dp) + .padding(bottom = 180.dp) ) { Column(modifier = Modifier.fillMaxSize()) { UsersAdventuresInformation( @@ -103,9 +103,9 @@ fun HomeScreen( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) - .padding(bottom = 230.dp) + //.padding(bottom = 196.dp) ) { - ChatTextField( + HomeChatTextField( modifier = Modifier .imePadding(), isChatting = isChatting.value, From 45f50af073e45d5c39d7aa62a41de077c9783fb5 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 00:35:40 +0900 Subject: [PATCH 12/30] refactor: Edit keyboard --- .../home/presentation/HomeChatTextField.kt | 34 ++++++++++--------- .../feature/home/presentation/HomeScreen.kt | 10 +++--- .../home/presentation/component/HomeIcons.kt | 12 ------- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt index d47ca2f7..0373dbc7 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -49,7 +50,7 @@ import com.teamoffroad.offroad.feature.home.R fun HomeChatTextField( modifier: Modifier = Modifier, text: String = "", - isChatting: Boolean = false, + isChatting: MutableState, keyboard: Boolean, onValueChange: (String) -> Unit = {}, onFocusChange: (Boolean) -> Unit = {}, @@ -63,7 +64,7 @@ fun HomeChatTextField( var keyboardVisible by remember { mutableStateOf(keyboard) } LaunchedEffect(isChatting) { - if (isChatting) { + if (isChatting.value) { focusRequester.requestFocus() } } @@ -71,25 +72,26 @@ fun HomeChatTextField( LaunchedEffect(keyboardVisible) { if (!keyboardVisible) { focusManager.clearFocus() + isChatting.value = false } } - DisposableEffect(contextView) { - val rect = Rect() - val listener = ViewTreeObserver.OnGlobalLayoutListener { - contextView.getWindowVisibleDisplayFrame(rect) - val screenHeight = contextView.rootView.height - val keypadHeight = screenHeight - rect.bottom - keyboardVisible = keypadHeight > screenHeight * 0.15 - } - contextView.viewTreeObserver.addOnGlobalLayoutListener(listener) - onDispose { - contextView.viewTreeObserver.removeOnGlobalLayoutListener(listener) - } - } +// DisposableEffect(contextView) { +// val rect = Rect() +// val listener = ViewTreeObserver.OnGlobalLayoutListener { +// contextView.getWindowVisibleDisplayFrame(rect) +// val screenHeight = contextView.rootView.height +// val keypadHeight = screenHeight - rect.bottom +// keyboardVisible = keypadHeight > screenHeight * 0.15 +// } +// contextView.viewTreeObserver.addOnGlobalLayoutListener(listener) +// onDispose { +// contextView.viewTreeObserver.removeOnGlobalLayoutListener(listener) +// } +// } AnimatedVisibility( - visible = isChatting, + visible = isChatting.value, ) { Box( modifier = modifier diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 54ecc04d..1e485028 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -56,7 +56,6 @@ val homeGradientBackground = Brush.verticalGradient( colors = listOf(HomeGradi1, HomeGradi2, HomeGradi3, HomeGradi4, HomeGradi5, HomeGradi6) ) -@OptIn(ExperimentalLayoutApi::class) @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable fun HomeScreen( @@ -96,6 +95,7 @@ fun HomeScreen( ) Spacer(modifier = Modifier.padding(top = 12.dp)) UsersQuestInformation(context, viewModel) + } if (isChatting.value) { @@ -103,12 +103,12 @@ fun HomeScreen( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) - //.padding(bottom = 196.dp) + .imePadding() + //.padding(bottom = with(LocalDensity.current) { imeHeight.toDp() }) + .padding(bottom = 196.dp) ) { HomeChatTextField( - modifier = Modifier - .imePadding(), - isChatting = isChatting.value, + isChatting = isChatting, keyboard = true ) } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt index 3ca76717..fbe7aaef 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt @@ -1,18 +1,12 @@ package com.teamoffroad.feature.home.presentation.component -import android.Manifest import android.content.Context import android.os.Build -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -20,16 +14,10 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.graphics.drawscope.Fill -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.dp import com.teamoffroad.core.designsystem.component.clickableWithoutRipple import com.teamoffroad.core.designsystem.theme.ErrorNew From e3ca4d70e3620ab236db3ef68fe25d4699fcb8cf Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 01:36:00 +0900 Subject: [PATCH 13/30] feature: Add finish chatting button --- .../home/presentation/HomeChatTextField.kt | 149 +++++++++++------- .../feature/home/presentation/HomeScreen.kt | 62 ++++++-- .../home/presentation/component/HomeIcons.kt | 11 +- feature/home/src/main/res/values/strings.xml | 2 + 4 files changed, 147 insertions(+), 77 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt index 0373dbc7..0983db1d 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt @@ -6,6 +6,8 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -15,6 +17,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable @@ -36,6 +39,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.teamoffroad.core.designsystem.component.clickableWithoutRipple import com.teamoffroad.core.designsystem.theme.BtnInactive @@ -76,19 +80,19 @@ fun HomeChatTextField( } } -// DisposableEffect(contextView) { -// val rect = Rect() -// val listener = ViewTreeObserver.OnGlobalLayoutListener { -// contextView.getWindowVisibleDisplayFrame(rect) -// val screenHeight = contextView.rootView.height -// val keypadHeight = screenHeight - rect.bottom -// keyboardVisible = keypadHeight > screenHeight * 0.15 -// } -// contextView.viewTreeObserver.addOnGlobalLayoutListener(listener) -// onDispose { -// contextView.viewTreeObserver.removeOnGlobalLayoutListener(listener) -// } -// } + DisposableEffect(contextView) { + val rect = Rect() + val listener = ViewTreeObserver.OnGlobalLayoutListener { + contextView.getWindowVisibleDisplayFrame(rect) + val screenHeight = contextView.rootView.height + val keypadHeight = screenHeight - rect.bottom + keyboardVisible = keypadHeight > screenHeight * 0.15 + } + contextView.viewTreeObserver.addOnGlobalLayoutListener(listener) + onDispose { + contextView.viewTreeObserver.removeOnGlobalLayoutListener(listener) + } + } AnimatedVisibility( visible = isChatting.value, @@ -97,57 +101,82 @@ fun HomeChatTextField( modifier = modifier .fillMaxWidth() .wrapContentHeight() - .background(color = White, shape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp)) + .background( + color = White, + shape = RoundedCornerShape(topStart = 18.dp, topEnd = 18.dp) + ) .padding(horizontal = 22.dp, vertical = 4.dp), ) { val textFieldHeight = remember { mutableIntStateOf(0) } - Box( - modifier = Modifier - .fillMaxWidth() - .height(with(LocalDensity.current) { textFieldHeight.intValue.toDp() }) - .align(Alignment.Center) - .padding(vertical = 10.dp) - .padding(end = 44.dp) - .background( - color = BtnInactive, - shape = RoundedCornerShape(10.dp), - ), - ) - TextField( - value = text, - onValueChange = { onValueChange(it) }, - textStyle = OffroadTheme.typography.textRegular, - modifier = Modifier - .verticalScroll(scrollState) - .padding(end = 44.dp) - .padding(horizontal = 2.dp) - .fillMaxWidth() - .focusRequester(focusRequester) - .onGloballyPositioned { layoutCoordinates -> - textFieldHeight.intValue = layoutCoordinates.size.height - } - .onFocusChanged { focusState -> - onFocusChange(focusState.isFocused) - }, - maxLines = 2, - colors = TextFieldDefaults.textFieldColors( - containerColor = Transparent, - focusedIndicatorColor = Transparent, - unfocusedIndicatorColor = Transparent, - focusedTextColor = Main2, - ), - shape = RoundedCornerShape(12.dp), - ) - Image( - painter = painterResource(id = R.drawable.ic_character_chat_send), - contentDescription = null, - modifier = Modifier - .padding(end = 2.dp) - .size(36.dp) - .align(Alignment.CenterEnd) - .clickableWithoutRipple { onSendClick() }, - ) + Column { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 18.dp, bottom = 4.dp) + ) { + Text( + text = stringResource(id = R.string.home_chat_me), + color = Main2, + style = OffroadTheme.typography.textBold + ) + Text( + text = "text", + modifier = Modifier.weight(1f), + style = OffroadTheme.typography.textRegular + ) + } + + Box { + Box( + modifier = Modifier + .fillMaxWidth() + .height(with(LocalDensity.current) { textFieldHeight.intValue.toDp() }) + .align(Alignment.Center) + .padding(vertical = 10.dp) + .padding(end = 44.dp) + .background( + color = BtnInactive, + shape = RoundedCornerShape(10.dp), + ), + ) + TextField( + value = text, + onValueChange = { onValueChange(it) }, + textStyle = OffroadTheme.typography.textRegular, + modifier = Modifier + .verticalScroll(scrollState) + .padding(end = 44.dp) + .padding(horizontal = 2.dp) + .fillMaxWidth() + .focusRequester(focusRequester) + .onGloballyPositioned { layoutCoordinates -> + textFieldHeight.intValue = layoutCoordinates.size.height + } + .onFocusChanged { focusState -> + onFocusChange(focusState.isFocused) + }, + maxLines = 2, + colors = TextFieldDefaults.textFieldColors( + containerColor = Transparent, + focusedIndicatorColor = Transparent, + unfocusedIndicatorColor = Transparent, + focusedTextColor = Main2, + ), + shape = RoundedCornerShape(12.dp), + ) + Image( + painter = painterResource(id = R.drawable.ic_character_chat_send), + contentDescription = null, + modifier = Modifier + .padding(end = 2.dp) + .size(36.dp) + .align(Alignment.CenterEnd) + .clickableWithoutRipple { onSendClick() }, + ) + } + } + } } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 1e485028..db31ebc3 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -6,6 +6,7 @@ import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -17,6 +18,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -26,6 +29,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource @@ -34,6 +38,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.teamoffroad.core.designsystem.component.actionBarPadding +import com.teamoffroad.core.designsystem.component.clickableWithoutRipple +import com.teamoffroad.core.designsystem.theme.ErrorNew import com.teamoffroad.core.designsystem.theme.HomeGradi1 import com.teamoffroad.core.designsystem.theme.HomeGradi2 import com.teamoffroad.core.designsystem.theme.HomeGradi3 @@ -41,6 +47,9 @@ import com.teamoffroad.core.designsystem.theme.HomeGradi4 import com.teamoffroad.core.designsystem.theme.HomeGradi5 import com.teamoffroad.core.designsystem.theme.HomeGradi6 import com.teamoffroad.core.designsystem.theme.OffroadTheme +import com.teamoffroad.core.designsystem.theme.Sub +import com.teamoffroad.core.designsystem.theme.Sub55 +import com.teamoffroad.core.designsystem.theme.White import com.teamoffroad.feature.home.domain.model.UserQuests import com.teamoffroad.feature.home.presentation.component.CompleteQuestDialog import com.teamoffroad.feature.home.presentation.component.HomeIcons @@ -107,10 +116,25 @@ fun HomeScreen( //.padding(bottom = with(LocalDensity.current) { imeHeight.toDp() }) .padding(bottom = 196.dp) ) { - HomeChatTextField( - isChatting = isChatting, - keyboard = true - ) + Column { + Box( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 20.dp), + contentAlignment = Alignment.CenterEnd + ) { + FinishChatting(isChatting) + } + } + + HomeChatTextField( + isChatting = isChatting, + keyboard = true + ) + } } } } @@ -184,11 +208,31 @@ private fun UsersAdventuresInformation( } @Composable -private fun HomeBackground() { - Image( - painter = painterResource(id = R.drawable.img_home_stamp), - contentDescription = "stamp", - modifier = Modifier.padding(top = 16.dp) +fun FinishChatting( + isChatting: MutableState, + backgroundColor: Color = Sub55, + borderColor: Color = Sub +) { + Text( + style = OffroadTheme.typography.subtitle2Semibold, + text = stringResource(id = R.string.home_chat_finish), + modifier = Modifier + .padding(bottom = 8.dp) + .background( + color = backgroundColor, + shape = RoundedCornerShape(20.dp) + ) + .border( + width = 1.dp, + shape = RoundedCornerShape(20.dp), + color = borderColor + ) + .padding(horizontal = 16.dp) + .padding(vertical = 8.dp) + .clickableWithoutRipple { + isChatting.value = false + }, + color = White ) } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt index fbe7aaef..aaa9067d 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt @@ -31,17 +31,12 @@ fun HomeIcons( imageUrl: String, navigateToGainedCharacter: () -> Unit, ) { -// val showTextField = remember { mutableStateOf(false) } -// val textState = remember { mutableStateOf(TextFieldValue()) } -// val focusRequester = remember { FocusRequester() } -// val focusManager = LocalFocusManager.current - Box( modifier = Modifier .fillMaxSize() - .clickableWithoutRipple { - isChatting.value = false - } +// .clickableWithoutRipple { +// isChatting.value = false +// } ) { Column( modifier = Modifier diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml index b966f03a..24c07e7c 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -12,4 +12,6 @@ 퀘스트 \'%s\' 외 %d개를\n클리어했어요! 마이페이지에서\n보상을 확인해보세요. 확인 - + 나 :   + 채팅 종료 \ No newline at end of file From b156f075959b27c22213cfb969cd54ccfabcea59 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 01:57:57 +0900 Subject: [PATCH 14/30] feature: Add textfield onValueChange --- .../feature/home/presentation/HomeScreen.kt | 11 ++++++++++- .../feature/home/presentation/HomeViewModel.kt | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index db31ebc3..83293678 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -37,6 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.teamoffroad.core.designsystem.component.actionBarPadding import com.teamoffroad.core.designsystem.component.clickableWithoutRipple import com.teamoffroad.core.designsystem.theme.ErrorNew @@ -77,6 +78,7 @@ fun HomeScreen( val isCompleteQuestDialogShown = remember { mutableStateOf(false) } val imeHeight = WindowInsets.ime.getBottom(LocalDensity.current) val isChatting = remember { mutableStateOf(false) } + val chattingText = viewModel.chattingText.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.updateAutoSignIn() @@ -131,8 +133,15 @@ fun HomeScreen( } HomeChatTextField( + text = chattingText.value, isChatting = isChatting, - keyboard = true + keyboard = true, + onValueChange = { text -> + viewModel.updateChattingText(text) + }, +// onFocusChange = { isFocused -> +// viewModel.updateIsChatting(isFocused) +// } ) } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index e38fb4c8..85142822 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -11,6 +11,7 @@ import com.teamoffroad.feature.home.presentation.component.UiState import com.teamoffroad.feature.home.presentation.component.getErrorMessage import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -54,6 +55,12 @@ class HomeViewModel @Inject constructor( private val _linearProgressBar = MutableStateFlow(0f) val linearProgressBar = _linearProgressBar.asStateFlow() + private val _isChatting: MutableStateFlow = MutableStateFlow(false) + val isChatting: StateFlow = _isChatting.asStateFlow() + + private val _chattingText: MutableStateFlow = MutableStateFlow("") + val chattingText: StateFlow = _chattingText.asStateFlow() + fun getUsersAdventuresInformation(category: String) { viewModelScope.launch { runCatching { @@ -140,4 +147,13 @@ class HomeViewModel @Inject constructor( } } + fun updateChattingText(text: String) { + _chattingText.value = text + } + + fun updateIsChatting(boolean: Boolean) { + _isChatting.value = boolean + _chattingText.value = "" + } + } \ No newline at end of file From ee666beb5bbe9efb7e07415aec7a73d8db2ae12f Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Sun, 17 Nov 2024 06:02:56 +0900 Subject: [PATCH 15/30] feature: Add character chat broadcast receiver --- app/src/main/AndroidManifest.xml | 5 ++ .../offroad.app/OffRoadMessagingService.kt | 88 +++++++++---------- .../common/domain/model/NotificationEvent.kt | 7 ++ feature/home/build.gradle.kts | 1 + .../home/presentation/HomeViewModel.kt | 26 ++++++ feature/main/build.gradle.kts | 2 +- .../main/CharacterChatBroadcastReceiver.kt | 26 ++++++ .../teamoffroad/feature/main/MainActivity.kt | 46 +++++++++- .../teamoffroad/feature/main/MainScreen.kt | 2 +- .../mypage/presentation/AnnouncementScreen.kt | 24 +++++ gradle/libs.versions.toml | 7 ++ 11 files changed, 182 insertions(+), 52 deletions(-) create mode 100644 core/common/src/main/java/com/teamoffroad/core/common/domain/model/NotificationEvent.kt create mode 100644 feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 06b5af1e..b3029ac6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,5 +50,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt index 7fd03c61..2b52b8a6 100644 --- a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt +++ b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt @@ -41,14 +41,20 @@ class OffRoadMessagingService : FirebaseMessagingService() { CoroutineScope(Dispatchers.IO).launch { dataStore.updateDeviceTokenEnabled(token) } } + //FCM 메세지를 받을 때 호출됨 override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) if (remoteMessage.data.isNotEmpty()) { if (ActivityLifecycleHandler.isAppInForeground) { if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) - sendForeGroundNotification(remoteMessage) + sendNotification(remoteMessage, true) + else { + // 앱이 포그라운드에 있고, 알림타임이 캐릭터채팅인 경우 + // 정현이 봐야할곳은 여기!! 요쪽 따라가십쇼 + sendCharacterNotificationInForeground(remoteMessage, true) + } } else { - sendBackGroundNotification(remoteMessage) + sendNotification(remoteMessage, false) } } } @@ -88,47 +94,30 @@ class OffRoadMessagingService : FirebaseMessagingService() { ) } - private fun createNotificationIntent(remoteMessage: RemoteMessage): Intent { - return Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK - putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) - if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) { - putExtra(KEY_ID, remoteMessage.data[KEY_ID]) - } - } - } - - private fun createForeGroundNotificationBuilder( + private fun createNotificationIntent( remoteMessage: RemoteMessage, - onLargeIconReady: (NotificationCompat.Builder) -> Unit - ): NotificationCompat.Builder { - - val notificationBuilder = NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) - .setContentTitle(remoteMessage.data[KEY_TITLE]) - .setContentText(remoteMessage.data[KEY_BODY]) - .setAutoCancel(true) - .setPriority(NotificationCompat.PRIORITY_MAX) - - val imageUrl = remoteMessage.data[KEY_IMAGE] - imageUrl?.let { - val request = ImageRequest.Builder(this) - .data(it) - .target { drawable -> - val bitmap = (drawable as BitmapDrawable).bitmap - notificationBuilder.setLargeIcon(bitmap) - onLargeIconReady(notificationBuilder) + isForeGround: Boolean + ): Intent { + if (isForeGround) { + return Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP + putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) + if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) { + putExtra(KEY_ID, remoteMessage.data[KEY_ID]) } - .build() - - Coil.imageLoader(this).enqueue(request) - } ?: run { - onLargeIconReady(notificationBuilder) + } + } else { + return Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) + if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) { + putExtra(KEY_ID, remoteMessage.data[KEY_ID]) + } + } } - return notificationBuilder } - private fun createBackGroundNotificationBuilder( + private fun createNotificationBuilder( remoteMessage: RemoteMessage, pendingIntent: PendingIntent, onLargeIconReady: (NotificationCompat.Builder) -> Unit @@ -160,20 +149,27 @@ class OffRoadMessagingService : FirebaseMessagingService() { return notificationBuilder } - private fun sendForeGroundNotification(remoteMessage: RemoteMessage) { + private fun sendNotification(remoteMessage: RemoteMessage, isForeGround: Boolean) { val uniqueIdentifier = generateUniqueIdentifier() - createForeGroundNotificationBuilder(remoteMessage) { notificationBuilder -> + val intent = createNotificationIntent(remoteMessage, isForeGround) + val pendingIntent = createPendingIntent(intent, uniqueIdentifier) + createNotificationBuilder(remoteMessage, pendingIntent) { notificationBuilder -> showNotification(notificationBuilder, uniqueIdentifier) } } - private fun sendBackGroundNotification(remoteMessage: RemoteMessage) { - val uniqueIdentifier = generateUniqueIdentifier() - val intent = createNotificationIntent(remoteMessage) - val pendingIntent = createPendingIntent(intent, uniqueIdentifier) - createBackGroundNotificationBuilder(remoteMessage, pendingIntent) { notificationBuilder -> - showNotification(notificationBuilder, uniqueIdentifier) + //브로드캐스트리시버에 필요한 데이터(캐릭터이름, 대화내용, 알림타입) 저장하고 브로드캐스트 발신 + //feature main의 CharacterChatBroadcastReceiver로 가면 됩니다. + private fun sendCharacterNotificationInForeground( + remoteMessage: RemoteMessage, + isForeGround: Boolean + ) { + val intent = Intent("com.teamoffroad.offroad.app.ACTION_RECEIVE_NOTIFICATION").apply { + putExtra(KEY_TITLE, remoteMessage.data[KEY_TITLE]) + putExtra(KEY_BODY, remoteMessage.data[KEY_BODY]) + putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) } + sendBroadcast(intent) } private fun showNotification( diff --git a/core/common/src/main/java/com/teamoffroad/core/common/domain/model/NotificationEvent.kt b/core/common/src/main/java/com/teamoffroad/core/common/domain/model/NotificationEvent.kt new file mode 100644 index 00000000..69b592a0 --- /dev/null +++ b/core/common/src/main/java/com/teamoffroad/core/common/domain/model/NotificationEvent.kt @@ -0,0 +1,7 @@ +package com.teamoffroad.core.common.domain.model + +data class NotificationEvent( + val characterName: String?, + val characterContent: String?, + val type: String?, +) diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 864f7460..d10f39b4 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -17,4 +17,5 @@ dependencies { implementation(libs.gson) implementation(libs.lottie.compose) implementation(libs.coil.svg) + implementation(libs.eventbus) } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index 38fd6f99..c5693758 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -1,7 +1,9 @@ package com.teamoffroad.feature.home.presentation +import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.teamoffroad.core.common.domain.model.NotificationEvent import com.teamoffroad.core.common.domain.repository.DeviceTokenRepository import com.teamoffroad.core.common.domain.usecase.SetAutoSignInUseCase import com.teamoffroad.feature.home.domain.model.Emblem @@ -16,6 +18,9 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import javax.inject.Inject @HiltViewModel @@ -59,6 +64,27 @@ class HomeViewModel @Inject constructor( private val _linearProgressBar = MutableStateFlow(0f) val linearProgressBar = _linearProgressBar.asStateFlow() + init { + //아까 CharacterChatBroadcastReceiver에서 게시한 브로드캐스트리시버를 여기서 받습니다. + EventBus.getDefault().register(this) + } + + //뷰모델이 삭제될때 이벤트버스도 해제시켜줍니다. + override fun onCleared() { + super.onCleared() + EventBus.getDefault().unregister(this) + } + + //브로드캐스트리시버가 작동할때마다 동작하는 함수(fcm발송 > 앱이 포그라운드에 있고, 타입이 캐릭터채팅이라면 작동) + //그런데 홈화면이 아니고 다른화면에서 이 함수가 호출되면 ui가 활성되있지 않기 때문에 ui작업을 할 수 없습니다.(함수 실행될때 로그는 찍힘) + //그래서 데이터스토어 같은 로컬저장소에 데이터와 캐릭터 채팅확인 여부를 저장해두었다가 + //홈화면에 들어와서 채팅확인 여부가 x라면 알림을 보여주고, 알림을 봤다면 다시 채팅확인 여부가 o로 만드는식으로 하면 될거같습니다. + //그래서 포스트맨으로 fcm쏴보면서 요함수에서 하면 될 것 같습니다. + @Subscribe(threadMode = ThreadMode.MAIN) + fun onNotificationEvent(event: NotificationEvent) { + Log.d("characterChat data", event.toString()) + } + fun getUsersAdventuresInformation(category: String) { viewModelScope.launch { runCatching { diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts index a25fadcc..a70c2e55 100644 --- a/feature/main/build.gradle.kts +++ b/feature/main/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.kotlinx.immutable) - + implementation(libs.eventbus) implementation(libs.androidx.constraintlayout.compose) implementation(libs.accompanist.insets) } diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt new file mode 100644 index 00000000..45811044 --- /dev/null +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt @@ -0,0 +1,26 @@ +package com.teamoffroad.feature.main + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.annotation.RequiresApi +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_BODY +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TITLE +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TYPE +import com.teamoffroad.core.common.domain.model.NotificationEvent +import org.greenrobot.eventbus.EventBus + +class CharacterChatBroadcastReceiver : BroadcastReceiver() { + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun onReceive(context: Context, intent: Intent) { + //아까 브로드캐스트한 값들을 이벤트버스에 담아서 게시합니다. 홈화면에서 실시간으로 받을 수 있게하기 위해서! + //다음 메인액티비티로 가면 됩니다. + val notificationTitle = intent.getStringExtra(KEY_TITLE) + val notificationBody = intent.getStringExtra(KEY_BODY) + val notificationType = intent.getStringExtra(KEY_TYPE) + + EventBus.getDefault() + .post(NotificationEvent(notificationTitle, notificationBody, notificationType)) + } +} \ No newline at end of file diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt index 0bc6f00f..4f658f93 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt @@ -2,8 +2,10 @@ package com.teamoffroad.feature.main import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.os.Build import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.annotation.RequiresApi @@ -13,20 +15,33 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.lifecycleScope import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_ID import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TYPE import com.teamoffroad.core.designsystem.theme.OffroadTheme import com.teamoffroad.feature.main.component.MainTransparentActionBar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay +import kotlinx.coroutines.launch @AndroidEntryPoint @RequiresApi(Build.VERSION_CODES.TIRAMISU) class MainActivity : ComponentActivity() { + private val notificationTypeState = mutableStateOf(null) + private val notificationIdState = mutableStateOf(null) + private val myBroadcastReceiver = CharacterChatBroadcastReceiver() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val notificationType = intent.getStringExtra(KEY_TYPE) - val notificationId = intent.getStringExtra(KEY_ID) + notificationTypeState.value = intent.getStringExtra(KEY_TYPE) + notificationIdState.value = intent.getStringExtra(KEY_ID) + + //액티비티 생명주기에 따라 브로드캐스터리시버 만들어주기 + //밑에 onDestroy에서 브로드캐스트리시버 해제도 해줍니다. + //이제 홈 뷰모델로 가면됩니다. + val filter = IntentFilter("com.teamoffroad.offroad.app.ACTION_RECEIVE_NOTIFICATION") + registerReceiver(myBroadcastReceiver, filter, RECEIVER_NOT_EXPORTED) + setContent { val navigator: MainNavigator = rememberMainNavigator() val showSplash = remember { mutableStateOf(true) } @@ -42,14 +57,37 @@ class MainActivity : ComponentActivity() { false -> MainScreen( navigator = navigator, modifier = Modifier, - notificationType = if (!notificationType.isNullOrBlank()) notificationType else null, - notificationId = if (!notificationId.isNullOrBlank()) notificationId else null, + notificationType = notificationTypeState.value, + notificationId = notificationIdState.value, ) } } } } + override fun onDestroy() { + super.onDestroy() + unregisterReceiver(myBroadcastReceiver) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + updateNotificationData(intent) + + Log.d("asdasd", "forground $notificationTypeState") + Log.d("asdasd", "forground $notificationIdState") + } + + private fun updateNotificationData(intent: Intent) { + notificationTypeState.value = intent.getStringExtra(KEY_TYPE) + notificationIdState.value = intent.getStringExtra(KEY_ID) + lifecycleScope.launch { + delay(1500L) + notificationTypeState.value = null + notificationIdState.value = null + } + } + companion object { @JvmStatic fun newInstance(context: Context) = Intent(context, MainActivity::class.java).apply { diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt index 4e0c63be..a5662a2e 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt @@ -23,7 +23,7 @@ internal fun MainScreen( notificationType: String?, notificationId: String?, ) { - LaunchedEffect(Unit) { + LaunchedEffect(notificationType, notificationId) { if (!notificationType.isNullOrBlank()) { if (notificationType == TYPE_ANNOUNCEMENT) notificationId?.let { diff --git a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt index 5c2d8bb7..4e28f9c4 100644 --- a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt +++ b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt @@ -1,5 +1,6 @@ package com.teamoffroad.feature.mypage.presentation +import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -40,9 +41,32 @@ internal fun AnnouncementScreen( viewModel: AnnouncementViewModel = hiltViewModel() ) { val isAnnouncementState by viewModel.announcementUiState.collectAsState() + + //TODO. id 어떻게 초기화? + Log.d("asdsad", announcementId.toString()) + LaunchedEffect(Unit) { viewModel.updateAnnouncement() } +// LaunchedEffect(isAnnouncementState) { +// if (announcementId != null) { +// isAnnouncementState.announcementList.forEach { +// if (it.title.trim().equals(announcementId.trim(), ignoreCase = true)) { +// Log.d("asdsad", "Navigating to detail for $announcementId") +// navigateToAnnouncementDetail( +// it.title, +// it.content, +// it.isImportant, +// it.updateAt, +// it.hasExternalLinks, +// it.externalLinks, +// it.externalLinksTitles, +// ) +// } +// } +// } +// } + Column( modifier = Modifier .navigationPadding() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 97c81079..ef65b808 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -90,6 +90,10 @@ barcodeScanning = "17.1.0" # Process phoenix processPhoenix = "3.0.0" +# Event Bus +eventbus = "3.2.0" + + [libraries] # Core Libraries androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -201,6 +205,9 @@ barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", vers # Process Phoenix process-phoenix = { group = "com.jakewharton", name = "process-phoenix", version.ref = "processPhoenix" } +# Event Bus +eventbus = { group = "org.greenrobot", name = "eventbus", version.ref = "eventbus" } + [plugins] # Android Application Plugin android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } From 669dba792e03c4ba9ae8653c066bb11d589ad06b Mon Sep 17 00:00:00 2001 From: leeseokchan00 <112953135+leeseokchan00@users.noreply.github.com> Date: Sun, 17 Nov 2024 12:21:27 +0900 Subject: [PATCH 16/30] refactor: Edit broadcast receiver --- .../offroad.app/OffRoadMessagingService.kt | 59 +++++++++++-------- .../feature/home/presentation/HomeScreen.kt | 1 - .../home/presentation/HomeViewModel.kt | 1 + .../main/CharacterChatBroadcastReceiver.kt | 54 ++++++++++++++++- .../teamoffroad/feature/main/MainActivity.kt | 50 ++++------------ .../teamoffroad/feature/main/MainScreen.kt | 12 ++++ .../teamoffroad/feature/main/MainUiState.kt | 7 +++ .../teamoffroad/feature/main/MainViewModel.kt | 34 +++++++++++ .../mypage/presentation/AnnouncementScreen.kt | 36 +++++------ 9 files changed, 168 insertions(+), 86 deletions(-) create mode 100644 feature/main/src/main/java/com/teamoffroad/feature/main/MainUiState.kt create mode 100644 feature/main/src/main/java/com/teamoffroad/feature/main/MainViewModel.kt diff --git a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt index 2b52b8a6..b509e89c 100644 --- a/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt +++ b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt @@ -51,7 +51,7 @@ class OffRoadMessagingService : FirebaseMessagingService() { else { // 앱이 포그라운드에 있고, 알림타임이 캐릭터채팅인 경우 // 정현이 봐야할곳은 여기!! 요쪽 따라가십쇼 - sendCharacterNotificationInForeground(remoteMessage, true) + sendCharacterChatNotificationInForeground(remoteMessage) } } else { sendNotification(remoteMessage, false) @@ -98,21 +98,12 @@ class OffRoadMessagingService : FirebaseMessagingService() { remoteMessage: RemoteMessage, isForeGround: Boolean ): Intent { - if (isForeGround) { - return Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_SINGLE_TOP - putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) - if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) { - putExtra(KEY_ID, remoteMessage.data[KEY_ID]) - } - } - } else { - return Intent(this, MainActivity::class.java).apply { + return Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) if (remoteMessage.data[KEY_TYPE] != TYPE_CHARACTER_CHAT) { putExtra(KEY_ID, remoteMessage.data[KEY_ID]) - } + } } } @@ -150,26 +141,44 @@ class OffRoadMessagingService : FirebaseMessagingService() { } private fun sendNotification(remoteMessage: RemoteMessage, isForeGround: Boolean) { - val uniqueIdentifier = generateUniqueIdentifier() - val intent = createNotificationIntent(remoteMessage, isForeGround) - val pendingIntent = createPendingIntent(intent, uniqueIdentifier) - createNotificationBuilder(remoteMessage, pendingIntent) { notificationBuilder -> - showNotification(notificationBuilder, uniqueIdentifier) + if (!isForeGround) { + val uniqueIdentifier = generateUniqueIdentifier() + val intent = createNotificationIntent(remoteMessage, isForeGround) + val pendingIntent = createPendingIntent(intent, uniqueIdentifier) + createNotificationBuilder(remoteMessage, pendingIntent) { notificationBuilder -> + showNotification(notificationBuilder, uniqueIdentifier) + } + } else { + val uniqueIdentifier = generateUniqueIdentifier() + val broadCastIntent = + Intent("com.teamoffroad.offroad.app.ANNOUNCEMENT_FOREGROUND").apply { + putExtra(KEY_TITLE, remoteMessage.data[KEY_TITLE]) + putExtra(KEY_ID, remoteMessage.data[KEY_ID]) + } + val pendingIntent = PendingIntent.getBroadcast( + this, + 0, + broadCastIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE // FLAG_IMMUTABLE 추가 + ) + createNotificationBuilder(remoteMessage, pendingIntent) { notificationBuilder -> + showNotification(notificationBuilder, uniqueIdentifier) + } } } //브로드캐스트리시버에 필요한 데이터(캐릭터이름, 대화내용, 알림타입) 저장하고 브로드캐스트 발신 //feature main의 CharacterChatBroadcastReceiver로 가면 됩니다. - private fun sendCharacterNotificationInForeground( + private fun sendCharacterChatNotificationInForeground( remoteMessage: RemoteMessage, - isForeGround: Boolean ) { - val intent = Intent("com.teamoffroad.offroad.app.ACTION_RECEIVE_NOTIFICATION").apply { - putExtra(KEY_TITLE, remoteMessage.data[KEY_TITLE]) - putExtra(KEY_BODY, remoteMessage.data[KEY_BODY]) - putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) - } - sendBroadcast(intent) + val broadCastIntent = + Intent("com.teamoffroad.offroad.app.CHARACTER_CHAT_FOREGROUND").apply { + putExtra(KEY_TITLE, remoteMessage.data[KEY_TITLE]) + putExtra(KEY_BODY, remoteMessage.data[KEY_BODY]) + putExtra(KEY_TYPE, remoteMessage.data[KEY_TYPE]) + } + sendBroadcast(broadCastIntent) } private fun showNotification( diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 41357691..0ac1a4ae 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -64,7 +64,6 @@ fun HomeScreen( val viewModel: HomeViewModel = hiltViewModel() val isCompleteQuestDialogShown = remember { mutableStateOf(false) } - LaunchedEffect(Unit) { viewModel.updateAutoSignIn() viewModel.updateFcmToken() diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index c5693758..b12129b7 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -64,6 +64,7 @@ class HomeViewModel @Inject constructor( private val _linearProgressBar = MutableStateFlow(0f) val linearProgressBar = _linearProgressBar.asStateFlow() + var asd = MutableStateFlow("") init { //아까 CharacterChatBroadcastReceiver에서 게시한 브로드캐스트리시버를 여기서 받습니다. EventBus.getDefault().register(this) diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt index 45811044..c8eac734 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt @@ -2,18 +2,43 @@ package com.teamoffroad.feature.main import android.content.BroadcastReceiver import android.content.Context +import android.content.Context.RECEIVER_EXPORTED import android.content.Intent +import android.content.IntentFilter import android.os.Build -import androidx.annotation.RequiresApi +import android.util.Log import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_BODY +import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_ID import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TITLE import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TYPE import com.teamoffroad.core.common.domain.model.NotificationEvent import org.greenrobot.eventbus.EventBus -class CharacterChatBroadcastReceiver : BroadcastReceiver() { - @RequiresApi(Build.VERSION_CODES.TIRAMISU) +class CharacterChatBroadcastReceiver( + private val navigateToAnnouncement: (id: String) -> Unit, + private val navigateToHome: (characterName: String, characterChatting: String) -> Unit, +) : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { + + when (intent.action) { + ACTION_ANNOUNCEMENT_FOREGROUND -> { + val announcementID = intent.getStringExtra(KEY_ID) + if (announcementID != null) { + Log.d("asdasd", announcementID) + navigateToAnnouncement(announcementID) + } + } + + ACTION_CHARACTER_CHAT_FOREGROUND -> { + val characterName = intent.getStringExtra(KEY_TITLE) + val characterChatting = intent.getStringExtra(KEY_BODY) + if (characterChatting != null && characterName != null) { + Log.d("asdasd", characterName) + navigateToHome(characterName, characterChatting) + } + } + } + //아까 브로드캐스트한 값들을 이벤트버스에 담아서 게시합니다. 홈화면에서 실시간으로 받을 수 있게하기 위해서! //다음 메인액티비티로 가면 됩니다. val notificationTitle = intent.getStringExtra(KEY_TITLE) @@ -23,4 +48,27 @@ class CharacterChatBroadcastReceiver : BroadcastReceiver() { EventBus.getDefault() .post(NotificationEvent(notificationTitle, notificationBody, notificationType)) } + + companion object { + const val ACTION_CHARACTER_CHAT_FOREGROUND = + "com.teamoffroad.offroad.app.CHARACTER_CHAT_FOREGROUND" + const val ACTION_ANNOUNCEMENT_FOREGROUND = + "com.teamoffroad.offroad.app.ANNOUNCEMENT_FOREGROUND" + + fun register(context: Context, receiver: CharacterChatBroadcastReceiver) { + val intentFilter = IntentFilter().apply { + addAction(ACTION_CHARACTER_CHAT_FOREGROUND) + addAction(ACTION_ANNOUNCEMENT_FOREGROUND) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver(receiver, intentFilter, RECEIVER_EXPORTED) + } else { + context.registerReceiver(receiver, intentFilter) + } + } + + fun unregister(context: Context, receiver: CharacterChatBroadcastReceiver) { + context.unregisterReceiver(receiver) + } + } } \ No newline at end of file diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt index 4f658f93..db53a4fa 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainActivity.kt @@ -2,34 +2,30 @@ package com.teamoffroad.feature.main import android.content.Context import android.content.Intent -import android.content.IntentFilter import android.os.Build import android.os.Bundle -import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.viewModels import androidx.annotation.RequiresApi -import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.lifecycleScope import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_ID import com.teamoffroad.core.common.domain.model.FcmNotificationKey.KEY_TYPE import com.teamoffroad.core.designsystem.theme.OffroadTheme import com.teamoffroad.feature.main.component.MainTransparentActionBar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay -import kotlinx.coroutines.launch @AndroidEntryPoint @RequiresApi(Build.VERSION_CODES.TIRAMISU) class MainActivity : ComponentActivity() { private val notificationTypeState = mutableStateOf(null) private val notificationIdState = mutableStateOf(null) - private val myBroadcastReceiver = CharacterChatBroadcastReceiver() + private lateinit var characterBroadcastReceiver: CharacterChatBroadcastReceiver + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -39,8 +35,11 @@ class MainActivity : ComponentActivity() { //액티비티 생명주기에 따라 브로드캐스터리시버 만들어주기 //밑에 onDestroy에서 브로드캐스트리시버 해제도 해줍니다. //이제 홈 뷰모델로 가면됩니다. - val filter = IntentFilter("com.teamoffroad.offroad.app.ACTION_RECEIVE_NOTIFICATION") - registerReceiver(myBroadcastReceiver, filter, RECEIVER_NOT_EXPORTED) + characterBroadcastReceiver = CharacterChatBroadcastReceiver( + navigateToAnnouncement = viewModel::navigateToAnnouncement, + navigateToHome = viewModel::navigateToHome, + ) + CharacterChatBroadcastReceiver.register(this, characterBroadcastReceiver) setContent { val navigator: MainNavigator = rememberMainNavigator() @@ -59,6 +58,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier, notificationType = notificationTypeState.value, notificationId = notificationIdState.value, + viewModel = viewModel ) } } @@ -67,25 +67,7 @@ class MainActivity : ComponentActivity() { override fun onDestroy() { super.onDestroy() - unregisterReceiver(myBroadcastReceiver) - } - - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - updateNotificationData(intent) - - Log.d("asdasd", "forground $notificationTypeState") - Log.d("asdasd", "forground $notificationIdState") - } - - private fun updateNotificationData(intent: Intent) { - notificationTypeState.value = intent.getStringExtra(KEY_TYPE) - notificationIdState.value = intent.getStringExtra(KEY_ID) - lifecycleScope.launch { - delay(1500L) - notificationTypeState.value = null - notificationIdState.value = null - } + CharacterChatBroadcastReceiver.unregister(this, characterBroadcastReceiver) } companion object { @@ -94,14 +76,4 @@ class MainActivity : ComponentActivity() { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK } } -} - -@Preview(showBackground = true) -@Composable -@RequiresApi(Build.VERSION_CODES.TIRAMISU) -fun GreetingPreview() { - OffroadTheme { - MainScreen(notificationType = "", notificationId = "") - } -} - +} \ No newline at end of file diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt index a5662a2e..d3885a62 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainScreen.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.teamoffroad.core.common.domain.model.FcmNotificationKey.TYPE_ANNOUNCEMENT import com.teamoffroad.core.common.util.OnBackButtonListener @@ -22,7 +24,17 @@ internal fun MainScreen( navigator: MainNavigator = rememberMainNavigator(), notificationType: String?, notificationId: String?, + viewModel: MainViewModel, ) { + val isMainUiState by viewModel.mainUiState.collectAsState() + LaunchedEffect(isMainUiState) { + if (!isMainUiState.characterName.isNullOrBlank() && !isMainUiState.characterChatting.isNullOrBlank()) { + // + } else if (!isMainUiState.announcementId.isNullOrBlank()) { + navigator.navigateToAnnouncement(isMainUiState.announcementId) + } + viewModel.initState() + } LaunchedEffect(notificationType, notificationId) { if (!notificationType.isNullOrBlank()) { if (notificationType == TYPE_ANNOUNCEMENT) diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainUiState.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainUiState.kt new file mode 100644 index 00000000..921092ab --- /dev/null +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainUiState.kt @@ -0,0 +1,7 @@ +package com.teamoffroad.feature.main + +data class MainUiState( + val announcementId: String? = null, + val characterName: String? = null, + val characterChatting: String? = null, +) diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/MainViewModel.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/MainViewModel.kt new file mode 100644 index 00000000..09fb73b0 --- /dev/null +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/MainViewModel.kt @@ -0,0 +1,34 @@ +package com.teamoffroad.feature.main + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class MainViewModel @Inject constructor() : ViewModel() { + private val _mainUiState = MutableStateFlow(MainUiState()) + val mainUiState = _mainUiState.asStateFlow() + + fun navigateToAnnouncement(announcementId: String) { + _mainUiState.value = _mainUiState.value.copy( + announcementId = announcementId + ) + } + + fun navigateToHome(characterName: String, characterChatting: String) { + _mainUiState.value = _mainUiState.value.copy( + characterName = characterName, + characterChatting = characterChatting, + ) + } + + fun initState() { + _mainUiState.value = _mainUiState.value.copy( + announcementId = null, + characterName = null, + characterChatting = null, + ) + } +} \ No newline at end of file diff --git a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt index 4e28f9c4..6c337916 100644 --- a/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt +++ b/feature/mypage/src/main/java/com/teamoffroad/feature/mypage/presentation/AnnouncementScreen.kt @@ -48,24 +48,24 @@ internal fun AnnouncementScreen( LaunchedEffect(Unit) { viewModel.updateAnnouncement() } -// LaunchedEffect(isAnnouncementState) { -// if (announcementId != null) { -// isAnnouncementState.announcementList.forEach { -// if (it.title.trim().equals(announcementId.trim(), ignoreCase = true)) { -// Log.d("asdsad", "Navigating to detail for $announcementId") -// navigateToAnnouncementDetail( -// it.title, -// it.content, -// it.isImportant, -// it.updateAt, -// it.hasExternalLinks, -// it.externalLinks, -// it.externalLinksTitles, -// ) -// } -// } -// } -// } + LaunchedEffect(isAnnouncementState) { + if (announcementId != null) { + isAnnouncementState.announcementList.forEach { + if (it.title.trim().equals(announcementId.trim(), ignoreCase = true)) { + Log.d("asdsad", "Navigating to detail for $announcementId") + navigateToAnnouncementDetail( + it.title, + it.content, + it.isImportant, + it.updateAt, + it.hasExternalLinks, + it.externalLinks, + it.externalLinksTitles, + ) + } + } + } + } Column( modifier = Modifier From bb5bcf8bc6bb2b0029b2545b53da5efcf57dfafe Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 16:18:31 +0900 Subject: [PATCH 17/30] feature: Add character chat api --- feature/home/build.gradle.kts | 1 + .../home/presentation/HomeChatTextField.kt | 9 ++--- .../feature/home/presentation/HomeScreen.kt | 12 +++++-- .../home/presentation/HomeViewModel.kt | 36 ++++++++++++++++++- .../home/presentation/component/HomeIcons.kt | 6 +--- 5 files changed, 51 insertions(+), 13 deletions(-) diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts index 864f7460..37920d0e 100644 --- a/feature/home/build.gradle.kts +++ b/feature/home/build.gradle.kts @@ -11,6 +11,7 @@ android { dependencies { implementation(project(":feature:auth")) + implementation(project(":feature:characterchat")) implementation(libs.retrofit.kotlinx.serialization) implementation(libs.androidx.appcompat) implementation(libs.google.accompanist.permissions) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt index 0983db1d..55ce7ee9 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt @@ -54,6 +54,7 @@ import com.teamoffroad.offroad.feature.home.R fun HomeChatTextField( modifier: Modifier = Modifier, text: String = "", + sentMessage: String, isChatting: MutableState, keyboard: Boolean, onValueChange: (String) -> Unit = {}, @@ -121,9 +122,9 @@ fun HomeChatTextField( style = OffroadTheme.typography.textBold ) Text( - text = "text", + text = sentMessage, modifier = Modifier.weight(1f), - style = OffroadTheme.typography.textRegular + style = OffroadTheme.typography.textRegular, ) } @@ -167,12 +168,12 @@ fun HomeChatTextField( ) Image( painter = painterResource(id = R.drawable.ic_character_chat_send), - contentDescription = null, + contentDescription = "send", modifier = Modifier .padding(end = 2.dp) .size(36.dp) .align(Alignment.CenterEnd) - .clickableWithoutRipple { onSendClick() }, + .clickableWithoutRipple { if (text.isNotBlank()) onSendClick() }, ) } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 83293678..648a8b26 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -76,9 +76,9 @@ fun HomeScreen( val context = LocalContext.current val viewModel: HomeViewModel = hiltViewModel() val isCompleteQuestDialogShown = remember { mutableStateOf(false) } - val imeHeight = WindowInsets.ime.getBottom(LocalDensity.current) val isChatting = remember { mutableStateOf(false) } val chattingText = viewModel.chattingText.collectAsStateWithLifecycle() + val sentMessage = remember { mutableStateOf("") } LaunchedEffect(Unit) { viewModel.updateAutoSignIn() @@ -114,8 +114,6 @@ fun HomeScreen( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) - .imePadding() - //.padding(bottom = with(LocalDensity.current) { imeHeight.toDp() }) .padding(bottom = 196.dp) ) { Column { @@ -134,6 +132,7 @@ fun HomeScreen( HomeChatTextField( text = chattingText.value, + sentMessage = sentMessage.value, isChatting = isChatting, keyboard = true, onValueChange = { text -> @@ -142,6 +141,13 @@ fun HomeScreen( // onFocusChange = { isFocused -> // viewModel.updateIsChatting(isFocused) // } + onSendClick = { + // 내가 보낸 메시지 표시 + sentMessage.value = chattingText.value + viewModel.updateChattingText("") + // 서버에 보내기 + viewModel.sendChat() + } ) } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index 85142822..c101d113 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -2,6 +2,11 @@ package com.teamoffroad.feature.home.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.teamoffroad.characterchat.domain.model.Chat +import com.teamoffroad.characterchat.domain.repository.CharacterChatRepository +import com.teamoffroad.characterchat.presentation.model.ChatModel +import com.teamoffroad.characterchat.presentation.model.ChatType +import com.teamoffroad.characterchat.presentation.model.TimeType import com.teamoffroad.core.common.domain.usecase.SetAutoSignInUseCase import com.teamoffroad.feature.home.domain.model.Emblem import com.teamoffroad.feature.home.domain.model.UserQuests @@ -14,11 +19,13 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import java.time.LocalDateTime import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( private val userRepository: UserRepository, + private val characterChatRepository: CharacterChatRepository, private val setAutoSignInUseCase: SetAutoSignInUseCase, ) : ViewModel() { @@ -49,6 +56,9 @@ class HomeViewModel @Inject constructor( private val _getUserQuestsState = MutableStateFlow>(UiState.Loading) val getUserQuestsState = _getUserQuestsState.asStateFlow() + private val _sendChatState = MutableStateFlow>(UiState.Loading) + val sendChatState = _sendChatState.asStateFlow() + private val _circleProgressBar = MutableStateFlow(0f) val circleProgressBar = _circleProgressBar.asStateFlow() @@ -141,7 +151,7 @@ class HomeViewModel @Inject constructor( } } - fun updateAutoSignIn(){ + fun updateAutoSignIn() { viewModelScope.launch { setAutoSignInUseCase.invoke(true) } @@ -156,4 +166,28 @@ class HomeViewModel @Inject constructor( _chattingText.value = "" } + fun sendChat() { + val chattingText = chattingText.value + + viewModelScope.launch { + runCatching { + val now = LocalDateTime.now() + val userChat = ChatModel( + chatType = ChatType.USER, + text = chattingText, + date = now.toLocalDate(), + time = Triple(TimeType.toTimeType(now.hour), now.hour, now.minute), + ) + characterChatRepository.saveChat(1, chattingText) + }.onSuccess { chat -> + // _patchEmblemState.emit(UiState.Success(state)) + // 보낸 채팅 내용 홈에 보여주어야 함 + _sendChatState.emit(UiState.Success(chat)) + }.onFailure { t -> + val errorMessage = getErrorMessage(t) + _sendChatState.emit(UiState.Failure(errorMessage)) + } + } + } + } \ No newline at end of file diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt index aaa9067d..81e69a1d 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt @@ -32,11 +32,7 @@ fun HomeIcons( navigateToGainedCharacter: () -> Unit, ) { Box( - modifier = Modifier - .fillMaxSize() -// .clickableWithoutRipple { -// isChatting.value = false -// } + modifier = Modifier.fillMaxSize() ) { Column( modifier = Modifier From c5404a2bdb1aa187932dea9f47cad4555ab74211 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 16:47:14 +0900 Subject: [PATCH 18/30] refactor: Edit my chatting text --- .../feature/home/presentation/HomeChatTextField.kt | 7 ++++++- .../teamoffroad/feature/home/presentation/HomeScreen.kt | 2 -- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt index 55ce7ee9..f4fd9a87 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt @@ -123,8 +123,13 @@ fun HomeChatTextField( ) Text( text = sentMessage, - modifier = Modifier.weight(1f), + modifier = Modifier + .fillMaxWidth() + .onGloballyPositioned { layoutCoordinates -> + textFieldHeight.intValue = layoutCoordinates.size.height + }, style = OffroadTheme.typography.textRegular, + maxLines = 2, ) } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 648a8b26..5e28a1b4 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -142,10 +142,8 @@ fun HomeScreen( // viewModel.updateIsChatting(isFocused) // } onSendClick = { - // 내가 보낸 메시지 표시 sentMessage.value = chattingText.value viewModel.updateChattingText("") - // 서버에 보내기 viewModel.sendChat() } ) From bb206bec6117a1cd123815537a4dfe74f9c59d53 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 16:56:40 +0900 Subject: [PATCH 19/30] refactor: Delete imports --- .../teamoffroad/feature/home/presentation/HomeScreen.kt | 8 -------- 1 file changed, 8 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 5e28a1b4..c9d5b279 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -4,19 +4,14 @@ import android.content.Context import android.os.Build import android.widget.Toast import androidx.annotation.RequiresApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -31,8 +26,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -40,7 +33,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.teamoffroad.core.designsystem.component.actionBarPadding import com.teamoffroad.core.designsystem.component.clickableWithoutRipple -import com.teamoffroad.core.designsystem.theme.ErrorNew import com.teamoffroad.core.designsystem.theme.HomeGradi1 import com.teamoffroad.core.designsystem.theme.HomeGradi2 import com.teamoffroad.core.designsystem.theme.HomeGradi3 From e6b9c9325287ba5e187960ba61e7abd2c90eb2bc Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 18:14:16 +0900 Subject: [PATCH 20/30] feature: Add home character chat --- .../feature/home/presentation/HomeScreen.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index c67f6e16..42a0d4aa 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -27,20 +27,25 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.teamoffroad.core.designsystem.component.actionBarPadding import com.teamoffroad.core.designsystem.component.clickableWithoutRipple +import com.teamoffroad.core.designsystem.theme.BtnInactive import com.teamoffroad.core.designsystem.theme.HomeGradi1 import com.teamoffroad.core.designsystem.theme.HomeGradi2 import com.teamoffroad.core.designsystem.theme.HomeGradi3 import com.teamoffroad.core.designsystem.theme.HomeGradi4 import com.teamoffroad.core.designsystem.theme.HomeGradi5 import com.teamoffroad.core.designsystem.theme.HomeGradi6 +import com.teamoffroad.core.designsystem.theme.Main2 +import com.teamoffroad.core.designsystem.theme.Main3 import com.teamoffroad.core.designsystem.theme.OffroadTheme import com.teamoffroad.core.designsystem.theme.Sub +import com.teamoffroad.core.designsystem.theme.Sub4 import com.teamoffroad.core.designsystem.theme.Sub55 import com.teamoffroad.core.designsystem.theme.White import com.teamoffroad.feature.home.domain.model.UserQuests @@ -143,6 +148,17 @@ fun HomeScreen( } } } + + + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier + .padding(start = 24.dp, top = 70.dp, end = 24.dp) + ) { + CharacterChat( + text = "맛있었겠다" + ) + } } if (isCompleteQuestDialogShown.value) { @@ -156,6 +172,47 @@ fun HomeScreen( } } +@Composable +fun CharacterChat( + text: String, + characterTextColor: Color = Sub4, + characterTextStyle: TextStyle = OffroadTheme.typography.textBold, + messageTextColor: Color = Main2, + messageTextStyle: TextStyle = OffroadTheme.typography.textRegular, + backgroundColor: Color = Main3, + borderColor: Color = BtnInactive +) { + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = backgroundColor, + shape = RoundedCornerShape(12.dp) + ) + .padding(vertical = 14.dp, horizontal = 18.dp) + ) { + Column { + Row { + Text( + text = "아루 : ", + modifier = Modifier, + color = characterTextColor, + style = characterTextStyle + ) + Text( + text = "맛있었겠다!!", + modifier = Modifier.fillMaxWidth(), + color = messageTextColor, + style = messageTextStyle, + maxLines = 2 + ) + } + AnswerCharacterChat() + } + + } +} + @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable private fun UsersAdventuresInformation( @@ -242,6 +299,31 @@ fun FinishChatting( ) } +@Composable +fun AnswerCharacterChat( + backgroundColor: Color = Main2, + textColor: Color = Main3, + textStyle: TextStyle = OffroadTheme.typography.textContents +) { + Box( + modifier = Modifier.fillMaxWidth() + ) { + Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxWidth()) { + Text( + text = "답장하기", + modifier = Modifier + .background( + color = backgroundColor, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 14.dp, vertical = 6.dp), + color = textColor, + style = textStyle + ) + } + } +} + @Composable private fun UsersQuestInformation( context: Context, From 5e8bcb880550288e33a9f514a57a95adc33f9c88 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 22:40:37 +0900 Subject: [PATCH 21/30] feature: Add Character Chat --- .../home/presentation/HomeChatTextField.kt | 59 +++++++++++++++---- .../feature/home/presentation/HomeScreen.kt | 56 ++++++++++++------ .../home/presentation/HomeViewModel.kt | 45 +++++++++++--- .../presentation/model/CharacterChatModel.kt | 6 ++ 4 files changed, 127 insertions(+), 39 deletions(-) create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/presentation/model/CharacterChatModel.kt diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt index f4fd9a87..52fe7132 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt @@ -8,6 +8,9 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -40,9 +43,17 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition import com.teamoffroad.core.designsystem.component.clickableWithoutRipple import com.teamoffroad.core.designsystem.theme.BtnInactive +import com.teamoffroad.core.designsystem.theme.ErrorNew +import com.teamoffroad.core.designsystem.theme.Kakao import com.teamoffroad.core.designsystem.theme.Main2 import com.teamoffroad.core.designsystem.theme.OffroadTheme import com.teamoffroad.core.designsystem.theme.Transparent @@ -65,6 +76,7 @@ fun HomeChatTextField( val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val contextView = LocalView.current + var showLottieLoading by remember { mutableStateOf(false) } var keyboardVisible by remember { mutableStateOf(keyboard) } @@ -110,27 +122,48 @@ fun HomeChatTextField( ) { val textFieldHeight = remember { mutableIntStateOf(0) } - Column { + Column{ Row( modifier = Modifier .fillMaxWidth() - .padding(top = 18.dp, bottom = 4.dp) + .padding(top = 6.dp, bottom = 4.dp) ) { Text( text = stringResource(id = R.string.home_chat_me), color = Main2, - style = OffroadTheme.typography.textBold - ) - Text( - text = sentMessage, - modifier = Modifier - .fillMaxWidth() - .onGloballyPositioned { layoutCoordinates -> - textFieldHeight.intValue = layoutCoordinates.size.height - }, - style = OffroadTheme.typography.textRegular, - maxLines = 2, + style = OffroadTheme.typography.textBold, + textAlign = TextAlign.Center, + modifier = Modifier.padding(vertical = 6.dp) ) + Box{ + if (text.isNotBlank()) { + Box( + modifier = Modifier + .size(width = 54.dp, height = 27.dp) + ) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(com.teamoffroad.offroad.core.designsystem.R.raw.loading_linear)) + val animationState = animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever) + + if (animationState.isAtEnd && animationState.isPlaying) { + LaunchedEffect(Unit) { } + } + + LottieAnimation(composition, animationState.progress) + } + } else { + Text( + text = sentMessage, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 6.dp) + .onGloballyPositioned { layoutCoordinates -> + textFieldHeight.intValue = layoutCoordinates.size.height + }, + style = OffroadTheme.typography.textRegular, + maxLines = 2, + ) + } + } } Box { diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 42a0d4aa..bd83f878 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -75,7 +75,10 @@ fun HomeScreen( val isCompleteQuestDialogShown = remember { mutableStateOf(false) } val isChatting = remember { mutableStateOf(false) } val chattingText = viewModel.chattingText.collectAsStateWithLifecycle() - val sentMessage = remember { mutableStateOf("") } + val sendMessage = remember { mutableStateOf("") } + val characterChat = viewModel.getCharacterChat.collectAsStateWithLifecycle() + val isCharacterChatting = viewModel.isCharacterChatting.collectAsStateWithLifecycle() + val answerCharacterChat = remember{ mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.updateAutoSignIn() @@ -130,34 +133,40 @@ fun HomeScreen( HomeChatTextField( text = chattingText.value, - sentMessage = sentMessage.value, + sentMessage = sendMessage.value, isChatting = isChatting, keyboard = true, onValueChange = { text -> + // TODO: 입력 중일 때 로딩 로티 띄우기 viewModel.updateChattingText(text) }, // onFocusChange = { isFocused -> // viewModel.updateIsChatting(isFocused) // } onSendClick = { - sentMessage.value = chattingText.value - viewModel.updateChattingText("") + answerCharacterChat.value = true + sendMessage.value = chattingText.value viewModel.sendChat() + viewModel.updateChattingText("") } ) } } } - - Box( - contentAlignment = Alignment.TopCenter, - modifier = Modifier - .padding(start = 24.dp, top = 70.dp, end = 24.dp) - ) { - CharacterChat( - text = "맛있었겠다" - ) + if (isCharacterChatting.value) { + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier + .padding(start = 24.dp, top = 70.dp, end = 24.dp) + ) { + CharacterChat( + isChatting = isChatting, + answerCharacterChat = answerCharacterChat, + characterName = characterChat.value.characterName, + characterContent = characterChat.value.characterContent + ) + } } } @@ -174,7 +183,10 @@ fun HomeScreen( @Composable fun CharacterChat( - text: String, + isChatting: MutableState, + answerCharacterChat: MutableState, + characterName: String, + characterContent: String, characterTextColor: Color = Sub4, characterTextStyle: TextStyle = OffroadTheme.typography.textBold, messageTextColor: Color = Main2, @@ -194,20 +206,24 @@ fun CharacterChat( Column { Row { Text( - text = "아루 : ", + text = "${characterName} : ", modifier = Modifier, color = characterTextColor, style = characterTextStyle ) Text( - text = "맛있었겠다!!", + text = characterContent, modifier = Modifier.fillMaxWidth(), color = messageTextColor, style = messageTextStyle, maxLines = 2 ) } - AnswerCharacterChat() + if (!answerCharacterChat.value) { + // 사용자가 답변을 보냈을 때 + AnswerCharacterChat(isChatting = isChatting) + // TODO: 알림에 로딩 로티 띄우기 + } } } @@ -301,6 +317,7 @@ fun FinishChatting( @Composable fun AnswerCharacterChat( + isChatting: MutableState, backgroundColor: Color = Main2, textColor: Color = Main3, textStyle: TextStyle = OffroadTheme.typography.textContents @@ -316,7 +333,10 @@ fun AnswerCharacterChat( color = backgroundColor, shape = RoundedCornerShape(8.dp) ) - .padding(horizontal = 14.dp, vertical = 6.dp), + .padding(horizontal = 14.dp, vertical = 6.dp) + .clickableWithoutRipple { + isChatting.value = true + }, color = textColor, style = textStyle ) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index bcf3c6d1..3b08ae1c 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -18,6 +18,7 @@ import com.teamoffroad.feature.home.domain.repository.UserRepository import com.teamoffroad.feature.home.domain.usecase.PostFcmTokenUseCase import com.teamoffroad.feature.home.presentation.component.UiState import com.teamoffroad.feature.home.presentation.component.getErrorMessage +import com.teamoffroad.feature.home.presentation.model.CharacterChatModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -38,6 +39,8 @@ class HomeViewModel @Inject constructor( private val deviceTokenRepository: DeviceTokenRepository, private val fcmTokenUseCase: PostFcmTokenUseCase, ) : ViewModel() { + private val _getCharacterChat = MutableStateFlow(CharacterChatModel("", "")) + val getCharacterChat = _getCharacterChat.asStateFlow() private val _getUsersAdventuresInformationState = MutableStateFlow>( @@ -75,12 +78,16 @@ class HomeViewModel @Inject constructor( private val _linearProgressBar = MutableStateFlow(0f) val linearProgressBar = _linearProgressBar.asStateFlow() - private val _isChatting: MutableStateFlow = MutableStateFlow(false) - val isChatting: StateFlow = _isChatting.asStateFlow() + private val _isCharacterChatting: MutableStateFlow = MutableStateFlow(false) + val isCharacterChatting: StateFlow = _isCharacterChatting.asStateFlow() private val _chattingText: MutableStateFlow = MutableStateFlow("") val chattingText: StateFlow = _chattingText.asStateFlow() + private val _characterName = MutableStateFlow("") + val characterName = _characterName.asStateFlow() + + var asd = MutableStateFlow("") init { //아까 CharacterChatBroadcastReceiver에서 게시한 브로드캐스트리시버를 여기서 받습니다. @@ -101,6 +108,19 @@ class HomeViewModel @Inject constructor( @Subscribe(threadMode = ThreadMode.MAIN) fun onNotificationEvent(event: NotificationEvent) { Log.d("characterChat data", event.toString()) + + // 1. 홈 화면에서 캐릭터한테 메시지가 왔을 때 + val characterName = event.characterName + val characterContent = event.characterContent + if(characterName != null && characterContent != null) { + _characterName.value = characterName + _getCharacterChat.value = CharacterChatModel(characterName, characterContent) + _isCharacterChatting.value = true + } + + + // 2. 로컬에 내용 저장 characterName,characterContent, confirm (이건 우선 나중에) + } fun getUsersAdventuresInformation(category: String) { @@ -109,6 +129,7 @@ class HomeViewModel @Inject constructor( userRepository.getUsersAdventuresInformation(category) }.onSuccess { state -> _getUsersAdventuresInformationState.emit(UiState.Success(state)) + _characterName.value = state.characterName updateSelectedEmblem(state.emblemName) updateCharacterImage(state.baseImageUrl) updateMotionImageUrl(state.motionImageUrl) @@ -193,11 +214,6 @@ class HomeViewModel @Inject constructor( _chattingText.value = text } - fun updateIsChatting(boolean: Boolean) { - _isChatting.value = boolean - _chattingText.value = "" - } - fun sendChat() { val chattingText = chattingText.value @@ -212,9 +228,22 @@ class HomeViewModel @Inject constructor( ) characterChatRepository.saveChat(1, chattingText) }.onSuccess { chat -> - // _patchEmblemState.emit(UiState.Success(state)) // 보낸 채팅 내용 홈에 보여주어야 함 _sendChatState.emit(UiState.Success(chat)) + /* + val characterName = event.characterName + val characterContent = event.characterContent + if(characterName != null && characterContent != null) { + _getCharacterChat.value = CharacterChatModel(characterName, characterContent) + _isCharacterChatting.value = true + } + */ + val characterContent = chat.content + if (characterContent != null) { + _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent) + _isCharacterChatting.value = true + } + }.onFailure { t -> val errorMessage = getErrorMessage(t) _sendChatState.emit(UiState.Failure(errorMessage)) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/model/CharacterChatModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/model/CharacterChatModel.kt new file mode 100644 index 00000000..8fda6f2d --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/model/CharacterChatModel.kt @@ -0,0 +1,6 @@ +package com.teamoffroad.feature.home.presentation.model + +data class CharacterChatModel( + val characterName: String, + val characterContent: String, +) \ No newline at end of file From 54a33c2f96ff5ed65c7b1d04275d5b65179a6d22 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 23:25:59 +0900 Subject: [PATCH 22/30] feature: Add isCharacterChattingLoading --- .../feature/home/presentation/HomeScreen.kt | 63 +++++++++++++------ .../home/presentation/HomeViewModel.kt | 18 +++--- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index bd83f878..64b8ae36 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -13,12 +13,15 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -32,6 +35,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition import com.teamoffroad.core.designsystem.component.actionBarPadding import com.teamoffroad.core.designsystem.component.clickableWithoutRipple import com.teamoffroad.core.designsystem.theme.BtnInactive @@ -78,7 +86,8 @@ fun HomeScreen( val sendMessage = remember { mutableStateOf("") } val characterChat = viewModel.getCharacterChat.collectAsStateWithLifecycle() val isCharacterChatting = viewModel.isCharacterChatting.collectAsStateWithLifecycle() - val answerCharacterChat = remember{ mutableStateOf(false) } + val isCharacterChattingLoading = viewModel.isCharacterChattingLoading.collectAsStateWithLifecycle() + val userSendChat = remember{ mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.updateAutoSignIn() @@ -137,17 +146,13 @@ fun HomeScreen( isChatting = isChatting, keyboard = true, onValueChange = { text -> - // TODO: 입력 중일 때 로딩 로티 띄우기 viewModel.updateChattingText(text) }, -// onFocusChange = { isFocused -> -// viewModel.updateIsChatting(isFocused) -// } onSendClick = { - answerCharacterChat.value = true - sendMessage.value = chattingText.value - viewModel.sendChat() - viewModel.updateChattingText("") + userSendChat.value = true // 사용자가 채팅 보냄 + sendMessage.value = chattingText.value // 보낼 메시지 + viewModel.sendChat() // 서버에 보내기 + viewModel.updateChattingText("") // 초기화 } ) } @@ -162,7 +167,8 @@ fun HomeScreen( ) { CharacterChat( isChatting = isChatting, - answerCharacterChat = answerCharacterChat, + isCharacterChattingLoading = isCharacterChattingLoading, + answerCharacterChat = userSendChat, characterName = characterChat.value.characterName, characterContent = characterChat.value.characterContent ) @@ -184,6 +190,7 @@ fun HomeScreen( @Composable fun CharacterChat( isChatting: MutableState, + isCharacterChattingLoading: State, answerCharacterChat: MutableState, characterName: String, characterContent: String, @@ -206,23 +213,39 @@ fun CharacterChat( Column { Row { Text( - text = "${characterName} : ", + text = "$characterName : ", modifier = Modifier, color = characterTextColor, style = characterTextStyle ) - Text( - text = characterContent, - modifier = Modifier.fillMaxWidth(), - color = messageTextColor, - style = messageTextStyle, - maxLines = 2 - ) + + // 메세지를 보냈을 때 로티 띄우기 + if (isCharacterChattingLoading.value) { + Box( + modifier = Modifier + .size(width = 54.dp, height = 27.dp) + ) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(com.teamoffroad.offroad.core.designsystem.R.raw.loading_linear)) + val animationState = animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever) + + if (animationState.isAtEnd && animationState.isPlaying) { + LaunchedEffect(Unit) { } + } + + LottieAnimation(composition, animationState.progress) + } + } else { + Text( + text = characterContent, + modifier = Modifier.fillMaxWidth(), + color = messageTextColor, + style = messageTextStyle, + maxLines = 2 + ) + } } if (!answerCharacterChat.value) { - // 사용자가 답변을 보냈을 때 AnswerCharacterChat(isChatting = isChatting) - // TODO: 알림에 로딩 로티 띄우기 } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index 3b08ae1c..6130ee13 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -81,13 +81,15 @@ class HomeViewModel @Inject constructor( private val _isCharacterChatting: MutableStateFlow = MutableStateFlow(false) val isCharacterChatting: StateFlow = _isCharacterChatting.asStateFlow() + private val _isCharacterChattingLoading = MutableStateFlow(false) + val isCharacterChattingLoading = _isCharacterChattingLoading.asStateFlow() + private val _chattingText: MutableStateFlow = MutableStateFlow("") val chattingText: StateFlow = _chattingText.asStateFlow() private val _characterName = MutableStateFlow("") val characterName = _characterName.asStateFlow() - var asd = MutableStateFlow("") init { //아까 CharacterChatBroadcastReceiver에서 게시한 브로드캐스트리시버를 여기서 받습니다. @@ -113,8 +115,7 @@ class HomeViewModel @Inject constructor( val characterName = event.characterName val characterContent = event.characterContent if(characterName != null && characterContent != null) { - _characterName.value = characterName - _getCharacterChat.value = CharacterChatModel(characterName, characterContent) + _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent) _isCharacterChatting.value = true } @@ -216,6 +217,7 @@ class HomeViewModel @Inject constructor( fun sendChat() { val chattingText = chattingText.value + _isCharacterChattingLoading.value = true viewModelScope.launch { runCatching { @@ -230,14 +232,8 @@ class HomeViewModel @Inject constructor( }.onSuccess { chat -> // 보낸 채팅 내용 홈에 보여주어야 함 _sendChatState.emit(UiState.Success(chat)) - /* - val characterName = event.characterName - val characterContent = event.characterContent - if(characterName != null && characterContent != null) { - _getCharacterChat.value = CharacterChatModel(characterName, characterContent) - _isCharacterChatting.value = true - } - */ + _isCharacterChattingLoading.value = false + val characterContent = chat.content if (characterContent != null) { _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent) From 9423e7ed9222d079b61e936d35ee84f5a7057b35 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 23:33:55 +0900 Subject: [PATCH 23/30] refactor: Edit home character image url --- .../component/character/CharacterItem.kt | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterItem.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterItem.kt index 3c776500..184271f0 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterItem.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterItem.kt @@ -73,23 +73,22 @@ class CharacterItem { .fillMaxHeight() .align(Alignment.BottomCenter) ) { - Image( - painter = painterResource(id = R.drawable.img_home_character), - contentDescription = "character", - modifier = Modifier.fillMaxSize() - ) - -// AsyncImage( -// model = ImageRequest.Builder(context) -// .data(baseCharacterImage) -// .decoderFactory(SvgDecoder.Factory()) -// .build(), -// contentDescription = "explorer", -// modifier = Modifier -// .fillMaxSize() -// .align(Alignment.BottomCenter), -// // TODO: placeholder, error일 때 +// Image( +// painter = painterResource(id = R.drawable.img_home_character), +// contentDescription = "character", +// modifier = Modifier.fillMaxSize() // ) + + AsyncImage( + model = ImageRequest.Builder(context) + .data(baseCharacterImage) + .decoderFactory(SvgDecoder.Factory()) + .build(), + contentDescription = "explorer", + modifier = Modifier + .fillMaxSize() + .align(Alignment.BottomCenter), + ) } } else { val composition by rememberLottieComposition( From 827637431c11ed0c73a67bff923c4dc4c6dab9db Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Sun, 17 Nov 2024 23:41:17 +0900 Subject: [PATCH 24/30] feature: Add update isCharacterChatting --- .../feature/home/presentation/HomeChatTextField.kt | 3 +++ .../feature/home/presentation/HomeScreen.kt | 1 + .../feature/home/presentation/HomeViewModel.kt | 10 ++++++++-- .../feature/home/presentation/component/HomeIcons.kt | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt index 52fe7132..28ca8e83 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt @@ -27,6 +27,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -68,6 +69,7 @@ fun HomeChatTextField( sentMessage: String, isChatting: MutableState, keyboard: Boolean, + isCharacterChatting: (Boolean) -> Unit, onValueChange: (String) -> Unit = {}, onFocusChange: (Boolean) -> Unit = {}, onSendClick: () -> Unit = {}, @@ -90,6 +92,7 @@ fun HomeChatTextField( if (!keyboardVisible) { focusManager.clearFocus() isChatting.value = false + isCharacterChatting(false) } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 64b8ae36..0f5dcaa8 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -145,6 +145,7 @@ fun HomeScreen( sentMessage = sendMessage.value, isChatting = isChatting, keyboard = true, + isCharacterChatting = viewModel::updateCharacterChatting, onValueChange = { text -> viewModel.updateChattingText(text) }, diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index 6130ee13..96ced1d3 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -116,7 +116,8 @@ class HomeViewModel @Inject constructor( val characterContent = event.characterContent if(characterName != null && characterContent != null) { _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent) - _isCharacterChatting.value = true + //_isCharacterChatting.value = true + updateCharacterChatting(true) } @@ -124,6 +125,10 @@ class HomeViewModel @Inject constructor( } + fun updateCharacterChatting(state: Boolean) { + _isCharacterChatting.value = state + } + fun getUsersAdventuresInformation(category: String) { viewModelScope.launch { runCatching { @@ -237,7 +242,8 @@ class HomeViewModel @Inject constructor( val characterContent = chat.content if (characterContent != null) { _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent) - _isCharacterChatting.value = true + //_isCharacterChatting.value = true + updateCharacterChatting(true) } }.onFailure { t -> diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt index 81e69a1d..80e01acd 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt @@ -44,6 +44,7 @@ fun HomeIcons( Image( painter = painterResource(id = R.drawable.ic_home_chat), contentDescription = "chat", + //없앨 부분 modifier = Modifier.clickableWithoutRipple { isChatting.value = true } From b673359c6b0327a8c860afe847df46a668c9bf26 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Mon, 18 Nov 2024 23:23:45 +0900 Subject: [PATCH 25/30] feature: Add ic_home_accordion --- .../feature/home/presentation/HomeScreen.kt | 19 +++++++++++++++---- .../home/presentation/HomeViewModel.kt | 5 ----- .../main/res/drawable/ic_home_accordion.xml | 17 +++++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 feature/home/src/main/res/drawable/ic_home_accordion.xml diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 0f5dcaa8..54f323ad 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Build import android.widget.Toast import androidx.annotation.RequiresApi +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box @@ -29,8 +30,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel @@ -202,6 +205,8 @@ fun CharacterChat( backgroundColor: Color = Main3, borderColor: Color = BtnInactive ) { + val checkCharacterChattingLines = remember { mutableStateOf(false) } + Box( modifier = Modifier .fillMaxWidth() @@ -220,7 +225,6 @@ fun CharacterChat( style = characterTextStyle ) - // 메세지를 보냈을 때 로티 띄우기 if (isCharacterChattingLoading.value) { Box( modifier = Modifier @@ -238,13 +242,20 @@ fun CharacterChat( } else { Text( text = characterContent, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.weight(1f), color = messageTextColor, style = messageTextStyle, - maxLines = 2 + onTextLayout = { textLayoutResult -> + checkCharacterChattingLines.value = textLayoutResult.lineCount >= 3 + }, + maxLines = 2, + overflow = TextOverflow.Ellipsis, ) } + + Image(painter = painterResource(id = R.drawable.ic_home_accordion), contentDescription = "accordion down") } + if (!answerCharacterChat.value) { AnswerCharacterChat(isChatting = isChatting) } @@ -347,7 +358,7 @@ fun AnswerCharacterChat( textStyle: TextStyle = OffroadTheme.typography.textContents ) { Box( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(top = 10.dp) ) { Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxWidth()) { Text( diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt index 96ced1d3..541118b2 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeViewModel.kt @@ -116,13 +116,9 @@ class HomeViewModel @Inject constructor( val characterContent = event.characterContent if(characterName != null && characterContent != null) { _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent) - //_isCharacterChatting.value = true updateCharacterChatting(true) } - - // 2. 로컬에 내용 저장 characterName,characterContent, confirm (이건 우선 나중에) - } fun updateCharacterChatting(state: Boolean) { @@ -242,7 +238,6 @@ class HomeViewModel @Inject constructor( val characterContent = chat.content if (characterContent != null) { _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent) - //_isCharacterChatting.value = true updateCharacterChatting(true) } diff --git a/feature/home/src/main/res/drawable/ic_home_accordion.xml b/feature/home/src/main/res/drawable/ic_home_accordion.xml new file mode 100644 index 00000000..c218271b --- /dev/null +++ b/feature/home/src/main/res/drawable/ic_home_accordion.xml @@ -0,0 +1,17 @@ + + + + + + From af89b847e8ef1caf41d0270d059fd2c0fc918783 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Mon, 18 Nov 2024 23:39:33 +0900 Subject: [PATCH 26/30] feature: Add accordion rotationX --- .../feature/home/presentation/HomeScreen.kt | 51 ++++++++++++++++--- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 54f323ad..36f6c576 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -2,8 +2,11 @@ package com.teamoffroad.feature.home.presentation import android.content.Context import android.os.Build +import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -29,6 +32,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -89,8 +93,9 @@ fun HomeScreen( val sendMessage = remember { mutableStateOf("") } val characterChat = viewModel.getCharacterChat.collectAsStateWithLifecycle() val isCharacterChatting = viewModel.isCharacterChatting.collectAsStateWithLifecycle() - val isCharacterChattingLoading = viewModel.isCharacterChattingLoading.collectAsStateWithLifecycle() - val userSendChat = remember{ mutableStateOf(false) } + val isCharacterChattingLoading = + viewModel.isCharacterChattingLoading.collectAsStateWithLifecycle() + val userSendChat = remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.updateAutoSignIn() @@ -206,6 +211,13 @@ fun CharacterChat( borderColor: Color = BtnInactive ) { val checkCharacterChattingLines = remember { mutableStateOf(false) } + val isExpanded = remember { mutableStateOf(false) } + + val rotationAngle by animateFloatAsState( + targetValue = if (isExpanded.value) 180f else 0f, + animationSpec = tween(durationMillis = 300), label = "" + ) + Box( modifier = Modifier @@ -214,6 +226,11 @@ fun CharacterChat( color = backgroundColor, shape = RoundedCornerShape(12.dp) ) + .border( + width = 1.dp, + shape = RoundedCornerShape(12.dp), + color = borderColor + ) .padding(vertical = 14.dp, horizontal = 18.dp) ) { Column { @@ -230,11 +247,18 @@ fun CharacterChat( modifier = Modifier .size(width = 54.dp, height = 27.dp) ) { - val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(com.teamoffroad.offroad.core.designsystem.R.raw.loading_linear)) - val animationState = animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever) + val composition by rememberLottieComposition( + LottieCompositionSpec.RawRes( + com.teamoffroad.offroad.core.designsystem.R.raw.loading_linear + ) + ) + val animationState = animateLottieCompositionAsState( + composition, + iterations = LottieConstants.IterateForever + ) if (animationState.isAtEnd && animationState.isPlaying) { - LaunchedEffect(Unit) { } + LaunchedEffect(Unit) { } } LottieAnimation(composition, animationState.progress) @@ -248,12 +272,21 @@ fun CharacterChat( onTextLayout = { textLayoutResult -> checkCharacterChattingLines.value = textLayoutResult.lineCount >= 3 }, - maxLines = 2, + maxLines = if (isExpanded.value) Int.MAX_VALUE else 2, overflow = TextOverflow.Ellipsis, ) + + Image( + painter = painterResource(id = R.drawable.ic_home_accordion), + contentDescription = "accordion down", + modifier = Modifier + .graphicsLayer(rotationX = rotationAngle) + .clickableWithoutRipple { + isExpanded.value = !isExpanded.value + } + ) } - Image(painter = painterResource(id = R.drawable.ic_home_accordion), contentDescription = "accordion down") } if (!answerCharacterChat.value) { @@ -358,7 +391,9 @@ fun AnswerCharacterChat( textStyle: TextStyle = OffroadTheme.typography.textContents ) { Box( - modifier = Modifier.fillMaxWidth().padding(top = 10.dp) + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp) ) { Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxWidth()) { Text( From ddf036157b4e51e520a39c2c1615b3ed2b6f39ed Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Mon, 18 Nov 2024 23:56:13 +0900 Subject: [PATCH 27/30] feature: Connect home and characterchat --- .../feature/home/navigation/HomeNavigation.kt | 2 ++ .../feature/home/presentation/HomeScreen.kt | 24 +++++++++++++++---- .../home/presentation/component/HomeIcons.kt | 10 ++++---- .../feature/main/component/MainNavHost.kt | 3 +++ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/navigation/HomeNavigation.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/navigation/HomeNavigation.kt index 4aaedea0..a3233928 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/navigation/HomeNavigation.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/navigation/HomeNavigation.kt @@ -21,6 +21,7 @@ fun NavController.navigateToHome( @RequiresApi(Build.VERSION_CODES.TIRAMISU) fun NavGraphBuilder.homeNavGraph( navigateToBack: () -> Unit, + navigateToCharacterChatScreen: (Int, String) -> Unit, navigateToGainedCharacter: () -> Unit, ) { composable { backStackEntry -> @@ -29,6 +30,7 @@ fun NavGraphBuilder.homeNavGraph( HomeScreen( category = category, completeQuests = completeQuests, + navigateToCharacterChatScreen = navigateToCharacterChatScreen, navigateToGainedCharacter = navigateToGainedCharacter, ) } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 36f6c576..3fc8040f 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -84,6 +84,7 @@ fun HomeScreen( category: String?, completeQuests: List = emptyList(), navigateToGainedCharacter: () -> Unit = {}, + navigateToCharacterChatScreen: (Int, String) -> Unit ) { val context = LocalContext.current val viewModel: HomeViewModel = hiltViewModel() @@ -96,6 +97,7 @@ fun HomeScreen( val isCharacterChattingLoading = viewModel.isCharacterChattingLoading.collectAsStateWithLifecycle() val userSendChat = remember { mutableStateOf(false) } + val characterName = viewModel.characterName.collectAsStateWithLifecycle() LaunchedEffect(Unit) { viewModel.updateAutoSignIn() @@ -116,11 +118,13 @@ fun HomeScreen( UsersAdventuresInformation( isChatting = isChatting, context = context, + characterName = characterName.value, modifier = Modifier .weight(1f) .actionBarPadding(), viewModel = viewModel, navigateToGainedCharacter = navigateToGainedCharacter, + navigateToCharacterChatScreen = navigateToCharacterChatScreen ) Spacer(modifier = Modifier.padding(top = 12.dp)) UsersQuestInformation(context, viewModel) @@ -178,8 +182,9 @@ fun HomeScreen( isChatting = isChatting, isCharacterChattingLoading = isCharacterChattingLoading, answerCharacterChat = userSendChat, - characterName = characterChat.value.characterName, - characterContent = characterChat.value.characterContent + characterName = characterName.value, + characterContent = characterChat.value.characterContent, + navigateToCharacterChatScreen = navigateToCharacterChatScreen ) } } @@ -208,7 +213,8 @@ fun CharacterChat( messageTextColor: Color = Main2, messageTextStyle: TextStyle = OffroadTheme.typography.textRegular, backgroundColor: Color = Main3, - borderColor: Color = BtnInactive + borderColor: Color = BtnInactive, + navigateToCharacterChatScreen: (Int, String) -> Unit ) { val checkCharacterChattingLines = remember { mutableStateOf(false) } val isExpanded = remember { mutableStateOf(false) } @@ -218,7 +224,6 @@ fun CharacterChat( animationSpec = tween(durationMillis = 300), label = "" ) - Box( modifier = Modifier .fillMaxWidth() @@ -232,6 +237,10 @@ fun CharacterChat( color = borderColor ) .padding(vertical = 14.dp, horizontal = 18.dp) + .clickableWithoutRipple { + // 채팅 로그로 이동 + navigateToCharacterChatScreen(1, characterName) + } ) { Column { Row { @@ -302,9 +311,11 @@ fun CharacterChat( private fun UsersAdventuresInformation( isChatting: MutableState, context: Context, + characterName: String, modifier: Modifier = Modifier, viewModel: HomeViewModel, navigateToGainedCharacter: () -> Unit, + navigateToCharacterChatScreen: (Int, String) -> Unit ) { val adventuresInformationState = viewModel.getUsersAdventuresInformationState.collectAsState(initial = UiState.Loading).value @@ -333,7 +344,9 @@ private fun UsersAdventuresInformation( isChatting = isChatting, context = context, imageUrl = imageUrl, + characterName = characterName, navigateToGainedCharacter = navigateToGainedCharacter, + navigateToCharacterChatScreen = navigateToCharacterChatScreen ) } @@ -474,7 +487,8 @@ fun HomeScreenPreview() { OffroadTheme { HomeScreen( //padding = PaddingValues(), - category = "NONE" + category = "NONE", + navigateToCharacterChatScreen = { _, _ -> } ) } } diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt index 80e01acd..40dbc91e 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt @@ -29,7 +29,9 @@ fun HomeIcons( isChatting: MutableState, context: Context, imageUrl: String, + characterName: String, navigateToGainedCharacter: () -> Unit, + navigateToCharacterChatScreen: (Int, String) -> Unit ) { Box( modifier = Modifier.fillMaxSize() @@ -44,10 +46,10 @@ fun HomeIcons( Image( painter = painterResource(id = R.drawable.ic_home_chat), contentDescription = "chat", - //없앨 부분 - modifier = Modifier.clickableWithoutRipple { - isChatting.value = true - } + modifier = Modifier + .clickableWithoutRipple { + navigateToCharacterChatScreen(-1, characterName) + } ) Box( modifier = Modifier.fillMaxWidth(), diff --git a/feature/main/src/main/java/com/teamoffroad/feature/main/component/MainNavHost.kt b/feature/main/src/main/java/com/teamoffroad/feature/main/component/MainNavHost.kt index f594d67a..15966659 100644 --- a/feature/main/src/main/java/com/teamoffroad/feature/main/component/MainNavHost.kt +++ b/feature/main/src/main/java/com/teamoffroad/feature/main/component/MainNavHost.kt @@ -41,6 +41,9 @@ internal fun MainNavHost( ) { homeNavGraph( navigateToBack = navigator::popBackStackIfNotMainTabRoute, + navigateToCharacterChatScreen = { id, characterName -> + navigator.navigateToCharacterChat(id, characterName) + }, navigateToGainedCharacter = { navigator.navigateToMyPage().also { navigator.navigateToGainedCharacter() From 062d3c191e667ae19492afa518f33d359529bf96 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Tue, 19 Nov 2024 00:06:15 +0900 Subject: [PATCH 28/30] feature: Add animation to character chat --- .../feature/home/presentation/HomeScreen.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 3fc8040f..716ae4b9 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -5,6 +5,7 @@ import android.os.Build import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.Image @@ -16,6 +17,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -28,6 +30,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -73,6 +76,7 @@ import com.teamoffroad.feature.home.presentation.component.quest.progressbar.Rec import com.teamoffroad.feature.home.presentation.component.user.NicknameText import com.teamoffroad.feature.home.presentation.model.HomeProgressBarModel import com.teamoffroad.offroad.feature.home.R +import kotlinx.coroutines.launch val homeGradientBackground = Brush.verticalGradient( colors = listOf(HomeGradi1, HomeGradi2, HomeGradi3, HomeGradi4, HomeGradi5, HomeGradi6) @@ -173,9 +177,22 @@ fun HomeScreen( } if (isCharacterChatting.value) { + val offsetY = remember { Animatable(-10.dp.value) } + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(isCharacterChatting) { + coroutineScope.launch { + offsetY.animateTo( + targetValue = 0.dp.value, + animationSpec = tween(durationMillis = 500) + ) + } + } + Box( contentAlignment = Alignment.TopCenter, modifier = Modifier + .offset(y = offsetY.value.dp) .padding(start = 24.dp, top = 70.dp, end = 24.dp) ) { CharacterChat( From c457f2bf51d36b868591221bc44a051d267f706e Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Tue, 19 Nov 2024 16:54:44 +0900 Subject: [PATCH 29/30] refactor: Edit HomeIcons --- .../home/presentation/component/HomeIcons.kt | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt index acc2322f..916cb5ea 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/HomeIcons.kt @@ -1,13 +1,19 @@ package com.teamoffroad.feature.home.presentation.component +import android.Manifest import android.content.Context import android.content.pm.PackageManager import android.os.Build +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi import androidx.compose.foundation.Canvas import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -15,6 +21,8 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.drawscope.Fill @@ -62,22 +70,7 @@ fun HomeIcons( } } - Box( - contentAlignment = Alignment.TopEnd, - modifier = Modifier - .aspectRatio(48f / 144f) - .padding(top = 80.dp, end = 20.dp) - ) { - Column { - val characterChatInteractionSource = remember { MutableInteractionSource() } - Image( - painter = painterResource(id = R.drawable.ic_home_chat), - contentDescription = "chat", - modifier = Modifier - .clickableWithoutRipple(interactionSource = characterChatInteractionSource) { - - } - ) + Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .align(Alignment.TopEnd) @@ -110,7 +103,6 @@ fun HomeIcons( } } - val uploadInteractionSource = remember { MutableInteractionSource() } Image( painter = painterResource(id = R.drawable.ic_home_upload), @@ -142,8 +134,8 @@ fun HomeIcons( } } -private suspend fun showToast(context: Context, message: String) { +suspend fun showToast(context: Context, message: String) { withContext(Dispatchers.Main) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } -} +} \ No newline at end of file From 5b422197a00207c713c7c0153814c826be3455c4 Mon Sep 17 00:00:00 2001 From: YuJeongHyun <0703olivia@naver.com> Date: Tue, 19 Nov 2024 17:13:41 +0900 Subject: [PATCH 30/30] refactor: HomeScreen --- .../feature/home/presentation/HomeScreen.kt | 260 ++---------------- ...tTextField.kt => HomeUserChatTextField.kt} | 8 +- .../component/character/CharacterChat.kt | 177 ++++++++++++ .../character/CharacterChatAnimation.kt | 56 ++++ .../presentation/component/user/UserChat.kt | 97 +++++++ 5 files changed, 359 insertions(+), 239 deletions(-) rename feature/home/src/main/java/com/teamoffroad/feature/home/presentation/{HomeChatTextField.kt => HomeUserChatTextField.kt} (96%) create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterChat.kt create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterChatAnimation.kt create mode 100644 feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/user/UserChat.kt diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt index 716ae4b9..b08a6175 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeScreen.kt @@ -2,7 +2,6 @@ package com.teamoffroad.feature.home.presentation import android.content.Context import android.os.Build -import android.util.Log import android.widget.Toast import androidx.annotation.RequiresApi import androidx.compose.animation.core.Animatable @@ -70,10 +69,13 @@ import com.teamoffroad.feature.home.domain.model.UserQuests import com.teamoffroad.feature.home.presentation.component.CompleteQuestDialog import com.teamoffroad.feature.home.presentation.component.HomeIcons import com.teamoffroad.feature.home.presentation.component.UiState +import com.teamoffroad.feature.home.presentation.component.character.CharacterChat +import com.teamoffroad.feature.home.presentation.component.character.CharacterChatAnimation import com.teamoffroad.feature.home.presentation.component.character.CharacterItem import com.teamoffroad.feature.home.presentation.component.quest.progressbar.CloseCompleteRequest import com.teamoffroad.feature.home.presentation.component.quest.progressbar.RecentQuest import com.teamoffroad.feature.home.presentation.component.user.NicknameText +import com.teamoffroad.feature.home.presentation.component.user.UserChat import com.teamoffroad.feature.home.presentation.model.HomeProgressBarModel import com.teamoffroad.offroad.feature.home.R import kotlinx.coroutines.launch @@ -93,9 +95,9 @@ fun HomeScreen( val context = LocalContext.current val viewModel: HomeViewModel = hiltViewModel() val isCompleteQuestDialogShown = remember { mutableStateOf(false) } - val isChatting = remember { mutableStateOf(false) } - val chattingText = viewModel.chattingText.collectAsStateWithLifecycle() - val sendMessage = remember { mutableStateOf("") } + val isUserChatting = remember { mutableStateOf(false) } + val userChattingText = viewModel.chattingText.collectAsStateWithLifecycle() + val userSendMessage = remember { mutableStateOf("") } val characterChat = viewModel.getCharacterChat.collectAsStateWithLifecycle() val isCharacterChatting = viewModel.isCharacterChatting.collectAsStateWithLifecycle() val isCharacterChattingLoading = @@ -120,7 +122,7 @@ fun HomeScreen( ) { Column(modifier = Modifier.fillMaxSize()) { UsersAdventuresInformation( - isChatting = isChatting, + isChatting = isUserChatting, context = context, characterName = characterName.value, modifier = Modifier @@ -135,75 +137,36 @@ fun HomeScreen( } - if (isChatting.value) { + if (isUserChatting.value) { Box( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) .padding(bottom = 196.dp) ) { - Column { - Box( - modifier = Modifier.fillMaxWidth() - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(end = 20.dp), - contentAlignment = Alignment.CenterEnd - ) { - FinishChatting(isChatting) - } - } - - HomeChatTextField( - text = chattingText.value, - sentMessage = sendMessage.value, - isChatting = isChatting, - keyboard = true, - isCharacterChatting = viewModel::updateCharacterChatting, - onValueChange = { text -> - viewModel.updateChattingText(text) - }, - onSendClick = { - userSendChat.value = true // 사용자가 채팅 보냄 - sendMessage.value = chattingText.value // 보낼 메시지 - viewModel.sendChat() // 서버에 보내기 - viewModel.updateChattingText("") // 초기화 - } - ) - } + UserChat( + isChatting = isUserChatting, + chattingText = userChattingText, + sendMessage = userSendMessage, + userSendChat = userSendChat, + updateCharacterChatting = viewModel::updateCharacterChatting, + updateChattingText = viewModel::updateChattingText, + sendChat = viewModel::sendChat + ) } } - if (isCharacterChatting.value) { - val offsetY = remember { Animatable(-10.dp.value) } - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(isCharacterChatting) { - coroutineScope.launch { - offsetY.animateTo( - targetValue = 0.dp.value, - animationSpec = tween(durationMillis = 500) - ) - } - } - - Box( - contentAlignment = Alignment.TopCenter, - modifier = Modifier - .offset(y = offsetY.value.dp) - .padding(start = 24.dp, top = 70.dp, end = 24.dp) - ) { - CharacterChat( - isChatting = isChatting, - isCharacterChattingLoading = isCharacterChattingLoading, - answerCharacterChat = userSendChat, - characterName = characterName.value, - characterContent = characterChat.value.characterContent, - navigateToCharacterChatScreen = navigateToCharacterChatScreen - ) - } + if (isCharacterChatting.value) { + CharacterChatAnimation( + isCharacterChatting = isCharacterChatting, + isChatting = isUserChatting, + isCharacterChattingLoading = isCharacterChattingLoading, + answerCharacterChat = userSendChat, + characterName = characterName.value, + characterContent = characterChat.value.characterContent, + navigateToCharacterChatScreen = navigateToCharacterChatScreen + ) } } @@ -211,118 +174,11 @@ fun HomeScreen( CompleteQuestDialog( isCompleteQuestDialogShown = isCompleteQuestDialogShown, completeQuests = completeQuests, - onClickCancel = { - isCompleteQuestDialogShown.value = false - }, + onClickCancel = { isCompleteQuestDialogShown.value = false }, ) } } -@Composable -fun CharacterChat( - isChatting: MutableState, - isCharacterChattingLoading: State, - answerCharacterChat: MutableState, - characterName: String, - characterContent: String, - characterTextColor: Color = Sub4, - characterTextStyle: TextStyle = OffroadTheme.typography.textBold, - messageTextColor: Color = Main2, - messageTextStyle: TextStyle = OffroadTheme.typography.textRegular, - backgroundColor: Color = Main3, - borderColor: Color = BtnInactive, - navigateToCharacterChatScreen: (Int, String) -> Unit -) { - val checkCharacterChattingLines = remember { mutableStateOf(false) } - val isExpanded = remember { mutableStateOf(false) } - - val rotationAngle by animateFloatAsState( - targetValue = if (isExpanded.value) 180f else 0f, - animationSpec = tween(durationMillis = 300), label = "" - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .background( - color = backgroundColor, - shape = RoundedCornerShape(12.dp) - ) - .border( - width = 1.dp, - shape = RoundedCornerShape(12.dp), - color = borderColor - ) - .padding(vertical = 14.dp, horizontal = 18.dp) - .clickableWithoutRipple { - // 채팅 로그로 이동 - navigateToCharacterChatScreen(1, characterName) - } - ) { - Column { - Row { - Text( - text = "$characterName : ", - modifier = Modifier, - color = characterTextColor, - style = characterTextStyle - ) - - if (isCharacterChattingLoading.value) { - Box( - modifier = Modifier - .size(width = 54.dp, height = 27.dp) - ) { - val composition by rememberLottieComposition( - LottieCompositionSpec.RawRes( - com.teamoffroad.offroad.core.designsystem.R.raw.loading_linear - ) - ) - val animationState = animateLottieCompositionAsState( - composition, - iterations = LottieConstants.IterateForever - ) - - if (animationState.isAtEnd && animationState.isPlaying) { - LaunchedEffect(Unit) { } - } - - LottieAnimation(composition, animationState.progress) - } - } else { - Text( - text = characterContent, - modifier = Modifier.weight(1f), - color = messageTextColor, - style = messageTextStyle, - onTextLayout = { textLayoutResult -> - checkCharacterChattingLines.value = textLayoutResult.lineCount >= 3 - }, - maxLines = if (isExpanded.value) Int.MAX_VALUE else 2, - overflow = TextOverflow.Ellipsis, - ) - - Image( - painter = painterResource(id = R.drawable.ic_home_accordion), - contentDescription = "accordion down", - modifier = Modifier - .graphicsLayer(rotationX = rotationAngle) - .clickableWithoutRipple { - isExpanded.value = !isExpanded.value - } - ) - } - - } - - if (!answerCharacterChat.value) { - AnswerCharacterChat(isChatting = isChatting) - } - } - - } -} - @RequiresApi(Build.VERSION_CODES.TIRAMISU) @Composable private fun UsersAdventuresInformation( @@ -384,66 +240,6 @@ private fun UsersAdventuresInformation( CharacterItem().EmblemNameText(context, Modifier) } -@Composable -fun FinishChatting( - isChatting: MutableState, - backgroundColor: Color = Sub55, - borderColor: Color = Sub -) { - Text( - style = OffroadTheme.typography.subtitle2Semibold, - text = stringResource(id = R.string.home_chat_finish), - modifier = Modifier - .padding(bottom = 8.dp) - .background( - color = backgroundColor, - shape = RoundedCornerShape(20.dp) - ) - .border( - width = 1.dp, - shape = RoundedCornerShape(20.dp), - color = borderColor - ) - .padding(horizontal = 16.dp) - .padding(vertical = 8.dp) - .clickableWithoutRipple { - isChatting.value = false - }, - color = White - ) -} - -@Composable -fun AnswerCharacterChat( - isChatting: MutableState, - backgroundColor: Color = Main2, - textColor: Color = Main3, - textStyle: TextStyle = OffroadTheme.typography.textContents -) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 10.dp) - ) { - Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxWidth()) { - Text( - text = "답장하기", - modifier = Modifier - .background( - color = backgroundColor, - shape = RoundedCornerShape(8.dp) - ) - .padding(horizontal = 14.dp, vertical = 6.dp) - .clickableWithoutRipple { - isChatting.value = true - }, - color = textColor, - style = textStyle - ) - } - } -} - @Composable private fun UsersQuestInformation( context: Context, diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeUserChatTextField.kt similarity index 96% rename from feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt rename to feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeUserChatTextField.kt index 28ca8e83..89885837 100644 --- a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeChatTextField.kt +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeUserChatTextField.kt @@ -8,9 +8,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -27,7 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -53,8 +49,6 @@ import com.airbnb.lottie.compose.animateLottieCompositionAsState import com.airbnb.lottie.compose.rememberLottieComposition import com.teamoffroad.core.designsystem.component.clickableWithoutRipple import com.teamoffroad.core.designsystem.theme.BtnInactive -import com.teamoffroad.core.designsystem.theme.ErrorNew -import com.teamoffroad.core.designsystem.theme.Kakao import com.teamoffroad.core.designsystem.theme.Main2 import com.teamoffroad.core.designsystem.theme.OffroadTheme import com.teamoffroad.core.designsystem.theme.Transparent @@ -63,7 +57,7 @@ import com.teamoffroad.offroad.feature.home.R @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeChatTextField( +fun HomeUserChatTextField( modifier: Modifier = Modifier, text: String = "", sentMessage: String, diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterChat.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterChat.kt new file mode 100644 index 00000000..19e83981 --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterChat.kt @@ -0,0 +1,177 @@ +package com.teamoffroad.feature.home.presentation.component.character + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.teamoffroad.core.designsystem.component.clickableWithoutRipple +import com.teamoffroad.core.designsystem.theme.BtnInactive +import com.teamoffroad.core.designsystem.theme.Main2 +import com.teamoffroad.core.designsystem.theme.Main3 +import com.teamoffroad.core.designsystem.theme.OffroadTheme +import com.teamoffroad.core.designsystem.theme.Sub4 +import com.teamoffroad.offroad.feature.home.R + +@Composable +fun CharacterChat( + isChatting: MutableState, + isCharacterChattingLoading: State, + answerCharacterChat: MutableState, + characterName: String, + characterContent: String, + characterTextColor: Color = Sub4, + characterTextStyle: TextStyle = OffroadTheme.typography.textBold, + messageTextColor: Color = Main2, + messageTextStyle: TextStyle = OffroadTheme.typography.textRegular, + backgroundColor: Color = Main3, + borderColor: Color = BtnInactive, + navigateToCharacterChatScreen: (Int, String) -> Unit +) { + val checkCharacterChattingLines = remember { mutableStateOf(false) } + val isExpanded = remember { mutableStateOf(false) } + + val rotationAngle by animateFloatAsState( + targetValue = if (isExpanded.value) 180f else 0f, + animationSpec = tween(durationMillis = 300), label = "" + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = backgroundColor, + shape = RoundedCornerShape(12.dp) + ) + .border( + width = 1.dp, + shape = RoundedCornerShape(12.dp), + color = borderColor + ) + .padding(vertical = 14.dp, horizontal = 18.dp) + .clickableWithoutRipple { + navigateToCharacterChatScreen(-1, characterName) + } + ) { + Column { + Row { + Text( + text = "$characterName : ", + modifier = Modifier, + color = characterTextColor, + style = characterTextStyle + ) + + if (isCharacterChattingLoading.value) { + Box( + modifier = Modifier + .size(width = 54.dp, height = 27.dp) + ) { + val composition by rememberLottieComposition( + LottieCompositionSpec.RawRes( + com.teamoffroad.offroad.core.designsystem.R.raw.loading_linear + ) + ) + val animationState = animateLottieCompositionAsState( + composition, + iterations = LottieConstants.IterateForever + ) + + if (animationState.isAtEnd && animationState.isPlaying) { + LaunchedEffect(Unit) { } + } + + LottieAnimation(composition, animationState.progress) + } + } else { + Text( + text = characterContent, + modifier = Modifier.weight(1f), + color = messageTextColor, + style = messageTextStyle, + onTextLayout = { textLayoutResult -> + checkCharacterChattingLines.value = textLayoutResult.lineCount >= 3 + }, + maxLines = if (isExpanded.value) Int.MAX_VALUE else 2, + overflow = TextOverflow.Ellipsis, + ) + + Image( + painter = painterResource(id = R.drawable.ic_home_accordion), + contentDescription = "accordion down", + modifier = Modifier + .graphicsLayer(rotationX = rotationAngle) + .clickableWithoutRipple { + isExpanded.value = !isExpanded.value + } + ) + } + + } + + if (!answerCharacterChat.value) { + AnswerCharacterChat(isChatting = isChatting) + } + } + + } +} + +@Composable +fun AnswerCharacterChat( + isChatting: MutableState, + backgroundColor: Color = Main2, + textColor: Color = Main3, + textStyle: TextStyle = OffroadTheme.typography.textContents +) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp) + ) { + Box(contentAlignment = Alignment.CenterEnd, modifier = Modifier.fillMaxWidth()) { + Text( + text = "답장하기", + modifier = Modifier + .background( + color = backgroundColor, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 14.dp, vertical = 6.dp) + .clickableWithoutRipple { + isChatting.value = true + }, + color = textColor, + style = textStyle + ) + } + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterChatAnimation.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterChatAnimation.kt new file mode 100644 index 00000000..f335a71a --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/character/CharacterChatAnimation.kt @@ -0,0 +1,56 @@ +package com.teamoffroad.feature.home.presentation.component.character + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +fun CharacterChatAnimation( + isCharacterChatting: State, + isChatting: MutableState, + isCharacterChattingLoading: State, + answerCharacterChat: MutableState, + characterName: String, + characterContent: String, + navigateToCharacterChatScreen: (Int, String) -> Unit +) { + val offsetY = remember { Animatable(-10.dp.value) } + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(isCharacterChatting) { + coroutineScope.launch { + offsetY.animateTo( + targetValue = 0.dp.value, + animationSpec = tween(durationMillis = 500) + ) + } + } + + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier + .offset(y = offsetY.value.dp) + .padding(start = 24.dp, top = 70.dp, end = 24.dp) + ) { + CharacterChat( + isChatting = isChatting, + isCharacterChattingLoading = isCharacterChattingLoading, + answerCharacterChat = answerCharacterChat, + characterName = characterName, + characterContent = characterContent, + navigateToCharacterChatScreen = navigateToCharacterChatScreen + ) + } +} \ No newline at end of file diff --git a/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/user/UserChat.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/user/UserChat.kt new file mode 100644 index 00000000..50a257a3 --- /dev/null +++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/component/user/UserChat.kt @@ -0,0 +1,97 @@ +package com.teamoffroad.feature.home.presentation.component.user + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.teamoffroad.core.designsystem.component.clickableWithoutRipple +import com.teamoffroad.core.designsystem.theme.OffroadTheme +import com.teamoffroad.core.designsystem.theme.Sub +import com.teamoffroad.core.designsystem.theme.Sub55 +import com.teamoffroad.core.designsystem.theme.White +import com.teamoffroad.feature.home.presentation.HomeUserChatTextField +import com.teamoffroad.offroad.feature.home.R + +@Composable +fun UserChat( + isChatting: MutableState, + chattingText: State, + sendMessage: MutableState, + userSendChat: MutableState, + updateCharacterChatting: (Boolean) -> Unit, + updateChattingText: (String) -> Unit, + sendChat: () -> Unit, +) { + Column { + Box( + modifier = Modifier.fillMaxWidth() + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 20.dp), + contentAlignment = Alignment.CenterEnd + ) { + FinishChatting(isChatting) + } + } + + HomeUserChatTextField( + text = chattingText.value, + sentMessage = sendMessage.value, + isChatting = isChatting, + keyboard = true, + isCharacterChatting = updateCharacterChatting, + onValueChange = { text -> + updateChattingText(text) + }, + onSendClick = { + userSendChat.value = true // 사용자가 채팅 보냄 + sendMessage.value = chattingText.value // 보낼 메시지 + sendChat() // 서버에 보내기 + updateChattingText("") // 초기화 + } + ) + } +} + +@Composable +fun FinishChatting( + isChatting: MutableState, + backgroundColor: Color = Sub55, + borderColor: Color = Sub +) { + Text( + style = OffroadTheme.typography.subtitle2Semibold, + text = stringResource(id = R.string.home_chat_finish), + modifier = Modifier + .padding(bottom = 8.dp) + .background( + color = backgroundColor, + shape = RoundedCornerShape(20.dp) + ) + .border( + width = 1.dp, + shape = RoundedCornerShape(20.dp), + color = borderColor + ) + .padding(horizontal = 16.dp) + .padding(vertical = 8.dp) + .clickableWithoutRipple { + isChatting.value = false + }, + color = White + ) +} \ No newline at end of file