feat(notifications): Add UI

This commit is contained in:
Peter Vacho 2025-01-04 12:12:04 +01:00
parent 6781410b87
commit 4cf5a35255
Signed by: school
GPG key ID: 8CFC3837052871B4
11 changed files with 295 additions and 13 deletions

View file

@ -1,21 +1,77 @@
package com.p_vacho.neat_calendar.activities
import android.os.Bundle
import android.widget.ImageButton
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.p_vacho.neat_calendar.R
import com.p_vacho.neat_calendar.adapters.NotificationAdapter
import com.p_vacho.neat_calendar.api.models.NotificationResponse
import java.time.OffsetDateTime
class NotificationsActivity : AppCompatActivity() {
private lateinit var rvNotifications: RecyclerView
private lateinit var btnBack: ImageButton
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_notification)
setContentView(R.layout.activity_notifications)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
rvNotifications = findViewById(R.id.rvNotifications)
btnBack = findViewById(R.id.btnBack)
val notifications = fetchNotifications()
val adapter = NotificationAdapter(notifications, ::handleNotificationAction, ::handleNotificationClick)
rvNotifications.layoutManager = LinearLayoutManager(this)
rvNotifications.adapter = adapter
btnBack.setOnClickListener { finish() }
}
private fun fetchNotifications(): List<NotificationResponse> {
// TODO: Replace with actual API calls
return listOf(
NotificationResponse(
"1", "2", "invitation",
"You've been invited to event X",
"12", false, OffsetDateTime.now(), null
),
NotificationResponse(
"2", "2", "message",
"System maintenance scheduled",
"", false, OffsetDateTime.now(), null
),
NotificationResponse(
"2", "2", "message",
"App update available",
"", true, OffsetDateTime.now(), null
),
)
}
private fun handleNotificationAction(notification: NotificationResponse, action: NotificationAdapter.Action, position: Int) {
when (action) {
NotificationAdapter.Action.ACCEPT -> {
//TODO("Handle accept action")
}
NotificationAdapter.Action.DECLINE -> {
//TODO("Handle decline action")
}
}
}
private fun handleNotificationClick(notification: NotificationResponse, position: Int) {
TODO("Handle marking notification as read")
}
}

View file

@ -0,0 +1,68 @@
package com.p_vacho.neat_calendar.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.NotificationResponse
class NotificationAdapter(
private val notifications: List<NotificationResponse>,
private val onActionClick: (NotificationResponse, Action, Int) -> Unit,
private val onNotificationClick: (NotificationResponse, Int) -> Unit,
) : RecyclerView.Adapter<NotificationAdapter.NotificationViewHolder>() {
enum class Action {
ACCEPT, DECLINE
}
inner class NotificationViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val message: TextView = view.findViewById(R.id.notificationMessage)
val acceptButton: ImageButton = view.findViewById(R.id.acceptButton)
val declineButton: ImageButton = view.findViewById(R.id.declineButton)
val invitationActions: View = view.findViewById(R.id.invitationActions)
val unreadIndicator: View = view.findViewById(R.id.notificationIndicator)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NotificationViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_notification, parent, false)
return NotificationViewHolder(view)
}
override fun onBindViewHolder(holder: NotificationViewHolder, position: Int) {
val notification = notifications[position]
holder.message.text = notification.message
// Set visibility based on read/unread status
holder.unreadIndicator.visibility = if (notification.read) View.GONE else View.VISIBLE
// Handle invitation actions
if (notification.event_type == "invitation") {
holder.invitationActions.visibility = View.VISIBLE
holder.acceptButton.setOnClickListener {
onActionClick(notification, Action.ACCEPT, position)
}
holder.declineButton.setOnClickListener {
onActionClick(notification, Action.DECLINE, position)
}
} else {
holder.invitationActions.visibility = View.GONE
}
// Set click listener for the whole notification (excluding buttons)
holder.itemView.setOnClickListener {
onNotificationClick(notification, position)
}
// Ensure buttons consume their click events
holder.acceptButton.isClickable = true
holder.declineButton.isClickable = true
}
override fun getItemCount(): Int = notifications.size
}

View file

@ -5,7 +5,7 @@ import java.time.OffsetDateTime
data class NotificationResponse(
val id: String,
val user_id: String,
val event_type: String, // "reminder" / "invitation"
val event_type: String, // "message" / "invitation"
val message: String,
val data: String,
val read: Boolean,

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="?attr/colorControlHighlight">
<item>
<shape android:shape="rectangle">
<solid android:color="?attr/colorSurface" />
<corners android:radius="8dp" />
</shape>
</item>
</ripple>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M382,720 L154,492l57,-57 171,171 367,-367 57,57 -424,424Z"
android:fillColor="#e8eaed"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,640q75,0 127.5,-52.5T660,460q0,-75 -52.5,-127.5T480,280q-75,0 -127.5,52.5T300,460q0,75 52.5,127.5T480,640ZM480,568q-45,0 -76.5,-31.5T372,460q0,-45 31.5,-76.5T480,352q45,0 76.5,31.5T588,460q0,45 -31.5,76.5T480,568ZM480,760q-146,0 -266,-81.5T40,460q54,-137 174,-218.5T480,160q146,0 266,81.5T920,460q-54,137 -174,218.5T480,760ZM480,460ZM480,680q113,0 207.5,-59.5T832,460q-50,-101 -144.5,-160.5T480,240q-113,0 -207.5,59.5T128,460q50,101 144.5,160.5T480,680Z"
android:fillColor="#e8eaed"/>
</vector>

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.NotificationsActivity">
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activities.NotificationsActivity">
<!-- Title Bar -->
<LinearLayout
android:id="@+id/titleBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="?android:attr/dividerHorizontal"
android:paddingStart="8dp"
android:paddingEnd="8dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<!-- Back Button -->
<ImageButton
android:id="@+id/btnBack"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_arrow_back"
android:contentDescription="@string/back"
app:tint="?android:attr/textColorPrimary" />
<!-- Title -->
<TextView
android:id="@+id/tvTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="18sp"
android:textStyle="bold"
android:gravity="center"
android:text="@string/notifications" />
</LinearLayout>
<!-- RecyclerView for displaying notifications -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvNotifications"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toBottomOf="@id/titleBar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:listitem="@layout/item_notification"
tools:itemCount="5"
android:clipToPadding="false"
android:padding="16dp"
android:paddingBottom="24dp"
android:scrollbars="vertical"
android:layout_marginBottom="16dp"
android:layout_marginTop="8dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,70 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:clickable="true"
android:padding="16dp"
android:elevation="16dp"
android:background="@drawable/bg_notification_item_ripple">
<!-- Read/Unread Indicator as vertical line -->
<View
android:id="@+id/notificationIndicator"
android:layout_width="4dp"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:background="@color/unreadIndicator"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- Message Text -->
<TextView
android:id="@+id/notificationMessage"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="Sample notification message"
android:textSize="16sp"
android:ellipsize="end"
android:maxLines="2"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/notificationIndicator"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginLeft="12dp" />
<!-- Buttons for invitations -->
<LinearLayout
android:id="@+id/invitationActions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="gone"
tools:visibility="visible"
android:orientation="horizontal"
app:layout_constraintTop_toBottomOf="@id/notificationMessage"
app:layout_constraintEnd_toEndOf="parent">
<ImageButton
android:id="@+id/acceptButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_check"
android:contentDescription="@string/accept"
app:tint="@android:color/holo_green_dark" />
<ImageButton
android:id="@+id/declineButton"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_close"
android:contentDescription="@string/decline"
app:tint="@android:color/holo_red_dark" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -7,4 +7,5 @@
<color name="splash_dark_background">#121212</color> <!-- Dark gray -->
<color name="event_indicator_color">#0035D0</color>
<color name="unreadIndicator">#bb6633</color>
</resources>

View file

@ -75,4 +75,8 @@
<string name="choose_event_color">Choose Event Color</string>
<string name="select_time">Select Time</string>
<string name="manage_categories">Manage categories</string>
<string name="accept">Accept</string>
<string name="decline">Decline</string>
<string name="notifications">Notifications</string>
<string name="view_event">View Event</string>
</resources>