diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4ba127f1..d9fb9b64 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -67,6 +67,9 @@
+
diff --git a/app/src/main/java/com/moive/app/MoiveApplication.kt b/app/src/main/java/com/moive/app/MoiveApplication.kt
index 4eedb429..4b6fae45 100644
--- a/app/src/main/java/com/moive/app/MoiveApplication.kt
+++ b/app/src/main/java/com/moive/app/MoiveApplication.kt
@@ -3,6 +3,7 @@ package com.moive.app
import android.app.Application
import com.kakao.sdk.common.KakaoSdk
import com.kakao.vectormap.KakaoMapSdk
+import com.moive.app.core.fcm.NotificationChannels
import dagger.hilt.android.HiltAndroidApp
import timber.log.Timber
@@ -12,6 +13,7 @@ class MoiveApplication : Application() {
super.onCreate()
initTimber()
+ NotificationChannels.createDefaultChannel(this)
initKakaoSdk()
initKakaoMapSdk()
}
diff --git a/app/src/main/java/com/moive/app/core/designsystem/component/toast/MoiveSnackbarHost.kt b/app/src/main/java/com/moive/app/core/designsystem/component/toast/MoiveSnackbarHost.kt
index f43d2e5d..4e67c09c 100644
--- a/app/src/main/java/com/moive/app/core/designsystem/component/toast/MoiveSnackbarHost.kt
+++ b/app/src/main/java/com/moive/app/core/designsystem/component/toast/MoiveSnackbarHost.kt
@@ -1,6 +1,7 @@
package com.moive.app.core.designsystem.component.toast
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
@@ -22,7 +23,7 @@ fun MoiveSnackbarHost(
text = visuals.message,
type = visuals.type,
modifier = Modifier
- .padding(horizontal = 16.dp)
+ .padding(horizontal = 20.dp)
.padding(bottom = 80.dp),
)
}
diff --git a/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt b/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt
index e4aa09e5..a3825552 100644
--- a/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt
+++ b/app/src/main/java/com/moive/app/core/fcm/MoiveFirebaseMessagingService.kt
@@ -1,12 +1,9 @@
package com.moive.app.core.fcm
import android.Manifest
-import android.app.NotificationChannel
-import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
-import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@@ -21,7 +18,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
-import java.util.concurrent.atomic.AtomicInteger
import javax.inject.Inject
@AndroidEntryPoint
@@ -61,8 +57,8 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() {
override fun onMessageReceived(remoteMessage: RemoteMessage) {
super.onMessageReceived(remoteMessage)
- val title = remoteMessage.data[MESSAGE_TITLE] ?: return
- val body = remoteMessage.data[MESSAGE_BODY]
+ val title = remoteMessage.notification?.title ?: remoteMessage.data[MESSAGE_TITLE] ?: return
+ val body = remoteMessage.notification?.body ?: remoteMessage.data[MESSAGE_BODY]
val meetingId = remoteMessage.data[MESSAGE_MEETING_ID]?.toLongOrNull()
val notificationId = remoteMessage.data[MESSAGE_NOTIFICATION_ID]?.toLongOrNull()
@@ -70,16 +66,7 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() {
}
private fun showNotification(title: String, body: String?, meetingId: Long?, notificationId: Long?) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- val channel = NotificationChannel(
- CHANNEL_ID,
- CHANNEL_NAME,
- NotificationManager.IMPORTANCE_HIGH,
- ).apply {
- enableVibration(true)
- }
- getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
- }
+ NotificationChannels.createDefaultChannel(this)
val granted = ContextCompat.checkSelfPermission(
this,
@@ -87,7 +74,7 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() {
) == PackageManager.PERMISSION_GRANTED
if (!granted) return
- val androidNotificationId = notificationIdGenerator.incrementAndGet()
+ val androidNotificationId = (notificationId ?: System.currentTimeMillis()).toInt()
val contentIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
@@ -101,7 +88,7 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
- val notification = NotificationCompat.Builder(this, CHANNEL_ID)
+ val notification = NotificationCompat.Builder(this, NotificationChannels.defaultChannelId(this))
.setSmallIcon(R.drawable.img_logo_white_bg)
.setContentTitle(title)
.setContentText(body)
@@ -118,9 +105,6 @@ class MoiveFirebaseMessagingService: FirebaseMessagingService() {
const val MESSAGE_NOTIFICATION_ID = "notificationId"
private const val MESSAGE_TITLE = "title"
private const val MESSAGE_BODY = "body"
- private const val CHANNEL_ID = "moive_default_channel"
- private const val CHANNEL_NAME = "일반 알림"
private const val DEVICE_TOKEN_PUT_SUCCESS_MESSAGE = "디바이스 토큰 등록 성공"
- private val notificationIdGenerator = AtomicInteger(0)
}
}
diff --git a/app/src/main/java/com/moive/app/core/fcm/NotificationChannels.kt b/app/src/main/java/com/moive/app/core/fcm/NotificationChannels.kt
new file mode 100644
index 00000000..a4176e69
--- /dev/null
+++ b/app/src/main/java/com/moive/app/core/fcm/NotificationChannels.kt
@@ -0,0 +1,27 @@
+package com.moive.app.core.fcm
+
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.content.Context
+import android.os.Build
+import com.moive.app.R
+
+object NotificationChannels {
+ private const val DEFAULT_CHANNEL_NAME = "일반 알림"
+
+ fun defaultChannelId(context: Context): String =
+ context.getString(R.string.default_notification_channel_id)
+
+ fun createDefaultChannel(context: Context) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
+
+ val channel = NotificationChannel(
+ defaultChannelId(context),
+ DEFAULT_CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_HIGH,
+ ).apply {
+ enableVibration(true)
+ }
+ context.getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
+ }
+}
diff --git a/app/src/main/java/com/moive/app/data/meeting/mapper/MeetingDetailMapper.kt b/app/src/main/java/com/moive/app/data/meeting/mapper/MeetingDetailMapper.kt
index 3bc1e7fa..79c1dec6 100644
--- a/app/src/main/java/com/moive/app/data/meeting/mapper/MeetingDetailMapper.kt
+++ b/app/src/main/java/com/moive/app/data/meeting/mapper/MeetingDetailMapper.kt
@@ -32,4 +32,7 @@ fun ParticipantResponse.toModel(): ParticipantItemModel =
statusLabel = participantStateLabel,
isMe = isMe,
isDone = participantState.endsWith("_DONE"),
+ isVoteDone = participantState == VOTE_DONE_STATE,
)
+
+private const val VOTE_DONE_STATE = "VOTE_DONE"
diff --git a/app/src/main/java/com/moive/app/data/meeting/model/MeetingDetailModel.kt b/app/src/main/java/com/moive/app/data/meeting/model/MeetingDetailModel.kt
index 696692f0..07e1159f 100644
--- a/app/src/main/java/com/moive/app/data/meeting/model/MeetingDetailModel.kt
+++ b/app/src/main/java/com/moive/app/data/meeting/model/MeetingDetailModel.kt
@@ -31,4 +31,5 @@ data class ParticipantItemModel(
val statusLabel: String,
val isMe: Boolean,
val isDone: Boolean,
+ val isVoteDone: Boolean,
)
diff --git a/app/src/main/java/com/moive/app/presentation/common/component/ShadowButton.kt b/app/src/main/java/com/moive/app/presentation/common/component/ShadowButton.kt
index edba5535..8d33b554 100644
--- a/app/src/main/java/com/moive/app/presentation/common/component/ShadowButton.kt
+++ b/app/src/main/java/com/moive/app/presentation/common/component/ShadowButton.kt
@@ -4,15 +4,14 @@ import androidx.compose.foundation.background
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.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp
import com.moive.app.core.designsystem.component.button.MoiveButton
import com.moive.app.core.designsystem.component.button.MoiveButtonSize
import com.moive.app.core.designsystem.component.button.MoiveButtonType
import com.moive.app.core.designsystem.theme.MoiveTheme.colors
-import com.moive.app.core.designsystem.theme.MoiveTheme.radius
import com.moive.app.core.extensions.customShadow
@Composable
@@ -30,10 +29,7 @@ fun ShadowButton(
.then(
if (showShadow) {
Modifier.customShadow(
- shape = RoundedCornerShape(
- topStart = radius.md,
- topEnd = radius.md,
- ),
+ shape = RectangleShape,
color = colors.shadowBlack8,
blur = 12.dp,
)
@@ -43,10 +39,6 @@ fun ShadowButton(
)
.background(
color = colors.background.default00,
- shape = RoundedCornerShape(
- topStart = radius.md,
- topEnd = radius.md,
- ),
)
) {
MoiveButton(
diff --git a/app/src/main/java/com/moive/app/presentation/condition/ConditionViewModel.kt b/app/src/main/java/com/moive/app/presentation/condition/ConditionViewModel.kt
index 3aae3bf4..98a2ee76 100644
--- a/app/src/main/java/com/moive/app/presentation/condition/ConditionViewModel.kt
+++ b/app/src/main/java/com/moive/app/presentation/condition/ConditionViewModel.kt
@@ -133,7 +133,7 @@ class ConditionViewModel @Inject constructor(
}
}
- fun onDateNextClick() {
+ fun onDateSaveClick() {
val state = _uiState.value
val pending = state.pendingDateTime
val isNewDate = pending != null && state.confirmedDateTimes.none {
@@ -156,7 +156,7 @@ class ConditionViewModel @Inject constructor(
}
}
- fun onDateSaveClick() {
+ fun onDateNextClick() {
_uiState.update {
it.copy(
isDateBottomSheetVisible = false,
diff --git a/app/src/main/java/com/moive/app/presentation/condition/component/DateBottomSheet.kt b/app/src/main/java/com/moive/app/presentation/condition/component/DateBottomSheet.kt
index 8ca85f46..0a7e6381 100644
--- a/app/src/main/java/com/moive/app/presentation/condition/component/DateBottomSheet.kt
+++ b/app/src/main/java/com/moive/app/presentation/condition/component/DateBottomSheet.kt
@@ -90,6 +90,7 @@ fun DateBottomSheet(
text = "저장",
type = MoiveButtonType.TERTIARY,
size = MoiveButtonSize.LARGE,
+ enabled = state.pendingDateTime != null,
onClick = onSaveDateClick,
modifier = Modifier.weight(1f),
)
@@ -97,7 +98,6 @@ fun DateBottomSheet(
text = "다음",
type = MoiveButtonType.PRIMARY,
size = MoiveButtonSize.LARGE,
- enabled = state.pendingDateTime != null,
onClick = onNextDateClick,
modifier = Modifier.weight(1f),
)
diff --git a/app/src/main/java/com/moive/app/presentation/home/HomeContract.kt b/app/src/main/java/com/moive/app/presentation/home/HomeContract.kt
index 54d7b4af..cf716e57 100644
--- a/app/src/main/java/com/moive/app/presentation/home/HomeContract.kt
+++ b/app/src/main/java/com/moive/app/presentation/home/HomeContract.kt
@@ -2,6 +2,7 @@ package com.moive.app.presentation.home
import androidx.compose.runtime.Immutable
import com.moive.app.data.home.mapper.MeetingTab
+import com.moive.app.data.home.mapper.toMeetingTab
import com.moive.app.data.home.model.MyMeetingCardItemModel
import com.moive.app.data.home.model.ConfirmedMeetingItemModel
import kotlinx.collections.immutable.ImmutableList
@@ -27,6 +28,13 @@ interface HomeContract {
val displayedMyMeetingList: ImmutableList
get() = myMeetingList.take(MY_MEETING_LIST_MAX_SIZE).toImmutableList()
+ val emptyMyMeetingMessage: String
+ get() = when (selectedTab.toMeetingTab()) {
+ MeetingTab.ALL -> "아직 참여 중인 모임이 없어요."
+ MeetingTab.UPCOMING -> "예정된 모임이 없어요."
+ MeetingTab.PAST -> "지난 모임이 없어요."
+ }
+
companion object {
private const val MY_MEETING_LIST_MAX_SIZE = 5
}
diff --git a/app/src/main/java/com/moive/app/presentation/home/HomeScreen.kt b/app/src/main/java/com/moive/app/presentation/home/HomeScreen.kt
index 8bde06d2..3139c36d 100644
--- a/app/src/main/java/com/moive/app/presentation/home/HomeScreen.kt
+++ b/app/src/main/java/com/moive/app/presentation/home/HomeScreen.kt
@@ -223,7 +223,7 @@ private fun HomeScreen(
)
Text(
- text = "아직 참여 중인 모임이 없어요.",
+ text = uiState.emptyMyMeetingMessage,
color = colors.text.subtle,
style = typography.body.smNormalR,
)
diff --git a/app/src/main/java/com/moive/app/presentation/home/component/ConfirmedMeetingCardItem.kt b/app/src/main/java/com/moive/app/presentation/home/component/ConfirmedMeetingCardItem.kt
index 36ed6262..1a33fa7a 100644
--- a/app/src/main/java/com/moive/app/presentation/home/component/ConfirmedMeetingCardItem.kt
+++ b/app/src/main/java/com/moive/app/presentation/home/component/ConfirmedMeetingCardItem.kt
@@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.moive.app.R
@@ -101,6 +102,8 @@ fun ConfirmedMeetingCardItem(
text = location,
color = colors.text.onBg,
style = typography.body.smNormalR,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
)
}
diff --git a/app/src/main/java/com/moive/app/presentation/login/component/AgreementBottomSheet.kt b/app/src/main/java/com/moive/app/presentation/login/component/AgreementBottomSheet.kt
index 717c89b6..a5818214 100644
--- a/app/src/main/java/com/moive/app/presentation/login/component/AgreementBottomSheet.kt
+++ b/app/src/main/java/com/moive/app/presentation/login/component/AgreementBottomSheet.kt
@@ -17,8 +17,10 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
+import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -36,6 +38,7 @@ import com.moive.app.core.designsystem.component.bottomsheet.MoiveBottomSheet
import com.moive.app.core.designsystem.component.button.MoiveButton
import com.moive.app.core.designsystem.component.button.MoiveButtonSize
import com.moive.app.core.designsystem.component.button.MoiveButtonType
+import com.moive.app.core.designsystem.component.toast.LocalSnackbarHostState
import com.moive.app.core.designsystem.theme.MoiveTheme
import com.moive.app.core.designsystem.theme.MoiveTheme.colors
import com.moive.app.core.designsystem.theme.MoiveTheme.radius
@@ -137,7 +140,7 @@ private fun AllAgreementButton(
shape = RoundedCornerShape(radius.md),
)
.background(
- color = if (isPressed) colors.fill.default06 else colors.fill.default07,
+ color = if (isPressed || isChecked) colors.fill.default06 else colors.fill.default07,
shape = RoundedCornerShape(radius.md),
)
.noRippleClickable(onClick = { onAllAgreementClick(!isChecked) })
@@ -216,31 +219,34 @@ private fun AgreementBottomSheetPreview() {
var isServiceAgreed by remember { mutableStateOf(false) }
var isPrivacyAgreed by remember { mutableStateOf(false) }
var isMarketingAgreed by remember { mutableStateOf(false) }
+ val snackbarHostState = remember { SnackbarHostState() }
+
+ CompositionLocalProvider(LocalSnackbarHostState provides snackbarHostState) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ MoiveButton(
+ text = "약관동의 바텀시트 열기",
+ type = MoiveButtonType.PRIMARY,
+ size = MoiveButtonSize.LARGE,
+ onClick = { showBottomSheet = true },
+ )
+ }
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center,
- ) {
- MoiveButton(
- text = "약관동의 바텀시트 열기",
- type = MoiveButtonType.PRIMARY,
- size = MoiveButtonSize.LARGE,
- onClick = { showBottomSheet = true },
- )
- }
-
- if (showBottomSheet) {
- AgreementBottomSheet(
- isServiceAgreed = isServiceAgreed,
- isPrivacyAgreed = isPrivacyAgreed,
- isMarketingAgreed = isMarketingAgreed,
- isConfirmEnabled = isServiceAgreed && isPrivacyAgreed,
- onServiceClick = { isServiceAgreed = it },
- onPrivacyClick = { isPrivacyAgreed = it },
- onMarketingClick = { isMarketingAgreed = it },
- onConfirmClick = {},
- onDismissRequest = { showBottomSheet = false },
- )
+ if (showBottomSheet) {
+ AgreementBottomSheet(
+ isServiceAgreed = isServiceAgreed,
+ isPrivacyAgreed = isPrivacyAgreed,
+ isMarketingAgreed = isMarketingAgreed,
+ isConfirmEnabled = isServiceAgreed && isPrivacyAgreed,
+ onServiceClick = { isServiceAgreed = it },
+ onPrivacyClick = { isPrivacyAgreed = it },
+ onMarketingClick = { isMarketingAgreed = it },
+ onConfirmClick = {},
+ onDismissRequest = { showBottomSheet = false },
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt b/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt
index 484075bf..39907aa9 100644
--- a/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt
+++ b/app/src/main/java/com/moive/app/presentation/main/MainActivity.kt
@@ -155,14 +155,15 @@ class MainActivity : ComponentActivity() {
private fun Intent.extractInviteCode(): String? =
takeIf { it.action == Intent.ACTION_VIEW }?.data?.lastPathSegment
-private fun Intent.extractMeetingId(): Long? {
- val meetingId = getLongExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID, -1L)
- if (meetingId != -1L) removeExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID)
- return meetingId.takeIf { it != -1L }
-}
+private fun Intent.extractMeetingId(): Long? =
+ extractLongExtra(MoiveFirebaseMessagingService.MESSAGE_MEETING_ID)
+
+private fun Intent.extractNotificationId(): Long? =
+ extractLongExtra(MoiveFirebaseMessagingService.MESSAGE_NOTIFICATION_ID)
-private fun Intent.extractNotificationId(): Long? {
- val notificationId = getLongExtra(MoiveFirebaseMessagingService.MESSAGE_NOTIFICATION_ID, -1L)
- if (notificationId != -1L) removeExtra(MoiveFirebaseMessagingService.MESSAGE_NOTIFICATION_ID)
- return notificationId.takeIf { it != -1L }
+private fun Intent.extractLongExtra(key: String): Long? {
+ val value = getLongExtra(key, -1L).takeIf { it != -1L }
+ ?: getStringExtra(key)?.toLongOrNull()
+ removeExtra(key)
+ return value
}
diff --git a/app/src/main/java/com/moive/app/presentation/main/component/MainBottomBar.kt b/app/src/main/java/com/moive/app/presentation/main/component/MainBottomBar.kt
index c9981e30..93b2755d 100644
--- a/app/src/main/java/com/moive/app/presentation/main/component/MainBottomBar.kt
+++ b/app/src/main/java/com/moive/app/presentation/main/component/MainBottomBar.kt
@@ -122,8 +122,8 @@ private fun MeetingFloatingButton(
.customShadow(
shape = RoundedCornerShape(radius.circular),
color = colors.shadow20,
- blur = 10.dp,
- offsetY = 4.dp,
+ blur = 5.dp,
+ offsetY = 2.dp,
)
.background(
color = colors.fill.default00,
diff --git a/app/src/main/java/com/moive/app/presentation/meeting/create/component/MeetingCreationContent.kt b/app/src/main/java/com/moive/app/presentation/meeting/create/component/MeetingCreationContent.kt
index 558bb239..aaaddd57 100644
--- a/app/src/main/java/com/moive/app/presentation/meeting/create/component/MeetingCreationContent.kt
+++ b/app/src/main/java/com/moive/app/presentation/meeting/create/component/MeetingCreationContent.kt
@@ -137,6 +137,7 @@ fun MeetingCreationContent(
onKeyboardAction = {
focusManager.clearFocus()
},
+ isError = uiState.isMeetingScheduleFormatInvalid,
)
if (uiState.isMeetingScheduleFormatInvalid) {
diff --git a/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailScreen.kt b/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailScreen.kt
index 4b738069..26d8ed39 100644
--- a/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailScreen.kt
+++ b/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailScreen.kt
@@ -2,13 +2,13 @@ package com.moive.app.presentation.meeting.detail
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -19,15 +19,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.zIndex
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
@@ -97,6 +100,7 @@ fun MeetingDetailRoute(
uiState.scheduledDate,
uiState.scheduledTime,
)
+
else -> Unit
}
},
@@ -137,6 +141,11 @@ private fun MeetingDetailScreen(
val isContentScrollable by remember {
derivedStateOf { lazyListState.canScrollForward || lazyListState.canScrollBackward }
}
+ val density = LocalDensity.current
+ var toolTipHeight by remember { mutableStateOf(0.dp) }
+ val isToolTipVisible = uiState.toolTipMessage.isNotEmpty()
+ val toolTipReservedHeight =
+ if (isToolTipVisible) toolTipHeight + 4.dp else 0.dp
Column(
modifier = modifier
@@ -150,74 +159,83 @@ private fun MeetingDetailScreen(
onTrailingIconClick = onMoreClick,
)
- LazyColumn(
- state = lazyListState,
+ Box(
modifier = Modifier.weight(1f),
- contentPadding = PaddingValues(horizontal = 20.dp, vertical = 20.dp),
) {
- item {
- MeetingInfoRow(
- thumbnailRes = uiState.thumbnailType.toMeetingThumbnailRes(),
- meetingName = uiState.meetingName,
- meetingPurpose = uiState.meetingPurpose,
- )
+ LazyColumn(
+ state = lazyListState,
+ modifier = Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(
+ start = 20.dp,
+ top = 20.dp,
+ end = 20.dp,
+ bottom = 20.dp + toolTipReservedHeight,
+ ),
+ ) {
+ item {
+ MeetingInfoRow(
+ thumbnailRes = uiState.thumbnailType.toMeetingThumbnailRes(),
+ meetingName = uiState.meetingName,
+ meetingPurpose = uiState.meetingPurpose,
+ )
- Spacer(modifier = Modifier.height(20.dp))
+ Spacer(modifier = Modifier.height(20.dp))
- MoiveButton(
- text = "친구 초대",
- icon = ImageVector.vectorResource(R.drawable.ic_mail_fill_20),
- type = MoiveButtonType.TERTIARY,
- size = MoiveButtonSize.MEDIUM,
- onClick = onInviteFriendClick,
- )
+ MoiveButton(
+ text = "친구 초대",
+ icon = ImageVector.vectorResource(R.drawable.ic_mail_fill_20),
+ type = MoiveButtonType.TERTIARY,
+ size = MoiveButtonSize.MEDIUM,
+ onClick = onInviteFriendClick,
+ )
- Spacer(modifier = Modifier.height(32.dp))
+ Spacer(modifier = Modifier.height(32.dp))
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(4.dp),
- ) {
- Icon(
- imageVector = ImageVector.vectorResource(R.drawable.ic_user_16),
- contentDescription = null,
- tint = colors.icon.tertiary,
- )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ ) {
+ Icon(
+ imageVector = ImageVector.vectorResource(R.drawable.ic_user_16),
+ contentDescription = null,
+ tint = colors.icon.tertiary,
+ )
- Text(
- text = "참여자 ${uiState.participants.size}명",
- color = colors.text.tertiary,
- style = typography.label.xsR,
- )
+ Text(
+ text = "참여자 ${uiState.participants.size}명",
+ color = colors.text.tertiary,
+ style = typography.label.xsR,
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
}
- Spacer(modifier = Modifier.height(12.dp))
+ items(
+ items = uiState.participants,
+ key = { it.id }
+ ) { participant ->
+ ParticipantItem(
+ participant = participant,
+ status = uiState.status,
+ onActionButtonClick = onActionButtonClick,
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+ }
}
- items(
- items = uiState.participants,
- key = { it.id }
- ) { participant ->
- ParticipantItem(
- participant = participant,
- status = uiState.status,
- onActionButtonClick = onActionButtonClick,
+ if (isToolTipVisible) {
+ MoiveToolTip(
+ text = uiState.toolTipMessage,
+ modifier = Modifier
+ .align(Alignment.BottomCenter)
+ .padding(bottom = 4.dp)
+ .onSizeChanged { toolTipHeight = with(density) { it.height.toDp() } },
)
-
- Spacer(modifier = Modifier.height(12.dp))
}
}
- if (uiState.toolTipMessage.isNotEmpty()) {
- MoiveToolTip(
- text = uiState.toolTipMessage,
- modifier = Modifier
- .align(Alignment.CenterHorizontally)
- .zIndex(1f)
- .offset(y = 8.dp),
- )
- }
-
ShadowButton(
text = uiState.primaryActionLabel,
isEnabled = uiState.primaryActionEnabled,
@@ -241,7 +259,7 @@ private fun MeetingDetailScreen(
}
}
-@Preview(showBackground = true,)
+@Preview(showBackground = true)
@Composable
private fun MeetingDetailScreenPreview() {
MoiveTheme {
@@ -260,6 +278,7 @@ private fun MeetingDetailScreenPreview() {
statusLabel = "조건 입력 전",
isMe = true,
isDone = false,
+ isVoteDone = false,
),
ParticipantItemModel(
id = 2L,
@@ -268,6 +287,7 @@ private fun MeetingDetailScreenPreview() {
statusLabel = "조건 입력 완료",
isMe = false,
isDone = true,
+ isVoteDone = false,
),
),
),
diff --git a/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailViewModel.kt b/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailViewModel.kt
index c357f73f..fa66440c 100644
--- a/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailViewModel.kt
+++ b/app/src/main/java/com/moive/app/presentation/meeting/detail/MeetingDetailViewModel.kt
@@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
+import com.moive.app.data.meeting.mapper.MeetingStatus
import com.moive.app.data.meeting.repository.MeetingRepository
import com.moive.app.data.meeting.repository.NotMeetingParticipantException
import com.moive.app.presentation.meeting.detail.MeetingDetailContract.SideEffect
@@ -43,6 +44,7 @@ class MeetingDetailViewModel @Inject constructor(
meetingRepository.getMeetingDetail(meetingId)
.onSuccess { detail ->
+ val isMyVoteDone = detail.participants.any { it.isMe && it.isVoteDone }
_uiState.update {
it.copy(
meetingDetailUiState = MeetingDetailUiState.Success,
@@ -58,7 +60,8 @@ class MeetingDetailViewModel @Inject constructor(
participants = detail.participants,
toolTipMessage = detail.homeMessage ?: "",
primaryActionLabel = detail.primaryActionLabel ?: "",
- primaryActionEnabled = detail.primaryActionEnabled,
+ primaryActionEnabled = detail.primaryActionEnabled &&
+ !(detail.status == MeetingStatus.VOTING && isMyVoteDone),
)
}
}
diff --git a/app/src/main/java/com/moive/app/presentation/meeting/detail/component/ParticipantItem.kt b/app/src/main/java/com/moive/app/presentation/meeting/detail/component/ParticipantItem.kt
index 04e13f2c..4be407d3 100644
--- a/app/src/main/java/com/moive/app/presentation/meeting/detail/component/ParticipantItem.kt
+++ b/app/src/main/java/com/moive/app/presentation/meeting/detail/component/ParticipantItem.kt
@@ -1,5 +1,6 @@
package com.moive.app.presentation.meeting.detail.component
+import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -13,6 +14,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import com.moive.app.core.designsystem.component.button.MoiveButton
import com.moive.app.core.designsystem.component.button.MoiveButtonSize
@@ -40,6 +42,10 @@ fun ParticipantItem(
color = if (participant.isMe) colors.stroke.default03 else colors.stroke.default04,
shape = RoundedCornerShape(radius.md),
)
+ .background(
+ color = if (participant.isMe) colors.fill.default08 else colors.fill.default07,
+ shape = RoundedCornerShape(radius.md),
+ )
.padding(12.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
@@ -52,6 +58,7 @@ fun ParticipantItem(
modifier = Modifier
.size(40.dp)
.clip(RoundedCornerShape(radius.circular)),
+ contentScale = ContentScale.Crop,
)
Column(
diff --git a/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListContract.kt b/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListContract.kt
index 232ffc29..600a1b06 100644
--- a/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListContract.kt
+++ b/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListContract.kt
@@ -2,6 +2,7 @@ package com.moive.app.presentation.meeting.list
import androidx.compose.runtime.Immutable
import com.moive.app.data.home.mapper.MeetingTab
+import com.moive.app.data.home.mapper.toMeetingTab
import com.moive.app.data.meeting.model.MeetingListCardItemModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -20,6 +21,13 @@ interface MeetingListContract {
MeetingTab.UPCOMING.label,
MeetingTab.PAST.label,
)
+
+ val emptyMeetingMessage: String
+ get() = when (selectedTab.toMeetingTab()) {
+ MeetingTab.ALL -> "아직 참여 중인 모임이 없어요."
+ MeetingTab.UPCOMING -> "예정된 모임이 없어요."
+ MeetingTab.PAST -> "지난 모임이 없어요."
+ }
}
}
diff --git a/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListScreen.kt b/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListScreen.kt
index 1a02a51c..8a222b83 100644
--- a/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListScreen.kt
+++ b/app/src/main/java/com/moive/app/presentation/meeting/list/MeetingListScreen.kt
@@ -118,7 +118,7 @@ private fun MeetingListScreen(
Spacer(modifier = Modifier.height(16.dp))
Text(
- text = "아직 참여 중인 모임이 없어요.",
+ text = uiState.emptyMeetingMessage,
color = colors.text.subtle,
style = typography.body.smNormalR,
)
diff --git a/app/src/main/java/com/moive/app/presentation/splash/SplashContract.kt b/app/src/main/java/com/moive/app/presentation/splash/SplashContract.kt
index cd58ad7b..c7e1394a 100644
--- a/app/src/main/java/com/moive/app/presentation/splash/SplashContract.kt
+++ b/app/src/main/java/com/moive/app/presentation/splash/SplashContract.kt
@@ -1,6 +1,7 @@
package com.moive.app.presentation.splash
import androidx.compose.runtime.Immutable
+import com.moive.app.core.designsystem.component.toast.ToastType
interface SplashContract {
@@ -11,6 +12,7 @@ interface SplashContract {
sealed interface SideEffect {
data class NavigateToHome(val isAutoLoginSuccess: Boolean) : SideEffect
+ data class OnShowToast(val msg: String, val type: ToastType) : SideEffect
}
}
diff --git a/app/src/main/java/com/moive/app/presentation/splash/SplashOverlay.kt b/app/src/main/java/com/moive/app/presentation/splash/SplashOverlay.kt
index c2defe74..65b3cdb8 100644
--- a/app/src/main/java/com/moive/app/presentation/splash/SplashOverlay.kt
+++ b/app/src/main/java/com/moive/app/presentation/splash/SplashOverlay.kt
@@ -22,9 +22,11 @@ import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.moive.app.R
+import com.moive.app.core.designsystem.component.toast.LocalToastTrigger
import com.moive.app.core.designsystem.theme.MoiveTheme
import com.moive.app.core.designsystem.theme.MoiveTheme.colors
import com.moive.app.presentation.splash.SplashContract.SideEffect.NavigateToHome
+import com.moive.app.presentation.splash.SplashContract.SideEffect.OnShowToast
@Composable
fun SplashOverlay(
@@ -34,6 +36,7 @@ fun SplashOverlay(
viewModel: SplashViewModel = hiltViewModel(),
) {
val lifeCycleOwner = LocalLifecycleOwner.current
+ val showToast = LocalToastTrigger.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
var isAnimationEnded by remember { mutableStateOf(false) }
@@ -56,6 +59,8 @@ fun SplashOverlay(
if (sideEffect.isAutoLoginSuccess) navigateToHome() else navigateToLogin()
viewModel.onReady()
}
+
+ is OnShowToast -> showToast(sideEffect.msg, sideEffect.type)
}
}
}
diff --git a/app/src/main/java/com/moive/app/presentation/splash/SplashViewModel.kt b/app/src/main/java/com/moive/app/presentation/splash/SplashViewModel.kt
index b687fb7e..f4c9fa82 100644
--- a/app/src/main/java/com/moive/app/presentation/splash/SplashViewModel.kt
+++ b/app/src/main/java/com/moive/app/presentation/splash/SplashViewModel.kt
@@ -2,9 +2,11 @@ package com.moive.app.presentation.splash
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
+import com.moive.app.core.designsystem.component.toast.ToastType
import com.moive.app.core.utils.suspendRunCatching
import com.moive.app.data.auth.repository.AuthRepository
import com.moive.app.presentation.splash.SplashContract.SideEffect.NavigateToHome
+import com.moive.app.presentation.splash.SplashContract.SideEffect.OnShowToast
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
@@ -15,6 +17,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
+import java.io.IOException
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
@@ -44,16 +47,18 @@ class SplashViewModel @Inject constructor(
}
delayTime.await()
- val isAutoLoginSuccess = reissueToken.await()
+ val reissueResult = reissueToken.await()
.onSuccess {
Timber.tag(AUTHORIZATION).d(AUTO_LOGIN_SUCCESS_MESSAGE)
}
.onFailure { error ->
Timber.tag(AUTHORIZATION).e("$AUTO_LOGIN_FAILURE_MESSAGE $error")
}
- .isSuccess
- _sideEffect.send(NavigateToHome(isAutoLoginSuccess = isAutoLoginSuccess))
+ if (reissueResult.exceptionOrNull() is IOException) {
+ _sideEffect.send(OnShowToast(NETWORK_ERROR_MESSAGE, ToastType.ERROR))
+ }
+ _sideEffect.send(NavigateToHome(isAutoLoginSuccess = reissueResult.isSuccess))
}
}
@@ -76,6 +81,7 @@ class SplashViewModel @Inject constructor(
private const val REISSUE_FAILURE_MESSAGE = "토큰 재발급 실패"
private const val AUTO_LOGIN_SUCCESS_MESSAGE = "자동 로그인 성공"
private const val AUTO_LOGIN_FAILURE_MESSAGE = "자동 로그인 실패"
+ private const val NETWORK_ERROR_MESSAGE = "네트워크 연결을 확인해주세요."
}
}
diff --git a/app/src/main/res/drawable/img_pin_marker_1.png b/app/src/main/res/drawable/img_pin_marker_1.png
index 66fd1cdc..c0844657 100644
Binary files a/app/src/main/res/drawable/img_pin_marker_1.png and b/app/src/main/res/drawable/img_pin_marker_1.png differ
diff --git a/app/src/main/res/drawable/img_pin_marker_2.png b/app/src/main/res/drawable/img_pin_marker_2.png
index dae6d2cd..3d4fc79c 100644
Binary files a/app/src/main/res/drawable/img_pin_marker_2.png and b/app/src/main/res/drawable/img_pin_marker_2.png differ
diff --git a/app/src/main/res/drawable/img_pin_marker_3.png b/app/src/main/res/drawable/img_pin_marker_3.png
index b7a7498e..bb0084f2 100644
Binary files a/app/src/main/res/drawable/img_pin_marker_3.png and b/app/src/main/res/drawable/img_pin_marker_3.png differ
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 69304061..0d696b71 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
MOIVE
+ moive_default_channel