feat(notifications): Various improvements
This commit is contained in:
parent
ef08d6ddd3
commit
3ae9081787
|
@ -95,7 +95,8 @@ class CalendarActivity : AppCompatActivity() {
|
|||
val invitedEvents = RetrofitClient.eventsService.userEventsInvited(
|
||||
userId = userId,
|
||||
startFrom = startFrom,
|
||||
startTo = endTo
|
||||
startTo = endTo,
|
||||
inviteStatus = "accepted"
|
||||
)
|
||||
|
||||
val allEvents = userEvents + invitedEvents
|
||||
|
|
|
@ -15,10 +15,12 @@ import com.p_vacho.neat_calendar.MyApplication
|
|||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.adapters.NotificationAdapter
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.EventResponse
|
||||
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.UserResponse
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONException
|
||||
|
@ -33,6 +35,7 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
|
||||
private lateinit var notifications: MutableList<NotificationResponse>
|
||||
private lateinit var invitations: MutableMap<String, InvitationResponse> // invitation id -> invitation
|
||||
private lateinit var events: MutableMap<String, EventResponse> // event id -> event
|
||||
private val users: MutableMap<String, UserResponse?> = mutableMapOf() // user id -> user (or null if not found)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -53,19 +56,30 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
|
||||
rvNotifications.layoutManager = LinearLayoutManager(this)
|
||||
lifecycleScope.launch {
|
||||
notifications = fetchNotifications().toMutableList()
|
||||
invitations = fetchInvitations().toMutableMap()
|
||||
// Use async to fetch data in parallel
|
||||
val notificationsDeferred = async { fetchNotifications() }
|
||||
val invitationsDeferred = async { fetchInvitations() }
|
||||
val eventsDeferred = async { fetchEvents() }
|
||||
|
||||
// Wait for all results
|
||||
notifications = notificationsDeferred.await().toMutableList()
|
||||
invitations = invitationsDeferred.await().toMutableMap()
|
||||
events = eventsDeferred.await().toMutableMap()
|
||||
|
||||
rvNotifications.adapter = NotificationAdapter(
|
||||
notifications,
|
||||
::handleNotificationAction,
|
||||
::handleNotificationClick,
|
||||
::getInvitationData,
|
||||
::getUserData
|
||||
::getUserData,
|
||||
::getEventData,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all notifications for the currently logged in user
|
||||
*/
|
||||
private suspend fun fetchNotifications(): List<NotificationResponse> {
|
||||
val userId = (application as MyApplication).tokenManager.userId
|
||||
if (userId == null) {
|
||||
|
@ -78,6 +92,30 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the events that the user is already attending (accepted invite),
|
||||
* the events that the user has a pending invite for, and user's
|
||||
* owned events (in case of invitation confirmations).
|
||||
*/
|
||||
private suspend fun fetchEvents(): Map<String, EventResponse> {
|
||||
val userId = (application as MyApplication).tokenManager.userId
|
||||
if (userId == null) {
|
||||
finish()
|
||||
return emptyMap()
|
||||
}
|
||||
|
||||
val fetchedEvents = withContext(Dispatchers.IO) {
|
||||
val invitedDeferred = async { RetrofitClient.eventsService.userEventsInvited(userId) }
|
||||
val ownedDeferred = async { RetrofitClient.eventsService.userEvents(userId) }
|
||||
val invitedEvents = invitedDeferred.await()
|
||||
val ownedEvents = ownedDeferred.await()
|
||||
(invitedEvents + ownedEvents).distinctBy { it.id }
|
||||
}
|
||||
|
||||
|
||||
return fetchedEvents.associateBy { it.id }
|
||||
}
|
||||
|
||||
private suspend fun fetchInvitations(): Map<String, InvitationResponse> {
|
||||
val userId = (application as MyApplication).tokenManager.userId
|
||||
if (userId == null) {
|
||||
|
@ -85,9 +123,15 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
return emptyMap()
|
||||
}
|
||||
|
||||
// Fetch the lists in parallel
|
||||
val fetchedInvitations = withContext(Dispatchers.IO) {
|
||||
RetrofitClient.invitationService.getIncomingInvitations(userId)
|
||||
val incomingDeferred = async { RetrofitClient.invitationService.getIncomingInvitations(userId) }
|
||||
val ownedDeferred = async { RetrofitClient.invitationService.getInvitations(userId) }
|
||||
val incomingInvitations = incomingDeferred.await()
|
||||
val ownedInvitations = ownedDeferred.await()
|
||||
(incomingInvitations + ownedInvitations).distinctBy { it.id }
|
||||
}
|
||||
|
||||
return fetchedInvitations.associateBy { it.id }
|
||||
}
|
||||
|
||||
|
@ -95,6 +139,16 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
val ret = invitations[invitationId]
|
||||
if (ret == null) {
|
||||
Log.w("NotificationsActivity", "NotificationAdapter requested unknown invitation: $invitationId")
|
||||
Log.w("NotificationsActivity", "Known invitations: $invitations")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
private fun getEventData(eventId: String, rvPosition: Int): EventResponse? {
|
||||
val ret = events[eventId]
|
||||
if (ret == null) {
|
||||
Log.w("NotificationsActivity", "NotificationAdapter requested unknown event: $eventId")
|
||||
Log.w("NotificationsActivity", "Known events: $events")
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
@ -116,15 +170,14 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
}
|
||||
} catch (e: HttpException) {
|
||||
if (e.code() != 404) { throw e }
|
||||
val errorBody = e.response()?.errorBody()?.string()
|
||||
if (errorBody == null) { throw e }
|
||||
val errorBody = e.response()?.errorBody()?.string() ?: 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)
|
||||
Log.e("NotificationsActivity", "Failed to fetch user: $userId")
|
||||
users[userId] = null // Cache null for non-existing users
|
||||
|
||||
// No need for an adapter update here, null is already the default
|
||||
|
@ -139,7 +192,7 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
NotificationAdapter.Action.ACCEPT -> {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val invitationId = notification.data
|
||||
RetrofitClient.invitationService.acceptInvitation(invitationId)
|
||||
invitations[invitationId] = RetrofitClient.invitationService.acceptInvitation(invitationId)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(this@NotificationsActivity, "Invitation accepted", Toast.LENGTH_SHORT).show()
|
||||
|
@ -152,7 +205,8 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
// but otherwise, we'll need to trigger an item update ourselves, to make sure that
|
||||
// the accept/decline buttons are removed
|
||||
rvNotifications.adapter!!.notifyItemChanged(position)
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NotificationAdapter.Action.DECLINE -> {
|
||||
|
@ -188,8 +242,7 @@ class NotificationsActivity : AppCompatActivity() {
|
|||
RetrofitClient.notificationsService.markNotificationRead(notification.id)
|
||||
|
||||
notifications[position] = updatedNotification
|
||||
val adapter = rvNotifications.adapter as NotificationAdapter
|
||||
adapter.notifyItemChanged(position)
|
||||
rvNotifications.adapter!!.notifyItemChanged(position)
|
||||
|
||||
if (sendToast) {
|
||||
Toast.makeText(this@NotificationsActivity, "Marked as read", Toast.LENGTH_SHORT)
|
||||
|
|
|
@ -7,6 +7,7 @@ import android.widget.ImageButton
|
|||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.api.models.EventResponse
|
||||
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.UserResponse
|
||||
|
@ -22,6 +23,7 @@ class NotificationAdapter(
|
|||
private val onNotificationClick: (NotificationResponse, Int) -> Unit,
|
||||
private val getInvitationData: (String, Int) -> InvitationResponse?,
|
||||
private val getUserData: (String, Int) -> UserResponse?,
|
||||
private val getEventData: (String, Int) -> EventResponse?,
|
||||
) : RecyclerView.Adapter<NotificationAdapter.NotificationViewHolder>() {
|
||||
|
||||
enum class Action {
|
||||
|
@ -88,19 +90,22 @@ class NotificationAdapter(
|
|||
}
|
||||
|
||||
val user = getUserData(usernameId, position)
|
||||
val username = user?.username ?: "Unknown User"
|
||||
val username = user?.username ?: "unknown-user"
|
||||
|
||||
val statusSuffix = when (invitation.status) {
|
||||
"accepted" -> " [already accepted]"
|
||||
"declined" -> " [already declined]"
|
||||
"pending" -> ""
|
||||
else -> throw IllegalStateException("Unexpected invitation status: ${invitation.status} for invitation ID: ${invitation.id}")
|
||||
}
|
||||
// We can't access events of incoming declined invitations
|
||||
val event = if (notification.message == "new-invitation" && invitation.status == "declined")
|
||||
null else getEventData(invitation.event_id, position)
|
||||
val event_name = event?.title ?: "unknown-title"
|
||||
|
||||
return when (notification.message) {
|
||||
"new-invitation" -> "You have received an event invitation from @$username$statusSuffix"
|
||||
"invitation-accepted" -> "@$username has accepted your event invitation$statusSuffix"
|
||||
"invitation-declined" -> "@$username has declined your event invitation$statusSuffix"
|
||||
"new-invitation" -> when (invitation.status) {
|
||||
"pending" -> "You have received an event invitation ($event_name) from @$username"
|
||||
"accepted" -> "You have received an event invitation ($event_name) from @$username [already accepted]"
|
||||
"declined" -> "You have received an event invitation from @$username [already declined]"
|
||||
else -> throw IllegalStateException("Unexpected invitation status: ${invitation.status} for invitation ID: ${invitation.id}")
|
||||
}
|
||||
"invitation-accepted" -> "@$username has accepted your event invitation ($event_name) "
|
||||
"invitation-declined" -> "@$username has declined your event invitation ($event_name) "
|
||||
else -> throw IllegalArgumentException("Unexpected notification message: ${notification.message}")
|
||||
}
|
||||
}
|
||||
|
@ -111,12 +116,24 @@ class NotificationAdapter(
|
|||
notification: NotificationResponse,
|
||||
position: Int
|
||||
) {
|
||||
// Set the listeners first, even though not all of these buttons
|
||||
// will actually be clickable/visible, it's easier to do here once
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
if (notification.event_type == "invitation") {
|
||||
val invitation = getInvitationData(notification.data, position)
|
||||
if (invitation == null) {
|
||||
// No buttons for deleted invitations
|
||||
holder.invitationActions.visibility = View.GONE
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -129,24 +146,18 @@ class NotificationAdapter(
|
|||
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)
|
||||
}
|
||||
}
|
||||
"accepted", "declined" -> {
|
||||
"accepted" -> {
|
||||
// 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)
|
||||
}
|
||||
"declined" -> {
|
||||
// No buttons, can't view events from declined invites
|
||||
holder.invitationActions.visibility = View.GONE
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected invitation status: ${invitation.status} for invite ID: ${invitation.id}")
|
||||
}
|
||||
|
@ -157,10 +168,6 @@ class NotificationAdapter(
|
|||
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}")
|
||||
|
|
|
@ -31,7 +31,8 @@ interface EventsService {
|
|||
@Query("start_from") startFrom: OffsetDateTime? = null,
|
||||
@Query("start_to") startTo: OffsetDateTime? = null,
|
||||
@Query("end_from") endFrom: OffsetDateTime? = null,
|
||||
@Query("end_to") endTo: OffsetDateTime? = null
|
||||
@Query("end_to") endTo: OffsetDateTime? = null,
|
||||
@Query("inviteStatus") inviteStatus: String? = null, // "pending" / "accepted" / null (both)
|
||||
): List<EventResponse>
|
||||
|
||||
@DELETE("events/{event_id}")
|
||||
|
|
Loading…
Reference in a new issue