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

".toRegex() + } + data class StylingConfig( val editText: TextInputKeyboardEdit, val fontStyleGroup: MaterialButtonToggleGroup, @@ -86,8 +91,15 @@ class TextStylingManager(private val app: App) { .build()*/ } - fun getHtmlText(config: StylingConfig): String { - val spanned = config.editText.text ?: return "" + fun getHtmlText(config: StylingConfig, enableHtmlCompatible: Boolean = true): String { + val text = config.editText.text?.trimEnd() ?: return "" + val spanned = SpannableStringBuilder(text) + + val htmlCompatibleMode = config.htmlCompatibleMode && enableHtmlCompatible + val toHtmlFlag = if (htmlCompatibleMode) + HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL + else + HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE // apparently setting the spans to a different Spannable calls the original EditText's // onSelectionChanged with selectionStart=-1, which in effect unchecks the format toggles @@ -101,7 +113,7 @@ class TextStylingManager(private val app: App) { if (spanStart == spanEnd && it::class.java in BetterHtml.customSpanClasses) spanned.removeSpan(it) } - var textHtml = HtmlCompat.toHtml(spanned, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL) + var textHtml = HtmlCompat.toHtml(spanned, toHtmlFlag) .replace("\n", "") .replace(" dir=\"ltr\"", "") .replace("", "") @@ -110,10 +122,15 @@ class TextStylingManager(private val app: App) { .replace("", "") .replace("", "") .replace("p style=\"margin-top:0; margin-bottom:0;\"", "p") + .replace("

", "


") + // replace multiple newlines so they convert fromHtml correctly + // this should not be breaking with htmlCompatibleMode == true, + // as line breaks cannot occur inside paragraphs with these flags + .replace(paragraphBrRegex, "

$1") config.watchSelectionChanged = true - if (config.htmlCompatibleMode) { + if (htmlCompatibleMode) { textHtml = textHtml .replace("
", "

 

") .replace("", "") diff --git a/app/src/main/res/layout/message_fragment.xml b/app/src/main/res/layout/message_fragment.xml index 5df4e5e6..7687cc4c 100644 --- a/app/src/main/res/layout/message_fragment.xml +++ b/app/src/main/res/layout/message_fragment.xml @@ -153,8 +153,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginVertical="8dp" - android:minHeight="250dp" + android:minHeight="200dp" android:paddingHorizontal="16dp" + android:paddingBottom="32dp" android:textIsSelectable="true" tools:text="To jest treść wiadomości.\n\nZazwyczaj ma wiele linijek.\n\nTak" /> @@ -252,7 +253,8 @@ android:layout_marginHorizontal="4dp" android:layout_weight="1" android:paddingHorizontal="4dp" - android:paddingTop="16dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" android:text="@string/message_download" android:textAllCaps="false" android:visibility="gone" diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 243c83b4..32f33a03 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -675,7 +675,7 @@ Bestätigen Sie das Senden der Nachricht Fügen Sie einen Anhang hinzu Nachricht abbrechen - Entwurf speichern + Entwurf speichern Senden Dieser Empfänger wurde bereits ausgewählt Wählen Sie die Empfänger aus diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 84e3c52b..a971ba01 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -677,7 +677,7 @@ Confirm sending the message Add an attachment Abort message - Save draft + Save draft Send This recipient has already been selected Select recipients diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44864c00..bec3ea36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -730,7 +730,7 @@ Potwierdź wysłanie wiadomości Dodaj załącznik Odrzuć wiadomość - Zapisz wersję roboczą + Zapisz wersję roboczą Wyślij Ten odbiorca został już wybrany Wybierz odbiorców @@ -1476,4 +1476,17 @@ Usuń Odpowiedz Pobierz ponownie + Re: %s + Fwd: %s + Nie udało się wczytać oryginalnej wiadomości. + Zapisz zmiany + Czy chcesz zapisać zmiany jako wersję roboczą?\n\nBędzie możliwa późniejsza edycja oraz wysłanie wiadomości. + Odrzuć + Zapisano wersję roboczą + Wersje robocze + Wyślij wiadomość + Usuń wersję roboczą + Usunięto wersję roboczą + Usuń wersję roboczą + Czy chcesz odrzucić zapisaną wersję wiadomości? Spowoduje to również anulowanie wprowadzonych zmian i usunięcie wiadomości.