feat: Finish event details page
This commit is contained in:
parent
025234a93b
commit
224f8642bb
|
@ -1,15 +1,51 @@
|
|||
package com.p_vacho.neat_calendar.activities
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.p_vacho.neat_calendar.MyApplication
|
||||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.adapters.CategoryChipAdapter
|
||||
import com.p_vacho.neat_calendar.adapters.UserChipAdapter
|
||||
import com.p_vacho.neat_calendar.api.RetrofitClient
|
||||
import com.p_vacho.neat_calendar.api.models.CategoryResponse
|
||||
import com.p_vacho.neat_calendar.api.models.EventResponse
|
||||
import com.p_vacho.neat_calendar.api.models.UserResponse
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class EventDetailsActivity : AppCompatActivity() {
|
||||
private lateinit var event: EventResponse
|
||||
private lateinit var categories: Map<String, CategoryResponse>
|
||||
private lateinit var users: Map<String, UserResponse>
|
||||
|
||||
// UI components
|
||||
private lateinit var btnBack: ImageButton
|
||||
private lateinit var tvEventTitle: TextView
|
||||
private lateinit var tvEventStartTime: TextView
|
||||
private lateinit var tvEventEndTime: TextView
|
||||
private lateinit var tvEventDescription: TextView
|
||||
private lateinit var rvEventCategories: RecyclerView
|
||||
private lateinit var rvEventAttendees: RecyclerView
|
||||
private lateinit var tvEventCreatedAt: TextView
|
||||
private lateinit var tvNoCategories: TextView
|
||||
private lateinit var tvNoAttendees: TextView
|
||||
private lateinit var tvEventOwner: TextView
|
||||
private lateinit var chipEventOwner: Chip
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -21,7 +57,205 @@ class EventDetailsActivity : AppCompatActivity() {
|
|||
insets
|
||||
}
|
||||
|
||||
initializeViews()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
event = intent.getParcelableExtra<EventResponse>("event")!!
|
||||
event = intent.getParcelableExtra("event")!!
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
categories = intent.getParcelableArrayListExtra<CategoryResponse>("categories")?.associateBy { it.id } ?: emptyMap()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
users = intent.getParcelableArrayListExtra<UserResponse>("users")?.associateBy { it.user_id } ?: emptyMap()
|
||||
|
||||
lifecycleScope.launch {
|
||||
// Collect tasks for missing data
|
||||
// This will also start the fetching coroutines
|
||||
val fetchCategoriesTask = determineCategoryFetchTask()
|
||||
val fetchUsersTask = determineUserFetchTask()
|
||||
|
||||
// Await results
|
||||
val fetchedCategories = fetchCategoriesTask?.await()
|
||||
val fetchedUsers = fetchUsersTask?.await()
|
||||
|
||||
// Update data maps
|
||||
if (fetchedCategories != null) categories = categories + fetchedCategories
|
||||
if (fetchedUsers != null) users = users + fetchedUsers
|
||||
|
||||
setEventDetails()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeViews() {
|
||||
btnBack = findViewById(R.id.btnBack)
|
||||
tvEventTitle = findViewById(R.id.tvEventTitle)
|
||||
tvEventStartTime = findViewById(R.id.tvEventStartTime)
|
||||
tvEventEndTime = findViewById(R.id.tvEventEndTime)
|
||||
tvEventDescription = findViewById(R.id.tvEventDescription)
|
||||
rvEventCategories = findViewById(R.id.rvEventCategories)
|
||||
rvEventAttendees = findViewById(R.id.rvEventAttendees)
|
||||
tvEventCreatedAt = findViewById(R.id.tvEventCreatedAt)
|
||||
tvNoCategories = findViewById(R.id.tvNoCategories)
|
||||
tvNoAttendees = findViewById(R.id.tvNoAttendees)
|
||||
tvEventOwner = findViewById(R.id.tvEventOwner)
|
||||
chipEventOwner = findViewById(R.id.chipEventOwner)
|
||||
|
||||
rvEventCategories.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
rvEventCategories.adapter = CategoryChipAdapter(emptyList(), isRemovable = false)
|
||||
|
||||
rvEventAttendees.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
rvEventAttendees.adapter = UserChipAdapter(emptyList())
|
||||
|
||||
btnBack.setOnClickListener { finish() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the individual UI items with actual content about the event.
|
||||
*
|
||||
* This can only be called once all of the necessary data is fetched.
|
||||
*/
|
||||
private fun setEventDetails() {
|
||||
// Set basic event details
|
||||
tvEventTitle.text = event.title
|
||||
tvEventStartTime.text = getString(R.string.event_start_time, formatDateTime(event.start_time.toLocalDateTime()))
|
||||
tvEventEndTime.text = getString(R.string.event_end_time, formatDateTime(event.end_time.toLocalDateTime()))
|
||||
tvEventDescription.text = event.description
|
||||
tvEventCreatedAt.text = getString(R.string.event_created_at, formatDateTime(event.created_at.toLocalDateTime()))
|
||||
|
||||
val userId = (application as MyApplication).tokenManager.userId
|
||||
|
||||
// Set categories or show the placeholder
|
||||
val categoryList = event.category_ids.mapNotNull { categories[it] }
|
||||
rvEventCategories.adapter = CategoryChipAdapter(categoryList, isRemovable = false)
|
||||
tvNoCategories.visibility = if (categoryList.isEmpty()) View.VISIBLE else View.GONE
|
||||
if (event.owner_user_id == userId) {
|
||||
tvNoCategories.text = getString(R.string.no_categories_placeholder)
|
||||
} else {
|
||||
tvNoCategories.text = getString(R.string.no_invited_categories_placeholder)
|
||||
}
|
||||
|
||||
|
||||
// Set attendees or show the placeholder
|
||||
val attendeeList = event.attendee_ids.mapNotNull { users[it] }
|
||||
rvEventAttendees.adapter = UserChipAdapter(attendeeList)
|
||||
tvNoAttendees.visibility = if (attendeeList.isEmpty()) View.VISIBLE else View.GONE
|
||||
tvNoAttendees.text = getString(R.string.no_attendees_placeholder)
|
||||
|
||||
// Show the owner section only if this event isn't owned by the logged user
|
||||
if (event.owner_user_id != userId) {
|
||||
val owner = users[event.owner_user_id]
|
||||
if (owner == null) throw IllegalStateException("Event owner wasn't fetched")
|
||||
tvEventOwner.visibility = View.VISIBLE
|
||||
chipEventOwner.visibility = View.VISIBLE
|
||||
chipEventOwner.text = owner.username
|
||||
} else {
|
||||
tvEventOwner.visibility = View.GONE
|
||||
chipEventOwner.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether event categories need to be fetched.
|
||||
*
|
||||
* Categories will be fetched if:
|
||||
* - They weren't already set
|
||||
* - One (or more) of the event categories wasn't found in the list of existing categories.
|
||||
* Note that this case will also produce a warning.
|
||||
*
|
||||
* Categories won't be fetched if the currently logged in user isn't also the event owner,
|
||||
* as they don't have the rights to access the invitor's categories.
|
||||
*
|
||||
* This will return back a Deferred object, being obtained in an already started coroutine.
|
||||
*/
|
||||
private fun determineCategoryFetchTask(): Deferred<Map<String, CategoryResponse>>? {
|
||||
val userId = (application as MyApplication).tokenManager.userId
|
||||
|
||||
if (userId != event.owner_user_id) return null
|
||||
|
||||
return if (categories.isEmpty()) {
|
||||
lifecycleScope.async { fetchEventCategories(event.id) }
|
||||
} else if (categories.keys.intersect(event.category_ids).size != event.category_ids.size) {
|
||||
Log.w(
|
||||
"EventDetailsActivity",
|
||||
"One or more of the event categories wasn't found in the list of categories. Categories will be re-fetched."
|
||||
)
|
||||
lifecycleScope.async { fetchEventCategories(event.id) }
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which users need to be fetched and creates a fetch task if necessary.
|
||||
*
|
||||
* This will fetch all the users that are attending the event and the event's user id,
|
||||
* skipping any users that are already fetched. Note that if any of the event attendees
|
||||
* aren't already in the users list, yet the list isn't empty, a warning will be produced.
|
||||
*
|
||||
* This will return back a Deferred object, being obtained in an already started coroutine.
|
||||
*/
|
||||
private fun determineUserFetchTask(): Deferred<Map<String, UserResponse>>? {
|
||||
val userIdsToFetch = mutableSetOf<String>().apply {
|
||||
if (users.isEmpty()) {
|
||||
addAll(event.attendee_ids)
|
||||
add(event.owner_user_id)
|
||||
} else {
|
||||
if (!users.containsKey(event.owner_user_id)) add(event.owner_user_id)
|
||||
|
||||
// Check for missing attendees and log a warning for each
|
||||
val missingAttendees = event.attendee_ids.filter { !users.containsKey(it) }
|
||||
if (missingAttendees.isNotEmpty()) {
|
||||
missingAttendees.forEach { attendeeId ->
|
||||
Log.w(
|
||||
"EventDetailsActivity",
|
||||
"Missing attendee ID: $attendeeId in the provided users list. Will fetch it additionally."
|
||||
)
|
||||
}
|
||||
addAll(missingAttendees)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return if (userIdsToFetch.isNotEmpty()) {
|
||||
lifecycleScope.async { fetchUsers(userIdsToFetch.toList()) }
|
||||
} else null
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the categories of given event.
|
||||
*
|
||||
* This will return a hash map, with the category IDs as keys.
|
||||
*/
|
||||
private suspend fun fetchEventCategories(eventId: String): Map<String, CategoryResponse> {
|
||||
val fetchedCategories = withContext(Dispatchers.IO) {
|
||||
RetrofitClient.categoryService.eventCategories(eventId)
|
||||
}
|
||||
|
||||
return fetchedCategories.associateBy { it.id }
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all the requested users (by IDs).
|
||||
*
|
||||
* This will return a hash map, with the user IDs as keys.
|
||||
*
|
||||
* Note that this will make an API request for each requested user ID.
|
||||
* These requests will be made in parallel.
|
||||
*/
|
||||
private suspend fun fetchUsers(userIds: List<String>): Map<String, UserResponse> {
|
||||
val fetchedUsers = withContext(Dispatchers.IO) {
|
||||
val usersDeferred = userIds.map { async { RetrofitClient.usersService.getUser(it) }}
|
||||
usersDeferred.map { it.await() }
|
||||
|
||||
}
|
||||
|
||||
return fetchedUsers.associateBy { it.user_id }
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date time object into a string.
|
||||
*
|
||||
* This is used to format the datetime shown for the event start, end & creation times.
|
||||
*/
|
||||
private fun formatDateTime(dateTime: java.time.LocalDateTime): String {
|
||||
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.p_vacho.neat_calendar.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.p_vacho.neat_calendar.R
|
||||
import com.p_vacho.neat_calendar.api.models.UserResponse
|
||||
|
||||
class UserChipAdapter(
|
||||
private val users: List<UserResponse>
|
||||
) : RecyclerView.Adapter<UserChipAdapter.UserChipViewHolder>() {
|
||||
|
||||
inner class UserChipViewHolder(val chip: Chip) : RecyclerView.ViewHolder(chip)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserChipViewHolder {
|
||||
val chip = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_user_chip, parent, false) as Chip
|
||||
return UserChipViewHolder(chip)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: UserChipViewHolder, position: Int) {
|
||||
val user = users[position]
|
||||
val chip = holder.chip
|
||||
|
||||
chip.text = user.username
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = users.size
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
package com.p_vacho.neat_calendar.api.models
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Parcelize
|
||||
data class UserResponse(
|
||||
val user_id: String,
|
||||
val username: String,
|
||||
val email: String,
|
||||
val created_at: OffsetDateTime,
|
||||
)
|
||||
) : Parcelable
|
||||
|
||||
data class PartialUserRequest(
|
||||
val username: String?,
|
||||
|
|
|
@ -121,7 +121,7 @@
|
|||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
tools:text="Categories:" />
|
||||
android:text="@string/categories_section" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvEventCategories"
|
||||
|
@ -133,6 +133,18 @@
|
|||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:orientation="horizontal" />
|
||||
|
||||
<!-- Placeholder for empty categories -->
|
||||
<TextView
|
||||
android:id="@+id/tvNoCategories"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_categories_placeholder"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Attendees Section -->
|
||||
<TextView
|
||||
android:id="@+id/tvEventAttendees"
|
||||
|
@ -142,18 +154,48 @@
|
|||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
tools:text="Attendees:" />
|
||||
android:text="@string/attendees_section" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvEventAttendees"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:listitem="@layout/item_attendee_chip"
|
||||
tools:listitem="@layout/item_user_chip"
|
||||
tools:itemCount="3"
|
||||
tools:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:orientation="horizontal" />
|
||||
|
||||
<!-- Placeholder for empty attendees -->
|
||||
<TextView
|
||||
android:id="@+id/tvNoAttendees"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_attendees_placeholder"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Event Owner Section -->
|
||||
<TextView
|
||||
android:id="@+id/tvEventOwner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textStyle="bold"
|
||||
android:text="@string/event_owner_section" />
|
||||
|
||||
<include
|
||||
android:id="@+id/chipEventOwner"
|
||||
layout="@layout/item_user_chip"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
<!-- Created At -->
|
||||
<TextView
|
||||
android:id="@+id/tvEventCreatedAt"
|
||||
|
@ -166,5 +208,4 @@
|
|||
|
||||
</LinearLayout>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -87,4 +87,13 @@
|
|||
<string name="event_details">Event Details</string>
|
||||
<string name="category_name">Category Name</string>
|
||||
<string name="open_categories">Open category management</string>
|
||||
<string name="event_start_time">"Start: %1$s"</string>
|
||||
<string name="event_end_time">End: %1$s</string>
|
||||
<string name="event_created_at">Created on: %1$s</string>
|
||||
<string name="categories_section">Categories:</string>
|
||||
<string name="attendees_section">Attendees:</string>
|
||||
<string name="no_attendees_placeholder">This event has no attendees.</string>
|
||||
<string name="no_categories_placeholder">This event has no categories.</string>
|
||||
<string name="event_owner_section">Event owner:</string>
|
||||
<string name="no_invited_categories_placeholder">You can\'t see categories for invited events</string>
|
||||
</resources>
|
Loading…
Reference in a new issue