Skip to content

Commit

Permalink
Merge pull request #142 from depromeet/feature/#133-notification-list
Browse files Browse the repository at this point in the history
[FEAT/#133] 알림 화면 연결
  • Loading branch information
Mnseo authored Feb 12, 2024
2 parents 2780c9c + 0405953 commit 086130b
Show file tree
Hide file tree
Showing 23 changed files with 488 additions and 20 deletions.
3 changes: 3 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@
<activity
android:name=".presentation.group.join.JoinFriendListActivity"
android:exported="false" />
<activity
android:name=".presentation.notification.AlertsListActivity"
android:exported="false" />

<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.google.firebase.messaging.RemoteMessage
import com.teumteum.data.model.request.toDeviceToken
import com.teumteum.data.service.UserService
import com.teumteum.domain.TeumTeumDataStore
import com.teumteum.domain.entity.Message
import com.teumteum.teumteum.R
import com.teumteum.teumteum.presentation.splash.SplashActivity
import dagger.hilt.android.AndroidEntryPoint
Expand Down Expand Up @@ -52,16 +53,19 @@ class TeumMessagingService : FirebaseMessagingService() {
override fun onMessageReceived(message: RemoteMessage) {
super.onMessageReceived(message)

if (dataStore.isLogin) {
if (message.data.isNotEmpty() && message.data["title"].toString() != EMPTY) {
sendNotificationAlarm(
Message(message.data["title"].toString(), message.data["content"].toString())
)
}
else {
message.notification?.let {
sendNotificationAlarm(Message(it.title.toString(), it.body.toString()))
if (dataStore.onNotification) {
if (dataStore.isLogin) {
val alertMessage = Message("", "", "")
if (message.data.isNotEmpty()) {
alertMessage.title = message.notification?.title.toString()
alertMessage.body = message.notification?.body.toString()
alertMessage.type = message.data["type"].toString()
}
if (alertMessage.type == END_MEETING) {
alertMessage.meetingId = message.data["meetingId"]?.toInt()
alertMessage.participants = message.data["participants"]?.toList()?.map { it.digitToInt() }
}
if (alertMessage.title.isNotEmpty()) sendNotificationAlarm(alertMessage)
}
}
}
Expand All @@ -70,6 +74,8 @@ class TeumMessagingService : FirebaseMessagingService() {
val requestCode = (System.currentTimeMillis() % 10000).toInt()
val intent = Intent(this, SplashActivity::class.java)
intent.putExtra("isFromAlarm", true)
intent.putExtra("message", message)
Timber.tag("teum-alerts").d("message: ${message.title}, ${message.type}, ${message.body}, ${message.meetingId}, ${message.participants.toString()}")
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent =
PendingIntent.getActivity(
Expand All @@ -94,7 +100,6 @@ class TeumMessagingService : FirebaseMessagingService() {

companion object {
const val EMPTY = "null"
private const val END_MEETING = "END_MEETING"
}

private data class Message(var title: String, var body: String)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import androidx.navigation.ui.setupWithNavController
import com.teumteum.base.BindingActivity
import com.teumteum.base.util.extension.boolExtra
import com.teumteum.base.util.extension.intExtra
import com.teumteum.domain.entity.Message
import com.teumteum.teumteum.R
import com.teumteum.teumteum.databinding.ActivityMainBinding
import com.teumteum.teumteum.presentation.home.HomeFragmentDirections
Expand Down Expand Up @@ -44,7 +45,20 @@ class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main


if (isFromAlarm) {
val action = HomeFragmentDirections.actionHomeFragmentToFragmentFamiliar()
val message = intent.getSerializableExtra(MESSAGE) as Message
var action = HomeFragmentDirections.actionHomeFragmentToFragmentFamiliar()
when (message.type) {
BEFORE_MEETING -> {
}
END_MEETING -> {
// val meetingId = message.meetingId
// val participants = message.participants
// action = HomeFragmentDirections.{홈 -> 유저리뷰로 이동하는 navi}
}
RECOMMEND_USER -> {
action = HomeFragmentDirections.actionHomeFragmentToFragmentMyPage()
}
}
val navHostFragment = supportFragmentManager.findFragmentById(R.id.fl_main) as NavHostFragment
navHostFragment.navController.navigate(action)
}
Expand Down Expand Up @@ -138,7 +152,14 @@ class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main
}
fun getIntent(context: Context, id: Int, isFromAlarm: Boolean = false) = Intent(context, MainActivity::class.java).apply {
putExtra("id", id)
putExtra("isFromAlarm", isFromAlarm)
putExtra(IS_FROM_ALARM, isFromAlarm)
}

private const val IS_FROM_ALARM = "isFromAlarm"
private const val MESSAGE = "message"

private const val BEFORE_MEETING = "BEFORE_MEETING"
private const val END_MEETING = "END_MEETING"
private const val RECOMMEND_USER = "RECOMMEND_USER"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.teumteum.teumteum.presentation.group.GroupListUiState
import com.teumteum.teumteum.presentation.group.GroupListViewModel
import com.teumteum.teumteum.presentation.group.join.GroupDetailActivity
import com.teumteum.teumteum.presentation.group.search.SearchActivity
import com.teumteum.teumteum.presentation.notification.AlertsListActivity
import com.teumteum.teumteum.util.callback.CustomBackPressedCallback
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
Expand Down Expand Up @@ -80,7 +81,12 @@ class HomeFragment :
AppBarMenu.IconStyle(
resourceId = R.drawable.ic_bell,
useRippleEffect = false,
clickEvent = null
clickEvent = {
Intent(requireActivity(), AlertsListActivity::class.java).apply {
startActivity(this)
(activity as? BindingActivity<*>)?.openActivitySlideAnimation()
}
}
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,12 @@ class SettingViewModel @Inject constructor(
return false
}

private val _alarmState = MutableStateFlow(false)
private val _alarmState = MutableStateFlow(settingRepository.getNotification())
val alarmState: StateFlow<Boolean> = _alarmState.asStateFlow()

fun onToggleChange(newToggleState: Boolean) {
_alarmState.value = newToggleState
settingRepository.setNotification(newToggleState)
_alarmState.value = settingRepository.getNotification()
}

private val _settingStatus = MutableStateFlow(SettingStatus.DEFAULT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.teumteum.teumteum.presentation.notification

import android.os.Bundle
import androidx.activity.viewModels
import androidx.core.view.isVisible
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.teumteum.base.BindingActivity
import com.teumteum.base.component.appbar.AppBarLayout
import com.teumteum.base.component.appbar.AppBarMenu
import com.teumteum.base.databinding.LayoutCommonAppbarBinding
import com.teumteum.base.util.extension.defaultToast
import com.teumteum.domain.entity.TeumAlert
import com.teumteum.teumteum.R
import com.teumteum.teumteum.databinding.ActivityAlertsListBinding
import com.teumteum.teumteum.presentation.group.GroupListUiState
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach

@AndroidEntryPoint
class AlertsListActivity
: BindingActivity<ActivityAlertsListBinding>(R.layout.activity_alerts_list), AppBarLayout {

private lateinit var adapter: AlertsListAdapter
private val viewModel by viewModels<AlertsViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

initAppBarLayout()
initList()
viewModel.getAlerts()
observe()
}

override val appBarBinding: LayoutCommonAppbarBinding
get() = binding.appBar

override fun initAppBarLayout() {
setAppBarHeight(48)

addMenuToLeft(
AppBarMenu.IconStyle(
resourceId = R.drawable.ic_arrow_left_l,
useRippleEffect = false,
clickEvent = ::finish
)
)
setAppBarTitleText(R.string.alerts_list_title)
}

private fun initItems() {
adapter.setItems(listOf(
TeumAlert("틈 채우기", "한별님이 당신을 추천했어요!", RECOMMEND_USER, "2024-01-30T16:33:00", false),
TeumAlert("틈 채우기", "한별님이 당신을 추천했어요!", RECOMMEND_USER, "2024-01-31T16:33:00", false),
TeumAlert("틈 채우기", "한별님이 당신을 추천했어요!", RECOMMEND_USER, "2024-02-10T16:33:00", false),
TeumAlert("틈 채우기", "한별님이 당신을 추천했어요!", RECOMMEND_USER, "2024-02-11T16:33:00", false),
TeumAlert("틈 채우기", "한별님이 당신을 추천했어요!", RECOMMEND_USER, "2024-02-12T09:33:00", false),
TeumAlert("틈 채우기", "한별님이 당신을 추천했어요!", RECOMMEND_USER, "2024-02-12T10:33:00", false)
))
}

private fun observe() {
viewModel.alertsData.flowWithLifecycle(lifecycle)
.onEach {
binding.clEmpty.isVisible = it is AlertsListUiState.Empty
binding.tvNoticeEmpty.isVisible = it !is AlertsListUiState.Empty
binding.rvAlertsList.isVisible = it !is AlertsListUiState.Empty
when (it) {
is AlertsListUiState.SetAlerts -> {
adapter.setItems(it.data)
}
is AlertsListUiState.Failure -> {
defaultToast(it.msg)
}
else -> {}
}
}.launchIn(lifecycleScope)
}

private fun initList() {
adapter = AlertsListAdapter {
when (it.type) {
// 알림 리스트에서 클릭 시 이동하는 정책이 있다면
BEFORE_MEETING -> {
// 틈틈으로 이동
// openActivitySlideAnimation()
}
END_MEETING -> {
// 해당 모임 종료 화면으로 이동
// openActivitySlideAnimation()
}
RECOMMEND_USER -> {
// 마이페이지로 이동
// openActivitySlideAnimation()
}
}
}
binding.rvAlertsList.adapter = adapter
}

companion object {
private const val BEFORE_MEETING = "BEFORE_MEETING"
private const val END_MEETING = "END_MEETING"
private const val RECOMMEND_USER = "RECOMMEND_USER"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.teumteum.teumteum.presentation.notification

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.teumteum.base.R.*
import com.teumteum.base.util.extension.setOnSingleClickListener
import com.teumteum.domain.entity.TeumAlert
import com.teumteum.teumteum.databinding.ItemAlertsListBinding
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

class AlertsListAdapter(private val itemClick: (TeumAlert) -> (Unit)) :
RecyclerView.Adapter<AlertsListAdapter.AlertsListViewHolder>() {
private val alertList = mutableListOf<TeumAlert>()

override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): AlertsListViewHolder {
val binding = ItemAlertsListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return AlertsListViewHolder(binding, parent.context, itemClick)
}

override fun onBindViewHolder(holder: AlertsListViewHolder, position: Int) {
holder.onBind(alertList[position])
}

override fun getItemCount(): Int = alertList.size

fun setItems(newItems: List<TeumAlert>) {
alertList.clear()
alertList.addAll(newItems.sortedByDescending {
LocalDateTime.parse(it.createdAt, DateTimeFormatter.ISO_LOCAL_DATE_TIME)
})
notifyDataSetChanged()
}

class AlertsListViewHolder(
private val binding: ItemAlertsListBinding,
private val context: Context,
private val itemClick: (TeumAlert) -> (Unit)
) : RecyclerView.ViewHolder(binding.root) {

fun onBind(item: TeumAlert) {
binding.tvTitle.text = item.title
binding.tvContent.text = item.body
binding.tvTime.text = getTimeDifference(item.createdAt)

if (!item.isRead) {
binding.root.setBackgroundColor(context.getColor(color.elevation_level01))
} else {
binding.root.setBackgroundColor(context.getColor(color.transparent))
}
binding.root.setOnSingleClickListener {
itemClick(item)
}
}

private fun getTimeDifference(inputDateTime: String): String {
val currentTime = LocalDateTime.now()
val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
val inputTime = LocalDateTime.parse(inputDateTime, formatter)

val diffInMinutes = java.time.Duration.between(inputTime, currentTime).toMinutes()
val diffInHours = java.time.Duration.between(inputTime, currentTime).toHours()

return when {
diffInMinutes < 60 -> "${diffInMinutes}분 전"
diffInHours < 24 -> "${diffInHours}시간 전"
else -> "${inputTime.monthValue}${inputTime.dayOfMonth}"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.teumteum.teumteum.presentation.notification

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.teumteum.domain.entity.TeumAlert
import com.teumteum.domain.repository.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class AlertsViewModel @Inject constructor(
private val repository: UserRepository
) : ViewModel() {

private val _alertsData = MutableStateFlow<AlertsListUiState>(AlertsListUiState.Init)
val alertsData: StateFlow<AlertsListUiState> = _alertsData

fun getAlerts() {
viewModelScope.launch {
repository.getAlerts()
.onSuccess {
if (it.alerts.isEmpty()) _alertsData.value = AlertsListUiState.Empty
else _alertsData.value = AlertsListUiState.SetAlerts(it.alerts)
}
.onFailure {
_alertsData.value = AlertsListUiState.Failure("알림 가져오기 실패")
}
}
}
}

sealed interface AlertsListUiState {
object Init : AlertsListUiState
object Empty : AlertsListUiState
data class SetAlerts(val data: List<TeumAlert>) : AlertsListUiState
data class Failure(val msg: String) : AlertsListUiState
}
Loading

0 comments on commit 086130b

Please sign in to comment.