diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 7643783a..ab75be24 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -1,6 +1,7 @@
+
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
index 6bf05279..e9865b05 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt
@@ -738,6 +738,8 @@ fun Bundle(vararg properties: Pair): Bundle {
is Short -> putShort(property.first, property.second as Short)
is Double -> putDouble(property.first, property.second as Double)
is Boolean -> putBoolean(property.first, property.second as Boolean)
+ is Bundle -> putBundle(property.first, property.second as Bundle)
+ is Parcelable -> putParcelable(property.first, property.second as Parcelable)
is Array<*> -> putParcelableArray(property.first, property.second as Array)
}
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
index b7540b44..48b872e4 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt
@@ -17,7 +17,6 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
-import androidx.lifecycle.Observer
import androidx.navigation.NavOptions
import com.danimahardhika.cafebar.CafeBar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@@ -30,20 +29,17 @@ import com.mikepenz.materialdrawer.model.DividerDrawerItem
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
import com.mikepenz.materialdrawer.model.interfaces.*
-import com.mikepenz.materialdrawer.model.utils.withIsHiddenInMiniDrawer
+import com.mikepenz.materialdrawer.model.utils.hiddenInMiniDrawer
import eu.szkolny.font.SzkolnyFont
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.droidsonroids.gif.GifDrawable
-import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE
import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
-import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
-import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
@@ -85,7 +81,6 @@ import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
-import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.NavTarget
@@ -102,15 +97,13 @@ import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
class MainActivity : AppCompatActivity(), CoroutineScope {
+ @Suppress("MemberVisibilityCanBePrivate")
companion object {
- var useOldMessages = false
-
const val TAG = "MainActivity"
const val DRAWER_PROFILE_ADD_NEW = 200
const val DRAWER_PROFILE_SYNC_ALL = 201
- const val DRAWER_PROFILE_EXPORT_DATA = 202
const val DRAWER_PROFILE_MANAGE = 203
const val DRAWER_PROFILE_MARK_ALL_AS_READ = 204
const val DRAWER_ITEM_HOME = 1
@@ -255,6 +248,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
+ var onBeforeNavigate: (() -> Boolean)? = null
+ var pausedNavigationData: PausedNavigationData? = null
+ private set
+
val app: App by lazy {
applicationContext as App
}
@@ -327,6 +324,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
window.statusBarColor = statusBarColor
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ColorUtils.calculateLuminance(statusBarColor) > 0.6) {
+ @Suppress("deprecation")
window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
@@ -370,13 +368,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
drawerProfileListEmptyListener = {
onProfileListEmptyEvent(ProfileListEmptyEvent())
}
- drawerItemSelectedListener = { id, position, drawerItem ->
+ drawerItemSelectedListener = { id, _, _ ->
loadTarget(id)
- true
}
- drawerProfileSelectedListener = { id, profile, _, _ ->
- loadProfile(id)
- false
+ drawerProfileSelectedListener = { id, _, _, _ ->
+ // why is this negated -_-
+ !loadProfile(id)
}
drawerProfileLongClickListener = { _, profile, _, view ->
if (view != null && profile is ProfileDrawerItem) {
@@ -408,7 +405,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
savedInstanceState.clear()
}
- app.db.profileDao().all.observe(this, Observer { profiles ->
+ app.db.profileDao().all.observe(this) { profiles ->
val allArchived = profiles.all { it.archived }
drawer.setProfileList(profiles.filter { it.id >= 0 && (!it.archived || allArchived) }.toMutableList())
//prepend the archived profile if loaded
@@ -424,18 +421,18 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
})
}
drawer.currentProfile = App.profileId
- })
+ }
setDrawerItems()
handleIntent(intent?.extras)
- app.db.metadataDao().unreadCounts.observe(this, Observer { unreadCounters ->
+ app.db.metadataDao().unreadCounts.observe(this) { unreadCounters ->
unreadCounters.map {
it.type = it.thingType
}
drawer.setUnreadCounterList(unreadCounters)
- })
+ }
b.swipeRefreshLayout.isEnabled = true
b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } }
@@ -543,29 +540,29 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_sync)
.withIcon(CommunityMaterial.Icon.cmd_download_outline)
- .withOnClickListener(View.OnClickListener {
+ .withOnClickListener {
bottomSheet.close()
SyncViewListDialog(this, navTargetId)
- }),
+ },
BottomSheetSeparatorItem(false),
BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_settings)
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
- .withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) }),
+ .withOnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) },
BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_feedback)
.withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline)
- .withOnClickListener(View.OnClickListener { loadTarget(TARGET_FEEDBACK) })
+ .withOnClickListener { loadTarget(TARGET_FEEDBACK) }
)
if (App.devMode) {
bottomSheet += BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_debug)
.withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge)
- .withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_DEBUG) })
+ .withOnClickListener { loadTarget(DRAWER_ITEM_DEBUG) }
}
}
- private var profileSettingClickListener = { id: Int, view: View? ->
+ private var profileSettingClickListener = { id: Int, _: View? ->
when (id) {
DRAWER_PROFILE_ADD_NEW -> {
requestHandler.requestLogin()
@@ -599,7 +596,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|_____/ \__, |_| |_|\___|
__/ |
|__*/
- suspend fun syncCurrentFeature() {
+ private suspend fun syncCurrentFeature() {
if (app.profile.archived) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.profile_archived_title)
@@ -756,7 +753,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.app_manager_dialog_title)
.setMessage(R.string.app_manager_dialog_text)
- .setPositiveButton(R.string.ok) { dialog, which ->
+ .setPositiveButton(R.string.ok) { _, _ ->
try {
for (intent in appManagerIntentList) {
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
@@ -772,7 +769,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
}
}
- .setNeutralButton(R.string.dont_ask_again) { dialog, which ->
+ .setNeutralButton(R.string.dont_ask_again) { _, _ ->
app.config.sync.dontShowAppManagerDialog = true
}
.setCancelable(false)
@@ -967,6 +964,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
super.onNewIntent(intent)
handleIntent(intent?.extras)
}
+
+ @Suppress("deprecation")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
requestHandler.handleResult(requestCode, resultCode, data)
@@ -985,31 +984,84 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.setPopExitAnim(R.anim.task_close_exit) // new fragment exit
.build()
+ private fun canNavigate(): Boolean = onBeforeNavigate?.invoke() != false
+
+ fun resumePausedNavigation(): Boolean {
+ if (pausedNavigationData == null)
+ return false
+ pausedNavigationData?.let { data ->
+ when (data) {
+ is PausedNavigationData.LoadProfile -> loadProfile(
+ id = data.id,
+ drawerSelection = data.drawerSelection,
+ arguments = data.arguments,
+ skipBeforeNavigate = true,
+ )
+ is PausedNavigationData.LoadTarget -> loadTarget(
+ id = data.id,
+ arguments = data.arguments,
+ skipBeforeNavigate = true,
+ )
+ else -> return false
+ }
+ }
+ pausedNavigationData = null
+ return true
+ }
+
fun loadProfile(id: Int) = loadProfile(id, navTargetId)
- fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments)
- fun loadProfile(profile: Profile) = loadProfile(
- profile,
- navTargetId,
- null,
- if (app.profile.archived) app.profile.id else null
- )
- private fun loadProfile(id: Int, drawerSelection: Int, arguments: Bundle? = null) {
+ // fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments)
+ fun loadProfile(profile: Profile): Boolean {
+ if (!canNavigate()) {
+ pausedNavigationData = PausedNavigationData.LoadProfile(
+ id = profile.id,
+ drawerSelection = navTargetId,
+ arguments = null,
+ )
+ return false
+ }
+
+ loadProfile(profile, navTargetId, null)
+ return true
+ }
+ private fun loadProfile(
+ id: Int,
+ drawerSelection: Int,
+ arguments: Bundle? = null,
+ skipBeforeNavigate: Boolean = false,
+ ): Boolean {
+ if (!skipBeforeNavigate && !canNavigate()) {
+ drawer.close()
+ // restore the previous profile after changing it with the drawer
+ // well, it still does not change the toolbar profile image,
+ // but that's now NavView's problem, not mine.
+ drawer.currentProfile = app.profile.id
+ pausedNavigationData = PausedNavigationData.LoadProfile(
+ id = id,
+ drawerSelection = drawerSelection,
+ arguments = arguments,
+ )
+ return false
+ }
+
if (App.profileId == id) {
drawer.currentProfile = app.profile.id
- loadTarget(drawerSelection, arguments)
- return
+ // skipBeforeNavigate because it's checked above already
+ loadTarget(drawerSelection, arguments, skipBeforeNavigate = true)
+ return true
}
- val previousArchivedId = if (app.profile.archived) app.profile.id else null
app.profileLoad(id) {
- loadProfile(it, drawerSelection, arguments, previousArchivedId)
+ loadProfile(it, drawerSelection, arguments)
}
+ return true
}
- private fun loadProfile(profile: Profile, drawerSelection: Int, arguments: Bundle?, previousArchivedId: Int?) {
+ private fun loadProfile(profile: Profile, drawerSelection: Int, arguments: Bundle?) {
App.profile = profile
MessagesFragment.pageSelection = -1
setDrawerItems()
+ val previousArchivedId = if (app.profile.archived) app.profile.id else null
if (previousArchivedId != null) {
// prevents accidentally removing the first item if the archived profile is not shown
drawer.removeProfileById(previousArchivedId)
@@ -1030,26 +1082,44 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// update it manually when switching profiles from other source
//if (drawer.currentProfile != app.profile.id)
drawer.currentProfile = app.profileId
- loadTarget(drawerSelection, arguments)
+ loadTarget(drawerSelection, arguments, skipBeforeNavigate = true)
}
- fun loadTarget(id: Int, arguments: Bundle? = null) {
+ fun loadTarget(
+ id: Int,
+ arguments: Bundle? = null,
+ skipBeforeNavigate: Boolean = false,
+ ): Boolean {
var loadId = id
if (loadId == -1) {
loadId = DRAWER_ITEM_HOME
}
val target = navTargetList
.firstOrNull { it.id == loadId }
- if (target == null) {
+ return if (target == null) {
Toast.makeText(this, getString(R.string.error_invalid_fragment, id), Toast.LENGTH_LONG).show()
- loadTarget(navTargetList.first(), arguments)
- }
- else {
- loadTarget(target, arguments)
+ loadTarget(navTargetList.first(), arguments, skipBeforeNavigate)
+ } else {
+ loadTarget(target, arguments, skipBeforeNavigate)
}
}
- private fun loadTarget(target: NavTarget, args: Bundle? = null) {
+ private fun loadTarget(
+ target: NavTarget,
+ args: Bundle? = null,
+ skipBeforeNavigate: Boolean = false,
+ ): Boolean {
d("NavDebug", "loadTarget(target = $target, args = $args)")
+ if (!skipBeforeNavigate && !canNavigate()) {
+ bottomSheet.close()
+ drawer.close()
+ pausedNavigationData = PausedNavigationData.LoadTarget(
+ id = target.id,
+ arguments = args,
+ )
+ return false
+ }
+ pausedNavigationData = null
+
val arguments = args ?: navBackStack.firstOrNull { it.first.id == target.id }?.second ?: Bundle()
bottomSheet.close()
bottomSheet.removeAllContextual()
@@ -1064,7 +1134,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
d("NavDebug", "Navigating from ${navTarget.fragmentClass?.java?.simpleName} to ${target.fragmentClass?.java?.simpleName}")
- val fragment = target.fragmentClass?.java?.newInstance() ?: return
+ val fragment = target.fragmentClass?.java?.newInstance() ?: return false
fragment.arguments = arguments
val transaction = fragmentManager.beginTransaction()
@@ -1134,6 +1204,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// TASK DESCRIPTION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val bm = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
+ @Suppress("deprecation")
val taskDesc = ActivityManager.TaskDescription(
if (target.id == HOME_ID) getString(R.string.app_name) else getString(R.string.app_task_format, getString(target.name)),
bm,
@@ -1141,32 +1212,32 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
)
setTaskDescription(taskDesc)
}
-
+ return true
}
fun reloadTarget() = loadTarget(navTarget)
- private fun popBackStack(): Boolean {
+ private fun popBackStack(skipBeforeNavigate: Boolean = false): Boolean {
if (navBackStack.size == 0) {
return false
}
// TODO back stack argument support
when {
navTarget.popToHome -> {
- loadTarget(HOME_ID)
+ loadTarget(HOME_ID, skipBeforeNavigate = skipBeforeNavigate)
}
navTarget.popTo != null -> {
- loadTarget(navTarget.popTo ?: HOME_ID)
+ loadTarget(navTarget.popTo ?: HOME_ID, skipBeforeNavigate = skipBeforeNavigate)
}
else -> {
navBackStack.last().let {
- loadTarget(it.first, it.second)
+ loadTarget(it.first, it.second, skipBeforeNavigate = skipBeforeNavigate)
}
}
}
return true
}
- fun navigateUp() {
- if (!popBackStack()) {
+ fun navigateUp(skipBeforeNavigate: Boolean = false) {
+ if (!popBackStack(skipBeforeNavigate)) {
super.onBackPressed()
}
}
@@ -1214,17 +1285,22 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
| | | | '__/ _` \ \ /\ / / _ \ '__| | | __/ _ \ '_ ` _ \/ __|
| |__| | | | (_| |\ V V / __/ | | | || __/ | | | | \__ \
|_____/|_| \__,_| \_/\_/ \___|_| |_|\__\___|_| |_| |_|__*/
+ @Suppress("UNUSED_PARAMETER")
private fun createDrawerItem(target: NavTarget, level: Int = 1): IDrawerItem<*> {
- val item = DrawerPrimaryItem()
- .withIdentifier(target.id.toLong())
- .withName(target.name)
- .withIsHiddenInMiniDrawer(!app.config.ui.miniMenuButtons.contains(target.id))
- .also { if (target.description != null) it.withDescription(target.description!!) }
- .also { if (target.icon != null) it.withIcon(target.icon!!) }
- .also { if (target.title != null) it.withAppTitle(getString(target.title!!)) }
- .also { if (target.badgeTypeId != null) it.withBadgeStyle(drawer.badgeStyle)}
- .withSelectedBackgroundAnimated(false)
-
+ val item = DrawerPrimaryItem().apply {
+ identifier = target.id.toLong()
+ nameRes = target.name
+ hiddenInMiniDrawer = !app.config.ui.miniMenuButtons.contains(target.id)
+ if (target.description != null)
+ descriptionRes = target.description!!
+ if (target.icon != null)
+ withIcon(target.icon!!)
+ if (target.title != null)
+ appTitle = getString(target.title!!)
+ if (target.badgeTypeId != null)
+ badgeStyle = drawer.badgeStyle
+ isSelectedBackgroundAnimated = false
+ }
if (target.badgeTypeId != null)
drawer.addUnreadCounterType(target.badgeTypeId!!, target.id)
// TODO sub items
@@ -1266,11 +1342,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
if (target.isInProfileList) {
- drawerProfiles += ProfileSettingDrawerItem()
- .withIdentifier(target.id.toLong())
- .withName(target.name)
- .also { if (target.description != null) it.withDescription(target.description!!) }
- .also { if (target.icon != null) it.withIcon(target.icon!!) }
+ drawerProfiles += ProfileSettingDrawerItem().apply {
+ identifier = target.id.toLong()
+ nameRes = target.name
+ if (target.description != null)
+ descriptionRes = target.description!!
+ if (target.icon != null)
+ withIcon(target.icon!!)
+ }
}
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt
index d4607bcc..af4e608e 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt
@@ -51,6 +51,9 @@ abstract class MessageDao : BaseDao {
@Query("DELETE FROM messages WHERE keep = 0")
abstract override fun removeNotKept()
+ @Query("DELETE FROM messages WHERE profileId = :profileId AND messageId = :messageId")
+ abstract fun delete(profileId: Int, messageId: Long)
+
// GET ALL - LIVE DATA
fun getAll(profileId: Int) =
getRaw("$QUERY WHERE messages.profileId = $profileId $ORDER_BY")
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageRecipientDao.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageRecipientDao.java
index 20e6fe90..b33adbff 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageRecipientDao.java
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageRecipientDao.java
@@ -4,8 +4,6 @@
package pl.szczodrzynski.edziennik.data.db.dao;
-import java.util.List;
-
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
@@ -14,6 +12,8 @@ import androidx.room.RawQuery;
import androidx.sqlite.db.SimpleSQLiteQuery;
import androidx.sqlite.db.SupportSQLiteQuery;
+import java.util.List;
+
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient;
import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull;
@@ -22,6 +22,9 @@ public abstract class MessageRecipientDao {
@Query("DELETE FROM messageRecipients WHERE profileId = :profileId")
public abstract void clear(int profileId);
+ @Query("DELETE FROM messageRecipients WHERE profileId = :profileId AND messageId = :messageId")
+ public abstract void clearFor(int profileId, long messageId);
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract long add(MessageRecipient messageRecipient);
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt
index 1849d2f8..d35ec67a 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt
@@ -57,33 +57,41 @@ class MessagesFragment : Fragment(), CoroutineScope {
val args = arguments
val pagerAdapter = FragmentLazyPagerAdapter(
- fragmentManager ?: return,
- b.refreshLayout,
- listOf(
- MessagesListFragment().apply {
- onPageDestroy = this@MessagesFragment.onPageDestroy
- arguments = Bundle("messageType" to Message.TYPE_RECEIVED)
- args?.getBundle("page0")?.let {
- arguments?.putAll(it)
- }
- } to getString(R.string.messages_tab_received),
+ fragmentManager = parentFragmentManager,
+ swipeRefreshLayout = b.refreshLayout,
+ fragments = listOf(
+ MessagesListFragment().apply {
+ onPageDestroy = this@MessagesFragment.onPageDestroy
+ arguments = Bundle("messageType" to Message.TYPE_RECEIVED)
+ args?.getBundle("page0")?.let {
+ arguments?.putAll(it)
+ }
+ } to getString(R.string.messages_tab_received),
- MessagesListFragment().apply {
- onPageDestroy = this@MessagesFragment.onPageDestroy
- arguments = Bundle("messageType" to Message.TYPE_SENT)
- args?.getBundle("page1")?.let {
- arguments?.putAll(it)
- }
- } to getString(R.string.messages_tab_sent),
+ MessagesListFragment().apply {
+ onPageDestroy = this@MessagesFragment.onPageDestroy
+ arguments = Bundle("messageType" to Message.TYPE_SENT)
+ args?.getBundle("page1")?.let {
+ arguments?.putAll(it)
+ }
+ } to getString(R.string.messages_tab_sent),
- MessagesListFragment().apply {
- onPageDestroy = this@MessagesFragment.onPageDestroy
- arguments = Bundle("messageType" to Message.TYPE_DELETED)
- args?.getBundle("page2")?.let {
- arguments?.putAll(it)
- }
- } to getString(R.string.messages_tab_deleted)
- )
+ MessagesListFragment().apply {
+ onPageDestroy = this@MessagesFragment.onPageDestroy
+ arguments = Bundle("messageType" to Message.TYPE_DELETED)
+ args?.getBundle("page2")?.let {
+ arguments?.putAll(it)
+ }
+ } to getString(R.string.messages_tab_deleted),
+
+ MessagesListFragment().apply {
+ onPageDestroy = this@MessagesFragment.onPageDestroy
+ arguments = Bundle("messageType" to Message.TYPE_DRAFT)
+ args?.getBundle("page3")?.let {
+ arguments?.putAll(it)
+ }
+ } to getString(R.string.messages_tab_draft),
+ ),
)
b.viewPager.apply {
offscreenPageLimit = 1
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt
index 65202979..314aee31 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt
@@ -11,9 +11,10 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView.NO_POSITION
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.*
+import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_MESSAGES_COMPOSE
+import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_MESSAGES_DETAILS
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
@@ -52,8 +53,8 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED)
- var topPosition = arguments.getInt("topPosition", NO_POSITION)
- var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION)
+ var recyclerViewState =
+ arguments?.getParcelable("recyclerViewState")
val searchText = arguments?.getString("searchText")
teachers = withContext(Dispatchers.Default) {
@@ -61,9 +62,13 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
}
adapter = MessagesAdapter(activity, teachers, onItemClick = {
- activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
- "messageId" to it.id
- ))
+ val (target, args) =
+ if (it.type == Message.TYPE_DRAFT) {
+ TARGET_MESSAGES_COMPOSE to Bundle("message" to app.gson.toJson(it))
+ } else {
+ TARGET_MESSAGES_DETAILS to Bundle("messageId" to it.id)
+ }
+ activity.loadTarget(target, args)
}, onStarClick = {
this@MessagesListFragment.launch {
manager.starMessage(it, !it.isStarred)
@@ -121,16 +126,13 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
// reapply the filter
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
- adapter.filter.filter(searchText ?: searchItem?.searchText, null)
-
- // restore the previously saved scroll position
- if (topPosition != NO_POSITION && topPosition > layoutManager.findLastCompletelyVisibleItemPosition()) {
- b.list.scrollToPosition(topPosition)
- } else if (bottomPosition != NO_POSITION && bottomPosition < layoutManager.findFirstVisibleItemPosition()) {
- b.list.scrollToPosition(bottomPosition)
+ adapter.filter.filter(searchText ?: searchItem?.searchText) {
+ // restore the previously saved scroll position
+ recyclerViewState?.let {
+ layoutManager.onRestoreInstanceState(it)
+ }
+ recyclerViewState = null
}
- topPosition = NO_POSITION
- bottomPosition = NO_POSITION
})
}; return true }
@@ -142,8 +144,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
onPageDestroy?.invoke(position, Bundle(
- "topPosition" to layoutManager?.findFirstVisibleItemPosition(),
- "bottomPosition" to layoutManager?.findLastCompletelyVisibleItemPosition(),
+ "recyclerViewState" to layoutManager?.onSaveInstanceState(),
"searchText" to searchItem?.searchText?.toString()
))
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt
index 305a2ac9..fcba191b 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt
@@ -13,17 +13,19 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AutoCompleteTextView
+import android.widget.ScrollView
+import android.widget.Toast
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.hootsuite.nachos.chip.ChipInfo
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.*
+import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
@@ -31,20 +33,20 @@ import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent
import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
+import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
-import pl.szczodrzynski.edziennik.data.db.full.MessageFull
import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
-import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
-import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
+import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.utils.Themes
+import pl.szczodrzynski.edziennik.utils.managers.MessageManager.UIConfig
import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig
-import pl.szczodrzynski.edziennik.utils.models.Date
-import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.span.*
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
+import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import kotlin.coroutines.CoroutineContext
+
class MessagesComposeFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "MessagesComposeFragment"
@@ -58,19 +60,25 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
-// private val manager
-// get() = app.messageManager
+ private val manager
+ get() = app.messageManager
private val textStylingManager
get() = app.textStylingManager
private val profileConfig by lazy { app.config.forProfile().ui }
private val greetingText
get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
- private var teachers = mutableListOf()
+ private val teachers = mutableListOf()
private lateinit var stylingConfig: StylingConfig
+ private lateinit var uiConfig: UIConfig
private val enableTextStyling
get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN
+ private var changedRecipients = false
+ private var changedSubject = false
+ private var changedBody = false
+ private var discardDraftItem: BottomSheetPrimaryItem? = null
+ private var draftMessageId: Long? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
@@ -99,7 +107,30 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
// do your job
}
- activity.bottomSheet.prependItem(
+ discardDraftItem = BottomSheetPrimaryItem(true)
+ .withTitle(R.string.messages_compose_discard_draft)
+ .withIcon(CommunityMaterial.Icon3.cmd_text_box_remove_outline)
+ .withOnClickListener {
+ activity.bottomSheet.close()
+ discardDraftDialog()
+ }
+
+ activity.bottomSheet.prependItems(
+ BottomSheetPrimaryItem(true)
+ .withTitle(R.string.messages_compose_send_long)
+ .withIcon(CommunityMaterial.Icon3.cmd_send_outline)
+ .withOnClickListener {
+ activity.bottomSheet.close()
+ sendMessage()
+ },
+ BottomSheetPrimaryItem(true)
+ .withTitle(R.string.messages_compose_save_draft)
+ .withIcon(CommunityMaterial.Icon.cmd_content_save_edit_outline)
+ .withOnClickListener {
+ activity.bottomSheet.close()
+ saveDraft()
+ },
+ BottomSheetSeparatorItem(true),
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_messages_config)
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
@@ -146,12 +177,15 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
b.recipients.addTextChangedListener(onTextChanged = { _, _, _, _ ->
b.recipientsLayout.error = null
+ changedRecipients = true
})
b.subject.addTextChangedListener(onTextChanged = { _, _, _, _ ->
b.subjectLayout.error = null
+ changedSubject = true
})
b.text.addTextChangedListener(onTextChanged = { _, _, _, _ ->
b.textLayout.error = null
+ changedBody = true
})
b.subjectLayout.counterMaxLength = when (app.profile.loginStoreType) {
@@ -238,6 +272,17 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
),
)
+ uiConfig = UIConfig(
+ context = activity,
+ recipients = b.recipients,
+ subject = b.subject,
+ body = b.text,
+ teachers = teachers,
+ greetingOnCompose = profileConfig.messagesGreetingOnCompose,
+ greetingOnReply = profileConfig.messagesGreetingOnReply,
+ greetingOnForward = profileConfig.messagesGreetingOnForward,
+ greetingText = greetingText,
+ )
stylingConfig = StylingConfig(
editText = b.text,
fontStyleGroup = b.fontStyle,
@@ -250,6 +295,9 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
b.fontStyleLayout.isVisible = enableTextStyling
if (enableTextStyling) {
textStylingManager.attach(stylingConfig)
+ b.fontStyle.addOnButtonCheckedListener { _, _, _ ->
+ changedBody = true
+ }
}
if (App.devMode) {
@@ -272,10 +320,68 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
activity.gainAttentionFAB()
}
+ private fun onBeforeNavigate(): Boolean {
+ val messageText = b.text.text?.toString()?.trim() ?: ""
+ val greetingText = this.greetingText.trim()
+ // navigateUp if nothing changed
+ if ((!changedRecipients || b.recipients.allChips.isEmpty())
+ && (!changedSubject || b.subject.text.isNullOrBlank())
+ && (!changedBody || messageText.isEmpty() || messageText == greetingText)
+ )
+ return true
+ saveDraftDialog()
+ return false
+ }
+
+ private fun saveDraftDialog() {
+ MaterialAlertDialogBuilder(activity)
+ .setTitle(R.string.messages_compose_save_draft_title)
+ .setMessage(R.string.messages_compose_save_draft_text)
+ .setPositiveButton(R.string.save) { _, _ ->
+ saveDraft()
+ MessagesFragment.pageSelection = Message.TYPE_DRAFT
+ activity.loadTarget(DRAWER_ITEM_MESSAGES, skipBeforeNavigate = true)
+ }
+ .setNegativeButton(R.string.discard) { _, _ ->
+ activity.resumePausedNavigation()
+ }
+ .show()
+ }
+
+ private fun saveDraft() {
+ launch {
+ manager.saveAsDraft(uiConfig, stylingConfig, App.profileId, draftMessageId)
+ Toast.makeText(activity, R.string.messages_compose_draft_saved, Toast.LENGTH_SHORT).show()
+ changedRecipients = false
+ changedSubject = false
+ changedBody = false
+ }
+ if (discardDraftItem != null)
+ activity.bottomSheet.addItemAt(2, discardDraftItem!!)
+ discardDraftItem = null
+ }
+
+ private fun discardDraftDialog() {
+ MaterialAlertDialogBuilder(activity)
+ .setTitle(R.string.messages_compose_discard_draft_title)
+ .setMessage(R.string.messages_compose_discard_draft_text)
+ .setPositiveButton(R.string.remove) { _, _ ->
+ launch {
+ if (draftMessageId != null)
+ manager.deleteDraft(App.profileId, draftMessageId!!)
+ Toast.makeText(activity, R.string.messages_compose_draft_discarded, Toast.LENGTH_SHORT).show()
+ activity.navigateUp(skipBeforeNavigate = true)
+ }
+ }
+ .setNegativeButton(R.string.cancel, null)
+ .show()
+ }
+
@SuppressLint("SetTextI18n")
private fun updateRecipientList(list: List) { launch {
withContext(Dispatchers.Default) {
- teachers = list.sortedBy { it.fullName }.toMutableList()
+ teachers.clear()
+ teachers.addAll(list.sortedBy { it.fullName })
Teacher.types.mapTo(teachers) {
Teacher(-1, -it.toLong(), Teacher.typeName(activity, it), "")
}
@@ -291,93 +397,30 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
val adapter = MessagesComposeSuggestionAdapter(activity, teachers)
b.recipients.setAdapter(adapter)
- if (profileConfig.messagesGreetingOnCompose)
- b.text.setText(greetingText)
+ val message = manager.fillWithBundle(uiConfig, arguments)
+ if (message != null && message.type == Message.TYPE_DRAFT) {
+ draftMessageId = message.id
+ if (discardDraftItem != null)
+ activity.bottomSheet.addItemAt(2, discardDraftItem!!)
+ discardDraftItem = null
+ }
- handleReplyMessage()
- handleMailToIntent()
+ when {
+ b.recipients.text.isBlank() -> b.recipients.requestFocus()
+ b.subject.text.isNullOrBlank() -> b.subject.requestFocus()
+ else -> b.text.requestFocus()
+ }
+
+ if (!enableTextStyling)
+ b.text.setText(b.text.text?.toString())
+ b.text.setSelection(0)
+ (b.root as? ScrollView)?.smoothScrollTo(0, 0)
+
+ changedRecipients = false
+ changedSubject = false
+ changedBody = false
}}
- private fun handleReplyMessage() = launch {
- val replyMessage = arguments?.getString("message")
- if (replyMessage != null) {
- val chipList = mutableListOf()
- var subject = ""
- val span = SpannableStringBuilder()
- var body: CharSequence = ""
-
- withContext(Dispatchers.Default) {
- val msg = app.gson.fromJson(replyMessage, MessageFull::class.java)
- val dateString = getString(R.string.messages_date_time_format, Date.fromMillis(msg.addedDate).formattedStringShort, Time.fromMillis(msg.addedDate).stringHM)
- // add original message info
- span.appendText("W dniu ")
- span.appendSpan(dateString, ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE)
- span.appendText(", ")
- span.appendSpan(msg.senderName.fixName(), ItalicSpan(), SPAN_EXCLUSIVE_EXCLUSIVE)
- span.appendText(" napisał(a):")
- span.setSpan(BoldSpan(), 0, span.length, SPAN_EXCLUSIVE_EXCLUSIVE)
- span.appendText("\n\n")
-
- if (arguments?.getString("type") == "reply") {
- // add greeting text
- if (profileConfig.messagesGreetingOnReply)
- span.replace(0, 0, "$greetingText\n\n\n")
- else
- span.replace(0, 0, "\n\n")
-
- teachers.firstOrNull { it.id == msg.senderId }?.let { teacher ->
- teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
- chipList += ChipInfo(teacher.fullName, teacher)
- }
- subject = "Re: ${msg.subject}"
- } else {
- // add greeting text
- if (profileConfig.messagesGreetingOnForward)
- span.replace(0, 0, "$greetingText\n\n\n")
- else
- span.replace(0, 0, "\n\n")
-
- subject = "Fwd: ${msg.subject}"
- }
- body = MessagesUtils.htmlToSpannable(activity, msg.body
- ?: "Nie udało się wczytać oryginalnej wiadomości.")//Html.fromHtml(msg.body?.replace(" ".toRegex(), "\n") ?: "Nie udało się wczytać oryginalnej wiadomości.")
- }
-
- b.recipients.addTextWithChips(chipList)
- if (b.recipients.text.isNullOrEmpty())
- b.recipients.requestFocus()
- else
- b.text.requestFocus()
- b.subject.setText(subject)
- b.text.apply {
- text = span.appendText(body)
- if (!enableTextStyling)
- setText(text?.toString())
- setSelection(0)
- }
- b.root.scrollTo(0, 0)
- }
- else {
- b.recipients.requestFocus()
- }
- }
-
- private fun handleMailToIntent() {
- val teacherId = arguments?.getLong("messageRecipientId")
- if (teacherId == 0L)
- return
-
- val chipList = mutableListOf()
- teachers.firstOrNull { it.id == teacherId }?.let { teacher ->
- teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName)
- chipList += ChipInfo(teacher.fullName, teacher)
- }
- b.recipients.addTextWithChips(chipList)
-
- val subject = arguments?.getString("messageSubject")
- b.subject.setText(subject ?: return)
- }
-
private fun sendMessage() {
b.recipientsLayout.error = null
b.subjectLayout.error = null
@@ -439,6 +482,20 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
.show()
}
+ override fun onResume() {
+ super.onResume()
+ if (!isAdded || !this::activity.isInitialized)
+ return
+ activity.onBeforeNavigate = this::onBeforeNavigate
+ }
+
+ override fun onPause() {
+ super.onPause()
+ if (!this::activity.isInitialized)
+ return
+ activity.onBeforeNavigate = null
+ }
+
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun onRecipientListGetEvent(event: RecipientListGetEvent) {
if (event.profileId != App.profileId)
@@ -460,11 +517,17 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
return
}
+ if (draftMessageId != null) {
+ launch {
+ manager.deleteDraft(App.profileId, draftMessageId!!)
+ }
+ }
+
activity.snackbar(app.getString(R.string.messages_sent_success), app.getString(R.string.ok))
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
"messageId" to event.message.id,
"message" to app.gson.toJson(event.message),
"sentDate" to event.sentDate
- ))
+ ), skipBeforeNavigate = true)
}
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/PausedNavigationData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/PausedNavigationData.kt
new file mode 100644
index 00000000..83d7ae96
--- /dev/null
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/PausedNavigationData.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) Kuba Szczodrzyński 2021-10-9.
+ */
+
+package pl.szczodrzynski.edziennik.utils
+
+import android.os.Bundle
+
+open class PausedNavigationData {
+
+ data class LoadProfile(
+ val id: Int,
+ val drawerSelection: Int,
+ val arguments: Bundle?,
+ ) : PausedNavigationData()
+
+ data class LoadTarget(
+ val id: Int,
+ val arguments: Bundle?,
+ ) : PausedNavigationData()
+}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt
index 4cf45486..70886ec9 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt
@@ -79,10 +79,13 @@ object BetterHtml {
@Suppress("DEPRECATION")
val htmlSpannable = HtmlCompat.fromHtml(
text,
- HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV,
+ HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM
+ or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST
+ or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV
+ or HtmlCompat.FROM_HTML_MODE_LEGACY,
null,
LiTagHandler()
- )
+ ).trimEnd() // fromHtml seems to add two line breaks at the end, needlessly
val spanned = SpannableStringBuilder(htmlSpannable)
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt
index 5c8fd604..600353a6 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt
@@ -4,20 +4,51 @@
package pl.szczodrzynski.edziennik.utils.managers
+import android.content.Context
import android.os.Bundle
+import android.text.SpannableStringBuilder
+import android.text.Spanned
+import android.widget.EditText
+import com.hootsuite.nachos.NachoTextView
+import com.hootsuite.nachos.chip.ChipInfo
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorRes
import com.mikepenz.iconics.view.IconicsImageView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
-import pl.szczodrzynski.edziennik.App
-import pl.szczodrzynski.edziennik.R
+import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Message
+import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient
+import pl.szczodrzynski.edziennik.data.db.entity.Metadata
+import pl.szczodrzynski.edziennik.data.db.entity.Teacher
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
+import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
+import pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit
+import pl.szczodrzynski.edziennik.utils.html.BetterHtml
+import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig
+import pl.szczodrzynski.edziennik.utils.models.Date
+import pl.szczodrzynski.edziennik.utils.models.Time
+import pl.szczodrzynski.edziennik.utils.span.BoldSpan
+import pl.szczodrzynski.edziennik.utils.span.ItalicSpan
import pl.szczodrzynski.navlib.colorAttr
class MessageManager(private val app: App) {
+ class UIConfig(
+ val context: Context,
+ val recipients: NachoTextView,
+ val subject: EditText,
+ val body: TextInputKeyboardEdit,
+ val teachers: List,
+ val greetingOnCompose: Boolean,
+ val greetingOnReply: Boolean,
+ val greetingOnForward: Boolean,
+ val greetingText: String,
+ )
+
+ private val textStylingManager
+ get() = app.textStylingManager
+
suspend fun getMessage(profileId: Int, args: Bundle?): MessageFull? {
val id = args?.getLong("messageId") ?: return null
val json = args.getString("message")
@@ -43,10 +74,9 @@ class MessageManager(private val app: App) {
}
} ?: return null
- // make recipients ID-unique
// this helps when multiple profiles receive the same message
// (there are multiple -1 recipients for the same message ID)
- val recipientsDistinct = message.recipients?.distinctBy { it.id } ?: return null
+ val recipientsDistinct = message.recipients?.filter { it.profileId == profileId } ?: return null
message.recipients?.clear()
message.recipients?.addAll(recipientsDistinct)
@@ -106,4 +136,149 @@ class MessageManager(private val app: App) {
app.db.messageDao().replace(message)
}
}
+
+ suspend fun deleteDraft(profileId: Int, messageId: Long) {
+ withContext(Dispatchers.Default) {
+ app.db.messageRecipientDao().clearFor(profileId, messageId)
+ app.db.messageDao().delete(profileId, messageId)
+ app.db.metadataDao().delete(profileId, Metadata.TYPE_MESSAGE, messageId)
+ }
+ }
+
+ suspend fun saveAsDraft(config: UIConfig, stylingConfig: StylingConfig, profileId: Int, messageId: Long?) {
+ val teachers = config.recipients.allChips.mapNotNull { it.data as? Teacher }
+ val subject = config.subject.text?.toString() ?: ""
+ val body = textStylingManager.getHtmlText(stylingConfig, enableHtmlCompatible = false)
+
+ withContext(Dispatchers.Default) {
+ if (messageId != null) {
+ app.db.messageRecipientDao().clearFor(profileId, messageId)
+ }
+
+ val message = Message(
+ profileId = profileId,
+ id = messageId ?: System.currentTimeMillis(),
+ type = Message.TYPE_DRAFT,
+ subject = subject,
+ body = body,
+ senderId = -1L,
+ addedDate = System.currentTimeMillis(),
+ )
+ val metadata = Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true)
+
+ val recipients = teachers.map {
+ MessageRecipient(profileId, it.id, message.id)
+ }
+
+ app.db.messageDao().replace(message)
+ app.db.messageRecipientDao().addAll(recipients)
+ app.db.metadataDao().add(metadata)
+ }
+ }
+
+ fun fillWithBundle(config: UIConfig, args: Bundle?): Message? {
+ args ?: return null
+ val messageJson = args.getString("message")
+ val teacherId = args.getLong("messageRecipientId")
+ val subject = args.getString("messageSubject")
+ val payloadType = args.getString("type")
+
+ if (config.greetingOnCompose)
+ config.body.setText(config.greetingText)
+ if (subject != null)
+ config.subject.setText(subject)
+
+ val message = if (messageJson != null)
+ app.gson.fromJson(messageJson, MessageFull::class.java)
+ else null
+
+ when {
+ message != null && message.type == Message.TYPE_DRAFT -> {
+ fillWithDraftMessage(config, message)
+ }
+ message != null -> {
+ fillWithMessage(config, message, payloadType)
+ }
+ teacherId != 0L -> {
+ fillWithRecipientIds(config, teacherId)
+ }
+ }
+
+ return message
+ }
+
+ private fun createRecipientChips(config: UIConfig, vararg teacherIds: Long?): List {
+ return teacherIds.mapNotNull { teacherId ->
+ val teacher = config.teachers.firstOrNull { it.id == teacherId } ?: return@mapNotNull null
+ teacher.image = MessagesUtils.getProfileImage(
+ diameterDp = 48,
+ textSizeBigDp = 24,
+ textSizeMediumDp = 16,
+ textSizeSmallDp = 12,
+ count = 1,
+ teacher.fullName
+ )
+ ChipInfo(teacher.fullName, teacher)
+ }
+ }
+
+ private fun fillWithRecipientIds(config: UIConfig, vararg teacherIds: Long?) {
+ config.recipients.addTextWithChips(createRecipientChips(config, *teacherIds))
+ }
+
+ private fun fillWithMessage(config: UIConfig, message: MessageFull, payloadType: String?) {
+ val spanned = SpannableStringBuilder()
+
+ val dateString = config.context.getString(
+ R.string.messages_reply_date_time_format,
+ Date.fromMillis(message.addedDate).formattedStringShort,
+ Time.fromMillis(message.addedDate).stringHM,
+ )
+ // add original message info
+ spanned.appendText("W dniu ")
+ spanned.appendSpan(dateString, ItalicSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ spanned.appendText(", ")
+ spanned.appendSpan(message.senderName.fixName(), ItalicSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ spanned.appendText(" napisał(a):")
+ spanned.setSpan(BoldSpan(), 0, spanned.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
+ spanned.appendText("\n\n")
+
+ val greeting = when (payloadType) {
+ "reply" -> {
+ config.subject.setText(R.string.messages_compose_subject_reply_format, message.subject)
+ if (config.greetingOnReply)
+ config.greetingText
+ else null
+ }
+ "forward" -> {
+ config.subject.setText(R.string.messages_compose_subject_forward_format, message.subject)
+ if (config.greetingOnForward)
+ config.greetingText
+ else null
+ }
+ else -> null
+ }
+
+ if (greeting == null) {
+ spanned.replace(0, 0, "\n\n")
+ } else {
+ spanned.replace(0, 0, "$greeting\n\n\n")
+ }
+
+ val body = message.body ?: config.context.getString(R.string.messages_compose_body_load_failed)
+ spanned.appendText(BetterHtml.fromHtml(config.context, body))
+
+ fillWithRecipientIds(config, message.senderId)
+ config.body.text = spanned
+ }
+
+ private fun fillWithDraftMessage(config: UIConfig, message: MessageFull) {
+ val recipientIds = message.recipients?.map { it.id }?.toTypedArray() ?: emptyArray()
+ fillWithRecipientIds(config, *recipientIds)
+
+ config.subject.setText(message.subject)
+
+ val body = message.body ?: config.context.getString(R.string.messages_compose_body_load_failed)
+ config.body.setText(BetterHtml.fromHtml(config.context, body))
+ }
}
diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt
index bd4ce3a6..b60df935 100644
--- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt
+++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt
@@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.utils.managers
+import android.text.SpannableStringBuilder
import android.text.Spanned
import android.widget.Button
import android.widget.TextView
@@ -23,6 +24,10 @@ class TextStylingManager(private val app: App) {
private const val TAG = "TextStylingManager"
}
+ private val paragraphBrRegex by lazy {
+ "((?: )+)
".toRegex()
+ }
+
data class StylingConfig(
val editText: TextInputKeyboardEdit,
val fontStyleGroup: MaterialButtonToggleGroup,
@@ -86,8 +91,15 @@ class TextStylingManager(private val app: App) {
.build()*/
}
- fun getHtmlText(config: StylingConfig): String {
- val spanned = config.editText.text ?: return ""
+ fun getHtmlText(config: StylingConfig, enableHtmlCompatible: Boolean = true): String {
+ val text = config.editText.text?.trimEnd() ?: return ""
+ val spanned = SpannableStringBuilder(text)
+
+ val htmlCompatibleMode = config.htmlCompatibleMode && enableHtmlCompatible
+ val toHtmlFlag = if (htmlCompatibleMode)
+ HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL
+ else
+ HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE
// apparently setting the spans to a different Spannable calls the original EditText's
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
@@ -101,7 +113,7 @@ class TextStylingManager(private val app: App) {
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
spanned.removeSpan(it)
}
- var textHtml = HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
+ var textHtml = HtmlCompat.toHtml(spanned, toHtmlFlag)
.replace("\n", "")
.replace(" dir=\"ltr\"", "")
.replace("", "")
@@ -110,10 +122,15 @@ class TextStylingManager(private val app: App) {
.replace("", "")
.replace("", "")
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
+ .replace(" ", " ")
+ // replace multiple newlines so they convert fromHtml correctly
+ // this should not be breaking with htmlCompatibleMode == true,
+ // as line breaks cannot occur inside paragraphs with these flags
+ .replace(paragraphBrRegex, "$1")
config.watchSelectionChanged = true
- if (config.htmlCompatibleMode) {
+ if (htmlCompatibleMode) {
textHtml = textHtml
.replace(" ", "
")
.replace("", "")
diff --git a/app/src/main/res/layout/message_fragment.xml b/app/src/main/res/layout/message_fragment.xml
index 5df4e5e6..7687cc4c 100644
--- a/app/src/main/res/layout/message_fragment.xml
+++ b/app/src/main/res/layout/message_fragment.xml
@@ -153,8 +153,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
- android:minHeight="250dp"
+ android:minHeight="200dp"
android:paddingHorizontal="16dp"
+ android:paddingBottom="32dp"
android:textIsSelectable="true"
tools:text="To jest treść wiadomości.\n\nZazwyczaj ma wiele linijek.\n\nTak" />
@@ -252,7 +253,8 @@
android:layout_marginHorizontal="4dp"
android:layout_weight="1"
android:paddingHorizontal="4dp"
- android:paddingTop="16dp"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
android:text="@string/message_download"
android:textAllCaps="false"
android:visibility="gone"
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 243c83b4..32f33a03 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -675,7 +675,7 @@
Bestätigen Sie das Senden der NachrichtFügen Sie einen Anhang hinzuNachricht abbrechen
- Entwurf speichern
+ Entwurf speichernSendenDieser Empfänger wurde bereits ausgewähltWählen Sie die Empfänger aus
diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml
index 84e3c52b..a971ba01 100644
--- a/app/src/main/res/values-en/strings.xml
+++ b/app/src/main/res/values-en/strings.xml
@@ -677,7 +677,7 @@
Confirm sending the messageAdd an attachmentAbort message
- Save draft
+ Save draftSendThis recipient has already been selectedSelect recipients
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 44864c00..bec3ea36 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -730,7 +730,7 @@
Potwierdź wysłanie wiadomościDodaj załącznikOdrzuć wiadomość
- Zapisz wersję roboczą
+ Zapisz wersję robocząWyślijTen odbiorca został już wybranyWybierz odbiorców
@@ -1476,4 +1476,17 @@
UsuńOdpowiedzPobierz ponownie
+ Re: %s
+ Fwd: %s
+ Nie udało się wczytać oryginalnej wiadomości.
+ Zapisz zmiany
+ Czy chcesz zapisać zmiany jako wersję roboczą?\n\nBędzie możliwa późniejsza edycja oraz wysłanie wiadomości.
+ Odrzuć
+ Zapisano wersję roboczą
+ Wersje robocze
+ Wyślij wiadomość
+ Usuń wersję roboczą
+ Usunięto wersję roboczą
+ Usuń wersję roboczą
+ Czy chcesz odrzucić zapisaną wersję wiadomości? Spowoduje to również anulowanie wprowadzonych zmian i usunięcie wiadomości.