feat(notifications): Fetch users & other necessary data
This commit is contained in:
parent
046da599b7
commit
7a0f3cea06
|
@ -1,6 +1,7 @@
|
||||||
package com.p_vacho.neat_calendar.activities
|
package com.p_vacho.neat_calendar.activities
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
@ -16,9 +17,13 @@ import com.p_vacho.neat_calendar.adapters.NotificationAdapter
|
||||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||||
import com.p_vacho.neat_calendar.api.models.InvitationResponse
|
import com.p_vacho.neat_calendar.api.models.InvitationResponse
|
||||||
import com.p_vacho.neat_calendar.api.models.NotificationResponse
|
import com.p_vacho.neat_calendar.api.models.NotificationResponse
|
||||||
|
import com.p_vacho.neat_calendar.api.models.UserResponse
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import retrofit2.HttpException
|
||||||
|
|
||||||
class NotificationsActivity : AppCompatActivity() {
|
class NotificationsActivity : AppCompatActivity() {
|
||||||
private lateinit var rvNotifications: RecyclerView
|
private lateinit var rvNotifications: RecyclerView
|
||||||
|
@ -26,6 +31,7 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private lateinit var notifications: MutableList<NotificationResponse>
|
private lateinit var notifications: MutableList<NotificationResponse>
|
||||||
private lateinit var invitations: MutableMap<String, InvitationResponse> // invitation id -> invitation
|
private lateinit var invitations: MutableMap<String, InvitationResponse> // invitation id -> invitation
|
||||||
|
private val users: MutableMap<String, UserResponse?> = mutableMapOf() // user id -> user (or null if not found)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -41,32 +47,33 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
rvNotifications = findViewById(R.id.rvNotifications)
|
rvNotifications = findViewById(R.id.rvNotifications)
|
||||||
btnBack = findViewById(R.id.btnBack)
|
btnBack = findViewById(R.id.btnBack)
|
||||||
|
|
||||||
rvNotifications.layoutManager = LinearLayoutManager(this)
|
btnBack.setOnClickListener { finish() }
|
||||||
|
|
||||||
|
rvNotifications.layoutManager = LinearLayoutManager(this)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
notifications = fetchNotifications().toMutableList()
|
notifications = fetchNotifications().toMutableList()
|
||||||
invitations = fetchInvitations().toMutableMap()
|
invitations = fetchInvitations().toMutableMap()
|
||||||
rvNotifications.adapter = NotificationAdapter(notifications, ::handleNotificationAction, ::handleNotificationClick) {
|
|
||||||
notification -> invitations[notification.data]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
btnBack.setOnClickListener { finish() }
|
rvNotifications.adapter = NotificationAdapter(
|
||||||
|
notifications,
|
||||||
|
::handleNotificationAction,
|
||||||
|
::handleNotificationClick,
|
||||||
|
::getInvitationData,
|
||||||
|
::getUserData
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchNotifications(): List<NotificationResponse> {
|
private suspend fun fetchNotifications(): List<NotificationResponse> {
|
||||||
val userId = (application as MyApplication).tokenManager.userId
|
val userId = (application as MyApplication).tokenManager.userId
|
||||||
|
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
finish()
|
finish()
|
||||||
return emptyList()
|
return emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val notifications = withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
RetrofitClient.notificationsService.getUserNotifications(userId)
|
RetrofitClient.notificationsService.getUserNotifications(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
return notifications
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun fetchInvitations(): Map<String, InvitationResponse> {
|
private suspend fun fetchInvitations(): Map<String, InvitationResponse> {
|
||||||
|
@ -82,16 +89,55 @@ class NotificationsActivity : AppCompatActivity() {
|
||||||
return fetchedInvitations.associateBy { it.id }
|
return fetchedInvitations.associateBy { it.id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getInvitationData(invitationId: String, rvPosition: Int): InvitationResponse? {
|
||||||
|
return invitations[invitationId]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUserData(userId: String, rvPosition: Int?): UserResponse? {
|
||||||
|
return users.getOrPut(userId) {
|
||||||
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
val user = RetrofitClient.usersService.getUser(userId)
|
||||||
|
users[userId] = user // Cache the user
|
||||||
|
|
||||||
|
// Update the adapter item once we fetched this value
|
||||||
|
// This will re-trigger the logic for obtaining the item re-calling
|
||||||
|
// this function, but this time, it will give back the cached value
|
||||||
|
if (rvPosition != null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
rvNotifications.adapter!!.notifyItemChanged(rvPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
if (e.code() != 404) { throw e }
|
||||||
|
val errorBody = e.response()?.errorBody()?.string()
|
||||||
|
if (errorBody == null) { throw e }
|
||||||
|
try {
|
||||||
|
val jsonObj = JSONObject(errorBody)
|
||||||
|
val errDetail = jsonObj.optString("detail")
|
||||||
|
if (errDetail != "No such user") { throw e }
|
||||||
|
} catch (jsonParseExc: JSONException) { throw e }
|
||||||
|
|
||||||
|
Log.e("NotificationsActivity", "Failed to fetch user: $userId", e)
|
||||||
|
users[userId] = null // Cache null for non-existing users
|
||||||
|
|
||||||
|
// No need for an adapter update here, null is already the default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
null // Return null until the data is fetched
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleNotificationAction(notification: NotificationResponse, action: NotificationAdapter.Action, position: Int) {
|
private fun handleNotificationAction(notification: NotificationResponse, action: NotificationAdapter.Action, position: Int) {
|
||||||
when (action) {
|
when (action) {
|
||||||
NotificationAdapter.Action.ACCEPT -> {
|
NotificationAdapter.Action.ACCEPT -> {
|
||||||
//TODO("Handle accept action")
|
// TODO: Handle accept action
|
||||||
}
|
}
|
||||||
NotificationAdapter.Action.DECLINE -> {
|
NotificationAdapter.Action.DECLINE -> {
|
||||||
//TODO("Handle decline action")
|
// TODO: Handle decline action
|
||||||
}
|
}
|
||||||
NotificationAdapter.Action.VIEW_EVENT -> {
|
NotificationAdapter.Action.VIEW_EVENT -> {
|
||||||
//TODO("Handle viewing the event")
|
// TODO: Handle viewing the event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.p_vacho.neat_calendar.R
|
import com.p_vacho.neat_calendar.R
|
||||||
import com.p_vacho.neat_calendar.api.models.InvitationResponse
|
import com.p_vacho.neat_calendar.api.models.InvitationResponse
|
||||||
import com.p_vacho.neat_calendar.api.models.NotificationResponse
|
import com.p_vacho.neat_calendar.api.models.NotificationResponse
|
||||||
|
import com.p_vacho.neat_calendar.api.models.UserResponse
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.OffsetDateTime
|
import java.time.OffsetDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
@ -17,8 +18,9 @@ class NotificationAdapter(
|
||||||
private val notifications: MutableList<NotificationResponse>,
|
private val notifications: MutableList<NotificationResponse>,
|
||||||
private val onActionClick: (NotificationResponse, Action, Int) -> Unit,
|
private val onActionClick: (NotificationResponse, Action, Int) -> Unit,
|
||||||
private val onNotificationClick: (NotificationResponse, Int) -> Unit,
|
private val onNotificationClick: (NotificationResponse, Int) -> Unit,
|
||||||
private val getInvitationData: (NotificationResponse) -> InvitationResponse?,
|
private val getInvitationData: (String, Int) -> InvitationResponse?,
|
||||||
) : RecyclerView.Adapter<NotificationAdapter.NotificationViewHolder>() {
|
private val getUserData: (String, Int) -> UserResponse?,
|
||||||
|
) : RecyclerView.Adapter<NotificationAdapter.NotificationViewHolder>() {
|
||||||
|
|
||||||
enum class Action {
|
enum class Action {
|
||||||
ACCEPT, DECLINE, VIEW_EVENT
|
ACCEPT, DECLINE, VIEW_EVENT
|
||||||
|
@ -44,81 +46,97 @@ class NotificationAdapter(
|
||||||
val notification = notifications[position]
|
val notification = notifications[position]
|
||||||
|
|
||||||
// Format and set the creation time
|
// Format and set the creation time
|
||||||
val formattedTime = formatNotificationTime(notification.created_at)
|
holder.notificationTime.text = formatNotificationTime(notification.created_at)
|
||||||
holder.notificationTime.text = formattedTime
|
|
||||||
|
|
||||||
// Set visibility based on read/unread status
|
// Set visibility based on read/unread status
|
||||||
holder.unreadIndicator.visibility = if (notification.read) View.GONE else View.VISIBLE
|
holder.unreadIndicator.visibility = if (notification.read) View.GONE else View.VISIBLE
|
||||||
|
|
||||||
|
// Handle notification message and invitation-specific logic
|
||||||
|
holder.message.text = resolveNotificationMessage(notification, position)
|
||||||
|
|
||||||
// Handle invitation actions
|
// Handle invitation actions
|
||||||
if (notification.event_type == "invitation") {
|
handleInvitationActions(holder, notification, position)
|
||||||
val invitation = getInvitationData(notification)
|
|
||||||
|
|
||||||
// TODO: Consider fetching the user names here to show
|
// Set click listener for the notification (only for unread ones)
|
||||||
// TODO: Localize
|
holder.itemView.isClickable = !notification.read
|
||||||
if (invitation != null) {
|
holder.itemView.setOnClickListener {
|
||||||
if (notification.message == "new-invitation") {
|
if (!notification.read) onNotificationClick(notification, position)
|
||||||
holder.message.setText("You have received an event invitation")
|
|
||||||
} else if (notification.message == "invitation-accepted") {
|
|
||||||
holder.message.setText("Your event invitation has been accepted")
|
|
||||||
} else if (notification.message == "invitation-declined") {
|
|
||||||
holder.message.setText("Your event invitation has been declined")
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.invitationActions.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
holder.acceptButton.setOnClickListener {
|
|
||||||
onActionClick(notification, Action.ACCEPT, position)
|
|
||||||
}
|
|
||||||
holder.declineButton.setOnClickListener {
|
|
||||||
onActionClick(notification, Action.DECLINE, position)
|
|
||||||
}
|
|
||||||
holder.viewEventButton.setOnClickListener {
|
|
||||||
onActionClick(notification, Action.VIEW_EVENT, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (notification.message == "new-invitation") {
|
|
||||||
holder.message.setText("You have received an event invitation [invitation deleted]")
|
|
||||||
} else if (notification.message == "invitation-accepted") {
|
|
||||||
holder.message.setText("Your event invitation has been accepted [invitation deleted]")
|
|
||||||
} else if (notification.message == "invitation-declined") {
|
|
||||||
holder.message.setText("Your event invitation has been declined [invitation deleted]")
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.invitationActions.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
holder.message.text = notification.message
|
|
||||||
|
|
||||||
holder.invitationActions.visibility = View.GONE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set click listener for the whole notification (only for unread ones though)
|
|
||||||
if (!notification.read) {
|
|
||||||
holder.itemView.isClickable = true
|
|
||||||
holder.itemView.setOnClickListener {
|
|
||||||
onNotificationClick(notification, position)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
holder.itemView.isClickable = false
|
|
||||||
holder.itemView.setOnClickListener(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure buttons consume their click events
|
|
||||||
holder.acceptButton.isClickable = true
|
|
||||||
holder.declineButton.isClickable = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount(): Int = notifications.size
|
override fun getItemCount(): Int = notifications.size
|
||||||
|
|
||||||
/**
|
private fun resolveNotificationMessage(notification: NotificationResponse, position: Int): String {
|
||||||
* Format the time at which a notification was received.
|
if (notification.event_type != "invitation") {
|
||||||
*
|
return notification.message
|
||||||
* If the notification is recent (setn within the last 24h), show the time
|
}
|
||||||
* in a format of (x hours/minutes/seconds ago), otherwise, use yyyy-MM-dd HH:mm
|
|
||||||
*/
|
val invitation = getInvitationData(notification.data, position)
|
||||||
|
?: return when (notification.message) {
|
||||||
|
"new-invitation" -> "You have received an event invitation, but it was since deleted"
|
||||||
|
"invitation-accepted" -> "Your event invitation has been accepted, but it was since deleted"
|
||||||
|
"invitation-declined" -> "Your event invitation has been declined, but it was since deleted"
|
||||||
|
else -> throw IllegalArgumentException("Unexpected notification message: ${notification.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val usernameId = when (notification.message) {
|
||||||
|
"new-invitation" -> invitation.invitor_id
|
||||||
|
"invitation-accepted", "invitation-declined" -> invitation.invitee_id
|
||||||
|
else -> throw IllegalArgumentException("Unexpected notification message: ${notification.message}")
|
||||||
|
}
|
||||||
|
|
||||||
|
val user = getUserData(usernameId, position)
|
||||||
|
val username = user?.username ?: "Unknown User"
|
||||||
|
|
||||||
|
return when (notification.message) {
|
||||||
|
"new-invitation" -> "You have received an event invitation from @$username"
|
||||||
|
"invitation-accepted" -> "@$username has accepted your event invitation"
|
||||||
|
"invitation-declined" -> "@$username has declined your event invitation"
|
||||||
|
else -> throw IllegalArgumentException("Unexpected notification message: ${notification.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleInvitationActions(
|
||||||
|
holder: NotificationViewHolder,
|
||||||
|
notification: NotificationResponse,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
|
if (notification.event_type == "invitation") {
|
||||||
|
when (notification.message) {
|
||||||
|
"new-invitation" -> {
|
||||||
|
// Show Accept/Decline & View buttons
|
||||||
|
holder.invitationActions.visibility = View.VISIBLE
|
||||||
|
holder.acceptButton.visibility = View.VISIBLE
|
||||||
|
holder.declineButton.visibility = View.VISIBLE
|
||||||
|
holder.viewEventButton.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
holder.acceptButton.setOnClickListener {
|
||||||
|
onActionClick(notification, Action.ACCEPT, position)
|
||||||
|
}
|
||||||
|
holder.declineButton.setOnClickListener {
|
||||||
|
onActionClick(notification, Action.DECLINE, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"invitation-accepted", "invitation-declined" -> {
|
||||||
|
// Show only View button
|
||||||
|
holder.invitationActions.visibility = View.VISIBLE
|
||||||
|
holder.acceptButton.visibility = View.GONE
|
||||||
|
holder.declineButton.visibility = View.GONE
|
||||||
|
holder.viewEventButton.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
holder.viewEventButton.setOnClickListener {
|
||||||
|
onActionClick(notification, Action.VIEW_EVENT, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw IllegalArgumentException("Unexpected notification message: ${notification.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.invitationActions.visibility = View.GONE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatNotificationTime(createdAt: OffsetDateTime): String {
|
private fun formatNotificationTime(createdAt: OffsetDateTime): String {
|
||||||
val now = OffsetDateTime.now()
|
val now = OffsetDateTime.now()
|
||||||
val duration = Duration.between(createdAt, now)
|
val duration = Duration.between(createdAt, now)
|
||||||
|
@ -127,11 +145,7 @@ class NotificationAdapter(
|
||||||
duration.seconds < 60 -> "${duration.seconds}s ago"
|
duration.seconds < 60 -> "${duration.seconds}s ago"
|
||||||
duration.toMinutes() < 60 -> "${duration.toMinutes()}m ago"
|
duration.toMinutes() < 60 -> "${duration.toMinutes()}m ago"
|
||||||
duration.toHours() < 24 -> "${duration.toHours()}h ago"
|
duration.toHours() < 24 -> "${duration.toHours()}h ago"
|
||||||
else -> {
|
else -> DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(createdAt)
|
||||||
// Format as a date/time for older notifications
|
|
||||||
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
|
||||||
createdAt.format(formatter)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue