diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 074f2dfa..f3c1785f 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 {
@@ -55,4 +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 b70a38e3..93af6fcc 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
+
-
+
+
+
+
+
@@ -55,5 +62,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
new file mode 100644
index 00000000..b509e89c
--- /dev/null
+++ b/app/src/main/java/com/teamoffroad/offroad.app/OffRoadMessagingService.kt
@@ -0,0 +1,199 @@
+package com.teamoffroad.offroad.app
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.BitmapDrawable
+import android.os.Build
+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
+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() {
+ @Inject
+ lateinit var dataStore: DeviceTokenRepository
+
+ override fun onNewToken(token: String) {
+ super.onNewToken(token)
+
+ 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)
+ sendNotification(remoteMessage, true)
+ else {
+ // 앱이 포그라운드에 있고, 알림타임이 캐릭터채팅인 경우
+ // 정현이 봐야할곳은 여기!! 요쪽 따라가십쇼
+ sendCharacterChatNotificationInForeground(remoteMessage)
+ }
+ } else {
+ sendNotification(remoteMessage, false)
+ }
+ }
+ }
+
+ override fun handleIntent(intent: Intent?) {
+ val new = intent?.apply {
+ val temp = extras?.apply {
+ remove(MessageNotificationKeys.ENABLE_NOTIFICATION)
+ remove(keyWithOldPrefix())
+ }
+ replaceExtras(temp)
+ }
+ super.handleIntent(new)
+ }
+
+ private fun keyWithOldPrefix(): String {
+ if (!MessageNotificationKeys.ENABLE_NOTIFICATION.startsWith(MessageNotificationKeys.NOTIFICATION_PREFIX)) {
+ return MessageNotificationKeys.ENABLE_NOTIFICATION
+ }
+
+ return MessageNotificationKeys.ENABLE_NOTIFICATION.replace(
+ MessageNotificationKeys.NOTIFICATION_PREFIX,
+ MessageNotificationKeys.NOTIFICATION_PREFIX_OLD
+ )
+ }
+
+ private fun generateUniqueIdentifier(): Int {
+ return (System.currentTimeMillis() / 7).toInt()
+ }
+
+ private fun createPendingIntent(intent: Intent, uniqueIdentifier: Int): PendingIntent {
+ return PendingIntent.getActivity(
+ this,
+ uniqueIdentifier,
+ intent,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+ }
+
+ private fun createNotificationIntent(
+ remoteMessage: RemoteMessage,
+ isForeGround: Boolean
+ ): 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 createNotificationBuilder(
+ remoteMessage: RemoteMessage,
+ pendingIntent: PendingIntent,
+ 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)
+ .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) {
+ 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 sendCharacterChatNotificationInForeground(
+ remoteMessage: RemoteMessage,
+ ) {
+ 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(
+ 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(CHANNEL_ID, NOTICE, NotificationManager.IMPORTANCE_HIGH)
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ notificationManager.notify(uniqueIdentifier, notificationBuilder.build())
+ }
+}
\ 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/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/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/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/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..61b6c7f4
--- /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 deviceToken: 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/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/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/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..f52e8959
--- /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 deviceToken: Flow
+ suspend fun updateDeviceTokenEnabled(deviceToken: String)
+}
\ No newline at end of file
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/core/navigation/src/main/java/com/teamoffroad/core/navigation/RouteModel.kt b/core/navigation/src/main/java/com/teamoffroad/core/navigation/RouteModel.kt
index 137d0fdd..9b22e7f2 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/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/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/home/build.gradle.kts b/feature/home/build.gradle.kts
index 4ddd5103..3c86824e 100644
--- a/feature/home/build.gradle.kts
+++ b/feature/home/build.gradle.kts
@@ -11,11 +11,12 @@ android {
dependencies {
implementation(project(":feature:auth"))
+ implementation(project(":feature:characterchat"))
implementation(libs.retrofit.kotlinx.serialization)
implementation(libs.androidx.appcompat)
implementation(libs.google.accompanist.permissions)
implementation(libs.gson)
implementation(libs.lottie.compose)
implementation(libs.coil.svg)
- //implementation(libs.androidsvg.aar)
+ implementation(libs.eventbus)
}
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/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 2dffa015..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
@@ -4,50 +4,81 @@ import android.content.Context
import android.os.Build
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
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.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.navigationBarsPadding
+import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.Surface
+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.runtime.rememberCoroutineScope
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
+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
-import com.teamoffroad.core.designsystem.component.StaticAnimationWrapper
+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
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
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
val homeGradientBackground = Brush.verticalGradient(
colors = listOf(HomeGradi1, HomeGradi2, HomeGradi3, HomeGradi4, HomeGradi5, HomeGradi6)
@@ -59,52 +90,91 @@ fun HomeScreen(
category: String?,
completeQuests: List = emptyList(),
navigateToGainedCharacter: () -> Unit = {},
+ navigateToCharacterChatScreen: (Int, String) -> Unit
) {
val context = LocalContext.current
val viewModel: HomeViewModel = hiltViewModel()
val isCompleteQuestDialogShown = remember { mutableStateOf(false) }
-
+ 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 =
+ viewModel.isCharacterChattingLoading.collectAsStateWithLifecycle()
+ val userSendChat = remember { mutableStateOf(false) }
+ val characterName = viewModel.characterName.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.updateAutoSignIn()
+ viewModel.updateFcmToken()
viewModel.updateCategory(if (category.isNullOrEmpty()) "NONE" else category)
viewModel.getUsersAdventuresInformation(viewModel.category.value)
viewModel.getUserQuests()
if (completeQuests.isNotEmpty()) isCompleteQuestDialogShown.value = true
}
- StaticAnimationWrapper {
- Surface(
- modifier = Modifier
- .background(homeGradientBackground)
- .padding(bottom = 140.dp)
- .navigationBarsPadding(),
- color = Color.Transparent
- ) {
- 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)
- }
+ Box(
+ modifier = Modifier
+ .background(homeGradientBackground)
+ .fillMaxSize()
+ .padding(bottom = 180.dp)
+ ) {
+ Column(modifier = Modifier.fillMaxSize()) {
+ UsersAdventuresInformation(
+ isChatting = isUserChatting,
+ 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)
+
+ }
+
+ if (isUserChatting.value) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 196.dp)
+ ) {
+ UserChat(
+ isChatting = isUserChatting,
+ chattingText = userChattingText,
+ sendMessage = userSendMessage,
+ userSendChat = userSendChat,
+ updateCharacterChatting = viewModel::updateCharacterChatting,
+ updateChattingText = viewModel::updateChattingText,
+ sendChat = viewModel::sendChat
+ )
}
}
+
+
+ if (isCharacterChatting.value) {
+ CharacterChatAnimation(
+ isCharacterChatting = isCharacterChatting,
+ isChatting = isUserChatting,
+ isCharacterChattingLoading = isCharacterChattingLoading,
+ answerCharacterChat = userSendChat,
+ characterName = characterName.value,
+ characterContent = characterChat.value.characterContent,
+ navigateToCharacterChatScreen = navigateToCharacterChatScreen
+ )
+ }
}
if (isCompleteQuestDialogShown.value) {
CompleteQuestDialog(
isCompleteQuestDialogShown = isCompleteQuestDialogShown,
completeQuests = completeQuests,
- onClickCancel = {
- isCompleteQuestDialogShown.value = false
- },
+ onClickCancel = { isCompleteQuestDialogShown.value = false },
)
}
}
@@ -112,10 +182,13 @@ fun HomeScreen(
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
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
@@ -141,9 +214,12 @@ private fun UsersAdventuresInformation(
contentAlignment = Alignment.TopEnd
) {
HomeIcons(
+ isChatting = isChatting,
context = context,
imageUrl = imageUrl,
+ characterName = characterName,
navigateToGainedCharacter = navigateToGainedCharacter,
+ navigateToCharacterChatScreen = navigateToCharacterChatScreen
)
}
@@ -224,7 +300,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/HomeUserChatTextField.kt b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeUserChatTextField.kt
new file mode 100644
index 00000000..89885837
--- /dev/null
+++ b/feature/home/src/main/java/com/teamoffroad/feature/home/presentation/HomeUserChatTextField.kt
@@ -0,0 +1,218 @@
+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.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
+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.Text
+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.MutableState
+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.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.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 HomeUserChatTextField(
+ modifier: Modifier = Modifier,
+ text: String = "",
+ sentMessage: String,
+ isChatting: MutableState,
+ keyboard: Boolean,
+ isCharacterChatting: (Boolean) -> Unit,
+ onValueChange: (String) -> Unit = {},
+ onFocusChange: (Boolean) -> Unit = {},
+ onSendClick: () -> Unit = {},
+) {
+ val scrollState = rememberScrollState()
+ val focusRequester = remember { FocusRequester() }
+ val focusManager = LocalFocusManager.current
+ val contextView = LocalView.current
+ var showLottieLoading by remember { mutableStateOf(false) }
+
+ var keyboardVisible by remember { mutableStateOf(keyboard) }
+
+ LaunchedEffect(isChatting) {
+ if (isChatting.value) {
+ focusRequester.requestFocus()
+ }
+ }
+
+ LaunchedEffect(keyboardVisible) {
+ if (!keyboardVisible) {
+ focusManager.clearFocus()
+ isChatting.value = false
+ isCharacterChatting(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)
+ }
+ }
+
+ AnimatedVisibility(
+ visible = isChatting.value,
+ ) {
+ 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) }
+
+ Column{
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 6.dp, bottom = 4.dp)
+ ) {
+ Text(
+ text = stringResource(id = R.string.home_chat_me),
+ color = Main2,
+ 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 {
+ 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 = "send",
+ modifier = Modifier
+ .padding(end = 2.dp)
+ .size(36.dp)
+ .align(Alignment.CenterEnd)
+ .clickableWithoutRipple { if (text.isNotBlank()) onSendClick() },
+ )
+ }
+ }
+
+ }
+ }
+}
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..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
@@ -1,25 +1,46 @@
package com.teamoffroad.feature.home.presentation
+import android.util.Log
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.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
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 com.teamoffroad.feature.home.presentation.model.CharacterChatModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
+import java.time.LocalDateTime
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
import javax.inject.Inject
@HiltViewModel
class HomeViewModel @Inject constructor(
private val userRepository: UserRepository,
+ private val characterChatRepository: CharacterChatRepository,
private val setAutoSignInUseCase: SetAutoSignInUseCase,
+ private val deviceTokenRepository: DeviceTokenRepository,
+ private val fcmTokenUseCase: PostFcmTokenUseCase,
) : ViewModel() {
+ private val _getCharacterChat = MutableStateFlow(CharacterChatModel("", ""))
+ val getCharacterChat = _getCharacterChat.asStateFlow()
private val _getUsersAdventuresInformationState =
MutableStateFlow>(
@@ -48,18 +69,69 @@ 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()
private val _linearProgressBar = MutableStateFlow(0f)
val linearProgressBar = _linearProgressBar.asStateFlow()
+ 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에서 게시한 브로드캐스트리시버를 여기서 받습니다.
+ 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())
+
+ // 1. 홈 화면에서 캐릭터한테 메시지가 왔을 때
+ val characterName = event.characterName
+ val characterContent = event.characterContent
+ if(characterName != null && characterContent != null) {
+ _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent)
+ updateCharacterChatting(true)
+ }
+
+ }
+
+ fun updateCharacterChatting(state: Boolean) {
+ _isCharacterChatting.value = state
+ }
+
fun getUsersAdventuresInformation(category: String) {
viewModelScope.launch {
runCatching {
userRepository.getUsersAdventuresInformation(category)
}.onSuccess { state ->
_getUsersAdventuresInformationState.emit(UiState.Success(state))
+ _characterName.value = state.characterName
updateSelectedEmblem(state.emblemName)
updateCharacterImage(state.baseImageUrl)
updateMotionImageUrl(state.motionImageUrl)
@@ -134,10 +206,56 @@ class HomeViewModel @Inject constructor(
}
}
- fun updateAutoSignIn(){
+ fun updateAutoSignIn() {
viewModelScope.launch {
setAutoSignInUseCase.invoke(true)
}
}
+ fun updateChattingText(text: String) {
+ _chattingText.value = text
+ }
+
+ fun sendChat() {
+ val chattingText = chattingText.value
+ _isCharacterChattingLoading.value = true
+
+ 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 ->
+ // 보낸 채팅 내용 홈에 보여주어야 함
+ _sendChatState.emit(UiState.Success(chat))
+ _isCharacterChattingLoading.value = false
+
+ val characterContent = chat.content
+ if (characterContent != null) {
+ _getCharacterChat.value = CharacterChatModel(_characterName.value, characterContent)
+ updateCharacterChatting(true)
+ }
+
+ }.onFailure { t ->
+ val errorMessage = getErrorMessage(t)
+ _sendChatState.emit(UiState.Failure(errorMessage))
+ }
+ }
+ }
+
+ 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
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 c7a42a9b..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
@@ -8,21 +8,29 @@ 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.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
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.teamoffroad.core.designsystem.component.clickableWithoutRipple
+import com.teamoffroad.core.designsystem.theme.ErrorNew
import com.teamoffroad.feature.home.presentation.component.upload.uploadImage
import com.teamoffroad.offroad.feature.home.R
import kotlinx.coroutines.Dispatchers
@@ -32,9 +40,12 @@ import kotlinx.coroutines.withContext
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
fun HomeIcons(
+ isChatting: MutableState,
context: Context,
imageUrl: String,
+ characterName: String,
navigateToGainedCharacter: () -> Unit,
+ navigateToCharacterChatScreen: (Int, String) -> Unit
) {
val scope = rememberCoroutineScope()
@@ -59,22 +70,38 @@ 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)
+ .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 {
+ navigateToCharacterChatScreen(-1, characterName)
+ }
+ )
+ 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
+ )
}
- )
+ }
+ }
val uploadInteractionSource = remember { MutableInteractionSource() }
Image(
@@ -107,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
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/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(
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
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
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 @@
+
+
+
+
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 @@
+
+
+
+
+
+
diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml
index 7bf2846c..ee791a42 100644
--- a/feature/home/src/main/res/values/strings.xml
+++ b/feature/home/src/main/res/values/strings.xml
@@ -12,6 +12,8 @@
퀘스트 \'%s\' 외 %d개를\n클리어했어요! 마이페이지에서\n보상을 확인해보세요.
확인
-
+ 나 :
+ 채팅 종료
권한이 허용되었습니다.
diff --git a/feature/main/build.gradle.kts b/feature/main/build.gradle.kts
index 019a27a3..a70c2e55 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"))
@@ -25,5 +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..c8eac734
--- /dev/null
+++ b/feature/main/src/main/java/com/teamoffroad/feature/main/CharacterChatBroadcastReceiver.kt
@@ -0,0 +1,74 @@
+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 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(
+ 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)
+ val notificationBody = intent.getStringExtra(KEY_BODY)
+ val notificationType = intent.getStringExtra(KEY_TYPE)
+
+ 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 d0e92c88..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
@@ -6,46 +6,74 @@ import android.os.Build
import android.os.Bundle
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 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
@AndroidEntryPoint
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
class MainActivity : ComponentActivity() {
+ private val notificationTypeState = mutableStateOf(null)
+ private val notificationIdState = mutableStateOf(null)
+ private lateinit var characterBroadcastReceiver: CharacterChatBroadcastReceiver
+ private val viewModel by viewModels()
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ notificationTypeState.value = intent.getStringExtra(KEY_TYPE)
+ notificationIdState.value = intent.getStringExtra(KEY_ID)
+
+ //액티비티 생명주기에 따라 브로드캐스터리시버 만들어주기
+ //밑에 onDestroy에서 브로드캐스트리시버 해제도 해줍니다.
+ //이제 홈 뷰모델로 가면됩니다.
+ characterBroadcastReceiver = CharacterChatBroadcastReceiver(
+ navigateToAnnouncement = viewModel::navigateToAnnouncement,
+ navigateToHome = viewModel::navigateToHome,
+ )
+ CharacterChatBroadcastReceiver.register(this, characterBroadcastReceiver)
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 = notificationTypeState.value,
+ notificationId = notificationIdState.value,
+ viewModel = viewModel
+ )
+ }
}
}
}
+ override fun onDestroy() {
+ super.onDestroy()
+ CharacterChatBroadcastReceiver.unregister(this, characterBroadcastReceiver)
+ }
+
companion object {
@JvmStatic
fun newInstance(context: Context) = Intent(context, MainActivity::class.java).apply {
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()
- }
-}
-
+}
\ No newline at end of file
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 17efa3ad..1933c7cc 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
@@ -22,9 +22,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
@@ -40,7 +40,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 ->
@@ -176,8 +176,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 8867ace2..9e0265d8 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
@@ -4,7 +4,11 @@ import android.os.Build
import androidx.annotation.RequiresApi
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
import com.teamoffroad.feature.main.component.MainBottomBar
import com.teamoffroad.feature.main.component.MainNavHost
@@ -15,7 +19,32 @@ import kotlinx.collections.immutable.toPersistentList
internal fun MainScreen(
modifier: Modifier = Modifier,
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)
+ notificationId?.let {
+ navigator.navigateToMyPage()
+ navigator.navigateToSetting()
+ navigator.navigateToAnnouncement(it)
+ }
+ else {
+ navigator.navigateToHome()
+ }
+ }
+ }
MainScreenContent(
navigator = navigator,
modifier = modifier,
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/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 51abfd46..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
@@ -17,7 +17,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)
@@ -40,12 +39,11 @@ internal fun MainNavHost(
exitTransition = { ExitTransition.None },
popExitTransition = { ExitTransition.None },
) {
- splashNavGraph(
- navigateToAuth = { navigator.navigateToAuth() },
- navigateToHome = { navigator.navigateToHome() }
- )
homeNavGraph(
navigateToBack = navigator::popBackStackIfNotMainTabRoute,
+ navigateToCharacterChatScreen = { id, characterName ->
+ navigator.navigateToCharacterChat(id, characterName)
+ },
navigateToGainedCharacter = {
navigator.navigateToMyPage().also {
navigator.navigateToGainedCharacter()
@@ -82,7 +80,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 743282e8..14f60070 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,
@@ -128,14 +130,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..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
@@ -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
@@ -34,14 +35,38 @@ 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()
) {
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/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
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index ac476aed..0241b4c2 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -74,6 +74,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"
@@ -81,6 +85,10 @@ kakao = "2.20.6"
# Process phoenix
processPhoenix = "3.0.0"
+# Event Bus
+eventbus = "3.2.0"
+
+
[libraries]
# Core Libraries
androidsvg-aar = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvgAar" }
@@ -171,12 +179,23 @@ 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" }
# 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" }
@@ -198,6 +217,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 = [
@@ -207,3 +229,11 @@ offroad-map = [
"google-accompanist-permissions",
]
+
+firebase = [
+ "firebase-analytics",
+ "firebase-database",
+ "firebase-messaging",
+ "firebase-remoteConfig"
+]
+