Add sending messages (#232)

This commit is contained in:
Kacper Ziubryniewicz 2019-02-24 15:11:32 +01:00 committed by Rafał Borcz
parent 5ba12cf8c6
commit c72c301039
44 changed files with 1094 additions and 27 deletions

View File

@ -72,8 +72,7 @@ play {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
implementation('com.github.wulkanowy:api:0a4317f651') { exclude module: "threetenbp" }
implementation('com.github.wulkanowy:api:46f09bdf34') { exclude module: "threetenbp" }
implementation "androidx.legacy:legacy-support-v4:1.0.0"
implementation "androidx.appcompat:appcompat:1.0.2"
@ -125,6 +124,8 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
androidTestImplementation 'org.mockito:mockito-android:2.23.4'
androidTestImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
implementation "com.hootsuite.android:nachos:1.1.1"
}
apply plugin: 'com.google.gms.google-services'

View File

@ -41,7 +41,7 @@ class AttendanceLocalTest {
))
val attendance = attendanceLocal
.getAttendance(Semester(1, 2, "", 1, 3, true),
.getAttendance(Semester(1, 2, "", 1, 3, true, 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
)

View File

@ -42,7 +42,7 @@ class CompletedLessonsLocalTest {
))
val completed = completedLessonsLocal
.getCompletedLessons(Semester(1, 2, "", 1, 3, true),
.getCompletedLessons(Semester(1, 2, "", 1, 3, true, 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
)

View File

@ -41,7 +41,7 @@ class ExamLocalTest {
))
val exams = examLocal
.getExams(Semester(1, 2, "", 1, 3, true),
.getExams(Semester(1, 2, "", 1, 3, true, 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
)

View File

@ -37,7 +37,7 @@ class LuckyNumberLocalTest {
fun saveAndReadTest() {
luckyNumberLocal.saveLuckyNumber(LuckyNumber(1, LocalDate.of(2019, 1, 20), 14))
val luckyNumber = luckyNumberLocal.getLuckyNumber(Semester(1, 1, "", 1, 3, true),
val luckyNumber = luckyNumberLocal.getLuckyNumber(Semester(1, 1, "", 1, 3, true, 1, 1),
LocalDate.of(2019, 1, 20)
).blockingGet()

View File

@ -0,0 +1,61 @@
package io.github.wulkanowy.data.repositories.local
import androidx.room.Room
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.repositories.recipient.RecipientLocal
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.threeten.bp.LocalDateTime
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class)
class RecipientLocalTest {
private lateinit var recipientLocal: RecipientLocal
private lateinit var testDb: AppDatabase
@Before
fun createDb() {
testDb = Room.inMemoryDatabaseBuilder(ApplicationProvider.getApplicationContext(), AppDatabase::class.java)
.build()
recipientLocal = RecipientLocal(testDb.recipientDao)
}
@After
fun closeDb() {
testDb.close()
}
@Test
fun saveAndReadTest() {
recipientLocal.saveRecipients(listOf(
Recipient(1, "2rPracownik", "Kowalski Jan", "Kowalski Jan [KJ] - Pracownik (Fake123456)", 3, 4, 2, "hash"),
Recipient(1, "3rPracownik", "Kowalska Karolina", "Kowalska Karolina [KK] - Pracownik (Fake123456)", 4, 4, 2, "hash"),
Recipient(1, "4rPracownik", "Krupa Stanisław", "Krupa Stanisław [KS] - Uczeń (Fake123456)", 5, 4, 1, "hash")
))
val recipients = recipientLocal.getRecipients(
Student("fakelog.cf", "AUTO", "", "", "", 1, "", "", "", true, LocalDateTime.now()),
2,
ReportingUnit(1, 4, "", 0, "", emptyList())
).blockingGet()
assertEquals(2, recipients.size)
assertEquals(1, recipients[0].studentId)
assertEquals("3rPracownik", recipients[1].realId)
assertEquals("Kowalski Jan", recipients[0].name)
assertEquals("Kowalska Karolina [KK] - Pracownik (Fake123456)", recipients[1].realName)
assertEquals(3, recipients[0].loginId)
assertEquals(4, recipients[1].unitId)
assertEquals(2, recipients[0].role)
assertEquals("hash", recipients[1].hash)
}
}

View File

@ -45,7 +45,7 @@ class TimetableLocalTest {
))
val exams = timetableDb.getTimetable(
Semester(1, 2, "", 1, 1, true),
Semester(1, 2, "", 1, 1, true, 1, 1),
LocalDate.of(2018, 9, 10),
LocalDate.of(2018, 9, 14)
).blockingGet()

View File

@ -121,4 +121,12 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideCompletedLessonsDao(database: AppDatabase) = database.completedLessonsDao
@Singleton
@Provides
fun provideReportingUnitDao(database: AppDatabase) = database.reportingUnitDao
@Singleton
@Provides
fun provideRecipientDao(database: AppDatabase) = database.recipientDao
}

View File

@ -16,6 +16,8 @@ import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
import io.github.wulkanowy.data.db.dao.MessagesDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.dao.SubjectDao
@ -30,6 +32,8 @@ import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.LuckyNumber
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Subject
@ -38,6 +42,7 @@ import io.github.wulkanowy.data.db.migrations.Migration2
import io.github.wulkanowy.data.db.migrations.Migration3
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
import javax.inject.Singleton
@Singleton
@ -56,7 +61,9 @@ import javax.inject.Singleton
Homework::class,
Subject::class,
LuckyNumber::class,
CompletedLesson::class
CompletedLesson::class,
ReportingUnit::class,
Recipient::class
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = false
@ -65,7 +72,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
const val VERSION_SCHEMA = 5
const val VERSION_SCHEMA = 6
fun newInstance(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
@ -76,7 +83,8 @@ abstract class AppDatabase : RoomDatabase() {
Migration2(),
Migration3(),
Migration4(),
Migration5()
Migration5(),
Migration6()
)
.build()
}
@ -109,4 +117,8 @@ abstract class AppDatabase : RoomDatabase() {
abstract val luckyNumberDao: LuckyNumberDao
abstract val completedLessonsDao: CompletedLessonsDao
abstract val reportingUnitDao: ReportingUnitDao
abstract val recipientDao: RecipientDao
}

View File

@ -8,6 +8,8 @@ import org.threeten.bp.LocalDateTime
import org.threeten.bp.Month
import org.threeten.bp.ZoneOffset
import java.util.Date
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class Converters {
@ -36,4 +38,14 @@ class Converters {
@TypeConverter
fun intToMonth(value: Int?) = value?.let { Month.of(it) }
@TypeConverter
fun intListToGson(list: List<Int>): String {
return Gson().toJson(list)
}
@TypeConverter
fun gsonToIntList(value: String): List<Int> {
return Gson().fromJson(value, object : TypeToken<List<Int>>() {}.type)
}
}

View File

@ -0,0 +1,23 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.Recipient
import io.reactivex.Maybe
import javax.inject.Singleton
@Singleton
@Dao
interface RecipientDao {
@Insert
fun insertAll(messages: List<Recipient>)
@Delete
fun deleteAll(messages: List<Recipient>)
@Query("SELECT * FROM Recipients WHERE student_id = :studentId AND role = :role AND unit_id = :unitId")
fun load(studentId: Int, role: Int, unitId: Int): Maybe<List<Recipient>>
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.reactivex.Maybe
import javax.inject.Singleton
@Singleton
@Dao
interface ReportingUnitDao {
@Insert
fun insertAll(reportingUnits: List<ReportingUnit>)
@Delete
fun deleteAll(reportingUnits: List<ReportingUnit>)
@Query("SELECT * FROM ReportingUnits WHERE student_id = :studentId")
fun load(studentId: Int): Maybe<List<ReportingUnit>>
@Query("SELECT * FROM ReportingUnits WHERE student_id = :studentId AND real_id = :unitId")
fun loadOne(studentId: Int, unitId: Int): Maybe<ReportingUnit>
}

View File

@ -0,0 +1,38 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity(tableName = "Recipients")
data class Recipient(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "real_id")
val realId: String,
val name: String,
@ColumnInfo(name = "real_name")
val realName: String,
@ColumnInfo(name = "login_id")
val loginId: Int,
@ColumnInfo(name = "unit_id")
val unitId: Int,
val role: Int,
val hash: String
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
override fun toString() = name
}

View File

@ -0,0 +1,32 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.io.Serializable
@Entity(tableName = "ReportingUnits")
data class ReportingUnit(
@ColumnInfo(name = "student_id")
val studentId: Int,
@ColumnInfo(name = "real_id")
val realId: Int,
@ColumnInfo(name = "short")
val shortName: String,
@ColumnInfo(name = "sender_id")
val senderId: Int,
@ColumnInfo(name = "sender_name")
val senderName: String,
val roles: List<Int>
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
}

View File

@ -24,7 +24,13 @@ data class Semester(
val semesterName: Int,
@ColumnInfo(name = "is_current")
val isCurrent: Boolean
val isCurrent: Boolean,
@ColumnInfo(name = "class_id")
val classId: Int,
@ColumnInfo(name = "unit_id")
val unitId: Int
) {
@PrimaryKey(autoGenerate = true)

View File

@ -0,0 +1,33 @@
package io.github.wulkanowy.data.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration6 : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE ReportingUnits (" +
"id INTEGER NOT NULL PRIMARY KEY," +
"student_id INTEGER NOT NULL," +
"real_id INTEGER NOT NULL," +
"short TEXT NOT NULL," +
"sender_id INTEGER NOT NULL," +
"sender_name TEXT NOT NULL," +
"roles TEXT NOT NULL)")
database.execSQL("CREATE TABLE Recipients (" +
"id INTEGER NOT NULL PRIMARY KEY," +
"student_id INTEGER NOT NULL," +
"real_id TEXT NOT NULL," +
"name TEXT NOT NULL," +
"real_name TEXT NOT NULL," +
"login_id INTEGER NOT NULL," +
"unit_id INTEGER NOT NULL," +
"role INTEGER NOT NULL," +
"hash TEXT NOT NULL)")
database.execSQL("DELETE FROM Semesters WHERE 1")
database.execSQL("ALTER TABLE Semesters ADD COLUMN class_id INTEGER DEFAULT 0 NOT NULL")
database.execSQL("ALTER TABLE Semesters ADD COLUMN unit_id INTEGER DEFAULT 0 NOT NULL")
}
}

View File

@ -2,13 +2,16 @@ package io.github.wulkanowy.data.repositories.message
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.api.messages.Folder
import io.github.wulkanowy.api.messages.SentMessage
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.utils.toLocalDateTime
import io.reactivex.Single
import org.threeten.bp.LocalDateTime.now
import javax.inject.Inject
import javax.inject.Singleton
import io.github.wulkanowy.api.messages.Message as ApiMessage
import io.github.wulkanowy.api.messages.Recipient as ApiRecipient
@Singleton
class MessageRemote @Inject constructor(private val api: Api) {
@ -39,4 +42,21 @@ class MessageRemote @Inject constructor(private val api: Api) {
fun getMessagesContent(message: Message, markAsRead: Boolean = false): Single<String> {
return api.getMessageContent(message.messageId, message.folderId, markAsRead, message.realId)
}
fun sendMessage(subject: String, content: String, recipients: List<Recipient>): Single<SentMessage> {
return api.sendMessage(
subject = subject,
content = content,
recipients = recipients.map {
ApiRecipient(
id = it.realId,
realName = it.realName,
loginId = it.loginId,
reportingUnitId = it.unitId,
role = it.role,
hash = it.hash
)
}
)
}
}

View File

@ -2,8 +2,10 @@ package io.github.wulkanowy.data.repositories.message
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.api.messages.SentMessage
import io.github.wulkanowy.data.ApiHelper
import io.github.wulkanowy.data.db.entities.Message
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Completable
import io.reactivex.Single
@ -82,4 +84,8 @@ class MessageRepository @Inject constructor(
fun updateMessages(messages: List<Message>): Completable {
return Completable.fromCallable { local.updateMessages(messages) }
}
fun sendMessage(subject: String, content: String, recipients: List<Recipient>): Single<SentMessage> {
return remote.sendMessage(subject, content, recipients)
}
}

View File

@ -0,0 +1,25 @@
package io.github.wulkanowy.data.repositories.recipient
import io.github.wulkanowy.data.db.dao.RecipientDao
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecipientLocal @Inject constructor(private val recipientDb: RecipientDao) {
fun getRecipients(student: Student, role: Int, unit: ReportingUnit): Maybe<List<Recipient>> {
return recipientDb.load(student.studentId, role, unit.realId).filter { !it.isEmpty() }
}
fun saveRecipients(recipients: List<Recipient>) {
return recipientDb.insertAll(recipients)
}
fun deleteRecipients(recipients: List<Recipient>) {
recipientDb.deleteAll(recipients)
}
}

View File

@ -0,0 +1,30 @@
package io.github.wulkanowy.data.repositories.recipient
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecipientRemote @Inject constructor(private val api: Api) {
fun getRecipients(role: Int, unit: ReportingUnit): Single<List<Recipient>> {
return api.getRecipients(role, unit.realId)
.map { recipients ->
recipients.map {
Recipient(
studentId = api.studentId,
name = it.name,
realName = it.realName,
realId = it.id,
hash = it.hash,
loginId = it.loginId,
role = it.role,
unitId = it.reportingUnitId
)
}
}
}
}

View File

@ -0,0 +1,42 @@
package io.github.wulkanowy.data.repositories.recipient
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.ApiHelper
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class RecipientRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: RecipientLocal,
private val remote: RecipientRemote,
private val apiHelper: ApiHelper
) {
fun getRecipients(student: Student, role: Int, unit: ReportingUnit, forceRefresh: Boolean = false): Single<List<Recipient>> {
return Single.just(apiHelper.initApi(student))
.flatMap { _ ->
local.getRecipients(student, role, unit).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getRecipients(role, unit)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getRecipients(student, role, unit).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteRecipients(old - new)
local.saveRecipients(new - old)
}
}.flatMap {
local.getRecipients(student, role, unit).toSingle(emptyList())
}
)
}
}
}

View File

@ -0,0 +1,28 @@
package io.github.wulkanowy.data.repositories.reportingunit
import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ReportingUnitLocal @Inject constructor(private val reportingUnitDb: ReportingUnitDao) {
fun getReportingUnits(student: Student): Maybe<List<ReportingUnit>> {
return reportingUnitDb.load(student.studentId).filter { !it.isEmpty() }
}
fun getReportingUnit(student: Student, unitId: Int): Maybe<ReportingUnit> {
return reportingUnitDb.loadOne(student.studentId, unitId)
}
fun saveReportingUnits(reportingUnits: List<ReportingUnit>) {
return reportingUnitDb.insertAll(reportingUnits)
}
fun deleteReportingUnits(reportingUnits: List<ReportingUnit>) {
reportingUnitDb.deleteAll(reportingUnits)
}
}

View File

@ -0,0 +1,26 @@
package io.github.wulkanowy.data.repositories.reportingunit
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ReportingUnitRemote @Inject constructor(private val api: Api) {
fun getReportingUnits(): Single<List<ReportingUnit>> {
return api.getReportingUnits().map {
it.map { unit ->
ReportingUnit(
studentId = api.studentId,
realId = unit.id,
roles = unit.roles,
senderId = unit.senderId,
senderName = unit.senderName,
shortName = unit.short
)
}
}
}
}

View File

@ -0,0 +1,55 @@
package io.github.wulkanowy.data.repositories.reportingunit
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.ApiHelper
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.Student
import io.reactivex.Maybe
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ReportingUnitRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: ReportingUnitLocal,
private val remote: ReportingUnitRemote,
private val apiHelper: ApiHelper
) {
fun getReportingUnits(student: Student, forceRefresh: Boolean = false): Single<List<ReportingUnit>> {
return Single.just(apiHelper.initApi(student))
.flatMap { _ ->
local.getReportingUnits(student).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getReportingUnits()
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getReportingUnits(student).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteReportingUnits(old - new)
local.saveReportingUnits(new - old)
}
}.flatMap { local.getReportingUnits(student).toSingle(emptyList()) }
)
}
}
fun getReportingUnit(student: Student, unitId: Int): Maybe<ReportingUnit> {
return Maybe.just(apiHelper.initApi(student))
.flatMap { _ ->
local.getReportingUnit(student, unitId)
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) getReportingUnits(student, true)
else Single.error(UnknownHostException())
}.flatMapMaybe {
local.getReportingUnit(student, unitId)
}
)
}
}
}

View File

@ -19,7 +19,9 @@ class SemesterRemote @Inject constructor(private val api: Api) {
diaryName = semester.diaryName,
semesterId = semester.semesterId,
semesterName = semester.semesterNumber,
isCurrent = semester.current
isCurrent = semester.current,
classId = semester.classId,
unitId = semester.unitId
)
}

View File

@ -14,6 +14,8 @@ import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.message.MessageRepository.MessageFolder.RECEIVED
import io.github.wulkanowy.data.repositories.note.NoteRepository
import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
import io.github.wulkanowy.data.repositories.recipient.RecipientRepository
import io.github.wulkanowy.data.repositories.reportingunit.ReportingUnitRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.data.repositories.timetable.TimetableRepository
@ -26,6 +28,7 @@ import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
import io.reactivex.Completable
import io.reactivex.Maybe
import io.reactivex.Single
import io.reactivex.disposables.CompositeDisposable
import org.threeten.bp.LocalDate
import timber.log.Timber
@ -69,6 +72,12 @@ class SyncWorker : SimpleJobService() {
@Inject
lateinit var completedLessons: CompletedLessonsRepository
@Inject
lateinit var reportingUnitRepository: ReportingUnitRepository
@Inject
lateinit var recipientRepository: RecipientRepository
@Inject
lateinit var prefRepository: PreferencesRepository
@ -98,21 +107,24 @@ class SyncWorker : SimpleJobService() {
disposable.add(student.isStudentSaved()
.flatMapMaybe { if (it) student.getCurrentStudent().toMaybe() else Maybe.empty() }
.flatMap { semester.getCurrentSemester(it, true).map { semester -> semester to it }.toMaybe() }
.flatMapCompletable {
.flatMapCompletable { c ->
Completable.merge(
listOf(
gradesDetails.getGrades(it.second, it.first, true, notify).ignoreElement(),
gradesSummary.getGradesSummary(it.first, true).ignoreElement(),
attendance.getAttendance(it.first, start, end, true).ignoreElement(),
exam.getExams(it.first, start, end, true).ignoreElement(),
timetable.getTimetable(it.first, start, end, true).ignoreElement(),
message.getMessages(it.second, RECEIVED, true, notify).ignoreElement(),
note.getNotes(it.second, it.first, true, notify).ignoreElement(),
homework.getHomework(it.first, LocalDate.now(), true).ignoreElement(),
homework.getHomework(it.first, LocalDate.now().plusDays(1), true).ignoreElement(),
luckyNumber.getLuckyNumber(it.first, true, notify).ignoreElement(),
completedLessons.getCompletedLessons(it.first, start, end, true).ignoreElement()
)
gradesDetails.getGrades(c.second, c.first, true, notify).ignoreElement(),
gradesSummary.getGradesSummary(c.first, true).ignoreElement(),
attendance.getAttendance(c.first, start, end, true).ignoreElement(),
exam.getExams(c.first, start, end, true).ignoreElement(),
timetable.getTimetable(c.first, start, end, true).ignoreElement(),
message.getMessages(c.second, RECEIVED, true, notify).ignoreElement(),
note.getNotes(c.second, c.first, true, notify).ignoreElement(),
homework.getHomework(c.first, LocalDate.now(), true).ignoreElement(),
homework.getHomework(c.first, LocalDate.now().plusDays(1), true).ignoreElement(),
luckyNumber.getLuckyNumber(c.first, true, notify).ignoreElement(),
completedLessons.getCompletedLessons(c.first, start, end, true).ignoreElement()
) + reportingUnitRepository.getReportingUnits(c.second, true)
.flatMapPublisher { reportingUnits ->
Single.merge(reportingUnits.map { recipientRepository.getRecipients(c.second, 2, it, true) })
}.ignoreElements()
)
}
.subscribe({}, { error = it }))

View File

@ -20,6 +20,7 @@ import io.github.wulkanowy.ui.modules.luckynumber.LuckyNumberFragment
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.message.send.SendMessageFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
@ -94,9 +95,13 @@ abstract class MainModule {
@PerFragment
@ContributesAndroidInjector
abstract fun bindAccountDialog(): AccountDialog
abstract fun bindCompletedLessonsFragment(): CompletedLessonsFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindCompletedLessonsFragment(): CompletedLessonsFragment
abstract fun bindSendMessageFragment(): SendMessageFragment
@PerFragment
@ContributesAndroidInjector
abstract fun bindAccountDialog(): AccountDialog
}

View File

@ -12,7 +12,9 @@ import io.github.wulkanowy.data.repositories.message.MessageRepository.MessageFo
import io.github.wulkanowy.data.repositories.message.MessageRepository.MessageFolder.TRASHED
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.base.BaseFragmentPagerAdapter
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.message.send.SendMessageFragment
import io.github.wulkanowy.ui.modules.message.tab.MessageTabFragment
import io.github.wulkanowy.utils.setOnSelectPageListener
import kotlinx.android.synthetic.main.fragment_message.*
@ -61,6 +63,8 @@ class MessageFragment : BaseFragment(), MessageView, MainView.TitledView {
setOnSelectPageListener { presenter.onPageSelected(it) }
}
messageTabLayout.setupWithViewPager(messageViewPager)
openSendMessageButton.setOnClickListener { presenter.onSendMessageButtonClicked() }
}
override fun showContent(show: Boolean) {
@ -80,6 +84,10 @@ class MessageFragment : BaseFragment(), MessageView, MainView.TitledView {
(pagerAdapter.getFragmentInstance(index) as? MessageView.MessageChildView)?.onParentLoadData(forceRefresh)
}
override fun openSendMessage() {
(activity as? MainActivity)?.pushView(SendMessageFragment.newInstance())
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()

View File

@ -42,4 +42,8 @@ class MessagePresenter @Inject constructor(
showProgress(false)
}
}
fun onSendMessageButtonClicked() {
view?.openSendMessage()
}
}

View File

@ -14,6 +14,8 @@ interface MessageView : BaseView {
fun notifyChildLoadData(index: Int, forceRefresh: Boolean)
fun openSendMessage()
interface MessageChildView {
fun onParentLoadData(forceRefresh: Boolean)

View File

@ -0,0 +1,154 @@
package io.github.wulkanowy.ui.modules.message.send
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.hootsuite.nachos.ChipConfiguration
import com.hootsuite.nachos.chip.ChipSpan
import com.hootsuite.nachos.chip.ChipSpanChipCreator
import com.hootsuite.nachos.tokenizer.SpanChipTokenizer
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.ui.base.session.BaseSessionFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.hideSoftInput
import io.github.wulkanowy.utils.setOnTextChangedListener
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.android.synthetic.main.fragment_send_message.*
import javax.inject.Inject
class SendMessageFragment : BaseSessionFragment(), SendMessageView, MainView.TitledView {
@Inject
lateinit var presenter: SendMessagePresenter
private var recipients: List<Recipient> = emptyList()
private lateinit var recipientsAdapter: ArrayAdapter<Recipient>
companion object {
fun newInstance() = SendMessageFragment()
}
override val titleStringId: Int
get() = R.string.send_message_title
override val formRecipientsData: List<Recipient>
get() = sendMessageRecipientInput.allChips.map { it.data as Recipient }
override val formSubjectValue: String
get() = sendMessageSubjectInput.text.toString()
override val formContentValue: String
get() = sendMessageContentInput.text.toString()
override val messageRequiredRecipients: String
get() = getString(R.string.send_message_required_recipients)
override val messageContentMinLength: String
get() = getString(R.string.send_message_content_min_length)
override val messageSuccess: String
get() = getString(R.string.send_message_successful)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_send_message, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
context?.let {
sendMessageRecipientInput.chipTokenizer = SpanChipTokenizer<ChipSpan>(it, object : ChipSpanChipCreator() {
override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan {
return ChipSpan(context, text, ContextCompat.getDrawable(context, R.drawable.ic_all_account_24dp), data)
}
override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) {
super.configureChip(chip, chipConfiguration)
chip.setShowIconOnLeft(true)
}
}, ChipSpan::class.java)
recipientsAdapter = ArrayAdapter(it, android.R.layout.simple_dropdown_item_1line)
}
sendMessageRecipientInput.setAdapter(recipientsAdapter)
sendMessageRecipientInput.setOnTextChangedListener { presenter.onTypingRecipients() }
}
override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
inflater?.inflate(R.menu.action_menu_send_message, menu)
}
override fun onOptionsItemSelected(item: MenuItem?): Boolean {
return if (item?.itemId == R.id.sendMessageMenuSend) presenter.onSend()
else false
}
override fun setReportingUnit(unit: ReportingUnit) {
sendMessageFromTextView.setText(unit.senderName)
}
override fun setRecipients(recipients: List<Recipient>) {
this.recipients = recipients
}
override fun refreshRecipientsAdapter() {
recipientsAdapter.run {
clear()
addAll(recipients - sendMessageRecipientInput.allChips.map { it.data as Recipient })
notifyDataSetChanged()
}
}
override fun showProgress(show: Boolean) {
sendMessageProgress.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showContent(show: Boolean) {
sendMessageContent.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showEmpty(show: Boolean) {
sendMessageEmpty.visibility = if (show) View.VISIBLE else View.GONE
}
override fun popView() {
(activity as? MainActivity)?.popView()
}
override fun hideSoftInput() {
activity?.hideSoftInput()
}
override fun showBottomNav(show: Boolean) {
(activity as? MainActivity)?.mainBottomNav?.visibility = if (show) View.VISIBLE else View.GONE
}
override fun showMessage(text: String) {
Toast.makeText(context, text, Toast.LENGTH_LONG).show()
}
override fun onDestroyView() {
super.onDestroyView()
presenter.onDetachView()
}
}

View File

@ -0,0 +1,137 @@
package io.github.wulkanowy.ui.modules.message.send
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.repositories.message.MessageRepository
import io.github.wulkanowy.data.repositories.recipient.RecipientRepository
import io.github.wulkanowy.data.repositories.reportingunit.ReportingUnitRepository
import io.github.wulkanowy.data.repositories.semester.SemesterRepository
import io.github.wulkanowy.data.repositories.student.StudentRepository
import io.github.wulkanowy.ui.base.session.BaseSessionPresenter
import io.github.wulkanowy.ui.base.session.SessionErrorHandler
import io.github.wulkanowy.utils.FirebaseAnalyticsHelper
import io.github.wulkanowy.utils.SchedulersProvider
import timber.log.Timber
import javax.inject.Inject
class SendMessagePresenter @Inject constructor(
private val errorHandler: SessionErrorHandler,
private val schedulers: SchedulersProvider,
private val studentRepository: StudentRepository,
private val semesterRepository: SemesterRepository,
private val messageRepository: MessageRepository,
private val reportingUnitRepository: ReportingUnitRepository,
private val recipientRepository: RecipientRepository,
private val analytics: FirebaseAnalyticsHelper
) : BaseSessionPresenter<SendMessageView>(errorHandler) {
private lateinit var reportingUnit: ReportingUnit
override fun onAttachView(view: SendMessageView) {
Timber.i("Send message view is attached")
super.onAttachView(view)
view.run {
initView()
showBottomNav(false)
}
loadRecipients()
}
private fun loadRecipients() {
Timber.i("Loading recipients started")
disposable.add(studentRepository.getCurrentStudent()
.flatMapMaybe { student ->
semesterRepository.getCurrentSemester(student)
.flatMapMaybe { reportingUnitRepository.getReportingUnit(student, it.unitId) }
.doOnSuccess { reportingUnit = it }
.flatMap { recipientRepository.getRecipients(student, 2, it).toMaybe() }
}
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.run {
showProgress(true)
showContent(false)
}
}
.doFinally {
view?.run {
showProgress(false)
}
}
.subscribe({
view?.apply {
setReportingUnit(reportingUnit)
setRecipients(it)
refreshRecipientsAdapter()
showContent(true)
}
Timber.i("Loading recipients result: Success, fetched %s recipients", it.size.toString())
}, {
Timber.i("Loading recipients result: An exception occurred")
view?.showContent(true)
errorHandler.dispatch(it)
}, {
Timber.i("Loading recipients result: Can't find the reporting unit")
view?.showEmpty(true)
})
)
}
private fun sendMessage(subject: String, content: String, recipients: List<Recipient>) {
Timber.i("Sending message started")
disposable.add(messageRepository.sendMessage(subject, content, recipients)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doOnSubscribe {
view?.run {
hideSoftInput()
showContent(false)
showProgress(true)
}
}
.doFinally {
view?.showProgress(false)
}
.subscribe({
Timber.i("Sending message result: Success")
analytics.logEvent("send_message", "recipients" to recipients.size)
view?.run {
showMessage(messageSuccess)
popView()
}
}, {
Timber.i("Sending message result: An exception occurred")
view?.showContent(true)
errorHandler.dispatch(it)
})
)
}
fun onTypingRecipients() {
view?.refreshRecipientsAdapter()
}
fun onSend(): Boolean {
view?.run {
when {
formRecipientsData.isEmpty() -> showMessage(messageRequiredRecipients)
formContentValue.length < 3 -> showMessage(messageContentMinLength)
else -> {
sendMessage(
subject = formSubjectValue,
content = formContentValue,
recipients = formRecipientsData
)
return true
}
}
}
return false
}
override fun onDetachView() {
view?.showBottomNav(true)
super.onDetachView()
}
}

View File

@ -0,0 +1,40 @@
package io.github.wulkanowy.ui.modules.message.send
import io.github.wulkanowy.data.db.entities.Recipient
import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.ui.base.session.BaseSessionView
interface SendMessageView : BaseSessionView {
val formRecipientsData: List<Recipient>
val formSubjectValue: String
val formContentValue: String
val messageRequiredRecipients: String
val messageContentMinLength: String
val messageSuccess: String
fun initView()
fun setReportingUnit(unit: ReportingUnit)
fun setRecipients(recipients: List<Recipient>)
fun refreshRecipientsAdapter()
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
fun showEmpty(show: Boolean)
fun popView()
fun hideSoftInput()
fun showBottomNav(show: Boolean)
}

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z" />
</vector>

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"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z" />
</vector>

View File

@ -24,10 +24,22 @@
android:layout_marginTop="48dp"
android:visibility="invisible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/openSendMessageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:clickable="true"
android:focusable="true"
android:tint="#FFFFFF"
app:srcCompat="@drawable/ic_send_message_24dp" />
<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,145 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
android:orientation="vertical">
<LinearLayout
android:id="@+id/sendMessageContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="14dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:text="@string/send_message_from"
android:textColor="@android:color/darker_gray"
android:textSize="18sp" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/sendMessageFromTextView"
android:layout_width="match_parent"
android:layout_height="38dp"
android:background="@android:color/transparent"
android:enabled="false"
android:textColor="?android:attr/textColorPrimaryNoDisable"
tools:text="Jan Kowalski" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/dividerColor" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="14dp"
android:paddingLeft="14dp"
android:paddingTop="14dp"
android:paddingEnd="0dp"
android:paddingRight="0dp"
android:paddingBottom="14dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginRight="10dp"
android:text="@string/send_message_to"
android:textColor="@android:color/darker_gray"
android:textSize="18sp" />
<com.hootsuite.nachos.NachoTextView
android:id="@+id/sendMessageRecipientInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:inputType="text"
android:maxLines="1"
android:textColor="?attr/chipTextColor"
app:chipBackground="?attr/chipBackgroundColor" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/dividerColor" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/sendMessageSubjectInput"
android:layout_width="match_parent"
android:layout_height="66dp"
android:background="@android:color/transparent"
android:hint="@string/send_message_subject"
android:inputType="text"
android:maxLines="1"
android:padding="14dp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/dividerColor" />
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/sendMessageContentInput"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent"
android:gravity="top|start"
android:hint="@string/send_message_content"
android:inputType="textMultiLine"
android:paddingStart="14dp"
android:paddingLeft="14dp"
android:paddingTop="18dp"
android:paddingEnd="14dp"
android:paddingRight="14dp"
android:singleLine="false" />
</LinearLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/sendMessageEmpty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_more_messages_24dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/error_unknown"
android:textSize="20sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/sendMessageProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/sendMessageMenuSend"
android:icon="@drawable/ic_menu_send_message_24dp"
android:orderInCategory="1"
android:title="@string/send_message_title"
app:showAsAction="ifRoom" />
</menu>

View File

@ -7,6 +7,9 @@
<item name="preferenceTheme">@style/PreferenceThemeOverlay.v14.Material</item>
<item name="bottomNavBackground">@color/bottom_nav_background</item>
<item name="android:navigationBarColor" tools:targetApi="21">@color/bottom_nav_background</item>
<item name="chipBackgroundColor">@color/chip_material_background_inverse</item>
<item name="chipTextColor">@color/chip_default_text_color_inverse</item>
<item name="dividerColor">@color/divider_inverse</item>
<!--AboutLibraries specific values-->
<item name="about_libraries_window_background">@color/about_libraries_window_background_dark</item>

View File

@ -168,6 +168,16 @@
<item quantity="many">Dostałeś %1$d wiadomości</item>
</plurals>
<!--Send message-->
<string name="send_message_title">Wyślij</string>
<string name="send_message_from">Od:</string>
<string name="send_message_to">Do:</string>
<string name="send_message_subject">Temat</string>
<string name="send_message_content">Treść</string>
<string name="send_message_successful">Wiadomość wysłana pomyślnie</string>
<string name="send_message_required_recipients">Musisz wybrać co najmniej 1 adresata</string>
<string name="send_message_content_min_length">Treść wiadomości musi zawierać co najmniej 3 znaki</string>
<!--About-->
<string name="about_source_code">Kod źródłowy</string>
<string name="about_feedback">Zgłoś błąd</string>

View File

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="bottomNavBackground" format="reference" />
<attr name="dividerColor" format="reference" />
</resources>

View File

@ -16,4 +16,10 @@
<color name="bottom_nav_background">#303030</color>
<color name="bottom_nav_background_inverse">#ffffff</color>
<color name="chip_material_background_inverse">#595959</color>
<color name="chip_default_text_color_inverse">#fefefe</color>
<color name="divider">#cccccc</color>
<color name="divider_inverse">#777777</color>
</resources>

View File

@ -155,6 +155,16 @@
<item quantity="other">You received %1$d messages</item>
</plurals>
<!--Send message-->
<string name="send_message_title">Send</string>
<string name="send_message_from">From:</string>
<string name="send_message_to">To:</string>
<string name="send_message_subject">Subject</string>
<string name="send_message_content">Content</string>
<string name="send_message_successful">Message sent successfully</string>
<string name="send_message_required_recipients">You need to choose at least 1 recipient</string>
<string name="send_message_content_min_length">The message content must be at least 3 characters</string>
<!--About-->
<string name="about_source_code">Source code</string>
<string name="about_feedback">Report a bug</string>

View File

@ -14,6 +14,9 @@
<item name="subtitleTextColor">@android:color/primary_text_dark</item>
<item name="android:colorBackground">@android:color/white</item>
<item name="bottomNavBackground">@color/bottom_nav_background_inverse</item>
<item name="chipBackgroundColor">@color/chip_material_background</item>
<item name="chipTextColor">@color/chip_default_text_color</item>
<item name="dividerColor">@color/divider</item>
<!-- AboutLibraries specific values -->
<item name="about_libraries_window_background">@color/about_libraries_window_background</item>