Add admin messages (#1553)

This commit is contained in:
Rafał Borcz
2021-10-13 23:58:24 +02:00
committed by GitHub
parent d6918077bf
commit e3122127c0
29 changed files with 2868 additions and 70 deletions

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.ui.base
import android.content.res.Resources
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.data.exceptions.NoCurrentStudentException
import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.sdk.scrapper.login.PasswordChangeRequiredException
@ -9,7 +10,7 @@ import io.github.wulkanowy.utils.security.ScramblerException
import timber.log.Timber
import javax.inject.Inject
open class ErrorHandler @Inject constructor(protected val resources: Resources) {
open class ErrorHandler @Inject constructor(@ApplicationContext protected val context: Context) {
var showErrorMessage: (String, Throwable) -> Unit = { _, _ -> }
@ -25,7 +26,7 @@ open class ErrorHandler @Inject constructor(protected val resources: Resources)
}
protected open fun proceed(error: Throwable) {
showErrorMessage(resources.getString(error), error)
showErrorMessage(context.resources.getString(error), error)
when (error) {
is PasswordChangeRequiredException -> onPasswordChangeRequired(error.redirectUrl)
is ScramblerException, is BadCredentialsException -> onSessionExpired()

View File

@ -1,6 +1,8 @@
package io.github.wulkanowy.ui.modules.dashboard
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface
import android.os.Handler
import android.os.Looper
@ -18,6 +20,7 @@ import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.databinding.ItemDashboardAccountBinding
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.databinding.ItemDashboardAnnouncementsBinding
import io.github.wulkanowy.databinding.ItemDashboardConferencesBinding
import io.github.wulkanowy.databinding.ItemDashboardExamsBinding
@ -63,6 +66,8 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
var onConferencesTileClickListener: () -> Unit = {}
var onAdminMessageClickListener: (String?) -> Unit = {}
val items = mutableListOf<DashboardItem>()
fun submitList(newItems: List<DashboardItem>) {
@ -109,6 +114,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
DashboardItem.Type.CONFERENCES.ordinal -> ConferencesViewHolder(
ItemDashboardConferencesBinding.inflate(inflater, parent, false)
)
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false)
)
else -> throw IllegalArgumentException()
}
}
@ -123,6 +131,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> bindAdminMessage(holder, position)
}
}
@ -697,6 +706,34 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
}
}
private fun bindAdminMessage(adminMessageViewHolder: AdminMessageViewHolder, position: Int) {
val item = (items[position] as DashboardItem.AdminMessages).adminMessage ?: return
val context = adminMessageViewHolder.binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorPrimary) to
context.getThemeAttrColor(R.attr.colorOnPrimary)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(adminMessageViewHolder.binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
class AccountViewHolder(val binding: ItemDashboardAccountBinding) :
RecyclerView.ViewHolder(binding.root)
@ -736,6 +773,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val adapter by lazy { DashboardConferencesAdapter() }
}
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root)
private class DiffCallback(
private val newList: List<DashboardItem>,
private val oldList: List<DashboardItem>

View File

@ -10,6 +10,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R
import io.github.wulkanowy.databinding.FragmentDashboardBinding
@ -29,6 +30,7 @@ import io.github.wulkanowy.ui.modules.schoolannouncement.SchoolAnnouncementFragm
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.capitalise
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.openInternetBrowser
import io.github.wulkanowy.utils.toFormattedString
import java.time.LocalDate
import javax.inject.Inject
@ -97,6 +99,13 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
onConferencesTileClickListener = {
mainActivity.pushView(ConferenceFragment.newInstance())
}
onAdminMessageClickListener = presenter::onAdminMessageSelected
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
binding.dashboardRecycler.scrollToPosition(0)
}
})
}
with(binding) {
@ -188,6 +197,10 @@ class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragme
(requireActivity() as MainActivity).pushView(NotificationsCenterFragment.newInstance())
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
override fun onDestroyView() {
dashboardAdapter.clearTimers()
presenter.onDetachView()

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.dashboard
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
@ -16,6 +17,15 @@ sealed class DashboardItem(val type: Type) {
abstract val isDataLoaded: Boolean
data class AdminMessages(
val adminMessage: AdminMessage? = null,
override val error: Throwable? = null,
override val isLoading: Boolean = false
) : DashboardItem(Type.ADMIN_MESSAGE) {
override val isDataLoaded get() = adminMessage != null
}
data class Account(
val student: Student? = null,
override val error: Throwable? = null,
@ -96,6 +106,7 @@ sealed class DashboardItem(val type: Type) {
}
enum class Type {
ADMIN_MESSAGE,
ACCOUNT,
HORIZONTAL_GROUP,
LESSONS,
@ -108,6 +119,7 @@ sealed class DashboardItem(val type: Type) {
}
enum class Tile {
ADMIN_MESSAGE,
ACCOUNT,
LUCKY_NUMBER,
MESSAGES,
@ -123,6 +135,7 @@ sealed class DashboardItem(val type: Type) {
}
fun DashboardItem.Tile.toDashboardItemType() = when (this) {
DashboardItem.Tile.ADMIN_MESSAGE -> DashboardItem.Type.ADMIN_MESSAGE
DashboardItem.Tile.ACCOUNT -> DashboardItem.Type.ACCOUNT
DashboardItem.Tile.LUCKY_NUMBER -> DashboardItem.Type.HORIZONTAL_GROUP
DashboardItem.Tile.MESSAGES -> DashboardItem.Type.HORIZONTAL_GROUP

View File

@ -21,7 +21,7 @@ class DashboardItemMoveCallback(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = if (viewHolder.bindingAdapterPosition != 0) {
val dragFlags = if (!viewHolder.isAdminMessageOrAccountItem) {
ItemTouchHelper.UP or ItemTouchHelper.DOWN
} else 0
@ -32,7 +32,7 @@ class DashboardItemMoveCallback(
recyclerView: RecyclerView,
current: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
) = target.bindingAdapterPosition != 0
) = !target.isAdminMessageOrAccountItem
override fun onMove(
recyclerView: RecyclerView,
@ -52,4 +52,7 @@ class DashboardItemMoveCallback(
onUserInteractionEndListener(dashboardAdapter.items.toList())
}
private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean
get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder
}

View File

@ -5,6 +5,7 @@ import io.github.wulkanowy.data.Status
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.AttendanceSummaryRepository
import io.github.wulkanowy.data.repositories.ConferenceRepository
import io.github.wulkanowy.data.repositories.ExamRepository
@ -50,7 +51,8 @@ class DashboardPresenter @Inject constructor(
private val examRepository: ExamRepository,
private val conferenceRepository: ConferenceRepository,
private val preferencesRepository: PreferencesRepository,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository
private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val adminMessageRepository: AdminMessageRepository
) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
private val dashboardItemLoadedList = mutableListOf<DashboardItem>()
@ -179,6 +181,7 @@ class DashboardPresenter @Inject constructor(
loadConferences(student, forceRefresh)
}
DashboardItem.Type.ADS -> TODO()
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
}
}
}
@ -225,6 +228,10 @@ class DashboardPresenter @Inject constructor(
}.toSet()
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
private fun loadHorizontalGroup(student: Student, forceRefresh: Boolean) {
flow {
val semester = semesterRepository.getCurrentSemester(student)
@ -567,6 +574,38 @@ class DashboardPresenter @Inject constructor(
}.launch("dashboard_conferences")
}
private fun loadAdminMessage(student: Student, forceRefresh: Boolean) {
flowWithResourceIn { adminMessageRepository.getAdminMessages(student, forceRefresh) }
.onEach {
when (it.status) {
Status.LOADING -> {
Timber.i("Loading dashboard admin message data started")
if (forceRefresh) return@onEach
updateData(DashboardItem.AdminMessages(), forceRefresh)
}
Status.SUCCESS -> {
Timber.i("Loading dashboard admin message result: Success")
updateData(
dashboardItem = DashboardItem.AdminMessages(adminMessage = it.data),
forceRefresh = forceRefresh
)
}
Status.ERROR -> {
Timber.i("Loading dashboard admin message result: An exception occurred")
errorHandler.dispatch(it.error!!)
updateData(
dashboardItem = DashboardItem.AdminMessages(
adminMessage = it.data,
error = it.error
),
forceRefresh = forceRefresh
)
}
}
}
.launch("dashboard_admin_messages")
}
private fun updateData(dashboardItem: DashboardItem, forceRefresh: Boolean) {
val isForceRefreshError = forceRefresh && dashboardItem.error != null
val isFirstRunDataLoadedError =
@ -579,6 +618,11 @@ class DashboardPresenter @Inject constructor(
sortDashboardItems()
if (dashboardItem is DashboardItem.AdminMessages && !dashboardItem.isDataLoaded) {
dashboardItemsToLoad = dashboardItemsToLoad - DashboardItem.Type.ADMIN_MESSAGE
dashboardItemLoadedList.removeAll { it.type == DashboardItem.Type.ADMIN_MESSAGE }
}
if (forceRefresh) {
updateForceRefreshData(dashboardItem)
} else {
@ -644,7 +688,9 @@ class DashboardPresenter @Inject constructor(
itemsLoadedList: List<DashboardItem>,
forceRefresh: Boolean
) {
val filteredItems = itemsLoadedList.filterNot { it.type == DashboardItem.Type.ACCOUNT }
val filteredItems = itemsLoadedList.filterNot {
it.type == DashboardItem.Type.ACCOUNT || it.type == DashboardItem.Type.ADMIN_MESSAGE
}
val isAccountItemError =
itemsLoadedList.find { it.type == DashboardItem.Type.ACCOUNT }?.error != null
val isGeneralError =
@ -676,10 +722,13 @@ class DashboardPresenter @Inject constructor(
val dashboardItemsPosition = preferencesRepository.dashboardItemsPosition
dashboardItemLoadedList.sortBy { tile ->
dashboardItemsPosition?.getOrDefault(
tile.type,
val defaultPosition = if (tile is DashboardItem.AdminMessages) {
-1
} else {
tile.type.ordinal + 100
) ?: tile.type.ordinal
}
dashboardItemsPosition?.getOrDefault(tile.type, defaultPosition) ?: tile.type.ordinal
}
}
}

View File

@ -25,4 +25,6 @@ interface DashboardView : BaseView {
fun popViewToRoot()
fun openNotificationsCenterView()
fun openInternetBrowser(url: String)
}

View File

@ -1,7 +1,8 @@
package io.github.wulkanowy.ui.modules.login
import android.content.res.Resources
import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.R
import io.github.wulkanowy.sdk.mobile.exception.InvalidPinException
import io.github.wulkanowy.sdk.mobile.exception.InvalidSymbolException
@ -11,7 +12,8 @@ import io.github.wulkanowy.sdk.scrapper.login.BadCredentialsException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
class LoginErrorHandler @Inject constructor(@ApplicationContext context: Context) :
ErrorHandler(context) {
var onBadCredentials: (String?) -> Unit = {}
@ -24,6 +26,7 @@ class LoginErrorHandler @Inject constructor(resources: Resources) : ErrorHandler
var onStudentDuplicate: (String) -> Unit = {}
override fun proceed(error: Throwable) {
val resources = context.resources
when (error) {
is BadCredentialsException -> onBadCredentials(error.message)
is SQLiteConstraintException -> onStudentDuplicate(resources.getString(R.string.login_duplicate_student))

View File

@ -1,13 +1,15 @@
package io.github.wulkanowy.ui.modules.login.recover
import android.content.res.Resources
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.sdk.scrapper.exception.InvalidCaptchaException
import io.github.wulkanowy.sdk.scrapper.exception.InvalidEmailException
import io.github.wulkanowy.sdk.scrapper.exception.NoAccountFoundException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
class RecoverErrorHandler @Inject constructor(@ApplicationContext context: Context) :
ErrorHandler(context) {
var onInvalidUsername: (String) -> Unit = {}
@ -15,7 +17,8 @@ class RecoverErrorHandler @Inject constructor(resources: Resources) : ErrorHandl
override fun proceed(error: Throwable) {
when (error) {
is InvalidEmailException, is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty())
is InvalidEmailException,
is NoAccountFoundException -> onInvalidUsername(error.localizedMessage.orEmpty())
is InvalidCaptchaException -> onInvalidCaptcha(error.localizedMessage.orEmpty(), error)
else -> super.proceed(error)
}

View File

@ -1,11 +1,13 @@
package io.github.wulkanowy.ui.modules.timetable.completed
import android.content.res.Resources
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
import io.github.wulkanowy.ui.base.ErrorHandler
import javax.inject.Inject
class CompletedLessonsErrorHandler @Inject constructor(resources: Resources) : ErrorHandler(resources) {
class CompletedLessonsErrorHandler @Inject constructor(@ApplicationContext context: Context) :
ErrorHandler(context) {
var onFeatureDisabled: () -> Unit = {}