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

Add messages (#148)

This commit is contained in:
Mikołaj Pich 2018-12-06 18:35:02 +01:00 committed by Rafał Borcz
parent 48f96b5932
commit 92baecbd0d
46 changed files with 1356 additions and 41 deletions

View File

@ -1,4 +1,4 @@
image: circleci/android:api-27-alpha
image: circleci/android:api-28-alpha
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle

View File

@ -52,6 +52,7 @@ script:
- ./gradlew createDebugCoverageReport --stacktrace -PdisableCrashlytics --daemon
- ./gradlew jacocoTestReport --stacktrace --daemon
- if [ "$TRAVIS_PULL_REQUEST" != "false" ] || [ "$TRAVIS_BRANCH" == "master" ]; then
git fetch --unshallow;
./gradlew sonarqube -x test -x lint -x fabricGenerateResourcesRelease -Dsonar.host.url=$SONAR_HOST -Dsonar.organization=$SONAR_ORG -Dsonar.login=$SONAR_KEY -Dsonar.branch.name=${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} -PdisableCrashlytics --stacktrace --daemon;
fi
- |

View File

@ -1,12 +1,13 @@
# Wulkanowy
[![CircleCI](https://img.shields.io/circleci/project/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://circleci.com/gh/wulkanowy/wulkanowy)
[![Travis](https://img.shields.io/travis/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://travis-ci.com/wulkanowy/wulkanowy)
[![Travis](https://img.shields.io/travis/com/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://travis-ci.com/wulkanowy/wulkanowy)
[![Bitrise](https://img.shields.io/bitrise/daeff1893f3c8128/master.svg?token=Hjm1ACamk86JDeVVJHOeqQ&style=flat-square)](https://www.bitrise.io/app/daeff1893f3c8128)
[![Codecov](https://img.shields.io/codecov/c/github/wulkanowy/wulkanowy/master.svg?style=flat-square)](https://codecov.io/gh/wulkanowy/wulkanowy)
[![BCH compliance](https://bettercodehub.com/edge/badge/wulkanowy/wulkanowy?branch=master)](https://bettercodehub.com/)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[![Sonarcloud](https://sonarcloud.io/api/project_badges/measure?project=io.github.wulkanowy%3Aapp&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=io.github.wulkanowy%3Aapp)
[![FOSSA Status](https://app.fossa.io/api/projects/custom%2B5644%2Fgit%40github.com%3Awulkanowy%2Fwulkanowy.git.svg?type=shield)](https://app.fossa.io/projects/custom%2B5644%2Fgit%40github.com%3Awulkanowy%2Fwulkanowy.git?ref=badge_shield)
[![Discord](https://img.shields.io/discord/390889354199040011.svg?style=flat-square)](https://discord.gg/vccAQBr)
[Pobierz wersję beta z Google Play](https://play.google.com/store/apps/details?id=io.github.wulkanowy&utm_source=vcs)

View File

@ -51,7 +51,7 @@ android {
signingConfig signingConfigs.release
}
debug {
buildConfigField "boolean", "FABRIC_ENABLED", fabricApiKey == "null" ? "false" : "true"
buildConfigField "boolean", "FABRIC_ENABLED", fabricApiKey != "null" && !project.hasProperty("disableCrashlytics") ? "true" : "false"
applicationIdSuffix ".dev"
versionNameSuffix "-dev"
testCoverageEnabled = true
@ -81,7 +81,7 @@ configurations.all {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation('com.github.wulkanowy:api:0ac961607b') { exclude module: "threetenbp" }
implementation('com.github.wulkanowy:api:ba17abc') { exclude module: "threetenbp" }
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation "androidx.appcompat:appcompat:1.0.2"

View File

@ -45,7 +45,7 @@ class WulkanowyApp : DaggerApplication() {
private fun initializeFabric() {
Fabric.with(Fabric.Builder(this).kits(
Crashlytics.Builder().core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG || !BuildConfig.FABRIC_ENABLED).build()).build(),
Crashlytics.Builder().core(CrashlyticsCore.Builder().disabled(!BuildConfig.FABRIC_ENABLED).build()).build(),
Answers()
).debuggable(BuildConfig.DEBUG).build())
Timber.plant(CrashlyticsTree())

View File

@ -65,6 +65,10 @@ internal class RepositoryModule {
@Provides
fun provideGradeSummaryDao(database: AppDatabase) = database.gradeSummaryDao
@Singleton
@Provides
fun provideMessagesDao(database: AppDatabase) = database.messagesDao
@Singleton
@Provides
fun provideExamDao(database: AppDatabase) = database.examsDao

View File

@ -10,6 +10,7 @@ import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.SemesterDao
@ -19,6 +20,7 @@ import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
@ -36,6 +38,7 @@ import javax.inject.Singleton
Attendance::class,
Grade::class,
GradeSummary::class,
Message::class,
Note::class,
Homework::class
],
@ -67,6 +70,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract val gradeSummaryDao: GradeSummaryDao
abstract val messagesDao: MessagesDao
abstract val noteDao: NoteDao
abstract val homeworkDao: HomeworkDao

View File

@ -0,0 +1,37 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Message
import io.reactivex.Maybe
@Dao
interface MessagesDao {
@Insert
fun insertAll(messages: List<Message>): List<Long>
@Delete
fun deleteAll(messages: List<Message>)
@Update
fun update(message: Message)
@Update
fun updateAll(messages: List<Message>)
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND real_id = :id")
fun loadOne(studentId: Int, id: Int): Maybe<Message>
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND folder_id = :folder ORDER BY date DESC")
fun load(studentId: Int, folder: Int): Maybe<List<Message>>
@Query("SELECT * FROM Messages WHERE student_id = :studentId AND removed = 1 ORDER BY date DESC")
fun loadDeleted(studentId: Int): Maybe<List<Message>>
@Query("SELECT * FROM Messages WHERE unread = 1 AND student_id = :studentId")
fun loadNewMessages(studentId: Int): Maybe<List<Message>>
}

View File

@ -0,0 +1,56 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDateTime
import java.io.Serializable
@Entity(tableName = "Messages")
data class Message(
@ColumnInfo(name = "student_id")
var studentId: Int? = null,
@ColumnInfo(name = "real_id")
val realId: Int? = null,
@ColumnInfo(name = "message_id")
val messageId: Int? = null,
@ColumnInfo(name = "sender_name")
val sender: String? = null,
@ColumnInfo(name = "sender_id")
val senderId: Int? = null,
@ColumnInfo(name = "recipient_id")
val recipientId: Int? = null,
@ColumnInfo(name = "recipient_name")
val recipient: String? = "",
val subject: String = "",
val date: LocalDateTime? = null,
@ColumnInfo(name = "folder_id")
val folderId: Int = 0,
var unread: Boolean? = false,
val unreadBy: Int? = 0,
val readBy: Int? = 0,
val removed: Boolean = false
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
var content: String? = null
}

View File

@ -16,30 +16,30 @@ import javax.inject.Singleton
@Singleton
class ExamRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: ExamLocal,
private val remote: ExamRemote
private val settings: InternetObservingSettings,
private val local: ExamLocal,
private val remote: ExamRemote
) {
fun getExams(semester: Semester, startDate: LocalDate, endDate: LocalDate, forceRefresh: Boolean = false): Single<List<Exam>> {
return Single.fromCallable { startDate.monday to endDate.friday }
.flatMap { dates ->
local.getExams(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getExams(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newExams ->
local.getExams(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldExams ->
local.deleteExams(oldExams - newExams)
local.saveExams(newExams - oldExams)
}
}.flatMap {
local.getExams(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
.flatMap { dates ->
local.getExams(semester, dates.first, dates.second).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getExams(semester, dates.first, dates.second)
else Single.error(UnknownHostException())
}.flatMap { newExams ->
local.getExams(semester, dates.first, dates.second)
.toSingle(emptyList())
.doOnSuccess { oldExams ->
local.deleteExams(oldExams - newExams)
local.saveExams(newExams - oldExams)
}
}.flatMap {
local.getExams(semester, dates.first, dates.second)
.toSingle(emptyList())
}).map { list -> list.filter { it.date in startDate..endDate } }
}
}
}

View File

@ -0,0 +1,79 @@
package io.github.wulkanowy.data.repositories
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.local.MessagesLocal
import io.github.wulkanowy.data.repositories.remote.MessagesRemote
import io.reactivex.Completable
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MessagesRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: MessagesLocal,
private val remote: MessagesRemote
) {
enum class MessageFolder(val id: Int = 1) {
RECEIVED(1),
SENT(2),
TRASHED(3)
}
fun getMessages(studentId: Int, folder: MessageFolder, forceRefresh: Boolean = false, notify: Boolean = false): Single<List<Message>> {
return local.getMessages(studentId, folder).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getMessages(studentId, folder)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getMessages(studentId, folder).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteMessages(old - new)
local.saveMessages((new - old)
.onEach {
it.isNotified = !notify
})
}
}.flatMap { local.getMessages(studentId, folder).toSingle(emptyList()) }
)
}
fun getMessage(studentId: Int, messageId: Int, markAsRead: Boolean = false): Single<Message> {
return local.getMessage(studentId, messageId)
.filter { !it.content.isNullOrEmpty() }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) local.getMessage(studentId, messageId).toSingle()
else Single.error(UnknownHostException())
}
.flatMap { dbMessage ->
remote.getMessagesContent(dbMessage, markAsRead).doOnSuccess {
local.updateMessage(dbMessage.copy(unread = false).apply {
id = dbMessage.id
content = it
})
}
}.flatMap {
local.getMessage(studentId, messageId).toSingle()
}
)
}
fun getNewMessages(student: Student): Single<List<Message>> {
return local.getNewMessages(student).toSingle(emptyList())
}
fun updateMessage(message: Message): Completable {
return Completable.fromCallable { local.updateMessage(message) }
}
fun updateMessages(messages: List<Message>): Completable {
return Completable.fromCallable { local.updateMessages(messages) }
}
}

View File

@ -27,8 +27,8 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) {
return Completable.fromCallable { gradeDb.update(grade) }
}
fun updateGrades(grade: List<Grade>): Completable {
return Completable.fromCallable { gradeDb.updateAll(grade) }
fun updateGrades(grades: List<Grade>): Completable {
return Completable.fromCallable { gradeDb.updateAll(grades) }
}
fun deleteGrades(grades: List<Grade>) {

View File

@ -0,0 +1,44 @@
package io.github.wulkanowy.data.repositories.local
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.MessagesRepository
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MessagesLocal @Inject constructor(private val messagesDb: MessagesDao) {
fun getMessage(studentId: Int, id: Int): Maybe<Message> {
return messagesDb.loadOne(studentId, id)
}
fun getMessages(studentId: Int, folder: MessagesRepository.MessageFolder): Maybe<List<Message>> {
return when (folder) {
MessagesRepository.MessageFolder.TRASHED -> messagesDb.loadDeleted(studentId)
else -> messagesDb.load(studentId, folder.id)
}.filter { !it.isEmpty() }
}
fun getNewMessages(student: Student): Maybe<List<Message>> {
return messagesDb.loadNewMessages(student.studentId)
}
fun saveMessages(messages: List<Message>): List<Long> {
return messagesDb.insertAll(messages)
}
fun updateMessage(message: Message) {
return messagesDb.update(message)
}
fun updateMessages(messages: List<Message>) {
return messagesDb.updateAll(messages)
}
fun deleteMessages(messages: List<Message>) {
messagesDb.deleteAll(messages)
}
}

View File

@ -0,0 +1,40 @@
package io.github.wulkanowy.data.repositories.remote
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.api.messages.Folder
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.MessagesRepository
import io.github.wulkanowy.utils.toLocalDateTime
import io.reactivex.Single
import javax.inject.Inject
import io.github.wulkanowy.api.messages.Message as ApiMessage
class MessagesRemote @Inject constructor(private val api: Api) {
fun getMessages(studentId: Int, folder: MessagesRepository.MessageFolder): Single<List<Message>> {
return api.getMessages(Folder.valueOf(folder.name)).map { messages ->
messages.map {
Message(
studentId = studentId,
realId = it.id,
messageId = it.messageId,
sender = it.sender,
senderId = it.senderId,
recipient = it.recipient,
recipientId = it.recipientId,
subject = it.subject.trim(),
date = it.date?.toLocalDateTime(),
folderId = it.folderId,
unread = it.unread,
unreadBy = it.unreadBy,
readBy = it.readBy,
removed = it.removed
)
}
}
}
fun getMessagesContent(message: Message, markAsRead: Boolean = false): Single<String> {
return api.getMessageContent(message.messageId ?: 0, message.folderId, markAsRead, message.realId ?: 0)
}
}

View File

@ -8,12 +8,15 @@ import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.HomeworkRepository
import io.github.wulkanowy.data.repositories.MessagesRepository
import io.github.wulkanowy.data.repositories.MessagesRepository.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.NoteRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SemesterRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.services.notification.GradeNotification
import io.github.wulkanowy.services.notification.MessageNotification
import io.github.wulkanowy.services.notification.NoteNotification
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.isHolidays
@ -47,6 +50,9 @@ class SyncWorker : SimpleJobService() {
@Inject
lateinit var timetable: TimetableRepository
@Inject
lateinit var message: MessagesRepository
@Inject
lateinit var note: NoteRepository
@ -87,6 +93,7 @@ class SyncWorker : SimpleJobService() {
attendance.getAttendance(it, start, end, true),
exam.getExams(it, start, end, true),
timetable.getTimetable(it, start, end, true),
message.getMessages(it.studentId, RECEIVED, true, true),
note.getNotes(it, true, true),
homework.getHomework(it, LocalDate.now(), true),
homework.getHomework(it, LocalDate.now().plusDays(1), true)
@ -107,35 +114,57 @@ class SyncWorker : SimpleJobService() {
private fun sendNotifications() {
sendGradeNotifications()
sendMessageNotification()
sendNoteNotification()
}
private fun sendGradeNotifications() {
disposable.add(student.getCurrentStudent()
.flatMap { semester.getCurrentSemester(it, true) }
.flatMap { semester.getCurrentSemester(it) }
.flatMap { gradesDetails.getNewGrades(it) }
.map { it.filter { grade -> !grade.isNotified } }
.subscribe({
.doOnSuccess {
if (it.isNotEmpty()) {
Timber.d("Found ${it.size} unread grades")
GradeNotification(applicationContext).sendNotification(it)
gradesDetails.updateGrades(it.map { grade -> grade.apply { isNotified = true } }).subscribe()
}
}) { Timber.e("Notifications sending failed") })
}
.map { it.map { grade -> grade.apply { isNotified = true } } }
.flatMapCompletable { gradesDetails.updateGrades(it) }
.subscribe({}, { Timber.e(it, "Grade notifications sending failed") }))
}
private fun sendMessageNotification() {
disposable.add(student.getCurrentStudent()
.flatMap { message.getNewMessages(it) }
.map { it.filter { message -> !message.isNotified } }
.doOnSuccess{
if (it.isNotEmpty()) {
Timber.d("Found ${it.size} unread messages")
MessageNotification(applicationContext).sendNotification(it)
}
}
.map { it.map { message -> message.apply { isNotified = true } } }
.flatMapCompletable { message.updateMessages(it) }
.subscribe({}, { Timber.e(it, "Message notifications sending failed") })
)
}
private fun sendNoteNotification() {
disposable.add(student.getCurrentStudent()
.flatMap { semester.getCurrentSemester(it, true) }
.flatMap { semester.getCurrentSemester(it) }
.flatMap { note.getNewNotes(it) }
.map { it.filter { note -> !note.isNotified } }
.subscribe({
.doOnSuccess {
if (it.isNotEmpty()) {
Timber.d("Found ${it.size} unread notes")
NoteNotification(applicationContext).sendNotification(it)
note.updateNotes(it.map { note -> note.apply { isNotified = true } }).subscribe()
}
}) { Timber.e("Notifications sending failed") })
}
.map { it.map { note -> note.apply { isNotified = true } } }
.flatMapCompletable { note.updateNotes(it) }
.subscribe({}, { Timber.e("Notifications sending failed") })
)
}
override fun onDestroy() {

View File

@ -0,0 +1,58 @@
package io.github.wulkanowy.services.notification
import android.annotation.TargetApi
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.ui.modules.main.MainActivity
import timber.log.Timber
class MessageNotification(context: Context) : BaseNotification(context) {
private val channelId = "Message_Notify"
@TargetApi(26)
override fun createChannel(channelId: String) {
notificationManager.createNotificationChannel(NotificationChannel(
channelId, context.getString(R.string.notify_message_channel), NotificationManager.IMPORTANCE_HIGH
).apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
fun sendNotification(items: List<Message>) {
notify(notificationBuilder(channelId)
.setContentTitle(context.resources.getQuantityString(R.plurals.message_new_items, items.size, items.size))
.setContentText(context.resources.getQuantityString(R.plurals.notify_message_new_items, items.size, items.size))
.setSmallIcon(R.drawable.ic_stat_notify_message)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(context, 0,
MainActivity.getStartIntent(context).putExtra(MainActivity.EXTRA_START_MENU_INDEX, 4),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.message_number_item, items.size, items.size))
items.forEach {
addLine("${it.sender}: ${it.subject}")
}
this
})
.build()
)
Timber.d("Notification sent")
}
}

View File

@ -22,7 +22,7 @@ abstract class GradeModule {
}
@PerChildFragment
@ContributesAndroidInjector()
@ContributesAndroidInjector
abstract fun bindGradeDetailsFragment(): GradeDetailsFragment
@PerChildFragment

View File

@ -15,6 +15,9 @@ import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeModule
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.MessageModule
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
@ -46,6 +49,14 @@ abstract class MainModule {
@ContributesAndroidInjector(modules = [GradeModule::class])
abstract fun bindGradeFragment(): GradeFragment
@PerFragment
@ContributesAndroidInjector(modules = [MessageModule::class])
abstract fun bindMessagesFragment(): MessageFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindMessagePreviewFragment(): MessagePreviewFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindMoreFragment(): MoreFragment

View File

@ -0,0 +1,83 @@
package io.github.wulkanowy.ui.modules.message
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.MessagesRepository.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.MessagesRepository.MessageFolder.SENT
import io.github.wulkanowy.data.repositories.MessagesRepository.MessageFolder.TRASHED
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BasePagerAdapter
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.tab.MessageTabFragment
import io.github.wulkanowy.utils.setOnSelectPageListener
import kotlinx.android.synthetic.main.fragment_message.*
import javax.inject.Inject
class MessageFragment : BaseFragment(), MessageView, MainView.TitledView {
@Inject
lateinit var presenter: MessagePresenter
@Inject
lateinit var pagerAdapter: BasePagerAdapter
companion object {
fun newInstance() = MessageFragment()
}
override val titleStringId: Int
get() = R.string.message_title
override val currentPageIndex: Int
get() = messageViewPager.currentItem
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_message, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
pagerAdapter.fragments.putAll(mapOf(
getString(R.string.message_inbox) to MessageTabFragment.newInstance(RECEIVED),
getString(R.string.message_sent) to MessageTabFragment.newInstance(SENT),
getString(R.string.message_trash) to MessageTabFragment.newInstance(TRASHED)
))
messageViewPager.run {
adapter = pagerAdapter
offscreenPageLimit = 2
setOnSelectPageListener { presenter.onPageSelected(it) }
}
messageTabLayout.setupWithViewPager(messageViewPager)
}
override fun showContent(show: Boolean) {
messageViewPager.visibility = if (show) VISIBLE else INVISIBLE
messageTabLayout.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showProgress(show: Boolean) {
messageProgress.visibility = if (show) VISIBLE else INVISIBLE
}
fun onChildFragmentLoaded() {
presenter.onChildViewLoaded()
}
override fun notifyChildLoadData(index: Int, forceRefresh: Boolean) {
(childFragmentManager.fragments[index] as MessageView.MessageChildView).onParentLoadData(forceRefresh)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,67 @@
package io.github.wulkanowy.ui.modules.message
import android.graphics.Typeface.BOLD
import android.graphics.Typeface.NORMAL
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_message.*
class MessageItem(val message: Message, private val noSubjectString: String) :
AbstractFlexibleItem<MessageItem.ViewHolder>() {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): ViewHolder {
return ViewHolder(view, adapter)
}
override fun getLayoutRes(): Int = R.layout.item_message
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>>, holder: ViewHolder,
position: Int, payloads: MutableList<Any>?
) {
holder.apply {
val style = if (message.unread == true) BOLD else NORMAL
messageItemAuthor.run {
text = if (message.recipient?.isNotBlank() == true) message.recipient else message.sender
setTypeface(null, style)
}
messageItemSubject.run {
text = if (message.subject.isNotBlank()) message.subject else noSubjectString
setTypeface(null, style)
}
messageItemDate.run {
text = message.date?.toFormattedString()
setTypeface(null, style)
}
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MessageItem
if (message != other.message) return false
return true
}
override fun hashCode(): Int {
return message.hashCode()
}
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter),
LayoutContainer {
override val containerView: View
get() = contentView
}
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.ui.modules.message
import dagger.Module
import dagger.Provides
import dagger.android.ContributesAndroidInjector
import io.github.wulkanowy.di.scopes.PerChildFragment
import io.github.wulkanowy.di.scopes.PerFragment
import io.github.wulkanowy.ui.base.BasePagerAdapter
import io.github.wulkanowy.ui.modules.message.tab.MessageTabFragment
@Module
abstract class MessageModule {
@Module
companion object {
@JvmStatic
@PerFragment
@Provides
fun provideGradePagerAdapter(fragment: MessageFragment) = BasePagerAdapter(fragment.childFragmentManager)
}
@PerChildFragment
@ContributesAndroidInjector
abstract fun bindMessageTabFragment(): MessageTabFragment
}

View File

@ -0,0 +1,42 @@
package io.github.wulkanowy.ui.modules.message
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider
import io.reactivex.Completable
import java.util.concurrent.TimeUnit.MILLISECONDS
import javax.inject.Inject
class MessagePresenter @Inject constructor(
errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider
) : BasePresenter<MessageView>(errorHandler) {
override fun onAttachView(view: MessageView) {
super.onAttachView(view)
disposable.add(Completable.timer(150, MILLISECONDS, schedulers.mainThread)
.subscribe {
view.initView()
loadData()
})
}
fun onPageSelected(index: Int) {
loadChild(index)
}
private fun loadData() {
view?.run { loadChild(currentPageIndex) }
}
private fun loadChild(index: Int, forceRefresh: Boolean = false) {
view?.notifyChildLoadData(index, forceRefresh)
}
fun onChildViewLoaded() {
view?.apply {
showContent(true)
showProgress(false)
}
}
}

View File

@ -0,0 +1,21 @@
package io.github.wulkanowy.ui.modules.message
import io.github.wulkanowy.ui.base.BaseView
interface MessageView : BaseView {
val currentPageIndex: Int
fun initView()
fun showContent(show: Boolean)
fun showProgress(show: Boolean)
fun notifyChildLoadData(index: Int, forceRefresh: Boolean)
interface MessageChildView {
fun onParentLoadData(forceRefresh: Boolean)
}
}

View File

@ -0,0 +1,81 @@
package io.github.wulkanowy.ui.modules.message.preview
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import io.github.wulkanowy.R
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import kotlinx.android.synthetic.main.fragment_message_preview.*
import javax.inject.Inject
class MessagePreviewFragment : BaseFragment(), MessagePreviewView, MainView.TitledView {
@Inject
lateinit var presenter: MessagePreviewPresenter
override val titleStringId: Int
get() = R.string.message_title
override val noSubjectString: String
get() = getString(R.string.message_no_subject)
companion object {
const val MESSAGE_ID_KEY = "message_id"
fun newInstance(messageId: Int?): MessagePreviewFragment {
return MessagePreviewFragment().apply {
arguments = Bundle().apply { putInt(MESSAGE_ID_KEY, messageId ?: 0) }
}
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_message_preview, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = message
presenter.onAttachView(this, (savedInstanceState ?: arguments)?.getInt(MESSAGE_ID_KEY) ?: 0)
}
override fun setSubject(subject: String) {
messageSubject.text = subject
}
override fun setRecipient(recipient: String?) {
messageAuthor.text = getString(R.string.message_to, recipient)
}
override fun setSender(sender: String?) {
messageAuthor.text = getString(R.string.message_from, sender)
}
override fun setDate(date: String?) {
messageDate.text = getString(R.string.message_date, date)
}
override fun setContent(content: String?) {
messageContent.text = content
}
override fun showProgress(show: Boolean) {
messageProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showMessageError() {
messageError.visibility = View.VISIBLE
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(MESSAGE_ID_KEY, presenter.messageId)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,53 @@
package io.github.wulkanowy.ui.modules.message.preview
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.repositories.MessagesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logEvent
import io.github.wulkanowy.utils.toFormattedString
import javax.inject.Inject
class MessagePreviewPresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider,
private val messagesRepository: MessagesRepository,
private val studentRepository: StudentRepository
) : BasePresenter<MessagePreviewView>(errorHandler) {
var messageId: Int = 0
fun onAttachView(view: MessagePreviewView, id: Int) {
super.onAttachView(view)
loadData(id)
}
private fun loadData(id: Int) {
messageId = id
disposable.apply {
clear()
add(studentRepository.getCurrentStudent()
.flatMap { messagesRepository.getMessage(it.studentId, messageId, true) }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally { view?.showProgress(false) }
.subscribe({ messages ->
view?.run {
messages.let {
setSubject(if (it.subject.isNotBlank()) it.subject else noSubjectString)
setDate(it.date?.toFormattedString("yyyy-MM-dd HH:mm:ss"))
setContent(it.content)
if (it.recipient?.isNotBlank() == true) setRecipient(it.recipient)
else setSender(it.sender)
}
}
logEvent("Message load", mapOf("length" to messages.content?.length))
}) {
view?.showMessageError()
errorHandler.dispatch(it)
})
}
}
}

View File

@ -0,0 +1,22 @@
package io.github.wulkanowy.ui.modules.message.preview
import io.github.wulkanowy.ui.base.BaseView
interface MessagePreviewView : BaseView {
val noSubjectString: String
fun setSubject(subject: String)
fun setRecipient(recipient: String?)
fun setSender(sender: String?)
fun setDate(date: String?)
fun setContent(content: String?)
fun showProgress(show: Boolean)
fun showMessageError()
}

View File

@ -0,0 +1,122 @@
package io.github.wulkanowy.ui.modules.message.tab
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.data.repositories.MessagesRepository
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.message.MessageItem
import io.github.wulkanowy.ui.modules.message.MessageView
import io.github.wulkanowy.ui.modules.message.preview.MessagePreviewFragment
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_message_tab.*
import javax.inject.Inject
class MessageTabFragment : BaseFragment(), MessageTabView, MessageView.MessageChildView {
@Inject
lateinit var presenter: MessageTabPresenter
@Inject
lateinit var tabAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
companion object {
const val MESSAGE_TAB_FOLDER_ID = "message_tab_folder_id"
fun newInstance(folder: MessagesRepository.MessageFolder): MessageTabFragment {
return MessageTabFragment().apply {
arguments = Bundle().apply {
putString(MESSAGE_TAB_FOLDER_ID, folder.name)
}
}
}
}
override val noSubjectString: String
get() = getString(R.string.message_no_subject)
override val isViewEmpty
get() = tabAdapter.isEmpty
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_message_tab, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
messageContainer = messageTabRecycler
presenter.onAttachView(this, MessagesRepository.MessageFolder.valueOf(
(savedInstanceState ?: arguments)?.getString(MessageTabFragment.MESSAGE_TAB_FOLDER_ID) ?: ""
))
}
override fun initView() {
tabAdapter.setOnItemClickListener { presenter.onMessageItemSelected(it) }
messageTabRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = tabAdapter
}
messageTabSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
}
override fun updateData(data: List<MessageItem>) {
tabAdapter.updateDataSet(data, true)
}
override fun updateItem(item: AbstractFlexibleItem<*>) {
tabAdapter.updateItem(item)
}
override fun clearView() {
tabAdapter.clear()
}
override fun showProgress(show: Boolean) {
messageTabProgress.visibility = if (show) VISIBLE else GONE
}
override fun showContent(show: Boolean) {
messageTabRecycler.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showEmpty(show: Boolean) {
messageTabEmpty.visibility = if (show) VISIBLE else INVISIBLE
}
override fun showRefresh(show: Boolean) {
messageTabSwipe.isRefreshing = show
}
override fun openMessage(messageId: Int?) {
(activity as? MainActivity)?.pushView(MessagePreviewFragment.newInstance(messageId))
}
override fun notifyParentDataLoaded() {
(parentFragment as? MessageFragment)?.onChildFragmentLoaded()
}
override fun onParentLoadData(forceRefresh: Boolean) {
presenter.onParentViewLoadData(forceRefresh)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(MessageTabFragment.MESSAGE_TAB_FOLDER_ID, presenter.folder.name)
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,85 @@
package io.github.wulkanowy.ui.modules.message.tab
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.repositories.MessagesRepository
import io.github.wulkanowy.data.repositories.StudentRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.ui.modules.message.MessageItem
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logEvent
import timber.log.Timber
import javax.inject.Inject
class MessageTabPresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider,
private val messagesRepository: MessagesRepository,
private val studentRepository: StudentRepository
) : BasePresenter<MessageTabView>(errorHandler) {
lateinit var folder: MessagesRepository.MessageFolder
fun onAttachView(view: MessageTabView, folder: MessagesRepository.MessageFolder) {
super.onAttachView(view)
view.initView()
this.folder = folder
}
fun onSwipeRefresh() {
onParentViewLoadData(true)
}
fun onParentViewLoadData(forceRefresh: Boolean) {
disposable.apply {
clear()
add(studentRepository.getCurrentStudent()
.flatMap { messagesRepository.getMessages(it.studentId, folder, forceRefresh) }
.map { items -> items.map { MessageItem(it, view?.noSubjectString.orEmpty()) } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
showRefresh(false)
showProgress(false)
notifyParentDataLoaded()
}
}
.subscribe({
view?.run {
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
updateData(it)
}
logEvent("Message tab load", mapOf("items" to it.size, "forceRefresh" to forceRefresh))
}) {
view?.run { showEmpty(isViewEmpty) }
errorHandler.dispatch(it)
})
}
}
fun onMessageItemSelected(item: AbstractFlexibleItem<*>) {
if (item is MessageItem) {
view?.run {
openMessage(item.message.realId)
if (item.message.unread == true) {
item.message.unread = false
updateItem(item)
updateMessage(item.message)
}
}
}
}
private fun updateMessage(message: Message) {
disposable.add(messagesRepository.updateMessage(message)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
Timber.d("Message ${message.realId} updated")
}) { error -> errorHandler.dispatch(error) }
)
}
}

View File

@ -0,0 +1,32 @@
package io.github.wulkanowy.ui.modules.message.tab
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.ui.base.BaseView
import io.github.wulkanowy.ui.modules.message.MessageItem
interface MessageTabView : BaseView {
val noSubjectString: String
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<MessageItem>)
fun updateItem(item: AbstractFlexibleItem<*>)
fun clearView()
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
fun showEmpty(show: Boolean)
fun showRefresh(show: Boolean)
fun openMessage(messageId: Int?)
fun notifyParentDataLoaded()
}

View File

@ -15,6 +15,7 @@ import io.github.wulkanowy.ui.modules.about.AboutFragment
import io.github.wulkanowy.ui.modules.homework.HomeworkFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.MessageFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.utils.setOnItemClickListener
@ -36,6 +37,15 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
override val titleStringId: Int
get() = R.string.more_title
override val messagesRes: Pair<String, Drawable?>?
get() {
return context?.run {
getString(R.string.message_title) to
ContextCompat.getDrawable(this, R.drawable.ic_more_messages_24dp)
}
}
override val homeworkRes: Pair<String, Drawable?>?
get() {
return context?.run {
@ -92,6 +102,10 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
moreAdapter.updateDataSet(data)
}
override fun openMessagesView() {
(activity as? MainActivity)?.pushView(MessageFragment.newInstance())
}
override fun openHomeworkView() {
(activity as? MainActivity)?.pushView(HomeworkFragment.newInstance())
}

View File

@ -17,6 +17,7 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresen
if (item is MoreItem) {
view?.run {
when (item.title) {
messagesRes?.first -> openMessagesView()
homeworkRes?.first -> openHomeworkView()
noteRes?.first -> openNoteView()
settingsRes?.first -> openSettingsView()
@ -33,6 +34,7 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresen
private fun loadData() {
view?.run {
updateData(listOfNotNull(
messagesRes?.let { MoreItem(it.first, it.second) },
homeworkRes?.let { MoreItem(it.first, it.second) },
noteRes?.let { MoreItem(it.first, it.second) },
settingsRes?.let { MoreItem(it.first, it.second) },

View File

@ -5,6 +5,8 @@ import io.github.wulkanowy.ui.base.BaseView
interface MoreView : BaseView {
val messagesRes: Pair<String, Drawable?>?
val homeworkRes: Pair<String, Drawable?>?
val noteRes: Pair<String, Drawable?>?
@ -23,6 +25,8 @@ interface MoreView : BaseView {
fun popView()
fun openMessagesView()
fun openHomeworkView()
fun openNoteView()

View File

@ -1,5 +1,6 @@
package io.github.wulkanowy.utils
import org.threeten.bp.DateTimeUtils
import org.threeten.bp.DayOfWeek.*
import org.threeten.bp.Instant
import org.threeten.bp.LocalDate

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 401 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z" />
</vector>

View File

@ -57,4 +57,4 @@
android:text="@string/grade_no_items"
android:textSize="20sp" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.tabs.TabLayout
android:id="@+id/messageTabLayout"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/colorPrimary"
android:elevation="5dp"
android:visibility="invisible"
app:tabGravity="fill"
app:tabIndicatorColor="@android:color/white"
app:tabMaxWidth="0dp"
app:tabMode="fixed"
app:tabTextColor="@android:color/white" />
<androidx.viewpager.widget.ViewPager
android:id="@+id/messageViewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="48dp"
android:visibility="invisible" />
<ProgressBar
android:id="@+id/messageProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,90 @@
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/messageSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:lineSpacingMultiplier="1.2"
android:textSize="22sp"
tools:text="@tools:sample/lorem" />
<TextView
android:id="@+id/messageAuthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="15sp"
tools:text="@tools:sample/full_names" />
<TextView
android:id="@+id/messageDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="15dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="15sp"
tools:text="@tools:sample/date/ddmmyy" />
<TextView
android:id="@+id/messageContent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autoLink="web"
android:lineSpacingMultiplier="1.2"
android:textIsSelectable="true"
tools:text="@tools:sample/lorem" />
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/messageError"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:visibility="invisible">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_more_messages_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/message_preview_error"
android:textSize="20sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/messageProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
</FrameLayout>

View File

@ -0,0 +1,50 @@
<FrameLayout 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:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/messageTabSwipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messageTabRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/messageTabProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
<LinearLayout
android:id="@+id/messageTabEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="invisible">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_more_messages_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/message_no_items"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,52 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tool="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingRight="16dp"
android:paddingBottom="10dp">
<TextView
android:id="@+id/messageItemAuthor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginEnd="40dp"
android:layout_marginRight="40dp"
android:ellipsize="end"
android:maxLines="1"
android:textSize="15sp"
tool:text="@tools:sample/full_names" />
<TextView
android:id="@+id/messageItemDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_toEndOf="@id/messageItemAuthor"
android:layout_toRightOf="@id/messageItemAuthor"
android:gravity="end"
android:textSize="13sp"
tool:text="@tools:sample/date/mmddyy" />
<TextView
android:id="@+id/messageItemSubject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/messageItemAuthor"
android:layout_alignStart="@id/messageItemAuthor"
android:layout_alignLeft="@id/messageItemAuthor"
android:layout_marginTop="5dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:attr/android:textColorSecondary"
android:textSize="12sp"
tool:text="@tools:sample/lorem" />
</RelativeLayout>

View File

@ -11,6 +11,7 @@
<string name="settings_title">Ustawienia</string>
<string name="more_title">Więcej</string>
<string name="about_title">O aplikacji</string>
<string name="message_title">Wiadomości</string>
<string name="note_title">Uwagi i osiągnięcia</string>
<string name="homework_title">Zadania domowe</string>
<string name="account_title">Wybierz konto</string>
@ -68,8 +69,7 @@
<plurals name="grade_new_items">
<item quantity="one">Nowa ocena</item>
<item quantity="few">Nowe oceny</item>
<item quantity="many">Nowych ocen</item>
<item quantity="other">Nowych ocen</item>
<item quantity="many">Nowe oceny</item>
</plurals>
@ -124,6 +124,38 @@
<string name="exam_type">Typ</string>
<string name="exam_entry_date">Data wpisu</string>
<!--Message-->
<string name="message_inbox">Odebrane</string>
<string name="message_sent">Wysłane</string>
<string name="message_trash">Kosz</string>
<string name="message_no_subject">(brak tematu)</string>
<string name="message_no_items">Brak wiadomości</string>
<string name="message_preview_error">Wystąpił błąd podczas pobierania treści wiadomości</string>
<string name="message_from">Od: %s</string>
<string name="message_to">Do: %s</string>
<string name="message_date">Data: %s</string>
<plurals name="message_number_item">
<item quantity="one">%d wiadomość</item>
<item quantity="few">%d wiadomości</item>
<item quantity="many">%d wiadomości</item>
</plurals>
<plurals name="message_new_items">
<item quantity="one">Nowa wiadomość</item>
<item quantity="few">Nowe wiadomości</item>
<item quantity="many">Nowe wiadomości</item>
</plurals>
<!--Message notify-->
<string name="notify_message_channel">Nowe wiadomości</string>
<plurals name="notify_message_new_items">
<item quantity="one">Dostałeś %1$d wiadomość</item>
<item quantity="few">"Dostałeś %1$d wiadomości</item>
<item quantity="many">Dostałeś %1$d wiadomości</item>
</plurals>
<!--About-->
<string name="about_source_code">Kod źródłowy</string>
<string name="about_feedback">Zgłoś błąd</string>

View File

@ -11,6 +11,7 @@
<string name="settings_title">Settings</string>
<string name="more_title">More</string>
<string name="about_title">About</string>
<string name="message_title">Messages</string>
<string name="note_title">Notes and achievements</string>
<string name="homework_title">Homework</string>
<string name="account_title">Choose account</string>
@ -113,6 +114,34 @@
<string name="exam_type">Type</string>
<string name="exam_entry_date">Entry date</string>
<!--Message-->
<string name="message_inbox">Inbox</string>
<string name="message_sent">Sent</string>
<string name="message_trash">Trash</string>
<string name="message_no_subject">(no subject)</string>
<string name="message_no_items">No messages</string>
<string name="message_preview_error">An error occurred while downloading message content</string>
<string name="message_from">From: %s</string>
<string name="message_to">To: %s</string>
<string name="message_date">Date: %s</string>
<plurals name="message_number_item">
<item quantity="one">%d message</item>
<item quantity="other">%d messages</item>
</plurals>
<plurals name="message_new_items">
<item quantity="one">New message</item>
<item quantity="other">New messages</item>
</plurals>
<!--Message notify-->
<string name="notify_message_channel">New messages</string>
<plurals name="notify_message_new_items">
<item quantity="one">You received %1$d message</item>
<item quantity="other">You received %1$d messages</item>
</plurals>
<!--About-->
<string name="about_source_code">Source code</string>
<string name="about_feedback">Report a bug</string>