feat: Add support for inviting users to join events

This commit is contained in:
Peter Vacho 2025-01-02 16:43:15 +01:00
parent f51a25d46f
commit 86f4e66215
Signed by: school
GPG key ID: 8CFC3837052871B4
9 changed files with 129 additions and 2 deletions

View file

@ -5,7 +5,9 @@ import android.os.Bundle
import android.util.Log
import android.widget.ImageButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
@ -17,6 +19,8 @@ import com.p_vacho.neat_calendar.R
import com.p_vacho.neat_calendar.adapters.EventCardAdapter
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.InvitationRequest
import com.p_vacho.neat_calendar.api.models.UserResponse
import com.p_vacho.neat_calendar.models.CalendarDay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@ -86,7 +90,10 @@ class DayViewActivity : AppCompatActivity() {
return
}
rvEvents.layoutManager = LinearLayoutManager(this)
rvEvents.adapter = EventCardAdapter(events, userId, this, ::onEventEdit, ::onEventDelete, ::onEventLeave)
rvEvents.adapter = EventCardAdapter(
events, userId, this,
::onEventEdit, ::onEventDelete, ::onEventLeave, ::onEventInvite,
)
btnBack.setOnClickListener { finish() }
btnAddEvent.setOnClickListener { onEventCreate() }
@ -144,6 +151,41 @@ class DayViewActivity : AppCompatActivity() {
}
}
/**
* This is triggered on the invite button click from the event card adapter.
*/
private fun onEventInvite(event: EventResponse, position: Int) {
lifecycleScope.launch {
val users = withContext(Dispatchers.IO) {
RetrofitClient.usersService.getUsers()
}
// Remove the event owner (current user) and the existing event attendees
// from the selection.
val filteredUsers = users.filter {
it.user_id != event.owner_user_id && it.user_id !in event.attendee_ids
}
showUserSelectionDialog(filteredUsers) { user ->
lifecycleScope.launch(Dispatchers.IO) {
val data = InvitationRequest(
event_id=event.id,
invitee_id = user.user_id
)
RetrofitClient.invitationService.createInvitation(data)
withContext(Dispatchers.Main) {
Toast.makeText(
this@DayViewActivity,
"Invitation sent to ${user.username}",
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
/**
* Triggered with the CreateEventActivity return value.
*/
@ -172,4 +214,20 @@ class DayViewActivity : AppCompatActivity() {
@Suppress("NotifyDataSetChanged")
rvEvents.adapter!!.notifyDataSetChanged()
}
private fun showUserSelectionDialog(
users: List<UserResponse>,
onUserSelected: (UserResponse) -> Unit
) {
val userNames = users.map { it.username }.toTypedArray()
AlertDialog.Builder(this)
.setTitle("Select a user to invite")
.setItems(userNames) { dialog, which ->
// Pass the selected user back
onUserSelected(users[which])
dialog.dismiss()
}
.show()
}
}

View file

@ -33,7 +33,8 @@ class EventCardAdapter(
private val onEditEvent: (EventResponse, Int) -> Unit,
private val onDeleteEvent: (EventResponse, Int) -> Unit,
private val onLeaveEvent: (EventResponse, Int) -> Unit,
) :
private val onInviteEvent: (EventResponse, Int) -> Unit,
) :
RecyclerView.Adapter<EventCardAdapter.EventViewHolder>() {
inner class EventViewHolder(view: View) : RecyclerView.ViewHolder(view) {
@ -45,6 +46,7 @@ class EventCardAdapter(
val btnEdit: ImageButton = view.findViewById(R.id.btnEditEvent)
val btnDelete: ImageButton = view.findViewById(R.id.btnDeleteEvent)
val btnLeave: ImageButton = view.findViewById(R.id.btnLeaveEvent)
val btnInvite: ImageButton = view.findViewById(R.id.btnInviteEvent)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EventViewHolder {
@ -72,6 +74,7 @@ class EventCardAdapter(
holder.btnEdit.setOnClickListener { onEditEvent(event, position) }
holder.btnDelete.setOnClickListener { onDeleteEvent(event, position) }
holder.btnLeave.setOnClickListener { onLeaveEvent(event, position) }
holder.btnInvite.setOnClickListener { onInviteEvent(event, position) }
// Initialize empty state for categories
holder.rvCategories.layoutManager =
@ -84,6 +87,7 @@ class EventCardAdapter(
if (event.owner_user_id != userId) {
holder.btnEdit.visibility = View.GONE
holder.btnDelete.visibility = View.GONE
holder.btnInvite.visibility = View.GONE
holder.btnLeave.visibility = View.VISIBLE
} else {
// Fetch categories dynamically

View file

@ -15,6 +15,8 @@ import com.fatboyindustrial.gsonjavatime.OffsetDateTimeConverter
import com.p_vacho.neat_calendar.api.converters.ColorConverter
import com.p_vacho.neat_calendar.api.services.CategoryService
import com.p_vacho.neat_calendar.api.services.EventsService
import com.p_vacho.neat_calendar.api.services.InvitationsService
import com.p_vacho.neat_calendar.api.services.UsersService
object RetrofitClient {
private const val DEFAULT_BASE_URL = "http://10.0.2.2:8000"
@ -37,7 +39,11 @@ object RetrofitClient {
val categoryService: CategoryService
get() = retrofitClient!!.create(CategoryService::class.java)
val invitationService: InvitationsService
get() = retrofitClient!!.create(InvitationsService::class.java)
val usersService: UsersService
get() = retrofitClient!!.create(UsersService::class.java)
fun initialize(context: Context) {
appContext = context

View file

@ -0,0 +1,6 @@
package com.p_vacho.neat_calendar.api.models
data class InvitationRequest(
val event_id: String,
val invitee_id: String,
)

View file

@ -0,0 +1,10 @@
package com.p_vacho.neat_calendar.api.models
import java.time.OffsetDateTime
data class UserResponse(
val user_id: String,
val username: String,
val email: String,
val created_at: OffsetDateTime,
)

View file

@ -0,0 +1,10 @@
package com.p_vacho.neat_calendar.api.services
import com.p_vacho.neat_calendar.api.models.InvitationRequest
import retrofit2.http.Body
import retrofit2.http.POST
interface InvitationsService {
@POST("invitations")
suspend fun createInvitation(@Body invitationData: InvitationRequest)
}

View file

@ -0,0 +1,13 @@
package com.p_vacho.neat_calendar.api.services
import com.p_vacho.neat_calendar.api.models.UserResponse
import retrofit2.http.GET
import retrofit2.http.Path
interface UsersService {
@GET("users")
suspend fun getUsers(): List<UserResponse>
@GET("users/{user_id}")
suspend fun getUser(@Path("user_id") userId: String): UserResponse
}

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="M720,560v-120L600,440v-80h120v-120h80v120h120v80L800,440v120h-80ZM360,480q-66,0 -113,-47t-47,-113q0,-66 47,-113t113,-47q66,0 113,47t47,113q0,66 -47,113t-113,47ZM40,800v-112q0,-34 17.5,-62.5T104,582q62,-31 126,-46.5T360,520q66,0 130,15.5T616,582q29,15 46.5,43.5T680,688v112L40,800ZM120,720h480v-32q0,-11 -5.5,-20T580,654q-54,-27 -109,-40.5T360,600q-56,0 -111,13.5T140,654q-9,5 -14.5,14t-5.5,20v32ZM360,400q33,0 56.5,-23.5T440,320q0,-33 -23.5,-56.5T360,240q-33,0 -56.5,23.5T280,320q0,33 23.5,56.5T360,400ZM360,320ZM360,720Z"
android:fillColor="#e8eaed"/>
</vector>

View file

@ -52,6 +52,17 @@
android:maxLines="2"
tools:text="Event Title" />
<!-- Invite button -->
<ImageButton
android:id="@+id/btnInviteEvent"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="4dp"
android:src="@drawable/ic_person_add"
android:contentDescription="@string/leave_invited_event"
app:tint="?android:attr/textColorSecondary" />
<!-- Edit Button -->
<ImageButton
android:id="@+id/btnEditEvent"