mirror of
https://github.com/szkolny-eu/szkolny-android.git
synced 2025-01-18 12:56:45 -06:00
[Messages] Add saving messages as draft. (#92)
* [Messages/Compose] Move original message handling code to MessageManager. * [Messages/Compose] Add draft saving dialog on back button press. * [Messages/Compose] Implement saving messages as draft. * [Messages/Compose] Fix missing line breaks when saving/loading HTML. * [Messages] Fix download button icon padding. * [Messages] Fix showing correct message read date. * [Messages] Improve (and fix) scrolling to previous list position. * [Messages] Fix message body trimming. * [Messages/Compose] Add draft-related bottom sheet items. * [Refactor] Cleanup MainActivity code. * [Messages/Compose] Set htmlCompatible to true by default. * [Messages/Compose] Show confirmation dialog when navigating with unsaved changes. * [Messages] Restore message body bottom padding. * [Messages] Fix download button icon padding, again.
This commit is contained in:
parent
44263ac95f
commit
50ae767fcd
1
.idea/codeStyles/Project.xml
generated
1
.idea/codeStyles/Project.xml
generated
@ -1,6 +1,7 @@
|
|||||||
<component name="ProjectCodeStyleConfiguration">
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
<code_scheme name="Project" version="173">
|
<code_scheme name="Project" version="173">
|
||||||
<JetCodeStyleSettings>
|
<JetCodeStyleSettings>
|
||||||
|
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
</JetCodeStyleSettings>
|
</JetCodeStyleSettings>
|
||||||
<codeStyleSettings language="XML">
|
<codeStyleSettings language="XML">
|
||||||
|
@ -738,6 +738,8 @@ fun Bundle(vararg properties: Pair<String, Any?>): Bundle {
|
|||||||
is Short -> putShort(property.first, property.second as Short)
|
is Short -> putShort(property.first, property.second as Short)
|
||||||
is Double -> putDouble(property.first, property.second as Double)
|
is Double -> putDouble(property.first, property.second as Double)
|
||||||
is Boolean -> putBoolean(property.first, property.second as Boolean)
|
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<out Parcelable>)
|
is Array<*> -> putParcelableArray(property.first, property.second as Array<out Parcelable>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,6 @@ import android.widget.Toast
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
import com.danimahardhika.cafebar.CafeBar
|
import com.danimahardhika.cafebar.CafeBar
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
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.ProfileDrawerItem
|
||||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
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 eu.szkolny.font.SzkolnyFont
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import pl.droidsonroids.gif.GifDrawable
|
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.ERROR_VULCAN_API_DEPRECATED
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
||||||
import pl.szczodrzynski.edziennik.data.api.events.*
|
import pl.szczodrzynski.edziennik.data.api.events.*
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
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.api.szkolny.response.Update
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
|
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.*
|
||||||
import pl.szczodrzynski.edziennik.utils.Utils.d
|
import pl.szczodrzynski.edziennik.utils.Utils.d
|
||||||
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
|
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.managers.AvailabilityManager.Error.Type
|
||||||
import pl.szczodrzynski.edziennik.utils.models.Date
|
import pl.szczodrzynski.edziennik.utils.models.Date
|
||||||
import pl.szczodrzynski.edziennik.utils.models.NavTarget
|
import pl.szczodrzynski.edziennik.utils.models.NavTarget
|
||||||
@ -102,15 +97,13 @@ import kotlin.coroutines.CoroutineContext
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), CoroutineScope {
|
class MainActivity : AppCompatActivity(), CoroutineScope {
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
var useOldMessages = false
|
|
||||||
|
|
||||||
const val TAG = "MainActivity"
|
const val TAG = "MainActivity"
|
||||||
|
|
||||||
const val DRAWER_PROFILE_ADD_NEW = 200
|
const val DRAWER_PROFILE_ADD_NEW = 200
|
||||||
const val DRAWER_PROFILE_SYNC_ALL = 201
|
const val DRAWER_PROFILE_SYNC_ALL = 201
|
||||||
const val DRAWER_PROFILE_EXPORT_DATA = 202
|
|
||||||
const val DRAWER_PROFILE_MANAGE = 203
|
const val DRAWER_PROFILE_MANAGE = 203
|
||||||
const val DRAWER_PROFILE_MARK_ALL_AS_READ = 204
|
const val DRAWER_PROFILE_MARK_ALL_AS_READ = 204
|
||||||
const val DRAWER_ITEM_HOME = 1
|
const val DRAWER_ITEM_HOME = 1
|
||||||
@ -255,6 +248,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
|
|
||||||
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
|
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
|
||||||
|
|
||||||
|
var onBeforeNavigate: (() -> Boolean)? = null
|
||||||
|
var pausedNavigationData: PausedNavigationData? = null
|
||||||
|
private set
|
||||||
|
|
||||||
val app: App by lazy {
|
val app: App by lazy {
|
||||||
applicationContext as App
|
applicationContext as App
|
||||||
}
|
}
|
||||||
@ -327,6 +324,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
window.statusBarColor = statusBarColor
|
window.statusBarColor = statusBarColor
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ColorUtils.calculateLuminance(statusBarColor) > 0.6) {
|
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
|
window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -370,13 +368,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
drawerProfileListEmptyListener = {
|
drawerProfileListEmptyListener = {
|
||||||
onProfileListEmptyEvent(ProfileListEmptyEvent())
|
onProfileListEmptyEvent(ProfileListEmptyEvent())
|
||||||
}
|
}
|
||||||
drawerItemSelectedListener = { id, position, drawerItem ->
|
drawerItemSelectedListener = { id, _, _ ->
|
||||||
loadTarget(id)
|
loadTarget(id)
|
||||||
true
|
|
||||||
}
|
}
|
||||||
drawerProfileSelectedListener = { id, profile, _, _ ->
|
drawerProfileSelectedListener = { id, _, _, _ ->
|
||||||
loadProfile(id)
|
// why is this negated -_-
|
||||||
false
|
!loadProfile(id)
|
||||||
}
|
}
|
||||||
drawerProfileLongClickListener = { _, profile, _, view ->
|
drawerProfileLongClickListener = { _, profile, _, view ->
|
||||||
if (view != null && profile is ProfileDrawerItem) {
|
if (view != null && profile is ProfileDrawerItem) {
|
||||||
@ -408,7 +405,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
savedInstanceState.clear()
|
savedInstanceState.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
app.db.profileDao().all.observe(this, Observer { profiles ->
|
app.db.profileDao().all.observe(this) { profiles ->
|
||||||
val allArchived = profiles.all { it.archived }
|
val allArchived = profiles.all { it.archived }
|
||||||
drawer.setProfileList(profiles.filter { it.id >= 0 && (!it.archived || allArchived) }.toMutableList())
|
drawer.setProfileList(profiles.filter { it.id >= 0 && (!it.archived || allArchived) }.toMutableList())
|
||||||
//prepend the archived profile if loaded
|
//prepend the archived profile if loaded
|
||||||
@ -424,18 +421,18 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
drawer.currentProfile = App.profileId
|
drawer.currentProfile = App.profileId
|
||||||
})
|
}
|
||||||
|
|
||||||
setDrawerItems()
|
setDrawerItems()
|
||||||
|
|
||||||
handleIntent(intent?.extras)
|
handleIntent(intent?.extras)
|
||||||
|
|
||||||
app.db.metadataDao().unreadCounts.observe(this, Observer { unreadCounters ->
|
app.db.metadataDao().unreadCounts.observe(this) { unreadCounters ->
|
||||||
unreadCounters.map {
|
unreadCounters.map {
|
||||||
it.type = it.thingType
|
it.type = it.thingType
|
||||||
}
|
}
|
||||||
drawer.setUnreadCounterList(unreadCounters)
|
drawer.setUnreadCounterList(unreadCounters)
|
||||||
})
|
}
|
||||||
|
|
||||||
b.swipeRefreshLayout.isEnabled = true
|
b.swipeRefreshLayout.isEnabled = true
|
||||||
b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } }
|
b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } }
|
||||||
@ -543,29 +540,29 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
BottomSheetPrimaryItem(false)
|
BottomSheetPrimaryItem(false)
|
||||||
.withTitle(R.string.menu_sync)
|
.withTitle(R.string.menu_sync)
|
||||||
.withIcon(CommunityMaterial.Icon.cmd_download_outline)
|
.withIcon(CommunityMaterial.Icon.cmd_download_outline)
|
||||||
.withOnClickListener(View.OnClickListener {
|
.withOnClickListener {
|
||||||
bottomSheet.close()
|
bottomSheet.close()
|
||||||
SyncViewListDialog(this, navTargetId)
|
SyncViewListDialog(this, navTargetId)
|
||||||
}),
|
},
|
||||||
BottomSheetSeparatorItem(false),
|
BottomSheetSeparatorItem(false),
|
||||||
BottomSheetPrimaryItem(false)
|
BottomSheetPrimaryItem(false)
|
||||||
.withTitle(R.string.menu_settings)
|
.withTitle(R.string.menu_settings)
|
||||||
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||||
.withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) }),
|
.withOnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) },
|
||||||
BottomSheetPrimaryItem(false)
|
BottomSheetPrimaryItem(false)
|
||||||
.withTitle(R.string.menu_feedback)
|
.withTitle(R.string.menu_feedback)
|
||||||
.withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline)
|
.withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline)
|
||||||
.withOnClickListener(View.OnClickListener { loadTarget(TARGET_FEEDBACK) })
|
.withOnClickListener { loadTarget(TARGET_FEEDBACK) }
|
||||||
)
|
)
|
||||||
if (App.devMode) {
|
if (App.devMode) {
|
||||||
bottomSheet += BottomSheetPrimaryItem(false)
|
bottomSheet += BottomSheetPrimaryItem(false)
|
||||||
.withTitle(R.string.menu_debug)
|
.withTitle(R.string.menu_debug)
|
||||||
.withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge)
|
.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) {
|
when (id) {
|
||||||
DRAWER_PROFILE_ADD_NEW -> {
|
DRAWER_PROFILE_ADD_NEW -> {
|
||||||
requestHandler.requestLogin()
|
requestHandler.requestLogin()
|
||||||
@ -599,7 +596,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
|_____/ \__, |_| |_|\___|
|
|_____/ \__, |_| |_|\___|
|
||||||
__/ |
|
__/ |
|
||||||
|__*/
|
|__*/
|
||||||
suspend fun syncCurrentFeature() {
|
private suspend fun syncCurrentFeature() {
|
||||||
if (app.profile.archived) {
|
if (app.profile.archived) {
|
||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.profile_archived_title)
|
.setTitle(R.string.profile_archived_title)
|
||||||
@ -756,7 +753,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
MaterialAlertDialogBuilder(this)
|
MaterialAlertDialogBuilder(this)
|
||||||
.setTitle(R.string.app_manager_dialog_title)
|
.setTitle(R.string.app_manager_dialog_title)
|
||||||
.setMessage(R.string.app_manager_dialog_text)
|
.setMessage(R.string.app_manager_dialog_text)
|
||||||
.setPositiveButton(R.string.ok) { dialog, which ->
|
.setPositiveButton(R.string.ok) { _, _ ->
|
||||||
try {
|
try {
|
||||||
for (intent in appManagerIntentList) {
|
for (intent in appManagerIntentList) {
|
||||||
if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
|
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
|
app.config.sync.dontShowAppManagerDialog = true
|
||||||
}
|
}
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
@ -967,6 +964,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
handleIntent(intent?.extras)
|
handleIntent(intent?.extras)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("deprecation")
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
requestHandler.handleResult(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
|
.setPopExitAnim(R.anim.task_close_exit) // new fragment exit
|
||||||
.build()
|
.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) = loadProfile(id, navTargetId)
|
||||||
fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments)
|
// fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments)
|
||||||
fun loadProfile(profile: Profile) = loadProfile(
|
fun loadProfile(profile: Profile): Boolean {
|
||||||
profile,
|
if (!canNavigate()) {
|
||||||
navTargetId,
|
pausedNavigationData = PausedNavigationData.LoadProfile(
|
||||||
null,
|
id = profile.id,
|
||||||
if (app.profile.archived) app.profile.id else null
|
drawerSelection = navTargetId,
|
||||||
)
|
arguments = null,
|
||||||
private fun loadProfile(id: Int, drawerSelection: Int, arguments: Bundle? = 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) {
|
if (App.profileId == id) {
|
||||||
drawer.currentProfile = app.profile.id
|
drawer.currentProfile = app.profile.id
|
||||||
loadTarget(drawerSelection, arguments)
|
// skipBeforeNavigate because it's checked above already
|
||||||
return
|
loadTarget(drawerSelection, arguments, skipBeforeNavigate = true)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
val previousArchivedId = if (app.profile.archived) app.profile.id else null
|
|
||||||
app.profileLoad(id) {
|
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
|
App.profile = profile
|
||||||
MessagesFragment.pageSelection = -1
|
MessagesFragment.pageSelection = -1
|
||||||
|
|
||||||
setDrawerItems()
|
setDrawerItems()
|
||||||
|
|
||||||
|
val previousArchivedId = if (app.profile.archived) app.profile.id else null
|
||||||
if (previousArchivedId != null) {
|
if (previousArchivedId != null) {
|
||||||
// prevents accidentally removing the first item if the archived profile is not shown
|
// prevents accidentally removing the first item if the archived profile is not shown
|
||||||
drawer.removeProfileById(previousArchivedId)
|
drawer.removeProfileById(previousArchivedId)
|
||||||
@ -1030,26 +1082,44 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
// update it manually when switching profiles from other source
|
// update it manually when switching profiles from other source
|
||||||
//if (drawer.currentProfile != app.profile.id)
|
//if (drawer.currentProfile != app.profile.id)
|
||||||
drawer.currentProfile = app.profileId
|
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
|
var loadId = id
|
||||||
if (loadId == -1) {
|
if (loadId == -1) {
|
||||||
loadId = DRAWER_ITEM_HOME
|
loadId = DRAWER_ITEM_HOME
|
||||||
}
|
}
|
||||||
val target = navTargetList
|
val target = navTargetList
|
||||||
.firstOrNull { it.id == loadId }
|
.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()
|
Toast.makeText(this, getString(R.string.error_invalid_fragment, id), Toast.LENGTH_LONG).show()
|
||||||
loadTarget(navTargetList.first(), arguments)
|
loadTarget(navTargetList.first(), arguments, skipBeforeNavigate)
|
||||||
}
|
} else {
|
||||||
else {
|
loadTarget(target, arguments, skipBeforeNavigate)
|
||||||
loadTarget(target, arguments)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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)")
|
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()
|
val arguments = args ?: navBackStack.firstOrNull { it.first.id == target.id }?.second ?: Bundle()
|
||||||
bottomSheet.close()
|
bottomSheet.close()
|
||||||
bottomSheet.removeAllContextual()
|
bottomSheet.removeAllContextual()
|
||||||
@ -1064,7 +1134,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
|
|
||||||
d("NavDebug", "Navigating from ${navTarget.fragmentClass?.java?.simpleName} to ${target.fragmentClass?.java?.simpleName}")
|
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
|
fragment.arguments = arguments
|
||||||
val transaction = fragmentManager.beginTransaction()
|
val transaction = fragmentManager.beginTransaction()
|
||||||
|
|
||||||
@ -1134,6 +1204,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
// TASK DESCRIPTION
|
// TASK DESCRIPTION
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
val bm = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
val bm = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||||
|
@Suppress("deprecation")
|
||||||
val taskDesc = ActivityManager.TaskDescription(
|
val taskDesc = ActivityManager.TaskDescription(
|
||||||
if (target.id == HOME_ID) getString(R.string.app_name) else getString(R.string.app_task_format, getString(target.name)),
|
if (target.id == HOME_ID) getString(R.string.app_name) else getString(R.string.app_task_format, getString(target.name)),
|
||||||
bm,
|
bm,
|
||||||
@ -1141,32 +1212,32 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
)
|
)
|
||||||
setTaskDescription(taskDesc)
|
setTaskDescription(taskDesc)
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
fun reloadTarget() = loadTarget(navTarget)
|
fun reloadTarget() = loadTarget(navTarget)
|
||||||
|
|
||||||
private fun popBackStack(): Boolean {
|
private fun popBackStack(skipBeforeNavigate: Boolean = false): Boolean {
|
||||||
if (navBackStack.size == 0) {
|
if (navBackStack.size == 0) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
// TODO back stack argument support
|
// TODO back stack argument support
|
||||||
when {
|
when {
|
||||||
navTarget.popToHome -> {
|
navTarget.popToHome -> {
|
||||||
loadTarget(HOME_ID)
|
loadTarget(HOME_ID, skipBeforeNavigate = skipBeforeNavigate)
|
||||||
}
|
}
|
||||||
navTarget.popTo != null -> {
|
navTarget.popTo != null -> {
|
||||||
loadTarget(navTarget.popTo ?: HOME_ID)
|
loadTarget(navTarget.popTo ?: HOME_ID, skipBeforeNavigate = skipBeforeNavigate)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
navBackStack.last().let {
|
navBackStack.last().let {
|
||||||
loadTarget(it.first, it.second)
|
loadTarget(it.first, it.second, skipBeforeNavigate = skipBeforeNavigate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
fun navigateUp() {
|
fun navigateUp(skipBeforeNavigate: Boolean = false) {
|
||||||
if (!popBackStack()) {
|
if (!popBackStack(skipBeforeNavigate)) {
|
||||||
super.onBackPressed()
|
super.onBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1214,17 +1285,22 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
| | | | '__/ _` \ \ /\ / / _ \ '__| | | __/ _ \ '_ ` _ \/ __|
|
| | | | '__/ _` \ \ /\ / / _ \ '__| | | __/ _ \ '_ ` _ \/ __|
|
||||||
| |__| | | | (_| |\ V V / __/ | | | || __/ | | | | \__ \
|
| |__| | | | (_| |\ V V / __/ | | | || __/ | | | | \__ \
|
||||||
|_____/|_| \__,_| \_/\_/ \___|_| |_|\__\___|_| |_| |_|__*/
|
|_____/|_| \__,_| \_/\_/ \___|_| |_|\__\___|_| |_| |_|__*/
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
private fun createDrawerItem(target: NavTarget, level: Int = 1): IDrawerItem<*> {
|
private fun createDrawerItem(target: NavTarget, level: Int = 1): IDrawerItem<*> {
|
||||||
val item = DrawerPrimaryItem()
|
val item = DrawerPrimaryItem().apply {
|
||||||
.withIdentifier(target.id.toLong())
|
identifier = target.id.toLong()
|
||||||
.withName(target.name)
|
nameRes = target.name
|
||||||
.withIsHiddenInMiniDrawer(!app.config.ui.miniMenuButtons.contains(target.id))
|
hiddenInMiniDrawer = !app.config.ui.miniMenuButtons.contains(target.id)
|
||||||
.also { if (target.description != null) it.withDescription(target.description!!) }
|
if (target.description != null)
|
||||||
.also { if (target.icon != null) it.withIcon(target.icon!!) }
|
descriptionRes = target.description!!
|
||||||
.also { if (target.title != null) it.withAppTitle(getString(target.title!!)) }
|
if (target.icon != null)
|
||||||
.also { if (target.badgeTypeId != null) it.withBadgeStyle(drawer.badgeStyle)}
|
withIcon(target.icon!!)
|
||||||
.withSelectedBackgroundAnimated(false)
|
if (target.title != null)
|
||||||
|
appTitle = getString(target.title!!)
|
||||||
|
if (target.badgeTypeId != null)
|
||||||
|
badgeStyle = drawer.badgeStyle
|
||||||
|
isSelectedBackgroundAnimated = false
|
||||||
|
}
|
||||||
if (target.badgeTypeId != null)
|
if (target.badgeTypeId != null)
|
||||||
drawer.addUnreadCounterType(target.badgeTypeId!!, target.id)
|
drawer.addUnreadCounterType(target.badgeTypeId!!, target.id)
|
||||||
// TODO sub items
|
// TODO sub items
|
||||||
@ -1266,11 +1342,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target.isInProfileList) {
|
if (target.isInProfileList) {
|
||||||
drawerProfiles += ProfileSettingDrawerItem()
|
drawerProfiles += ProfileSettingDrawerItem().apply {
|
||||||
.withIdentifier(target.id.toLong())
|
identifier = target.id.toLong()
|
||||||
.withName(target.name)
|
nameRes = target.name
|
||||||
.also { if (target.description != null) it.withDescription(target.description!!) }
|
if (target.description != null)
|
||||||
.also { if (target.icon != null) it.withIcon(target.icon!!) }
|
descriptionRes = target.description!!
|
||||||
|
if (target.icon != null)
|
||||||
|
withIcon(target.icon!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,6 +51,9 @@ abstract class MessageDao : BaseDao<Message, MessageFull> {
|
|||||||
@Query("DELETE FROM messages WHERE keep = 0")
|
@Query("DELETE FROM messages WHERE keep = 0")
|
||||||
abstract override fun removeNotKept()
|
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
|
// GET ALL - LIVE DATA
|
||||||
fun getAll(profileId: Int) =
|
fun getAll(profileId: Int) =
|
||||||
getRaw("$QUERY WHERE messages.profileId = $profileId $ORDER_BY")
|
getRaw("$QUERY WHERE messages.profileId = $profileId $ORDER_BY")
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.data.db.dao;
|
package pl.szczodrzynski.edziennik.data.db.dao;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Insert;
|
import androidx.room.Insert;
|
||||||
import androidx.room.OnConflictStrategy;
|
import androidx.room.OnConflictStrategy;
|
||||||
@ -14,6 +12,8 @@ import androidx.room.RawQuery;
|
|||||||
import androidx.sqlite.db.SimpleSQLiteQuery;
|
import androidx.sqlite.db.SimpleSQLiteQuery;
|
||||||
import androidx.sqlite.db.SupportSQLiteQuery;
|
import androidx.sqlite.db.SupportSQLiteQuery;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient;
|
import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient;
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull;
|
import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull;
|
||||||
|
|
||||||
@ -22,6 +22,9 @@ public abstract class MessageRecipientDao {
|
|||||||
@Query("DELETE FROM messageRecipients WHERE profileId = :profileId")
|
@Query("DELETE FROM messageRecipients WHERE profileId = :profileId")
|
||||||
public abstract void clear(int 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)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
public abstract long add(MessageRecipient messageRecipient);
|
public abstract long add(MessageRecipient messageRecipient);
|
||||||
|
|
||||||
|
@ -57,33 +57,41 @@ class MessagesFragment : Fragment(), CoroutineScope {
|
|||||||
val args = arguments
|
val args = arguments
|
||||||
|
|
||||||
val pagerAdapter = FragmentLazyPagerAdapter(
|
val pagerAdapter = FragmentLazyPagerAdapter(
|
||||||
fragmentManager ?: return,
|
fragmentManager = parentFragmentManager,
|
||||||
b.refreshLayout,
|
swipeRefreshLayout = b.refreshLayout,
|
||||||
listOf(
|
fragments = listOf(
|
||||||
MessagesListFragment().apply {
|
MessagesListFragment().apply {
|
||||||
onPageDestroy = this@MessagesFragment.onPageDestroy
|
onPageDestroy = this@MessagesFragment.onPageDestroy
|
||||||
arguments = Bundle("messageType" to Message.TYPE_RECEIVED)
|
arguments = Bundle("messageType" to Message.TYPE_RECEIVED)
|
||||||
args?.getBundle("page0")?.let {
|
args?.getBundle("page0")?.let {
|
||||||
arguments?.putAll(it)
|
arguments?.putAll(it)
|
||||||
}
|
}
|
||||||
} to getString(R.string.messages_tab_received),
|
} to getString(R.string.messages_tab_received),
|
||||||
|
|
||||||
MessagesListFragment().apply {
|
MessagesListFragment().apply {
|
||||||
onPageDestroy = this@MessagesFragment.onPageDestroy
|
onPageDestroy = this@MessagesFragment.onPageDestroy
|
||||||
arguments = Bundle("messageType" to Message.TYPE_SENT)
|
arguments = Bundle("messageType" to Message.TYPE_SENT)
|
||||||
args?.getBundle("page1")?.let {
|
args?.getBundle("page1")?.let {
|
||||||
arguments?.putAll(it)
|
arguments?.putAll(it)
|
||||||
}
|
}
|
||||||
} to getString(R.string.messages_tab_sent),
|
} to getString(R.string.messages_tab_sent),
|
||||||
|
|
||||||
MessagesListFragment().apply {
|
MessagesListFragment().apply {
|
||||||
onPageDestroy = this@MessagesFragment.onPageDestroy
|
onPageDestroy = this@MessagesFragment.onPageDestroy
|
||||||
arguments = Bundle("messageType" to Message.TYPE_DELETED)
|
arguments = Bundle("messageType" to Message.TYPE_DELETED)
|
||||||
args?.getBundle("page2")?.let {
|
args?.getBundle("page2")?.let {
|
||||||
arguments?.putAll(it)
|
arguments?.putAll(it)
|
||||||
}
|
}
|
||||||
} to getString(R.string.messages_tab_deleted)
|
} 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 {
|
b.viewPager.apply {
|
||||||
offscreenPageLimit = 1
|
offscreenPageLimit = 1
|
||||||
|
@ -11,9 +11,10 @@ import android.view.ViewGroup
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView.NO_POSITION
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import pl.szczodrzynski.edziennik.*
|
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.Message
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
|
import pl.szczodrzynski.edziennik.data.db.entity.Teacher
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
||||||
@ -52,8 +53,8 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
|
|
||||||
override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
|
override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
|
||||||
val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED)
|
val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED)
|
||||||
var topPosition = arguments.getInt("topPosition", NO_POSITION)
|
var recyclerViewState =
|
||||||
var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION)
|
arguments?.getParcelable<LinearLayoutManager.SavedState>("recyclerViewState")
|
||||||
val searchText = arguments?.getString("searchText")
|
val searchText = arguments?.getString("searchText")
|
||||||
|
|
||||||
teachers = withContext(Dispatchers.Default) {
|
teachers = withContext(Dispatchers.Default) {
|
||||||
@ -61,9 +62,13 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
adapter = MessagesAdapter(activity, teachers, onItemClick = {
|
adapter = MessagesAdapter(activity, teachers, onItemClick = {
|
||||||
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
|
val (target, args) =
|
||||||
"messageId" to it.id
|
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 = {
|
}, onStarClick = {
|
||||||
this@MessagesListFragment.launch {
|
this@MessagesListFragment.launch {
|
||||||
manager.starMessage(it, !it.isStarred)
|
manager.starMessage(it, !it.isStarred)
|
||||||
@ -121,16 +126,13 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
|
|
||||||
// reapply the filter
|
// reapply the filter
|
||||||
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
||||||
adapter.filter.filter(searchText ?: searchItem?.searchText, null)
|
adapter.filter.filter(searchText ?: searchItem?.searchText) {
|
||||||
|
// restore the previously saved scroll position
|
||||||
// restore the previously saved scroll position
|
recyclerViewState?.let {
|
||||||
if (topPosition != NO_POSITION && topPosition > layoutManager.findLastCompletelyVisibleItemPosition()) {
|
layoutManager.onRestoreInstanceState(it)
|
||||||
b.list.scrollToPosition(topPosition)
|
}
|
||||||
} else if (bottomPosition != NO_POSITION && bottomPosition < layoutManager.findFirstVisibleItemPosition()) {
|
recyclerViewState = null
|
||||||
b.list.scrollToPosition(bottomPosition)
|
|
||||||
}
|
}
|
||||||
topPosition = NO_POSITION
|
|
||||||
bottomPosition = NO_POSITION
|
|
||||||
})
|
})
|
||||||
}; return true }
|
}; return true }
|
||||||
|
|
||||||
@ -142,8 +144,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope {
|
|||||||
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
val searchItem = adapter.items.firstOrNull { it is MessagesSearch } as? MessagesSearch
|
||||||
|
|
||||||
onPageDestroy?.invoke(position, Bundle(
|
onPageDestroy?.invoke(position, Bundle(
|
||||||
"topPosition" to layoutManager?.findFirstVisibleItemPosition(),
|
"recyclerViewState" to layoutManager?.onSaveInstanceState(),
|
||||||
"bottomPosition" to layoutManager?.findLastCompletelyVisibleItemPosition(),
|
|
||||||
"searchText" to searchItem?.searchText?.toString()
|
"searchText" to searchItem?.searchText?.toString()
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -13,17 +13,19 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.AutoCompleteTextView
|
import android.widget.AutoCompleteTextView
|
||||||
|
import android.widget.ScrollView
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.addTextChangedListener
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.hootsuite.nachos.chip.ChipInfo
|
|
||||||
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.greenrobot.eventbus.EventBus
|
import org.greenrobot.eventbus.EventBus
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
import org.greenrobot.eventbus.ThreadMode
|
import org.greenrobot.eventbus.ThreadMode
|
||||||
import pl.szczodrzynski.edziennik.*
|
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.ERROR_MESSAGE_NOT_SENT
|
||||||
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
|
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK
|
||||||
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
|
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.events.RecipientListGetEvent
|
||||||
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
import pl.szczodrzynski.edziennik.data.api.models.ApiError
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
|
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.entity.Teacher
|
||||||
import pl.szczodrzynski.edziennik.data.db.full.MessageFull
|
|
||||||
import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
|
import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding
|
||||||
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
import pl.szczodrzynski.edziennik.ui.dialogs.MessagesConfigDialog
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils
|
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
|
||||||
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage
|
|
||||||
import pl.szczodrzynski.edziennik.utils.Themes
|
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.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.edziennik.utils.span.*
|
||||||
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
|
||||||
|
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
|
||||||
class MessagesComposeFragment : Fragment(), CoroutineScope {
|
class MessagesComposeFragment : Fragment(), CoroutineScope {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MessagesComposeFragment"
|
private const val TAG = "MessagesComposeFragment"
|
||||||
@ -58,19 +60,25 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
override val coroutineContext: CoroutineContext
|
override val coroutineContext: CoroutineContext
|
||||||
get() = job + Dispatchers.Main
|
get() = job + Dispatchers.Main
|
||||||
|
|
||||||
// private val manager
|
private val manager
|
||||||
// get() = app.messageManager
|
get() = app.messageManager
|
||||||
private val textStylingManager
|
private val textStylingManager
|
||||||
get() = app.textStylingManager
|
get() = app.textStylingManager
|
||||||
private val profileConfig by lazy { app.config.forProfile().ui }
|
private val profileConfig by lazy { app.config.forProfile().ui }
|
||||||
private val greetingText
|
private val greetingText
|
||||||
get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
|
get() = profileConfig.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}"
|
||||||
|
|
||||||
private var teachers = mutableListOf<Teacher>()
|
private val teachers = mutableListOf<Teacher>()
|
||||||
|
|
||||||
private lateinit var stylingConfig: StylingConfig
|
private lateinit var stylingConfig: StylingConfig
|
||||||
|
private lateinit var uiConfig: UIConfig
|
||||||
private val enableTextStyling
|
private val enableTextStyling
|
||||||
get() = app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN
|
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? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
activity = (getActivity() as MainActivity?) ?: return null
|
activity = (getActivity() as MainActivity?) ?: return null
|
||||||
@ -99,7 +107,30 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
// do your job
|
// 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)
|
BottomSheetPrimaryItem(true)
|
||||||
.withTitle(R.string.menu_messages_config)
|
.withTitle(R.string.menu_messages_config)
|
||||||
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
|
||||||
@ -146,12 +177,15 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
|
|
||||||
b.recipients.addTextChangedListener(onTextChanged = { _, _, _, _ ->
|
b.recipients.addTextChangedListener(onTextChanged = { _, _, _, _ ->
|
||||||
b.recipientsLayout.error = null
|
b.recipientsLayout.error = null
|
||||||
|
changedRecipients = true
|
||||||
})
|
})
|
||||||
b.subject.addTextChangedListener(onTextChanged = { _, _, _, _ ->
|
b.subject.addTextChangedListener(onTextChanged = { _, _, _, _ ->
|
||||||
b.subjectLayout.error = null
|
b.subjectLayout.error = null
|
||||||
|
changedSubject = true
|
||||||
})
|
})
|
||||||
b.text.addTextChangedListener(onTextChanged = { _, _, _, _ ->
|
b.text.addTextChangedListener(onTextChanged = { _, _, _, _ ->
|
||||||
b.textLayout.error = null
|
b.textLayout.error = null
|
||||||
|
changedBody = true
|
||||||
})
|
})
|
||||||
|
|
||||||
b.subjectLayout.counterMaxLength = when (app.profile.loginStoreType) {
|
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(
|
stylingConfig = StylingConfig(
|
||||||
editText = b.text,
|
editText = b.text,
|
||||||
fontStyleGroup = b.fontStyle,
|
fontStyleGroup = b.fontStyle,
|
||||||
@ -250,6 +295,9 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
b.fontStyleLayout.isVisible = enableTextStyling
|
b.fontStyleLayout.isVisible = enableTextStyling
|
||||||
if (enableTextStyling) {
|
if (enableTextStyling) {
|
||||||
textStylingManager.attach(stylingConfig)
|
textStylingManager.attach(stylingConfig)
|
||||||
|
b.fontStyle.addOnButtonCheckedListener { _, _, _ ->
|
||||||
|
changedBody = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (App.devMode) {
|
if (App.devMode) {
|
||||||
@ -272,10 +320,68 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
activity.gainAttentionFAB()
|
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")
|
@SuppressLint("SetTextI18n")
|
||||||
private fun updateRecipientList(list: List<Teacher>) { launch {
|
private fun updateRecipientList(list: List<Teacher>) { launch {
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
teachers = list.sortedBy { it.fullName }.toMutableList()
|
teachers.clear()
|
||||||
|
teachers.addAll(list.sortedBy { it.fullName })
|
||||||
Teacher.types.mapTo(teachers) {
|
Teacher.types.mapTo(teachers) {
|
||||||
Teacher(-1, -it.toLong(), Teacher.typeName(activity, it), "")
|
Teacher(-1, -it.toLong(), Teacher.typeName(activity, it), "")
|
||||||
}
|
}
|
||||||
@ -291,93 +397,30 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
val adapter = MessagesComposeSuggestionAdapter(activity, teachers)
|
val adapter = MessagesComposeSuggestionAdapter(activity, teachers)
|
||||||
b.recipients.setAdapter(adapter)
|
b.recipients.setAdapter(adapter)
|
||||||
|
|
||||||
if (profileConfig.messagesGreetingOnCompose)
|
val message = manager.fillWithBundle(uiConfig, arguments)
|
||||||
b.text.setText(greetingText)
|
if (message != null && message.type == Message.TYPE_DRAFT) {
|
||||||
|
draftMessageId = message.id
|
||||||
|
if (discardDraftItem != null)
|
||||||
|
activity.bottomSheet.addItemAt(2, discardDraftItem!!)
|
||||||
|
discardDraftItem = null
|
||||||
|
}
|
||||||
|
|
||||||
handleReplyMessage()
|
when {
|
||||||
handleMailToIntent()
|
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<ChipInfo>()
|
|
||||||
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("<br\\s?/?>".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<ChipInfo>()
|
|
||||||
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() {
|
private fun sendMessage() {
|
||||||
b.recipientsLayout.error = null
|
b.recipientsLayout.error = null
|
||||||
b.subjectLayout.error = null
|
b.subjectLayout.error = null
|
||||||
@ -439,6 +482,20 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
.show()
|
.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)
|
@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
|
||||||
fun onRecipientListGetEvent(event: RecipientListGetEvent) {
|
fun onRecipientListGetEvent(event: RecipientListGetEvent) {
|
||||||
if (event.profileId != App.profileId)
|
if (event.profileId != App.profileId)
|
||||||
@ -460,11 +517,17 @@ class MessagesComposeFragment : Fragment(), CoroutineScope {
|
|||||||
return
|
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.snackbar(app.getString(R.string.messages_sent_success), app.getString(R.string.ok))
|
||||||
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
|
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle(
|
||||||
"messageId" to event.message.id,
|
"messageId" to event.message.id,
|
||||||
"message" to app.gson.toJson(event.message),
|
"message" to app.gson.toJson(event.message),
|
||||||
"sentDate" to event.sentDate
|
"sentDate" to event.sentDate
|
||||||
))
|
), skipBeforeNavigate = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
}
|
@ -79,10 +79,13 @@ object BetterHtml {
|
|||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val htmlSpannable = HtmlCompat.fromHtml(
|
val htmlSpannable = HtmlCompat.fromHtml(
|
||||||
text,
|
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,
|
null,
|
||||||
LiTagHandler()
|
LiTagHandler()
|
||||||
)
|
).trimEnd() // fromHtml seems to add two line breaks at the end, needlessly
|
||||||
|
|
||||||
val spanned = SpannableStringBuilder(htmlSpannable)
|
val spanned = SpannableStringBuilder(htmlSpannable)
|
||||||
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
|
spanned.getSpans(0, spanned.length, Any::class.java).forEach {
|
||||||
|
@ -4,20 +4,51 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.utils.managers
|
package pl.szczodrzynski.edziennik.utils.managers
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
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.typeface.library.community.material.CommunityMaterial
|
||||||
import com.mikepenz.iconics.utils.colorRes
|
import com.mikepenz.iconics.utils.colorRes
|
||||||
import com.mikepenz.iconics.view.IconicsImageView
|
import com.mikepenz.iconics.view.IconicsImageView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import pl.szczodrzynski.edziennik.App
|
import pl.szczodrzynski.edziennik.*
|
||||||
import pl.szczodrzynski.edziennik.R
|
|
||||||
import pl.szczodrzynski.edziennik.data.db.entity.Message
|
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.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
|
import pl.szczodrzynski.navlib.colorAttr
|
||||||
|
|
||||||
class MessageManager(private val app: App) {
|
class MessageManager(private val app: App) {
|
||||||
|
|
||||||
|
class UIConfig(
|
||||||
|
val context: Context,
|
||||||
|
val recipients: NachoTextView,
|
||||||
|
val subject: EditText,
|
||||||
|
val body: TextInputKeyboardEdit,
|
||||||
|
val teachers: List<Teacher>,
|
||||||
|
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? {
|
suspend fun getMessage(profileId: Int, args: Bundle?): MessageFull? {
|
||||||
val id = args?.getLong("messageId") ?: return null
|
val id = args?.getLong("messageId") ?: return null
|
||||||
val json = args.getString("message")
|
val json = args.getString("message")
|
||||||
@ -43,10 +74,9 @@ class MessageManager(private val app: App) {
|
|||||||
}
|
}
|
||||||
} ?: return null
|
} ?: return null
|
||||||
|
|
||||||
// make recipients ID-unique
|
|
||||||
// this helps when multiple profiles receive the same message
|
// this helps when multiple profiles receive the same message
|
||||||
// (there are multiple -1 recipients for the same message ID)
|
// (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?.clear()
|
||||||
message.recipients?.addAll(recipientsDistinct)
|
message.recipients?.addAll(recipientsDistinct)
|
||||||
|
|
||||||
@ -106,4 +136,149 @@ class MessageManager(private val app: App) {
|
|||||||
app.db.messageDao().replace(message)
|
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<ChipInfo> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
package pl.szczodrzynski.edziennik.utils.managers
|
package pl.szczodrzynski.edziennik.utils.managers
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
@ -23,6 +24,10 @@ class TextStylingManager(private val app: App) {
|
|||||||
private const val TAG = "TextStylingManager"
|
private const val TAG = "TextStylingManager"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val paragraphBrRegex by lazy {
|
||||||
|
"((?:<br>)+)</p>".toRegex()
|
||||||
|
}
|
||||||
|
|
||||||
data class StylingConfig(
|
data class StylingConfig(
|
||||||
val editText: TextInputKeyboardEdit,
|
val editText: TextInputKeyboardEdit,
|
||||||
val fontStyleGroup: MaterialButtonToggleGroup,
|
val fontStyleGroup: MaterialButtonToggleGroup,
|
||||||
@ -86,8 +91,15 @@ class TextStylingManager(private val app: App) {
|
|||||||
.build()*/
|
.build()*/
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getHtmlText(config: StylingConfig): String {
|
fun getHtmlText(config: StylingConfig, enableHtmlCompatible: Boolean = true): String {
|
||||||
val spanned = config.editText.text ?: return ""
|
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
|
// apparently setting the spans to a different Spannable calls the original EditText's
|
||||||
// onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles
|
// 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)
|
if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses)
|
||||||
spanned.removeSpan(it)
|
spanned.removeSpan(it)
|
||||||
}
|
}
|
||||||
var textHtml = HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)
|
var textHtml = HtmlCompat.toHtml(spanned, toHtmlFlag)
|
||||||
.replace("\n", "")
|
.replace("\n", "")
|
||||||
.replace(" dir=\"ltr\"", "")
|
.replace(" dir=\"ltr\"", "")
|
||||||
.replace("</b><b>", "")
|
.replace("</b><b>", "")
|
||||||
@ -110,10 +122,15 @@ class TextStylingManager(private val app: App) {
|
|||||||
.replace("</sub><sub>", "")
|
.replace("</sub><sub>", "")
|
||||||
.replace("</sup><sup>", "")
|
.replace("</sup><sup>", "")
|
||||||
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
|
.replace("p style=\"margin-top:0; margin-bottom:0;\"", "p")
|
||||||
|
.replace("<br></p>", "</p><br>")
|
||||||
|
// 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, "</p>$1")
|
||||||
|
|
||||||
config.watchSelectionChanged = true
|
config.watchSelectionChanged = true
|
||||||
|
|
||||||
if (config.htmlCompatibleMode) {
|
if (htmlCompatibleMode) {
|
||||||
textHtml = textHtml
|
textHtml = textHtml
|
||||||
.replace("<br>", "<p> </p>")
|
.replace("<br>", "<p> </p>")
|
||||||
.replace("<b>", "<strong>")
|
.replace("<b>", "<strong>")
|
||||||
|
@ -153,8 +153,9 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginVertical="8dp"
|
android:layout_marginVertical="8dp"
|
||||||
android:minHeight="250dp"
|
android:minHeight="200dp"
|
||||||
android:paddingHorizontal="16dp"
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingBottom="32dp"
|
||||||
android:textIsSelectable="true"
|
android:textIsSelectable="true"
|
||||||
tools:text="To jest treść wiadomości.\n\nZazwyczaj ma wiele linijek.\n\nTak" />
|
tools:text="To jest treść wiadomości.\n\nZazwyczaj ma wiele linijek.\n\nTak" />
|
||||||
|
|
||||||
@ -252,7 +253,8 @@
|
|||||||
android:layout_marginHorizontal="4dp"
|
android:layout_marginHorizontal="4dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:paddingHorizontal="4dp"
|
android:paddingHorizontal="4dp"
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
android:text="@string/message_download"
|
android:text="@string/message_download"
|
||||||
android:textAllCaps="false"
|
android:textAllCaps="false"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
@ -675,7 +675,7 @@
|
|||||||
<string name="messages_compose_confirm_title">Bestätigen Sie das Senden der Nachricht</string>
|
<string name="messages_compose_confirm_title">Bestätigen Sie das Senden der Nachricht</string>
|
||||||
<string name="messages_compose_menu_attachment">Fügen Sie einen Anhang hinzu</string>
|
<string name="messages_compose_menu_attachment">Fügen Sie einen Anhang hinzu</string>
|
||||||
<string name="messages_compose_menu_discard">Nachricht abbrechen</string>
|
<string name="messages_compose_menu_discard">Nachricht abbrechen</string>
|
||||||
<string name="messages_compose_menu_save_draft">Entwurf speichern</string>
|
<string name="messages_compose_save_draft">Entwurf speichern</string>
|
||||||
<string name="messages_compose_menu_send">Senden</string>
|
<string name="messages_compose_menu_send">Senden</string>
|
||||||
<string name="messages_compose_recipient_exists">Dieser Empfänger wurde bereits ausgewählt</string>
|
<string name="messages_compose_recipient_exists">Dieser Empfänger wurde bereits ausgewählt</string>
|
||||||
<string name="messages_compose_recipients_empty">Wählen Sie die Empfänger aus</string>
|
<string name="messages_compose_recipients_empty">Wählen Sie die Empfänger aus</string>
|
||||||
|
@ -677,7 +677,7 @@
|
|||||||
<string name="messages_compose_confirm_title">Confirm sending the message</string>
|
<string name="messages_compose_confirm_title">Confirm sending the message</string>
|
||||||
<string name="messages_compose_menu_attachment">Add an attachment</string>
|
<string name="messages_compose_menu_attachment">Add an attachment</string>
|
||||||
<string name="messages_compose_menu_discard">Abort message</string>
|
<string name="messages_compose_menu_discard">Abort message</string>
|
||||||
<string name="messages_compose_menu_save_draft">Save draft</string>
|
<string name="messages_compose_save_draft">Save draft</string>
|
||||||
<string name="messages_compose_menu_send">Send</string>
|
<string name="messages_compose_menu_send">Send</string>
|
||||||
<string name="messages_compose_recipient_exists">This recipient has already been selected</string>
|
<string name="messages_compose_recipient_exists">This recipient has already been selected</string>
|
||||||
<string name="messages_compose_recipients_empty">Select recipients</string>
|
<string name="messages_compose_recipients_empty">Select recipients</string>
|
||||||
|
@ -730,7 +730,7 @@
|
|||||||
<string name="messages_compose_confirm_title">Potwierdź wysłanie wiadomości</string>
|
<string name="messages_compose_confirm_title">Potwierdź wysłanie wiadomości</string>
|
||||||
<string name="messages_compose_menu_attachment">Dodaj załącznik</string>
|
<string name="messages_compose_menu_attachment">Dodaj załącznik</string>
|
||||||
<string name="messages_compose_menu_discard">Odrzuć wiadomość</string>
|
<string name="messages_compose_menu_discard">Odrzuć wiadomość</string>
|
||||||
<string name="messages_compose_menu_save_draft">Zapisz wersję roboczą</string>
|
<string name="messages_compose_save_draft">Zapisz wersję roboczą</string>
|
||||||
<string name="messages_compose_menu_send">Wyślij</string>
|
<string name="messages_compose_menu_send">Wyślij</string>
|
||||||
<string name="messages_compose_recipient_exists">Ten odbiorca został już wybrany</string>
|
<string name="messages_compose_recipient_exists">Ten odbiorca został już wybrany</string>
|
||||||
<string name="messages_compose_recipients_empty">Wybierz odbiorców</string>
|
<string name="messages_compose_recipients_empty">Wybierz odbiorców</string>
|
||||||
@ -1476,4 +1476,17 @@
|
|||||||
<string name="message_delete">Usuń</string>
|
<string name="message_delete">Usuń</string>
|
||||||
<string name="message_reply">Odpowiedz</string>
|
<string name="message_reply">Odpowiedz</string>
|
||||||
<string name="message_download">Pobierz ponownie</string>
|
<string name="message_download">Pobierz ponownie</string>
|
||||||
|
<string name="messages_compose_subject_reply_format" translatable="false">Re: %s</string>
|
||||||
|
<string name="messages_compose_subject_forward_format" translatable="false">Fwd: %s</string>
|
||||||
|
<string name="messages_compose_body_load_failed">Nie udało się wczytać oryginalnej wiadomości.</string>
|
||||||
|
<string name="messages_compose_save_draft_title">Zapisz zmiany</string>
|
||||||
|
<string name="messages_compose_save_draft_text">Czy chcesz zapisać zmiany jako wersję roboczą?\n\nBędzie możliwa późniejsza edycja oraz wysłanie wiadomości.</string>
|
||||||
|
<string name="discard">Odrzuć</string>
|
||||||
|
<string name="messages_compose_draft_saved">Zapisano wersję roboczą</string>
|
||||||
|
<string name="messages_tab_draft">Wersje robocze</string>
|
||||||
|
<string name="messages_compose_send_long">Wyślij wiadomość</string>
|
||||||
|
<string name="messages_compose_discard_draft">Usuń wersję roboczą</string>
|
||||||
|
<string name="messages_compose_draft_discarded">Usunięto wersję roboczą</string>
|
||||||
|
<string name="messages_compose_discard_draft_title">Usuń wersję roboczą</string>
|
||||||
|
<string name="messages_compose_discard_draft_text">Czy chcesz odrzucić zapisaną wersję wiadomości? Spowoduje to również anulowanie wprowadzonych zmian i usunięcie wiadomości.</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user