diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index bf80ae049..0d1bcd5d2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: circleci/android:api-27-alpha +image: circleci/android:api-28-alpha before_script: - export GRADLE_USER_HOME=`pwd`/.gradle diff --git a/.travis.yml b/.travis.yml index 3d2c2d5b4..493813dde 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 - | diff --git a/README.md b/README.md index 6c5ad04f3..ccbf14ca5 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/app/build.gradle b/app/build.gradle index 1f244f907..e16846db2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt index 28a7d945c..dcdd24af7 100644 --- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt +++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt @@ -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()) diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt index 05041f1cb..274b4d86b 100644 --- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt +++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt @@ -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 diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt index e7d15cebc..640d30ede 100644 --- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt +++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt @@ -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 diff --git a/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt b/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt new file mode 100644 index 000000000..5018b6900 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/dao/MessagesDao.kt @@ -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): List + + @Delete + fun deleteAll(messages: List) + + @Update + fun update(message: Message) + + @Update + fun updateAll(messages: List) + + @Query("SELECT * FROM Messages WHERE student_id = :studentId AND real_id = :id") + fun loadOne(studentId: Int, id: Int): Maybe + + @Query("SELECT * FROM Messages WHERE student_id = :studentId AND folder_id = :folder ORDER BY date DESC") + fun load(studentId: Int, folder: Int): Maybe> + + @Query("SELECT * FROM Messages WHERE student_id = :studentId AND removed = 1 ORDER BY date DESC") + fun loadDeleted(studentId: Int): Maybe> + + @Query("SELECT * FROM Messages WHERE unread = 1 AND student_id = :studentId") + fun loadNewMessages(studentId: Int): Maybe> +} diff --git a/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt b/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt new file mode 100644 index 000000000..320e9322e --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/db/entities/Message.kt @@ -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 +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt index d956d9139..cb78df53d 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/ExamRepository.kt @@ -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> { 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 } } + } } } diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/MessagesRepository.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/MessagesRepository.kt new file mode 100644 index 000000000..06a0f6f95 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/MessagesRepository.kt @@ -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> { + 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 { + 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> { + return local.getNewMessages(student).toSingle(emptyList()) + } + + fun updateMessage(message: Message): Completable { + return Completable.fromCallable { local.updateMessage(message) } + } + + fun updateMessages(messages: List): Completable { + return Completable.fromCallable { local.updateMessages(messages) } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/local/GradeLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/local/GradeLocal.kt index be3ed1eff..b5d0d345a 100644 --- a/app/src/main/java/io/github/wulkanowy/data/repositories/local/GradeLocal.kt +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/local/GradeLocal.kt @@ -27,8 +27,8 @@ class GradeLocal @Inject constructor(private val gradeDb: GradeDao) { return Completable.fromCallable { gradeDb.update(grade) } } - fun updateGrades(grade: List): Completable { - return Completable.fromCallable { gradeDb.updateAll(grade) } + fun updateGrades(grades: List): Completable { + return Completable.fromCallable { gradeDb.updateAll(grades) } } fun deleteGrades(grades: List) { diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/local/MessagesLocal.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/local/MessagesLocal.kt new file mode 100644 index 000000000..531cd8b0d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/local/MessagesLocal.kt @@ -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 { + return messagesDb.loadOne(studentId, id) + } + + fun getMessages(studentId: Int, folder: MessagesRepository.MessageFolder): Maybe> { + return when (folder) { + MessagesRepository.MessageFolder.TRASHED -> messagesDb.loadDeleted(studentId) + else -> messagesDb.load(studentId, folder.id) + }.filter { !it.isEmpty() } + } + + fun getNewMessages(student: Student): Maybe> { + return messagesDb.loadNewMessages(student.studentId) + } + + fun saveMessages(messages: List): List { + return messagesDb.insertAll(messages) + } + + fun updateMessage(message: Message) { + return messagesDb.update(message) + } + + fun updateMessages(messages: List) { + return messagesDb.updateAll(messages) + } + + fun deleteMessages(messages: List) { + messagesDb.deleteAll(messages) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/data/repositories/remote/MessagesRemote.kt b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/MessagesRemote.kt new file mode 100644 index 000000000..bc0f3400a --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/data/repositories/remote/MessagesRemote.kt @@ -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> { + 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 { + return api.getMessageContent(message.messageId ?: 0, message.folderId, markAsRead, message.realId ?: 0) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt b/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt index 2e39463f4..65fb3abcf 100644 --- a/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt +++ b/app/src/main/java/io/github/wulkanowy/services/job/SyncWorker.kt @@ -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() { diff --git a/app/src/main/java/io/github/wulkanowy/services/notification/MessageNotification.kt b/app/src/main/java/io/github/wulkanowy/services/notification/MessageNotification.kt new file mode 100644 index 000000000..8733cadab --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/services/notification/MessageNotification.kt @@ -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) { + 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") + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt index eb032f795..c47ddc574 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/grade/GradeModule.kt @@ -22,7 +22,7 @@ abstract class GradeModule { } @PerChildFragment - @ContributesAndroidInjector() + @ContributesAndroidInjector abstract fun bindGradeDetailsFragment(): GradeDetailsFragment @PerChildFragment diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt index cee0f9be7..1aebf27c1 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/main/MainModule.kt @@ -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 diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt new file mode 100644 index 000000000..f75c6131d --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageFragment.kt @@ -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() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageItem.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageItem.kt new file mode 100644 index 000000000..e9aa242d6 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageItem.kt @@ -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() { + + override fun createViewHolder(view: View, adapter: FlexibleAdapter>): ViewHolder { + return ViewHolder(view, adapter) + } + + override fun getLayoutRes(): Int = R.layout.item_message + + override fun bindViewHolder( + adapter: FlexibleAdapter>, holder: ViewHolder, + position: Int, payloads: MutableList? + ) { + 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 + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageModule.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageModule.kt new file mode 100644 index 000000000..dd7a22ae8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageModule.kt @@ -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 +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt new file mode 100644 index 000000000..5a52cbed8 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessagePresenter.kt @@ -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(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) + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt new file mode 100644 index 000000000..b0bb8aa7c --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/MessageView.kt @@ -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) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt new file mode 100644 index 000000000..7130a9254 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewFragment.kt @@ -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() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt new file mode 100644 index 000000000..298e299b9 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewPresenter.kt @@ -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(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) + }) + } + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt new file mode 100644 index 000000000..4f2365337 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/preview/MessagePreviewView.kt @@ -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() +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt new file mode 100644 index 000000000..07f10fc04 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabFragment.kt @@ -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> + + 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) { + 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() + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt new file mode 100644 index 000000000..93ee033a1 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabPresenter.kt @@ -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(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) } + ) + } +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt new file mode 100644 index 000000000..1bacd3523 --- /dev/null +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/message/tab/MessageTabView.kt @@ -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) + + 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() +} diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt index 1dd8558d0..60d3beafa 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreFragment.kt @@ -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? + get() { + return context?.run { + getString(R.string.message_title) to + ContextCompat.getDrawable(this, R.drawable.ic_more_messages_24dp) + } + } + override val homeworkRes: Pair? 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()) } diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt index 1b746e420..6890f2e02 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MorePresenter.kt @@ -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) }, diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt index 0785961c9..08ff82adb 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/more/MoreView.kt @@ -5,6 +5,8 @@ import io.github.wulkanowy.ui.base.BaseView interface MoreView : BaseView { + val messagesRes: Pair? + val homeworkRes: Pair? val noteRes: Pair? @@ -23,6 +25,8 @@ interface MoreView : BaseView { fun popView() + fun openMessagesView() + fun openHomeworkView() fun openNoteView() diff --git a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt index 0e832f96c..ffc4cc669 100644 --- a/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt +++ b/app/src/main/java/io/github/wulkanowy/utils/TimeExtension.kt @@ -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 diff --git a/app/src/main/res/drawable-hdpi/ic_stat_notify_message.png b/app/src/main/res/drawable-hdpi/ic_stat_notify_message.png new file mode 100644 index 000000000..86d63c587 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_stat_notify_message.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_stat_notify_message.png b/app/src/main/res/drawable-mdpi/ic_stat_notify_message.png new file mode 100644 index 000000000..73a1653a0 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_stat_notify_message.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_stat_notify_message.png b/app/src/main/res/drawable-xhdpi/ic_stat_notify_message.png new file mode 100644 index 000000000..b373ffd4a Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_stat_notify_message.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_stat_notify_message.png b/app/src/main/res/drawable-xxhdpi/ic_stat_notify_message.png new file mode 100644 index 000000000..dc15bbd96 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_stat_notify_message.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_message.png b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_message.png new file mode 100644 index 000000000..3b40ad3f1 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_stat_notify_message.png differ diff --git a/app/src/main/res/drawable/ic_more_messages_24dp.xml b/app/src/main/res/drawable/ic_more_messages_24dp.xml new file mode 100644 index 000000000..65f32169d --- /dev/null +++ b/app/src/main/res/drawable/ic_more_messages_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_grade.xml b/app/src/main/res/layout/fragment_grade.xml index 366fda8c3..522312a28 100644 --- a/app/src/main/res/layout/fragment_grade.xml +++ b/app/src/main/res/layout/fragment_grade.xml @@ -57,4 +57,4 @@ android:text="@string/grade_no_items" android:textSize="20sp" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/fragment_message.xml b/app/src/main/res/layout/fragment_message.xml new file mode 100644 index 000000000..53920bb2b --- /dev/null +++ b/app/src/main/res/layout/fragment_message.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_message_preview.xml b/app/src/main/res/layout/fragment_message_preview.xml new file mode 100644 index 000000000..c0f960457 --- /dev/null +++ b/app/src/main/res/layout/fragment_message_preview.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_message_tab.xml b/app/src/main/res/layout/fragment_message_tab.xml new file mode 100644 index 000000000..31f1c8aa4 --- /dev/null +++ b/app/src/main/res/layout/fragment_message_tab.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_message.xml b/app/src/main/res/layout/item_message.xml new file mode 100644 index 000000000..cfa03786b --- /dev/null +++ b/app/src/main/res/layout/item_message.xml @@ -0,0 +1,52 @@ + + + + + + + + + diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 5a1b6ce4a..79a51e2a1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -11,6 +11,7 @@ Ustawienia Więcej O aplikacji + Wiadomości Uwagi i osiągnięcia Zadania domowe Wybierz konto @@ -68,8 +69,7 @@ Nowa ocena Nowe oceny - Nowych ocen - Nowych ocen + Nowe oceny @@ -124,6 +124,38 @@ Typ Data wpisu + + + Odebrane + Wysłane + Kosz + (brak tematu) + Brak wiadomości + Wystąpił błąd podczas pobierania treści wiadomości + Od: %s + Do: %s + Data: %s + + %d wiadomość + %d wiadomości + %d wiadomości + + + + Nowa wiadomość + Nowe wiadomości + Nowe wiadomości + + + + + Nowe wiadomości + + Dostałeś %1$d wiadomość + "Dostałeś %1$d wiadomości + Dostałeś %1$d wiadomości + + Kod źródłowy Zgłoś błąd diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 91e456ac6..201d311a2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ Settings More About + Messages Notes and achievements Homework Choose account @@ -113,6 +114,34 @@ Type Entry date + + + Inbox + Sent + Trash + (no subject) + No messages + An error occurred while downloading message content + From: %s + To: %s + Date: %s + + %d message + %d messages + + + + New message + New messages + + + + New messages + + You received %1$d message + You received %1$d messages + + Source code Report a bug