notificaciones
This commit is contained in:
parent
bd994a17e8
commit
9667eafa49
@ -1,10 +1,15 @@
|
||||
package com.isolaatti.notifications.data
|
||||
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface NotificationsApi {
|
||||
@GET("/api/Notifications/list")
|
||||
fun getNotifications(@Query("after") after: Long?): Call<NotificationsDto>
|
||||
|
||||
@POST("/api/Notifications/delete_many")
|
||||
fun deleteNotifications(@Body ids: DeleteNotificationsDto): Call<Unit>
|
||||
}
|
||||
@ -1,6 +1,5 @@
|
||||
package com.isolaatti.notifications.data
|
||||
|
||||
import com.google.gson.internal.LinkedTreeMap
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
data class NotificationsDto(
|
||||
@ -12,13 +11,7 @@ data class NotificationDto(
|
||||
val date: ZonedDateTime,
|
||||
val userId: Int,
|
||||
val read: Boolean,
|
||||
val payload: NotificationPayload
|
||||
)
|
||||
|
||||
data class NotificationPayload(
|
||||
val type: String,
|
||||
val authorId: Int,
|
||||
val authorName: String?,
|
||||
val intentData: String?,
|
||||
val data: Map<String, String>
|
||||
)
|
||||
|
||||
data class DeleteNotificationsDto(val ids: List<Long>)
|
||||
@ -14,10 +14,11 @@ class NotificationsRepositoryImpl(private val notificationsApi: NotificationsApi
|
||||
}
|
||||
override fun getNotifications(after: Long?): Flow<Resource<List<Notification>>> = flow {
|
||||
try {
|
||||
emit(Resource.Loading())
|
||||
val response = notificationsApi.getNotifications(after).awaitResponse()
|
||||
|
||||
if(response.isSuccessful) {
|
||||
|
||||
emit(Resource.Success(response.body()!!.result.mapNotNull { Notification.fromDto(it) }))
|
||||
} else {
|
||||
Log.e(LOG_TAG, "getNotifications(): Request is not successful, response code is ${response.code()}")
|
||||
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
|
||||
@ -27,4 +28,21 @@ class NotificationsRepositoryImpl(private val notificationsApi: NotificationsApi
|
||||
emit(Resource.Error(Resource.Error.ErrorType.OtherError))
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteNotifications(vararg notification: Notification): Flow<Resource<Boolean>> = flow {
|
||||
try {
|
||||
emit(Resource.Loading())
|
||||
val response = notificationsApi.deleteNotifications(DeleteNotificationsDto(notification.map { it.id })).awaitResponse()
|
||||
|
||||
if(response.isSuccessful) {
|
||||
emit(Resource.Success(true))
|
||||
} else {
|
||||
Log.e(LOG_TAG, "deleteNotifications(): Request is not successful, response code is ${response.code()}")
|
||||
emit(Resource.Error(Resource.Error.mapErrorCode(response.code())))
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Log.e(LOG_TAG, e.message.toString())
|
||||
emit(Resource.Error(Resource.Error.ErrorType.OtherError))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
package com.isolaatti.notifications.domain
|
||||
|
||||
import com.isolaatti.databinding.NotificationItemBinding
|
||||
import com.isolaatti.notifications.data.NotificationDto
|
||||
import com.isolaatti.notifications.data.NotificationPayload
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
|
||||
@ -11,14 +9,29 @@ class GenericNotification(id: Long, date: ZonedDateTime, userId: Int, read: Bool
|
||||
var title: String? = null
|
||||
var message: String? = null
|
||||
|
||||
override fun ingestPayload(notificationPayload: NotificationPayload) {
|
||||
override fun ingestPayload(data: Map<String, String>) {
|
||||
|
||||
}
|
||||
|
||||
override fun bind(notificationBinding: NotificationItemBinding) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as GenericNotification
|
||||
|
||||
if (title != other.title) return false
|
||||
if (message != other.message) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = title?.hashCode() ?: 0
|
||||
result = 31 * result + (message?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
const val TYPE = "generic"
|
||||
}
|
||||
@ -29,13 +42,39 @@ class LikeNotification(id: Long, date: ZonedDateTime, userId: Int, read: Boolean
|
||||
const val TYPE = "like"
|
||||
}
|
||||
|
||||
override fun ingestPayload(notificationPayload: NotificationPayload) {
|
||||
TODO("Not yet implemented")
|
||||
var likeId: String? = null
|
||||
var postId: Long? = null
|
||||
var authorId: Int? = null
|
||||
var authorName: String? = null
|
||||
override fun ingestPayload(data: Map<String, String>) {
|
||||
likeId = data["likeId"]
|
||||
postId = data["postId"]?.toLongOrNull()
|
||||
authorId = data["authorId"]?.toIntOrNull()
|
||||
authorName = data["authorName"]
|
||||
}
|
||||
|
||||
override fun bind(notificationBinding: NotificationItemBinding) {
|
||||
TODO("Not yet implemented")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as LikeNotification
|
||||
|
||||
if (likeId != other.likeId) return false
|
||||
if (postId != other.postId) return false
|
||||
if (authorId != other.authorId) return false
|
||||
if (authorName != other.authorName) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = likeId?.hashCode() ?: 0
|
||||
result = 31 * result + (postId?.hashCode() ?: 0)
|
||||
result = 31 * result + (authorId ?: 0)
|
||||
result = 31 * result + (authorName?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class FollowNotification(id: Long, date: ZonedDateTime, userId: Int, read: Boolean) : Notification(id, date, userId, read) {
|
||||
@ -44,12 +83,30 @@ class FollowNotification(id: Long, date: ZonedDateTime, userId: Int, read: Boole
|
||||
const val TYPE = "follower"
|
||||
}
|
||||
|
||||
override fun ingestPayload(notificationPayload: NotificationPayload) {
|
||||
TODO("Not yet implemented")
|
||||
var followerName: String? = null
|
||||
var followerUserId: Int? = null
|
||||
|
||||
override fun ingestPayload(data: Map<String, String>) {
|
||||
followerName = data["followerName"]
|
||||
followerUserId = data["followerUserId"]?.toIntOrNull()
|
||||
}
|
||||
|
||||
override fun bind(notificationBinding: NotificationItemBinding) {
|
||||
TODO("Not yet implemented")
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FollowNotification
|
||||
|
||||
if (followerName != other.followerName) return false
|
||||
if (followerUserId != other.followerUserId) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = followerName?.hashCode() ?: 0
|
||||
result = 31 * result + (followerUserId ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,13 +118,31 @@ abstract class Notification(
|
||||
var read: Boolean
|
||||
) {
|
||||
|
||||
abstract fun ingestPayload(notificationPayload: NotificationPayload)
|
||||
abstract fun ingestPayload(data: Map<String, String>)
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is Notification) return false
|
||||
|
||||
abstract fun bind(notificationBinding: NotificationItemBinding)
|
||||
if (id != other.id) return false
|
||||
if (date != other.date) return false
|
||||
if (userId != other.userId) return false
|
||||
if (read != other.read) return false
|
||||
if (other != this) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + date.hashCode()
|
||||
result = 31 * result + userId
|
||||
result = 31 * result + read.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromDto(notificationDto: NotificationDto): Notification? {
|
||||
return when(notificationDto.payload.type) {
|
||||
val type = notificationDto.data["type"]
|
||||
return when(type) {
|
||||
GenericNotification.TYPE -> {
|
||||
|
||||
GenericNotification(
|
||||
@ -76,7 +151,7 @@ abstract class Notification(
|
||||
notificationDto.userId,
|
||||
notificationDto.read
|
||||
).apply {
|
||||
ingestPayload(notificationDto.payload)
|
||||
ingestPayload(notificationDto.data)
|
||||
}
|
||||
}
|
||||
LikeNotification.TYPE -> {
|
||||
@ -86,7 +161,7 @@ abstract class Notification(
|
||||
notificationDto.userId,
|
||||
notificationDto.read
|
||||
).apply {
|
||||
ingestPayload(notificationDto.payload)
|
||||
ingestPayload(notificationDto.data)
|
||||
}
|
||||
}
|
||||
FollowNotification.TYPE -> {
|
||||
@ -96,7 +171,7 @@ abstract class Notification(
|
||||
notificationDto.userId,
|
||||
notificationDto.read
|
||||
).apply {
|
||||
ingestPayload(notificationDto.payload)
|
||||
ingestPayload(notificationDto.data)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
|
||||
@ -5,4 +5,5 @@ import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface NotificationsRepository {
|
||||
fun getNotifications(after: Long?): Flow<Resource<List<Notification>>>
|
||||
fun deleteNotifications(vararg notification: Notification): Flow<Resource<Boolean>>
|
||||
}
|
||||
@ -1,16 +1,23 @@
|
||||
package com.isolaatti.notifications.presentation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import coil.load
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.databinding.NotificationItemBinding
|
||||
import com.isolaatti.notifications.domain.FollowNotification
|
||||
import com.isolaatti.notifications.domain.LikeNotification
|
||||
import com.isolaatti.notifications.domain.Notification
|
||||
import com.isolaatti.utils.UrlGen
|
||||
|
||||
class NotificationsAdapter : ListAdapter<Notification, NotificationsAdapter.NotificationViewHolder>(
|
||||
diffCallback
|
||||
) {
|
||||
class NotificationsAdapter(
|
||||
private val onNotificationClick: (notification: Notification) -> Unit,
|
||||
private val onItemOptionsClick: (button: View, notification: Notification) -> Unit
|
||||
) : ListAdapter<Notification, NotificationsAdapter.NotificationViewHolder>(diffCallback) {
|
||||
inner class NotificationViewHolder(val notificationItemBinding: NotificationItemBinding) : ViewHolder(notificationItemBinding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder {
|
||||
@ -18,19 +25,61 @@ class NotificationsAdapter : ListAdapter<Notification, NotificationsAdapter.Noti
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) {
|
||||
getItem(position).bind(holder.notificationItemBinding)
|
||||
val context = holder.notificationItemBinding.root.context
|
||||
val notification = getItem(position)
|
||||
holder.notificationItemBinding.root.setOnClickListener {
|
||||
onNotificationClick(notification)
|
||||
}
|
||||
holder.notificationItemBinding.optionButton.setOnClickListener {
|
||||
onItemOptionsClick(it, notification)
|
||||
}
|
||||
when(notification) {
|
||||
is LikeNotification -> {
|
||||
holder.notificationItemBinding.notificationTitle.text = context.getString(R.string.like_notification_title, notification.authorName)
|
||||
holder.notificationItemBinding.notificationMessage.text = context.getString(R.string.like_notification_text)
|
||||
val authorProfileImageUrl = notification.authorId?.let { UrlGen.userProfileImage(it, false) }
|
||||
|
||||
if(authorProfileImageUrl != null) {
|
||||
holder.notificationItemBinding.notificationMainImage.load(authorProfileImageUrl){
|
||||
fallback(R.drawable.baseline_person_24)
|
||||
}
|
||||
} else {
|
||||
holder.notificationItemBinding.notificationMainImage.load(R.drawable.baseline_person_24)
|
||||
}
|
||||
|
||||
holder.notificationItemBinding.notificationSecondaryImage.load(R.drawable.hands_clapping_solid)
|
||||
}
|
||||
|
||||
is FollowNotification -> {
|
||||
holder.notificationItemBinding.notificationTitle.text = context.getString(R.string.new_follower_notification_title, notification.followerName)
|
||||
holder.notificationItemBinding.notificationMessage.text = context.getString(R.string.new_follower_notification_text)
|
||||
|
||||
val followerProfileImageUrl = notification.followerUserId?.let { UrlGen.userProfileImage(it, false) }
|
||||
if(followerProfileImageUrl != null) {
|
||||
holder.notificationItemBinding.notificationMainImage.load(followerProfileImageUrl) {
|
||||
fallback(R.drawable.baseline_person_24)
|
||||
}
|
||||
} else {
|
||||
holder.notificationItemBinding.notificationMainImage.load(R.drawable.baseline_person_24)
|
||||
}
|
||||
|
||||
holder.notificationItemBinding.notificationSecondaryImage.load(R.drawable.baseline_star_24)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
val diffCallback = object: DiffUtil.ItemCallback<Notification>() {
|
||||
override fun areItemsTheSame(oldItem: Notification, newItem: Notification): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Notification, newItem: Notification): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,71 @@
|
||||
package com.isolaatti.notifications.presentation
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.isolaatti.notifications.domain.Notification
|
||||
import com.isolaatti.notifications.domain.NotificationsRepository
|
||||
import com.isolaatti.utils.Resource
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NotificationsViewModel @Inject constructor(private val notificationsRepository: NotificationsRepository) : ViewModel() {
|
||||
companion object {
|
||||
const val LOG_TAG = "NotificationsViewModel"
|
||||
}
|
||||
val notifications: MutableLiveData<List<Notification>> = MutableLiveData()
|
||||
val loading: MutableLiveData<Boolean> = MutableLiveData()
|
||||
|
||||
val error: MutableLiveData<Boolean> = MutableLiveData()
|
||||
fun getData() {
|
||||
viewModelScope.launch {
|
||||
notificationsRepository.getNotifications(null).onEach {
|
||||
when(it) {
|
||||
is Resource.Error -> {
|
||||
loading.postValue(false)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
loading.postValue(true)
|
||||
}
|
||||
is Resource.Success -> {
|
||||
loading.postValue(false)
|
||||
notifications.postValue(it.data!!)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDeleted(notification: Notification) {
|
||||
val mutableList = notifications.value?.toMutableList()
|
||||
val removed = mutableList?.remove(notification)
|
||||
if(mutableList != null && removed == true) {
|
||||
notifications.postValue(mutableList)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteNotification(notification: Notification) {
|
||||
viewModelScope.launch {
|
||||
notificationsRepository.deleteNotifications(notification).onEach {
|
||||
when(it) {
|
||||
is Resource.Error -> {
|
||||
error.postValue(true)
|
||||
}
|
||||
is Resource.Loading -> {
|
||||
error.postValue(false)
|
||||
}
|
||||
is Resource.Success -> {
|
||||
error.postValue(false)
|
||||
onDeleted(notification)
|
||||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.IO).launchIn(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,33 +1,125 @@
|
||||
package com.isolaatti.notifications.ui
|
||||
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.isolaatti.R
|
||||
import com.isolaatti.databinding.FragmentNotificationsBinding
|
||||
import com.isolaatti.notifications.domain.FollowNotification
|
||||
import com.isolaatti.notifications.domain.LikeNotification
|
||||
import com.isolaatti.notifications.domain.Notification
|
||||
import com.isolaatti.notifications.presentation.NotificationsAdapter
|
||||
import com.isolaatti.notifications.presentation.NotificationsViewModel
|
||||
import com.isolaatti.posting.posts.viewer.ui.PostViewerActivity
|
||||
import com.isolaatti.profile.ui.ProfileActivity
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationsFragment : Fragment() {
|
||||
|
||||
companion object {
|
||||
fun newInstance() = NotificationsFragment()
|
||||
}
|
||||
|
||||
private lateinit var viewModel: NotificationsViewModel
|
||||
private lateinit var binding: FragmentNotificationsBinding
|
||||
private val viewModel: NotificationsViewModel by viewModels()
|
||||
private var adapter: NotificationsAdapter? = null
|
||||
|
||||
private fun showDeleteNotificationDialog(notification: Notification) {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.delete_notification)
|
||||
.setMessage(R.string.delete_notification_dialog_message)
|
||||
.setPositiveButton(R.string.accept) { _, _ ->
|
||||
viewModel.deleteNotification(notification)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private val onItemOptionsClick: (button: View, notification: Notification) -> Unit = { button, notification ->
|
||||
val popupMenu = PopupMenu(requireContext(), button)
|
||||
|
||||
popupMenu.inflate(R.menu.notification_menu)
|
||||
|
||||
popupMenu.setOnMenuItemClickListener {
|
||||
when(it.itemId) {
|
||||
R.id.delete_notification -> {
|
||||
showDeleteNotificationDialog(notification)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
private val onNotificationClick: (notification: Notification) -> Unit = { notification ->
|
||||
when(notification) {
|
||||
is LikeNotification -> {
|
||||
notification.postId?.also { postId ->
|
||||
PostViewerActivity.startActivity(requireContext(), postId)
|
||||
}
|
||||
}
|
||||
is FollowNotification -> {
|
||||
notification.followerUserId?.also { followerUserId ->
|
||||
ProfileActivity.startActivity(requireContext(), followerUserId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_notifications, container, false)
|
||||
binding = FragmentNotificationsBinding.inflate(inflater, container, false)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
viewModel = ViewModelProvider(this).get(NotificationsViewModel::class.java)
|
||||
// TODO: Use the ViewModel
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
adapter = NotificationsAdapter(onNotificationClick, onItemOptionsClick)
|
||||
binding.recycler.adapter = adapter
|
||||
binding.recycler.layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
|
||||
viewModel.getData()
|
||||
|
||||
setupObservers()
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
binding.swipeToRefresh.setOnRefreshListener {
|
||||
viewModel.getData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupObservers() {
|
||||
viewModel.notifications.observe(viewLifecycleOwner) {
|
||||
adapter?.submitList(it)
|
||||
}
|
||||
|
||||
viewModel.loading.observe(viewLifecycleOwner) {
|
||||
binding.swipeToRefresh.isRefreshing = it
|
||||
}
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner) {
|
||||
if(it){
|
||||
Toast.makeText(requireContext(), R.string.error_making_request, Toast.LENGTH_SHORT).show()
|
||||
viewModel.error.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user