1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-19 22:49:08 -05:00

Add admin messages to login screen (#2280)

This commit is contained in:
Mikołaj Pich 2023-08-24 11:33:40 +02:00 committed by GitHub
parent fbce9e58d0
commit 3dfc55c4d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 2722 additions and 85 deletions

View File

@ -27,7 +27,7 @@ android {
testApplicationId "io.github.tests.wulkanowy" testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 33
versionCode 130 versionCode 131 // todo: already bumped for 2.1.0 version
versionName "2.0.8" versionName "2.0.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

File diff suppressed because it is too large Load Diff

View File

@ -148,7 +148,7 @@ inline fun <ResultType, RequestType, T> networkBoundResource(
crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit, crossinline saveFetchResult: suspend (old: ResultType, new: RequestType) -> Unit,
crossinline onFetchFailed: (Throwable) -> Unit = { }, crossinline onFetchFailed: (Throwable) -> Unit = { },
crossinline shouldFetch: (ResultType) -> Boolean = { true }, crossinline shouldFetch: (ResultType) -> Boolean = { true },
crossinline mapResult: (ResultType) -> T crossinline mapResult: (ResultType) -> T,
) = flow { ) = flow {
emit(Resource.Loading()) emit(Resource.Loading())

View File

@ -50,6 +50,7 @@ import javax.inject.Singleton
AutoMigration(from = 51, to = 52), AutoMigration(from = 51, to = 52),
AutoMigration(from = 54, to = 55, spec = Migration55::class), AutoMigration(from = 54, to = 55, spec = Migration55::class),
AutoMigration(from = 55, to = 56), AutoMigration(from = 55, to = 56),
AutoMigration(from = 56, to = 57, spec = Migration57::class),
], ],
version = AppDatabase.VERSION_SCHEMA, version = AppDatabase.VERSION_SCHEMA,
exportSchema = true exportSchema = true
@ -58,7 +59,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
companion object { companion object {
const val VERSION_SCHEMA = 56 const val VERSION_SCHEMA = 57
fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf( fun getMigrations(sharedPrefProvider: SharedPrefProvider, appInfo: AppInfo) = arrayOf(
Migration2(), Migration2(),

View File

@ -1,6 +1,7 @@
package io.github.wulkanowy.data.db package io.github.wulkanowy.data.db
import androidx.room.TypeConverter import androidx.room.TypeConverter
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.ui.modules.Destination import io.github.wulkanowy.ui.modules.Destination
import io.github.wulkanowy.utils.toTimestamp import io.github.wulkanowy.utils.toTimestamp
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
@ -68,4 +69,9 @@ class Converters {
@TypeConverter @TypeConverter
fun stringToDestination(destination: String): Destination = json.decodeFromString(destination) fun stringToDestination(destination: String): Destination = json.decodeFromString(destination)
@TypeConverter
fun messageTypesToString(types: List<MessageType>): String = json.encodeToString(types)
@TypeConverter
fun stringToMessageTypes(text: String): List<MessageType> = json.decodeFromString(text)
} }

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import io.github.wulkanowy.data.enums.MessageType
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
@ -33,7 +34,8 @@ data class AdminMessage(
val priority: String, val priority: String,
val type: String, @ColumnInfo(name = "types", defaultValue = "[]")
val types: List<MessageType> = emptyList(),
@ColumnInfo(name = "is_dismissible") @ColumnInfo(name = "is_dismissible")
val isDismissible: Boolean = false val isDismissible: Boolean = false

View File

@ -0,0 +1,10 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
@DeleteColumn(
tableName = "AdminMessages",
columnName = "type",
)
class Migration57 : AutoMigrationSpec

View File

@ -0,0 +1,9 @@
package io.github.wulkanowy.data.enums
enum class MessageType {
GENERAL_MESSAGE,
DASHBOARD_MESSAGE,
LOGIN_MESSAGE,
PASS_RESET_MESSAGE,
ERROR_OVERRIDE,
}

View File

@ -1,10 +1,11 @@
package io.github.wulkanowy.data.repositories package io.github.wulkanowy.data.repositories
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.api.AdminMessageService import io.github.wulkanowy.data.api.AdminMessageService
import io.github.wulkanowy.data.db.dao.AdminMessageDao import io.github.wulkanowy.data.db.dao.AdminMessageDao
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.networkBoundResource import io.github.wulkanowy.data.networkBoundResource
import io.github.wulkanowy.utils.AppInfo import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -13,34 +14,20 @@ import javax.inject.Singleton
class AdminMessageRepository @Inject constructor( class AdminMessageRepository @Inject constructor(
private val adminMessageService: AdminMessageService, private val adminMessageService: AdminMessageService,
private val adminMessageDao: AdminMessageDao, private val adminMessageDao: AdminMessageDao,
private val appInfo: AppInfo
) { ) {
private val saveFetchResultMutex = Mutex() private val saveFetchResultMutex = Mutex()
suspend fun getAdminMessages(student: Student) = networkBoundResource( fun getAdminMessages(): Flow<Resource<List<AdminMessage>>> =
mutex = saveFetchResultMutex, networkBoundResource(
isResultEmpty = { it == null }, mutex = saveFetchResultMutex,
query = { adminMessageDao.loadAll() }, isResultEmpty = { false },
fetch = { adminMessageService.getAdminMessages() }, query = { adminMessageDao.loadAll() },
shouldFetch = { true }, fetch = { adminMessageService.getAdminMessages() },
saveFetchResult = { oldItems, newItems -> shouldFetch = { true },
adminMessageDao.removeOldAndSaveNew(oldItems, newItems) saveFetchResult = { oldItems, newItems ->
}, adminMessageDao.removeOldAndSaveNew(oldItems, newItems)
showSavedOnLoading = false, },
mapResult = { adminMessages -> showSavedOnLoading = false,
adminMessages.filter { adminMessage -> )
val isCorrectRegister = adminMessage.targetRegisterHost?.let {
student.scrapperBaseUrl.contains(it, true)
} ?: true
val isCorrectFlavor =
adminMessage.targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
val isCorrectMaxVersion =
adminMessage.versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion =
adminMessage.versionMin?.let { it <= appInfo.versionCode } ?: true
isCorrectRegister && isCorrectFlavor && isCorrectMaxVersion && isCorrectMinVersion
}.maxByOrNull { it.id }
}
)
} }

View File

@ -0,0 +1,64 @@
package io.github.wulkanowy.domain.adminmessage
import io.github.wulkanowy.data.Resource
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.mapResourceData
import io.github.wulkanowy.data.repositories.AdminMessageRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.utils.AppInfo
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class GetAppropriateAdminMessageUseCase @Inject constructor(
private val adminMessageRepository: AdminMessageRepository,
private val preferencesRepository: PreferencesRepository,
private val appInfo: AppInfo
) {
operator fun invoke(student: Student, type: MessageType): Flow<Resource<AdminMessage?>> {
return invoke(student.scrapperBaseUrl, type)
}
operator fun invoke(scrapperBaseUrl: String, type: MessageType): Flow<Resource<AdminMessage?>> {
return adminMessageRepository.getAdminMessages().mapResourceData { adminMessages ->
adminMessages
.asSequence()
.filter { it.isNotDismissed() }
.filter { it.isVersionMatch() }
.filter { it.isRegisterHostMatch(scrapperBaseUrl) }
.filter { it.isFlavorMatch() }
.filter { it.isTypeMatch(type) }
.maxByOrNull { it.id }
}
}
private fun AdminMessage.isNotDismissed(): Boolean {
return id !in preferencesRepository.dismissedAdminMessageIds
}
private fun AdminMessage.isRegisterHostMatch(scrapperBaseUrl: String): Boolean {
return targetRegisterHost?.let {
scrapperBaseUrl.contains(it, true)
} ?: true
}
private fun AdminMessage.isFlavorMatch(): Boolean {
return targetFlavor?.equals(appInfo.buildFlavor, true) ?: true
}
private fun AdminMessage.isVersionMatch(): Boolean {
val isCorrectMaxVersion = versionMax?.let { it >= appInfo.versionCode } ?: true
val isCorrectMinVersion = versionMin?.let { it <= appInfo.versionCode } ?: true
return isCorrectMaxVersion && isCorrectMinVersion
}
private fun AdminMessage.isTypeMatch(messageType: MessageType): Boolean {
if (messageType in types) return true
if (MessageType.GENERAL_MESSAGE in types) return true
return false
}
}

View File

@ -3,6 +3,7 @@ package io.github.wulkanowy.ui.modules.dashboard
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter import io.github.wulkanowy.ui.modules.dashboard.adapters.DashboardAdapter
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import java.util.* import java.util.*
class DashboardItemMoveCallback( class DashboardItemMoveCallback(
@ -55,5 +56,5 @@ class DashboardItemMoveCallback(
} }
private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean private val RecyclerView.ViewHolder.isAdminMessageOrAccountItem: Boolean
get() = this is DashboardAdapter.AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder get() = this is AdminMessageViewHolder || this is DashboardAdapter.AccountViewHolder
} }

View File

@ -5,7 +5,9 @@ import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.db.entities.LuckyNumber import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Student import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.enums.MessageFolder import io.github.wulkanowy.data.enums.MessageFolder
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.repositories.* import io.github.wulkanowy.data.repositories.*
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.base.ErrorHandler import io.github.wulkanowy.ui.base.ErrorHandler
import io.github.wulkanowy.utils.AdsHelper import io.github.wulkanowy.utils.AdsHelper
@ -32,7 +34,7 @@ class DashboardPresenter @Inject constructor(
private val conferenceRepository: ConferenceRepository, private val conferenceRepository: ConferenceRepository,
private val preferencesRepository: PreferencesRepository, private val preferencesRepository: PreferencesRepository,
private val schoolAnnouncementRepository: SchoolAnnouncementRepository, private val schoolAnnouncementRepository: SchoolAnnouncementRepository,
private val adminMessageRepository: AdminMessageRepository, private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val adsHelper: AdsHelper private val adsHelper: AdsHelper
) : BasePresenter<DashboardView>(errorHandler, studentRepository) { ) : BasePresenter<DashboardView>(errorHandler, studentRepository) {
@ -159,19 +161,23 @@ class DashboardPresenter @Inject constructor(
DashboardItem.Type.ACCOUNT -> { DashboardItem.Type.ACCOUNT -> {
updateData(DashboardItem.Account(student), forceRefresh) updateData(DashboardItem.Account(student), forceRefresh)
} }
DashboardItem.Type.HORIZONTAL_GROUP -> { DashboardItem.Type.HORIZONTAL_GROUP -> {
loadHorizontalGroup(student, forceRefresh) loadHorizontalGroup(student, forceRefresh)
} }
DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh) DashboardItem.Type.LESSONS -> loadLessons(student, forceRefresh)
DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh) DashboardItem.Type.GRADES -> loadGrades(student, forceRefresh)
DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh) DashboardItem.Type.HOMEWORK -> loadHomework(student, forceRefresh)
DashboardItem.Type.ANNOUNCEMENTS -> { DashboardItem.Type.ANNOUNCEMENTS -> {
loadSchoolAnnouncements(student, forceRefresh) loadSchoolAnnouncements(student, forceRefresh)
} }
DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh) DashboardItem.Type.EXAMS -> loadExams(student, forceRefresh)
DashboardItem.Type.CONFERENCES -> { DashboardItem.Type.CONFERENCES -> {
loadConferences(student, forceRefresh) loadConferences(student, forceRefresh)
} }
DashboardItem.Type.ADS -> loadAds(forceRefresh) DashboardItem.Type.ADS -> loadAds(forceRefresh)
DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh) DashboardItem.Type.ADMIN_MESSAGE -> loadAdminMessage(student, forceRefresh)
} }
@ -355,6 +361,7 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.GRADES firstLoadedItemList += DashboardItem.Type.GRADES
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard grades result: Success") Timber.i("Loading dashboard grades result: Success")
updateData( updateData(
@ -365,6 +372,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard grades result: An exception occurred") Timber.i("Loading dashboard grades result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -402,12 +410,14 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.LESSONS firstLoadedItemList += DashboardItem.Type.LESSONS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard lessons result: Success") Timber.i("Loading dashboard lessons result: Success")
updateData( updateData(
DashboardItem.Lessons(it.data), forceRefresh DashboardItem.Lessons(it.data), forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard lessons result: An exception occurred") Timber.i("Loading dashboard lessons result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -457,10 +467,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.HOMEWORK firstLoadedItemList += DashboardItem.Type.HOMEWORK
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard homework result: Success") Timber.i("Loading dashboard homework result: Success")
updateData(DashboardItem.Homework(it.data), forceRefresh) updateData(DashboardItem.Homework(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard homework result: An exception occurred") Timber.i("Loading dashboard homework result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -489,10 +501,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS firstLoadedItemList += DashboardItem.Type.ANNOUNCEMENTS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard announcements result: Success") Timber.i("Loading dashboard announcements result: Success")
updateData(DashboardItem.Announcements(it.data), forceRefresh) updateData(DashboardItem.Announcements(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard announcements result: An exception occurred") Timber.i("Loading dashboard announcements result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -530,10 +544,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.EXAMS firstLoadedItemList += DashboardItem.Type.EXAMS
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard exams result: Success") Timber.i("Loading dashboard exams result: Success")
updateData(DashboardItem.Exams(it.data), forceRefresh) updateData(DashboardItem.Exams(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard exams result: An exception occurred") Timber.i("Loading dashboard exams result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -569,10 +585,12 @@ class DashboardPresenter @Inject constructor(
firstLoadedItemList += DashboardItem.Type.CONFERENCES firstLoadedItemList += DashboardItem.Type.CONFERENCES
} }
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard conferences result: Success") Timber.i("Loading dashboard conferences result: Success")
updateData(DashboardItem.Conferences(it.data), forceRefresh) updateData(DashboardItem.Conferences(it.data), forceRefresh)
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard conferences result: An exception occurred") Timber.i("Loading dashboard conferences result: An exception occurred")
errorHandler.dispatch(it.error) errorHandler.dispatch(it.error)
@ -584,12 +602,12 @@ class DashboardPresenter @Inject constructor(
} }
private fun loadAdminMessage(student: Student, forceRefresh: Boolean) { private fun loadAdminMessage(student: Student, forceRefresh: Boolean) {
flatResourceFlow { adminMessageRepository.getAdminMessages(student) } flatResourceFlow {
.filter { getAppropriateAdminMessageUseCase(
val data = it.dataOrNull ?: return@filter true student = student,
val isDismissed = data.id in preferencesRepository.dismissedAdminMessageIds type = MessageType.DASHBOARD_MESSAGE,
!isDismissed )
} }
.onEach { .onEach {
when (it) { when (it) {
is Resource.Loading -> { is Resource.Loading -> {
@ -597,6 +615,7 @@ class DashboardPresenter @Inject constructor(
if (forceRefresh) return@onEach if (forceRefresh) return@onEach
updateData(DashboardItem.AdminMessages(), forceRefresh) updateData(DashboardItem.AdminMessages(), forceRefresh)
} }
is Resource.Success -> { is Resource.Success -> {
Timber.i("Loading dashboard admin message result: Success") Timber.i("Loading dashboard admin message result: Success")
updateData( updateData(
@ -604,6 +623,7 @@ class DashboardPresenter @Inject constructor(
forceRefresh = forceRefresh forceRefresh = forceRefresh
) )
} }
is Resource.Error -> { is Resource.Error -> {
Timber.i("Loading dashboard admin message result: An exception occurred") Timber.i("Loading dashboard admin message result: An exception occurred")
Timber.e(it.error) Timber.e(it.error)

View File

@ -1,8 +1,6 @@
package io.github.wulkanowy.ui.modules.dashboard.adapters package io.github.wulkanowy.ui.modules.dashboard.adapters
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
@ -24,6 +22,7 @@ import io.github.wulkanowy.data.db.entities.TimetableHeader
import io.github.wulkanowy.data.enums.GradeColorTheme import io.github.wulkanowy.data.enums.GradeColorTheme
import io.github.wulkanowy.databinding.* import io.github.wulkanowy.databinding.*
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
import timber.log.Timber import timber.log.Timber
import java.time.Duration import java.time.Duration
@ -109,7 +108,9 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
ItemDashboardConferencesBinding.inflate(inflater, parent, false) ItemDashboardConferencesBinding.inflate(inflater, parent, false)
) )
DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder( DashboardItem.Type.ADMIN_MESSAGE.ordinal -> AdminMessageViewHolder(
ItemDashboardAdminMessageBinding.inflate(inflater, parent, false) ItemDashboardAdminMessageBinding.inflate(inflater, parent, false),
onAdminMessageDismissClickListener = onAdminMessageDismissClickListener,
onAdminMessageClickListener = onAdminMessageClickListener,
) )
DashboardItem.Type.ADS.ordinal -> AdsViewHolder( DashboardItem.Type.ADS.ordinal -> AdsViewHolder(
ItemDashboardAdsBinding.inflate(inflater, parent, false) ItemDashboardAdsBinding.inflate(inflater, parent, false)
@ -128,7 +129,7 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position) is AnnouncementsViewHolder -> bindAnnouncementsViewHolder(holder, position)
is ExamsViewHolder -> bindExamsViewHolder(holder, position) is ExamsViewHolder -> bindExamsViewHolder(holder, position)
is ConferencesViewHolder -> bindConferencesViewHolder(holder, position) is ConferencesViewHolder -> bindConferencesViewHolder(holder, position)
is AdminMessageViewHolder -> bindAdminMessage(holder, position) is AdminMessageViewHolder -> holder.bind((items[position] as DashboardItem.AdminMessages).adminMessage)
is AdsViewHolder -> bindAdsViewHolder(holder, position) is AdsViewHolder -> bindAdsViewHolder(holder, position)
} }
} }
@ -733,39 +734,6 @@ 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.colorMessageHigh) to
context.getThemeAttrColor(R.attr.colorOnMessageHigh)
}
"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)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setTextColor(textColor)
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) { private fun bindAdsViewHolder(adsViewHolder: AdsViewHolder, position: Int) {
val item = (items[position] as DashboardItem.Ads).adBanner ?: return val item = (items[position] as DashboardItem.Ads).adBanner ?: return
val binding = adsViewHolder.binding val binding = adsViewHolder.binding
@ -819,9 +787,6 @@ class DashboardAdapter @Inject constructor() : RecyclerView.Adapter<RecyclerView
val adapter by lazy { DashboardConferencesAdapter() } val adapter by lazy { DashboardConferencesAdapter() }
} }
class AdminMessageViewHolder(val binding: ItemDashboardAdminMessageBinding) :
RecyclerView.ViewHolder(binding.root)
class AdsViewHolder(val binding: ItemDashboardAdsBinding) : class AdsViewHolder(val binding: ItemDashboardAdsBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)

View File

@ -0,0 +1,52 @@
package io.github.wulkanowy.ui.modules.dashboard.viewholders
import android.content.res.ColorStateList
import android.graphics.Color
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.databinding.ItemDashboardAdminMessageBinding
import io.github.wulkanowy.ui.modules.dashboard.DashboardItem
import io.github.wulkanowy.utils.getThemeAttrColor
class AdminMessageViewHolder(
private val binding: ItemDashboardAdminMessageBinding,
private val onAdminMessageDismissClickListener: (AdminMessage) -> Unit,
private val onAdminMessageClickListener: (String?) -> Unit,
) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: AdminMessage?) {
item ?: return
val context = binding.root.context
val (backgroundColor, textColor) = when (item.priority) {
"HIGH" -> {
context.getThemeAttrColor(R.attr.colorMessageHigh) to
context.getThemeAttrColor(R.attr.colorOnMessageHigh)
}
"MEDIUM" -> {
context.getThemeAttrColor(R.attr.colorMessageMedium) to Color.BLACK
}
else -> null to context.getThemeAttrColor(R.attr.colorOnSurface)
}
with(binding) {
dashboardAdminMessageItemTitle.text = item.title
dashboardAdminMessageItemTitle.setTextColor(textColor)
dashboardAdminMessageItemDescription.text = item.content
dashboardAdminMessageItemDescription.setTextColor(textColor)
dashboardAdminMessageItemIcon.setColorFilter(textColor)
dashboardAdminMessageItemDismiss.isVisible = item.isDismissible
dashboardAdminMessageItemDismiss.setTextColor(textColor)
dashboardAdminMessageItemDismiss.setOnClickListener {
onAdminMessageDismissClickListener(item)
}
root.setCardBackgroundColor(backgroundColor?.let { ColorStateList.valueOf(it) })
item.destinationUrl?.let { url ->
root.setOnClickListener { onAdminMessageClickListener(url) }
}
}
}
}

View File

@ -9,10 +9,12 @@ import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import io.github.wulkanowy.R import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.databinding.FragmentLoginFormBinding import io.github.wulkanowy.databinding.FragmentLoginFormBinding
import io.github.wulkanowy.ui.base.BaseFragment import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.dashboard.viewholders.AdminMessageViewHolder
import io.github.wulkanowy.ui.modules.login.LoginActivity import io.github.wulkanowy.ui.modules.login.LoginActivity
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.utils.* import io.github.wulkanowy.utils.*
@ -207,6 +209,19 @@ class LoginFormFragment : BaseFragment<FragmentLoginFormBinding>(R.layout.fragme
binding.loginFormContainer.visibility = if (show) VISIBLE else GONE binding.loginFormContainer.visibility = if (show) VISIBLE else GONE
} }
override fun showAdminMessage(message: AdminMessage?) {
AdminMessageViewHolder(
binding = binding.loginFormMessage,
onAdminMessageDismissClickListener = presenter::onAdminMessageDismissed,
onAdminMessageClickListener = presenter::onAdminMessageSelected,
).bind(message)
binding.loginFormMessage.root.isVisible = message != null
}
override fun openInternetBrowser(url: String) {
requireContext().openInternetBrowser(url)
}
override fun showDomainSuffixInput(show: Boolean) { override fun showDomainSuffixInput(show: Boolean) {
binding.loginFormDomainSuffixLayout.isVisible = show binding.loginFormDomainSuffixLayout.isVisible = show
} }

View File

@ -1,13 +1,19 @@
package io.github.wulkanowy.ui.modules.login.form package io.github.wulkanowy.ui.modules.login.form
import androidx.core.net.toUri import androidx.core.net.toUri
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.enums.MessageType
import io.github.wulkanowy.data.flatResourceFlow
import io.github.wulkanowy.data.logResourceStatus import io.github.wulkanowy.data.logResourceStatus
import io.github.wulkanowy.data.onResourceData
import io.github.wulkanowy.data.onResourceError import io.github.wulkanowy.data.onResourceError
import io.github.wulkanowy.data.onResourceLoading import io.github.wulkanowy.data.onResourceLoading
import io.github.wulkanowy.data.onResourceNotLoading import io.github.wulkanowy.data.onResourceNotLoading
import io.github.wulkanowy.data.onResourceSuccess import io.github.wulkanowy.data.onResourceSuccess
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.resourceFlow import io.github.wulkanowy.data.resourceFlow
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.ui.base.BasePresenter import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -22,7 +28,9 @@ class LoginFormPresenter @Inject constructor(
studentRepository: StudentRepository, studentRepository: StudentRepository,
private val loginErrorHandler: LoginErrorHandler, private val loginErrorHandler: LoginErrorHandler,
private val appInfo: AppInfo, private val appInfo: AppInfo,
private val analytics: AnalyticsHelper private val analytics: AnalyticsHelper,
private val getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase,
private val preferencesRepository: PreferencesRepository,
) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) { ) : BasePresenter<LoginFormView>(loginErrorHandler, studentRepository) {
private var lastError: Throwable? = null private var lastError: Throwable? = null
@ -41,6 +49,31 @@ class LoginFormPresenter @Inject constructor(
Timber.i("Entered wrong username or password") Timber.i("Entered wrong username or password")
} }
} }
reloadAdminMessage()
}
private fun reloadAdminMessage() {
flatResourceFlow {
getAppropriateAdminMessageUseCase(
scrapperBaseUrl = view?.formHostValue.orEmpty(),
type = MessageType.LOGIN_MESSAGE,
)
}
.logResourceStatus("load login admin message")
.onResourceData { view?.showAdminMessage(it) }
.onResourceError { view?.showAdminMessage(null) }
.launch()
}
fun onAdminMessageSelected(url: String?) {
url?.let { view?.openInternetBrowser(it) }
}
fun onAdminMessageDismissed(adminMessage: AdminMessage) {
preferencesRepository.dismissedAdminMessageIds += adminMessage.id
view?.showAdminMessage(null)
} }
fun onPrivacyLinkClick() { fun onPrivacyLinkClick() {
@ -63,6 +96,7 @@ class LoginFormPresenter @Inject constructor(
} }
updateCustomDomainSuffixVisibility() updateCustomDomainSuffixVisibility()
updateUsernameLabel() updateUsernameLabel()
reloadAdminMessage()
} }
} }
@ -103,7 +137,9 @@ class LoginFormPresenter @Inject constructor(
val email = view?.formUsernameValue.orEmpty().trim() val email = view?.formUsernameValue.orEmpty().trim()
val password = view?.formPassValue.orEmpty().trim() val password = view?.formPassValue.orEmpty().trim()
val host = view?.formHostValue.orEmpty().trim() val host = view?.formHostValue.orEmpty().trim()
val domainSuffix = view?.formDomainSuffix.orEmpty().trim() val domainSuffix = view?.formDomainSuffix.orEmpty().trim().takeIf {
"customSuffix" in host
}.orEmpty()
val symbol = view?.formHostSymbol.orEmpty().trim() val symbol = view?.formHostSymbol.orEmpty().trim()
if (!validateCredentials(email, password, host)) return if (!validateCredentials(email, password, host)) return

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.ui.modules.login.form package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.data.db.entities.AdminMessage
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.ui.base.BaseView import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.login.LoginData import io.github.wulkanowy.ui.modules.login.LoginData
@ -58,6 +59,10 @@ interface LoginFormView : BaseView {
fun showContent(show: Boolean) fun showContent(show: Boolean)
fun showAdminMessage(message: AdminMessage?)
fun openInternetBrowser(url: String)
fun showDomainSuffixInput(show: Boolean) fun showDomainSuffixInput(show: Boolean)
fun showOtherOptionsButton(show: Boolean) fun showOtherOptionsButton(show: Boolean)

View File

@ -105,6 +105,18 @@
android:background="?android:attr/listDivider" /> android:background="?android:attr/listDivider" />
</LinearLayout> </LinearLayout>
<include
android:id="@+id/login_form_message"
layout="@layout/item_dashboard_admin_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/loginFormContact"
app:layout_constraintVertical_chainStyle="packed"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/loginFormHeader" android:id="@+id/loginFormHeader"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -119,9 +131,8 @@
app:layout_constraintBottom_toTopOf="@+id/loginFormUsernameLayout" app:layout_constraintBottom_toTopOf="@+id/loginFormUsernameLayout"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loginFormContact" app:layout_constraintTop_toBottomOf="@+id/login_form_message"
app:layout_constraintVertical_bias="0" app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginTop="64dp" /> app:layout_goneMarginTop="64dp" />
<TextView <TextView

View File

@ -2,7 +2,9 @@ package io.github.wulkanowy.ui.modules.login.form
import io.github.wulkanowy.MainCoroutineRule import io.github.wulkanowy.MainCoroutineRule
import io.github.wulkanowy.data.pojos.RegisterUser import io.github.wulkanowy.data.pojos.RegisterUser
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.StudentRepository import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.domain.adminmessage.GetAppropriateAdminMessageUseCase
import io.github.wulkanowy.sdk.Sdk import io.github.wulkanowy.sdk.Sdk
import io.github.wulkanowy.sdk.scrapper.Scrapper import io.github.wulkanowy.sdk.scrapper.Scrapper
import io.github.wulkanowy.ui.modules.login.LoginErrorHandler import io.github.wulkanowy.ui.modules.login.LoginErrorHandler
@ -40,6 +42,12 @@ class LoginFormPresenterTest {
@MockK @MockK
lateinit var appInfo: AppInfo lateinit var appInfo: AppInfo
@MockK
lateinit var getAppropriateAdminMessageUseCase: GetAppropriateAdminMessageUseCase
@MockK
lateinit var preferencesRepository: PreferencesRepository
private lateinit var presenter: LoginFormPresenter private lateinit var presenter: LoginFormPresenter
private val registerUser = RegisterUser( private val registerUser = RegisterUser(
@ -72,6 +80,8 @@ class LoginFormPresenterTest {
loginErrorHandler = errorHandler, loginErrorHandler = errorHandler,
appInfo = appInfo, appInfo = appInfo,
analytics = analytics, analytics = analytics,
getAppropriateAdminMessageUseCase = getAppropriateAdminMessageUseCase,
preferencesRepository = preferencesRepository,
) )
presenter.onAttachView(loginFormView) presenter.onAttachView(loginFormView)
} }