forked from github/szkolny
[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,6 +1,7 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<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 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<out Parcelable>)
|
||||
}
|
||||
}
|
||||
|
@ -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!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,6 +51,9 @@ abstract class MessageDao : BaseDao<Message, MessageFull> {
|
||||
@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")
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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<LinearLayoutManager.SavedState>("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()
|
||||
))
|
||||
}
|
||||
|
@ -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<Teacher>()
|
||||
private val teachers = mutableListOf<Teacher>()
|
||||
|
||||
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<Teacher>) { 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<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() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
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 {
|
||||
|
@ -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<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? {
|
||||
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<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
|
||||
|
||||
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 {
|
||||
"((?:<br>)+)</p>".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("</b><b>", "")
|
||||
@ -110,10 +122,15 @@ class TextStylingManager(private val app: App) {
|
||||
.replace("</sub><sub>", "")
|
||||
.replace("</sup><sup>", "")
|
||||
.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
|
||||
|
||||
if (config.htmlCompatibleMode) {
|
||||
if (htmlCompatibleMode) {
|
||||
textHtml = textHtml
|
||||
.replace("<br>", "<p> </p>")
|
||||
.replace("<b>", "<strong>")
|
||||
|
@ -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"
|
||||
|
@ -675,7 +675,7 @@
|
||||
<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_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_recipient_exists">Dieser Empfänger wurde bereits ausgewählt</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_menu_attachment">Add an attachment</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_recipient_exists">This recipient has already been selected</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_menu_attachment">Dodaj załącznik</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_recipient_exists">Ten odbiorca został już wybrany</string>
|
||||
<string name="messages_compose_recipients_empty">Wybierz odbiorców</string>
|
||||
@ -1476,4 +1476,17 @@
|
||||
<string name="message_delete">Usuń</string>
|
||||
<string name="message_reply">Odpowiedz</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>
|
||||
|
Loading…
Reference in New Issue
Block a user