Compare commits

..

4 commits

5 changed files with 101 additions and 4 deletions

View file

@ -2,13 +2,16 @@ package com.p_vacho.neat_calendar.activities
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.View
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.p_vacho.neat_calendar.MyApplication import com.p_vacho.neat_calendar.MyApplication
@ -32,6 +35,8 @@ import retrofit2.HttpException
class NotificationsActivity : AppCompatActivity() { class NotificationsActivity : AppCompatActivity() {
private lateinit var rvNotifications: RecyclerView private lateinit var rvNotifications: RecyclerView
private lateinit var btnBack: ImageButton private lateinit var btnBack: ImageButton
private lateinit var tvEmptyState: TextView
private lateinit var tvSwipeHint: TextView
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
@ -51,6 +56,8 @@ class NotificationsActivity : AppCompatActivity() {
rvNotifications = findViewById(R.id.rvNotifications) rvNotifications = findViewById(R.id.rvNotifications)
btnBack = findViewById(R.id.btnBack) btnBack = findViewById(R.id.btnBack)
tvEmptyState = findViewById(R.id.tvEmptyState)
tvSwipeHint = findViewById(R.id.tvSwipeHint)
btnBack.setOnClickListener { finish() } btnBack.setOnClickListener { finish() }
@ -66,7 +73,7 @@ class NotificationsActivity : AppCompatActivity() {
invitations = invitationsDeferred.await().toMutableMap() invitations = invitationsDeferred.await().toMutableMap()
events = eventsDeferred.await().toMutableMap() events = eventsDeferred.await().toMutableMap()
rvNotifications.adapter = NotificationAdapter( val adapter = NotificationAdapter(
notifications, notifications,
::handleNotificationAction, ::handleNotificationAction,
::handleNotificationClick, ::handleNotificationClick,
@ -74,6 +81,10 @@ class NotificationsActivity : AppCompatActivity() {
::getUserData, ::getUserData,
::getEventData, ::getEventData,
) )
rvNotifications.adapter = adapter
setupSwipeToDelete(adapter)
updateEmptyState()
} }
} }
@ -139,7 +150,7 @@ class NotificationsActivity : AppCompatActivity() {
val ret = invitations[invitationId] val ret = invitations[invitationId]
if (ret == null) { if (ret == null) {
Log.w("NotificationsActivity", "NotificationAdapter requested unknown invitation: $invitationId") Log.w("NotificationsActivity", "NotificationAdapter requested unknown invitation: $invitationId")
Log.w("NotificationsActivity", "Known invitations: $invitations") Log.w("NotificationsActivity", "Known invitations (${invitations.size}): $invitations")
} }
return ret return ret
} }
@ -148,7 +159,7 @@ class NotificationsActivity : AppCompatActivity() {
val ret = events[eventId] val ret = events[eventId]
if (ret == null) { if (ret == null) {
Log.w("NotificationsActivity", "NotificationAdapter requested unknown event: $eventId") Log.w("NotificationsActivity", "NotificationAdapter requested unknown event: $eventId")
Log.w("NotificationsActivity", "Known events: $events") Log.w("NotificationsActivity", "Known events (${events.size}): $events")
} }
return ret return ret
} }
@ -250,4 +261,49 @@ class NotificationsActivity : AppCompatActivity() {
} }
} }
} }
private fun setupSwipeToDelete(adapter: NotificationAdapter) {
val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.RIGHT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false // We don't support moving items
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
val notification = notifications[position]
// Call the deletion method
lifecycleScope.launch(Dispatchers.IO) {
RetrofitClient.notificationsService.deleteNotification(notification.id)
withContext(Dispatchers.Main) {
// Remove the notification & notify the adapter about it
notifications.removeAt(position)
adapter.notifyItemRemoved(position)
// Annoyingly, we can't just use notifyItemRemoved for the single removed item,
// as all the items below it would now be using the wrong position that was
// already bounded to the callbacks from the click listeners, so we need to refresh
// all of the notifications below this one as well.
adapter.notifyItemRangeChanged(position, notifications.size - position)
Toast.makeText(this@NotificationsActivity, "Notification deleted", Toast.LENGTH_SHORT).show()
updateEmptyState()
}
}
}
})
// Attach ItemTouchHelper to the RecyclerView
itemTouchHelper.attachToRecyclerView(rvNotifications)
}
private fun updateEmptyState() {
val isEmpty = notifications.isEmpty()
tvEmptyState.visibility = if (isEmpty) View.VISIBLE else View.GONE
tvSwipeHint.visibility = if (isEmpty) View.GONE else View.VISIBLE
}
} }

View file

@ -32,7 +32,7 @@ interface EventsService {
@Query("start_to") startTo: OffsetDateTime? = null, @Query("start_to") startTo: OffsetDateTime? = null,
@Query("end_from") endFrom: 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) @Query("invite_status") inviteStatus: String? = null, // "pending" / "accepted" / null (both)
): List<EventResponse> ): List<EventResponse>
@DELETE("events/{event_id}") @DELETE("events/{event_id}")

View file

@ -1,6 +1,7 @@
package com.p_vacho.neat_calendar.api.services package com.p_vacho.neat_calendar.api.services
import com.p_vacho.neat_calendar.api.models.NotificationResponse import com.p_vacho.neat_calendar.api.models.NotificationResponse
import retrofit2.http.DELETE
import retrofit2.http.GET import retrofit2.http.GET
import retrofit2.http.POST import retrofit2.http.POST
import retrofit2.http.Path import retrofit2.http.Path
@ -14,4 +15,10 @@ interface NotificationService {
@POST("notifications/{notification_id}/read") @POST("notifications/{notification_id}/read")
suspend fun markNotificationRead(@Path("notification_id") notificationId: String): NotificationResponse suspend fun markNotificationRead(@Path("notification_id") notificationId: String): NotificationResponse
@POST("notifications/{notification_id}/unread")
suspend fun markNotificationUnread(@Path("notification_id") notificationId: String): NotificationResponse
@DELETE("notifications/{notification_id}")
suspend fun deleteNotification(@Path("notification_id") notificationId: String): Unit
} }

View file

@ -62,4 +62,36 @@
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:layout_marginTop="8dp" /> android:layout_marginTop="8dp" />
<!-- Swipe Hint -->
<TextView
android:id="@+id/tvSwipeHint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/swipe_to_delete_hint"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:gravity="center"
android:padding="16dp"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintTop_toBottomOf="@id/rvNotifications"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Empty State -->
<TextView
android:id="@+id/tvEmptyState"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/no_notifications"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary"
android:gravity="center"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/titleBar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -79,4 +79,6 @@
<string name="decline">Decline</string> <string name="decline">Decline</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>
<string name="view_event">View Event</string> <string name="view_event">View Event</string>
<string name="swipe_to_delete_hint">Swipe right on a notification to delete it.</string>
<string name="no_notifications">You\'re all caught up! No notifications right now.</string>
</resources> </resources>