full sdk 34 support (#4224)

builds upon work from #4082 

Additionally fixes some deprecations and adds support for [predictive
back](https://developer.android.com/guide/navigation/custom-back/predictive-back-gesture).
I also refactored how the activity transitions work because they are
closely related to predictive back. The awkward
`finishWithoutSlideOutAnimation` is gone, activities that have been
started with slide in will now automatically close with slide out.

To test predictive back you need an emulator or device with Sdk 34
(Android 14) and then enable it in the developer settings.

Predictive back requires the back action to be determined before it
actually occurs so the system can play the right predictive animation,
which made a few reorganisations necessary.

closes #4082 
closes #4005 
unlocks a bunch of dependency upgrades that require sdk 34

---------

Co-authored-by: Goooler <wangzongler@gmail.com>
This commit is contained in:
Konrad Pozniak 2024-02-23 10:27:19 +01:00 committed by GitHub
parent fa8bede7d6
commit b976fe5296
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 272 additions and 186 deletions

View File

@ -22,13 +22,13 @@ final def CUSTOM_INSTANCE = ""
final def SUPPORT_ACCOUNT_URL = "https://mastodon.social/@Tusky" final def SUPPORT_ACCOUNT_URL = "https://mastodon.social/@Tusky"
android { android {
compileSdk 33 compileSdk 34
namespace "com.keylesspalace.tusky" namespace "com.keylesspalace.tusky"
defaultConfig { defaultConfig {
applicationId APP_ID applicationId APP_ID
namespace "com.keylesspalace.tusky" namespace "com.keylesspalace.tusky"
minSdk 24 minSdk 24
targetSdk 33 targetSdk 34
versionCode 117 versionCode 117
versionName "24.1" versionName "24.1"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@ -18,7 +18,8 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/TuskyTheme" android:theme="@style/TuskyTheme"
android:usesCleartextTraffic="false" android:usesCleartextTraffic="false"
android:localeConfig="@xml/locales_config"> android:localeConfig="@xml/locales_config"
android:enableOnBackInvokedCallback="true">
<activity <activity
android:name=".SplashActivity" android:name=".SplashActivity"

View File

@ -21,6 +21,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NoUnderlineURLSpan import com.keylesspalace.tusky.util.NoUnderlineURLSpan
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@ -24,6 +24,7 @@ import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Color; import android.graphics.Color;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.MenuItem; import android.view.MenuItem;
@ -49,6 +50,7 @@ import com.keylesspalace.tusky.interfaces.AccountSelectionListener;
import com.keylesspalace.tusky.interfaces.PermissionRequester; import com.keylesspalace.tusky.interfaces.PermissionRequester;
import com.keylesspalace.tusky.settings.AppTheme; import com.keylesspalace.tusky.settings.AppTheme;
import com.keylesspalace.tusky.settings.PrefKeys; import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.ActivityExtensions;
import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ThemeUtils;
import java.util.ArrayList; import java.util.ArrayList;
@ -60,6 +62,9 @@ import javax.inject.Inject;
import static com.keylesspalace.tusky.settings.PrefKeys.APP_THEME; import static com.keylesspalace.tusky.settings.PrefKeys.APP_THEME;
public abstract class BaseActivity extends AppCompatActivity implements Injectable { public abstract class BaseActivity extends AppCompatActivity implements Injectable {
public static final String OPEN_WITH_SLIDE_IN = "OPEN_WITH_SLIDE_IN";
private static final String TAG = "BaseActivity"; private static final String TAG = "BaseActivity";
@Inject @Inject
@ -73,6 +78,11 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && getIntent().getBooleanExtra(OPEN_WITH_SLIDE_IN, false)) {
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, R.anim.slide_from_right, R.anim.slide_to_left);
overrideActivityTransition(OVERRIDE_TRANSITION_CLOSE, R.anim.slide_from_left, R.anim.slide_to_right);
}
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
/* There isn't presently a way to globally change the theme of a whole application at /* There isn't presently a way to globally change the theme of a whole application at
@ -166,11 +176,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
return style; return style;
} }
public void startActivityWithSlideInAnimation(@NonNull Intent intent) {
super.startActivity(intent);
overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
}
@Override @Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) { public boolean onOptionsItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == android.R.id.home) { if (item.getItemId() == android.R.id.home) {
@ -183,11 +188,10 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
@Override @Override
public void finish() { public void finish() {
super.finish(); super.finish();
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right); // if this activity was opened with slide-in, close it with slide out
} if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE && getIntent().getBooleanExtra(OPEN_WITH_SLIDE_IN, false)) {
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right);
public void finishWithoutSlideOutAnimation() { }
super.finish();
} }
protected void redirectIfNotLoggedIn() { protected void redirectIfNotLoggedIn() {
@ -195,7 +199,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
if (account == null) { if (account == null) {
Intent intent = new Intent(this, LoginActivity.class); Intent intent = new Intent(this, LoginActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivityWithSlideInAnimation(intent); ActivityExtensions.startActivityWithSlideInAnimation(this, intent);
finish(); finish();
} }
} }
@ -235,9 +239,9 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
adapter.addAll(accounts); adapter.addAll(accounts);
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(dialogTitle) .setTitle(dialogTitle)
.setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index))) .setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index)))
.show(); .show();
} }
public @Nullable String getOpenAsText() { public @Nullable String getOpenAsText() {
@ -263,7 +267,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
Intent intent = MainActivity.redirectIntent(this, account.getId(), url); Intent intent = MainActivity.redirectIntent(this, account.getId(), url);
startActivity(intent); startActivity(intent);
finishWithoutSlideOutAnimation(); finish();
} }
@Override @Override

View File

@ -31,6 +31,7 @@ import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.looksLikeMastodonUrl import com.keylesspalace.tusky.util.looksLikeMastodonUrl
import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import javax.inject.Inject import javax.inject.Inject

View File

@ -29,6 +29,7 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -230,18 +231,33 @@ class EditProfileActivity : BaseActivity(), Injectable {
} }
} }
val onBackCallback = object : OnBackPressedCallback(enabled = true) { binding.displayNameEditText.doAfterTextChanged {
override fun handleOnBackPressed() = checkForUnsavedChanges() viewModel.dataChanged(currentProfileData)
}
binding.displayNameEditText.doAfterTextChanged {
viewModel.dataChanged(currentProfileData)
}
binding.lockedCheckBox.setOnCheckedChangeListener { _, _ ->
viewModel.dataChanged(currentProfileData)
}
accountFieldEditAdapter.onFieldsChanged = {
viewModel.dataChanged(currentProfileData)
}
val onBackCallback = object : OnBackPressedCallback(enabled = false) {
override fun handleOnBackPressed() {
showUnsavedChangesDialog()
}
} }
onBackPressedDispatcher.addCallback(this, onBackCallback) onBackPressedDispatcher.addCallback(this, onBackCallback)
} lifecycleScope.launch {
viewModel.isChanged.collect { dataWasChanged ->
fun checkForUnsavedChanges() { onBackCallback.isEnabled = dataWasChanged
if (viewModel.hasUnsavedChanges(currentProfileData)) { }
showUnsavedChangesDialog()
} else {
finish()
} }
} }

View File

@ -43,6 +43,7 @@ import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewmodel.ListsViewModel import com.keylesspalace.tusky.viewmodel.ListsViewModel

View File

@ -51,6 +51,7 @@ import androidx.core.view.GravityCompat
import androidx.core.view.MenuProvider import androidx.core.view.MenuProvider
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.MarginPageTransformer import androidx.viewpager2.widget.MarginPageTransformer
@ -107,6 +108,7 @@ import com.keylesspalace.tusky.util.getDimension
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.reduceSwipeSensitivity import com.keylesspalace.tusky.util.reduceSwipeSensitivity
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.unsafeLazy import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.updateShortcut import com.keylesspalace.tusky.util.updateShortcut
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
@ -185,6 +187,19 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private var directMessageTab: TabLayout.Tab? = null private var directMessageTab: TabLayout.Tab? = null
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
when {
binding.mainDrawerLayout.isOpen -> {
binding.mainDrawerLayout.close()
}
binding.viewPager.currentItem != 0 -> {
binding.viewPager.currentItem = 0
}
}
}
}
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -373,24 +388,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
onBackPressedDispatcher.addCallback( onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
when {
binding.mainDrawerLayout.isOpen -> {
binding.mainDrawerLayout.close()
}
binding.viewPager.currentItem != 0 -> {
binding.viewPager.currentItem = 0
}
else -> {
finish()
}
}
}
}
)
if ( if (
Build.VERSION.SDK_INT >= 33 && Build.VERSION.SDK_INT >= 33 &&
@ -616,6 +614,19 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
) )
setSavedInstance(savedInstanceState) setSavedInstance(savedInstanceState)
} }
binding.mainDrawerLayout.addDrawerListener(object : DrawerLayout.DrawerListener {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) { }
override fun onDrawerOpened(drawerView: View) {
onBackPressedCallback.isEnabled = true
}
override fun onDrawerClosed(drawerView: View) {
onBackPressedCallback.isEnabled = binding.tabLayout.selectedTabPosition > 0
}
override fun onDrawerStateChanged(newState: Int) { }
})
} }
private fun refreshMainDrawerItems( private fun refreshMainDrawerItems(
@ -876,6 +887,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
onTabSelectedListener = object : OnTabSelectedListener { onTabSelectedListener = object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) { override fun onTabSelected(tab: TabLayout.Tab) {
onBackPressedCallback.isEnabled = tab.position > 0 || binding.mainDrawerLayout.isOpen
binding.mainToolbar.title = tab.contentDescription binding.mainToolbar.title = tab.contentDescription
refreshComposeButtonState(tabAdapter, tab.position) refreshComposeButtonState(tabAdapter, tab.position)
@ -964,8 +977,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
intent.putExtras(forward) intent.putExtras(forward)
} }
startActivity(intent) startActivity(intent)
finishWithoutSlideOutAnimation() finish()
overridePendingTransition(R.anim.explode, R.anim.explode) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, R.anim.explode, R.anim.explode)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.explode, R.anim.explode)
}
} }
private fun logout() { private fun logout() {
@ -988,7 +1006,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT) LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
} }
startActivity(intent) startActivity(intent)
finishWithoutSlideOutAnimation() finish()
} }
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)

View File

@ -35,6 +35,7 @@ import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.FilterV1 import com.keylesspalace.tusky.entity.FilterV1
import com.keylesspalace.tusky.util.isHttpNotFound import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector

View File

@ -58,6 +58,7 @@ import com.keylesspalace.tusky.fragment.ViewVideoFragment
import com.keylesspalace.tusky.pager.ImagePagerAdapter import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename import com.keylesspalace.tusky.util.getTemporaryMediaFilename
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector

View File

@ -24,7 +24,9 @@ import com.keylesspalace.tusky.entity.StringField
import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.fixTextSelection import com.keylesspalace.tusky.util.fixTextSelection
class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() { class AccountFieldEditAdapter(
var onFieldsChanged: () -> Unit = { }
) : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() {
private val fieldData = mutableListOf<MutableStringPair>() private val fieldData = mutableListOf<MutableStringPair>()
private var maxNameLength: Int? = null private var maxNameLength: Int? = null
@ -90,10 +92,12 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
holder.binding.accountFieldNameText.doAfterTextChanged { newText -> holder.binding.accountFieldNameText.doAfterTextChanged { newText ->
fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString() fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString()
onFieldsChanged()
} }
holder.binding.accountFieldValueText.doAfterTextChanged { newText -> holder.binding.accountFieldValueText.doAfterTextChanged { newText ->
fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString() fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString()
onFieldsChanged()
} }
// Ensure the textview contents are selectable // Ensure the textview contents are selectable

View File

@ -92,6 +92,7 @@ import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.reduceSwipeSensitivity import com.keylesspalace.tusky.util.reduceSwipeSensitivity
import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.setClickableText
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.unsafeLazy import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible

View File

@ -31,7 +31,6 @@ import at.connyduck.calladapter.networkresult.fold
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
import autodispose2.autoDispose import autodispose2.autoDispose
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.PostLookupFallbackBehavior import com.keylesspalace.tusky.PostLookupFallbackBehavior
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
@ -56,6 +55,7 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.view.EndlessOnScrollListener
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@ -144,17 +144,13 @@ class AccountListFragment :
} }
override fun onViewTag(tag: String) { override fun onViewTag(tag: String) {
(activity as BaseActivity?) activity?.startActivityWithSlideInAnimation(
?.startActivityWithSlideInAnimation( StatusListActivity.newHashtagIntent(requireContext(), tag)
StatusListActivity.newHashtagIntent(requireContext(), tag) )
)
} }
override fun onViewAccount(id: String) { override fun onViewAccount(id: String) {
(activity as BaseActivity?)?.let { activity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id))
val intent = AccountActivity.getIntent(it, id)
it.startActivityWithSlideInAnimation(intent)
}
} }
override fun onViewUrl(url: String) { override fun onViewUrl(url: String) {

View File

@ -44,6 +44,7 @@ import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.unsafeLazy import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EmojiPicker import com.keylesspalace.tusky.view.EmojiPicker

View File

@ -70,6 +70,7 @@ import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageContract import com.canhub.cropper.CropImageContract
import com.canhub.cropper.options import com.canhub.cropper.options
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
@ -215,6 +216,24 @@ class ComposeActivity :
viewModel.cropImageItemOld = null viewModel.cropImageItemOld = null
} }
private val onBackPressedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
) {
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
return
}
handleCloseButton()
}
}
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -425,7 +444,10 @@ class ComposeActivity :
if (startingContentWarning != null) { if (startingContentWarning != null) {
binding.composeContentWarningField.setText(startingContentWarning) binding.composeContentWarningField.setText(startingContentWarning)
} }
binding.composeContentWarningField.doOnTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } binding.composeContentWarningField.doOnTextChanged { newContentWarning, _, _, _ ->
updateVisibleCharactersLeft()
viewModel.updateContentWarning(newContentWarning?.toString())
}
} }
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) { private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
@ -456,6 +478,7 @@ class ComposeActivity :
binding.composeEditField.doAfterTextChanged { editable -> binding.composeEditField.doAfterTextChanged { editable ->
highlightSpans(editable!!, mentionColour) highlightSpans(editable!!, mentionColour)
updateVisibleCharactersLeft() updateVisibleCharactersLeft()
viewModel.updateContent(editable.toString())
} }
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093 // work around Android platform bug -> https://issuetracker.google.com/issues/67102093
@ -547,6 +570,12 @@ class ComposeActivity :
} }
} }
} }
lifecycleScope.launch {
viewModel.closeConfirmation.collect {
updateOnBackPressedCallbackState()
}
}
} }
private fun setupButtons() { private fun setupButtons() {
@ -557,6 +586,17 @@ class ComposeActivity :
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView) scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
emojiBehavior = BottomSheetBehavior.from(binding.emojiView) emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
val bottomSheetCallback = object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
updateOnBackPressedCallbackState()
}
override fun onSlide(bottomSheet: View, slideOffset: Float) { }
}
composeOptionsBehavior.addBottomSheetCallback(bottomSheetCallback)
addMediaBehavior.addBottomSheetCallback(bottomSheetCallback)
scheduleBehavior.addBottomSheetCallback(bottomSheetCallback)
emojiBehavior.addBottomSheetCallback(bottomSheetCallback)
enableButton(binding.composeEmojiButton, clickable = false, colorActive = false) enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)
// Setup the interface buttons. // Setup the interface buttons.
@ -618,26 +658,7 @@ class ComposeActivity :
binding.actionPhotoPick.setOnClickListener { onMediaPick() } binding.actionPhotoPick.setOnClickListener { onMediaPick() }
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() } binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
onBackPressedDispatcher.addCallback( onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
) {
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
return
}
handleCloseButton()
}
}
)
} }
private fun setupLanguageSpinner(initialLanguages: List<String>) { private fun setupLanguageSpinner(initialLanguages: List<String>) {
@ -690,6 +711,15 @@ class ComposeActivity :
) )
} }
private fun updateOnBackPressedCallbackState() {
val confirmation = viewModel.closeConfirmation.value
onBackPressedCallback.isEnabled = confirmation != ConfirmationKind.NONE ||
composeOptionsBehavior.state != BottomSheetBehavior.STATE_HIDDEN ||
addMediaBehavior.state != BottomSheetBehavior.STATE_HIDDEN ||
emojiBehavior.state != BottomSheetBehavior.STATE_HIDDEN ||
scheduleBehavior.state != BottomSheetBehavior.STATE_HIDDEN
}
private fun replaceTextAtCaret(text: CharSequence) { private fun replaceTextAtCaret(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd // If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = binding.composeEditField.selectionStart.coerceAtMost( val start = binding.composeEditField.selectionStart.coerceAtMost(
@ -1004,7 +1034,7 @@ class ComposeActivity :
} }
private fun removePoll() { private fun removePoll() {
viewModel.poll.value = null viewModel.updatePoll(null)
binding.pollPreview.hide() binding.pollPreview.hide()
} }
@ -1219,7 +1249,7 @@ class ComposeActivity :
} }
private fun pickMedia(uri: Uri, description: String? = null) { private fun pickMedia(uri: Uri, description: String? = null) {
var sanitizedDescription = sanitizePickMediaDescription(description) val sanitizedDescription = sanitizePickMediaDescription(description)
lifecycleScope.launch { lifecycleScope.launch {
viewModel.pickMedia(uri, sanitizedDescription).onFailure { throwable -> viewModel.pickMedia(uri, sanitizedDescription).onFailure { throwable ->
@ -1292,10 +1322,10 @@ class ComposeActivity :
private fun handleCloseButton() { private fun handleCloseButton() {
val contentText = binding.composeEditField.text.toString() val contentText = binding.composeEditField.text.toString()
val contentWarning = binding.composeContentWarningField.text.toString() val contentWarning = binding.composeContentWarningField.text.toString()
when (viewModel.handleCloseButton(contentText, contentWarning)) { when (viewModel.closeConfirmation.value) {
ConfirmationKind.NONE -> { ConfirmationKind.NONE -> {
viewModel.stopUploads() viewModel.stopUploads()
finishWithoutSlideOutAnimation() finish()
} }
ConfirmationKind.SAVE_OR_DISCARD -> ConfirmationKind.SAVE_OR_DISCARD ->
getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show() getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show()
@ -1355,7 +1385,7 @@ class ComposeActivity :
} }
.setNegativeButton(R.string.action_discard) { _, _ -> .setNegativeButton(R.string.action_discard) { _, _ ->
viewModel.stopUploads() viewModel.stopUploads()
finishWithoutSlideOutAnimation() finish()
} }
} }
@ -1371,7 +1401,7 @@ class ComposeActivity :
} }
.setNegativeButton(R.string.action_discard) { _, _ -> .setNegativeButton(R.string.action_discard) { _, _ ->
viewModel.stopUploads() viewModel.stopUploads()
finishWithoutSlideOutAnimation() finish()
} }
} }
@ -1385,7 +1415,7 @@ class ComposeActivity :
.setPositiveButton(R.string.action_delete) { _, _ -> .setPositiveButton(R.string.action_delete) { _, _ ->
viewModel.deleteDraft() viewModel.deleteDraft()
viewModel.stopUploads() viewModel.stopUploads()
finishWithoutSlideOutAnimation() finish()
} }
.setNegativeButton(R.string.action_continue_edit) { _, _ -> .setNegativeButton(R.string.action_continue_edit) { _, _ ->
// Do nothing, dialog will dismiss, user can continue editing // Do nothing, dialog will dismiss, user can continue editing
@ -1394,7 +1424,7 @@ class ComposeActivity :
private fun deleteDraftAndFinish() { private fun deleteDraftAndFinish() {
viewModel.deleteDraft() viewModel.deleteDraft()
finishWithoutSlideOutAnimation() finish()
} }
private fun saveDraftAndFinish(contentText: String, contentWarning: String) { private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
@ -1412,7 +1442,7 @@ class ComposeActivity :
} }
viewModel.saveDraft(contentText, contentWarning) viewModel.saveDraft(contentText, contentWarning)
dialog?.cancel() dialog?.cancel()
finishWithoutSlideOutAnimation() finish()
} }
} }

View File

@ -76,6 +76,9 @@ class ComposeViewModel @Inject constructor(
private var modifiedInitialState: Boolean = false private var modifiedInitialState: Boolean = false
private var hasScheduledTimeChanged: Boolean = false private var hasScheduledTimeChanged: Boolean = false
private var currentContent: String? = ""
private var currentContentWarning: String? = ""
val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow() val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
@ -99,6 +102,8 @@ class ComposeViewModel @Inject constructor(
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST
) )
val closeConfirmation = MutableStateFlow(ConfirmationKind.NONE)
private lateinit var composeKind: ComposeKind private lateinit var composeKind: ComposeKind
// Used in ComposeActivity to pass state to result function when cropImage contract inflight // Used in ComposeActivity to pass state to result function when cropImage contract inflight
@ -199,6 +204,7 @@ class ComposeViewModel @Inject constructor(
} }
} }
} }
updateCloseConfirmation()
return mediaItem return mediaItem
} }
@ -228,21 +234,37 @@ class ComposeViewModel @Inject constructor(
fun removeMediaFromQueue(item: QueuedMedia) { fun removeMediaFromQueue(item: QueuedMedia) {
mediaUploader.cancelUploadScope(item.localId) mediaUploader.cancelUploadScope(item.localId)
media.update { mediaList -> mediaList.filter { it.localId != item.localId } } media.update { mediaList -> mediaList.filter { it.localId != item.localId } }
updateCloseConfirmation()
} }
fun toggleMarkSensitive() { fun toggleMarkSensitive() {
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
} }
fun handleCloseButton(contentText: String?, contentWarning: String?): ConfirmationKind { fun updateContent(newContent: String?) {
return if (didChange(contentText, contentWarning)) { currentContent = newContent
updateCloseConfirmation()
}
fun updateContentWarning(newContentWarning: String?) {
currentContentWarning = newContentWarning
updateCloseConfirmation()
}
private fun updateCloseConfirmation() {
val contentWarning = if (showContentWarning.value) {
currentContentWarning
} else {
""
}
this.closeConfirmation.value = if (didChange(currentContent, contentWarning)) {
when (composeKind) { when (composeKind) {
ComposeKind.NEW -> if (isEmpty(contentText, contentWarning)) { ComposeKind.NEW -> if (isEmpty(currentContent, contentWarning)) {
ConfirmationKind.NONE ConfirmationKind.NONE
} else { } else {
ConfirmationKind.SAVE_OR_DISCARD ConfirmationKind.SAVE_OR_DISCARD
} }
ComposeKind.EDIT_DRAFT -> if (isEmpty(contentText, contentWarning)) { ComposeKind.EDIT_DRAFT -> if (isEmpty(currentContent, contentWarning)) {
ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT
} else { } else {
ConfirmationKind.UPDATE_OR_DISCARD ConfirmationKind.UPDATE_OR_DISCARD
@ -272,6 +294,7 @@ class ComposeViewModel @Inject constructor(
fun contentWarningChanged(value: Boolean) { fun contentWarningChanged(value: Boolean) {
showContentWarning.value = value showContentWarning.value = value
contentWarningStateChanged = true contentWarningStateChanged = true
updateCloseConfirmation()
} }
fun deleteDraft() { fun deleteDraft() {
@ -511,11 +534,14 @@ class ComposeViewModel @Inject constructor(
replyingStatusContent = composeOptions?.replyingStatusContent replyingStatusContent = composeOptions?.replyingStatusContent
replyingStatusAuthor = composeOptions?.replyingStatusAuthor replyingStatusAuthor = composeOptions?.replyingStatusAuthor
updateCloseConfirmation()
setupComplete = true setupComplete = true
} }
fun updatePoll(newPoll: NewPoll) { fun updatePoll(newPoll: NewPoll?) {
poll.value = newPoll poll.value = newPoll
updateCloseConfirmation()
} }
fun updateScheduledAt(newScheduledAt: String?) { fun updateScheduledAt(newScheduledAt: String?) {

View File

@ -12,6 +12,7 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import javax.inject.Inject import javax.inject.Inject
@ -110,8 +111,7 @@ class FiltersActivity : BaseActivity(), FiltersListener {
putExtra(EditFilterActivity.FILTER_TO_EDIT, filter) putExtra(EditFilterActivity.FILTER_TO_EDIT, filter)
} }
} }
startActivity(intent) startActivityWithSlideInAnimation(intent)
overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
} }
override fun deleteFilter(filter: Filter) { override fun deleteFilter(filter: Filter) {

View File

@ -227,14 +227,10 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
super.onDestroy() super.onDestroy()
} }
override fun finish() {
super.finishWithoutSlideOutAnimation()
}
override fun requiresLogin() = false override fun requiresLogin() = false
private fun sendResult(result: LoginResult) { private fun sendResult(result: LoginResult) {
setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result)) setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result))
finishWithoutSlideOutAnimation() finish()
} }
} }

View File

@ -51,6 +51,7 @@ import com.keylesspalace.tusky.util.getInitialLanguages
import com.keylesspalace.tusky.util.getLocaleList import com.keylesspalace.tusky.util.getLocaleList
import com.keylesspalace.tusky.util.getTuskyDisplayName import com.keylesspalace.tusky.util.getTuskyDisplayName
import com.keylesspalace.tusky.util.makeIcon import com.keylesspalace.tusky.util.makeIcon
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.unsafeLazy import com.keylesspalace.tusky.util.unsafeLazy
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -100,11 +101,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setIcon(R.drawable.ic_tabs) setIcon(R.drawable.ic_tabs)
setOnPreferenceClickListener { setOnPreferenceClickListener {
val intent = Intent(context, TabPreferenceActivity::class.java) val intent = Intent(context, TabPreferenceActivity::class.java)
activity?.startActivity(intent) activity?.startActivityWithSlideInAnimation(intent)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true true
} }
} }
@ -114,11 +111,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setIcon(R.drawable.ic_hashtag) setIcon(R.drawable.ic_hashtag)
setOnPreferenceClickListener { setOnPreferenceClickListener {
val intent = Intent(context, FollowedTagsActivity::class.java) val intent = Intent(context, FollowedTagsActivity::class.java)
activity?.startActivity(intent) activity?.startActivityWithSlideInAnimation(intent)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true true
} }
} }
@ -129,11 +122,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setOnPreferenceClickListener { setOnPreferenceClickListener {
val intent = Intent(context, AccountListActivity::class.java) val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.MUTES) intent.putExtra("type", AccountListActivity.Type.MUTES)
activity?.startActivity(intent) activity?.startActivityWithSlideInAnimation(intent)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true true
} }
} }
@ -147,11 +136,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setOnPreferenceClickListener { setOnPreferenceClickListener {
val intent = Intent(context, AccountListActivity::class.java) val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.BLOCKS) intent.putExtra("type", AccountListActivity.Type.BLOCKS)
activity?.startActivity(intent) activity?.startActivityWithSlideInAnimation(intent)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true true
} }
} }
@ -161,11 +146,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setIcon(R.drawable.ic_mute_24dp) setIcon(R.drawable.ic_mute_24dp)
setOnPreferenceClickListener { setOnPreferenceClickListener {
val intent = Intent(context, DomainBlocksActivity::class.java) val intent = Intent(context, DomainBlocksActivity::class.java)
activity?.startActivity(intent) activity?.startActivityWithSlideInAnimation(intent)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true true
} }
} }
@ -176,7 +157,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setIcon(R.drawable.ic_logout) setIcon(R.drawable.ic_logout)
setOnPreferenceClickListener { setOnPreferenceClickListener {
val intent = LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION) val intent = LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)
(activity as BaseActivity).startActivityWithSlideInAnimation(intent) activity?.startActivityWithSlideInAnimation(intent)
true true
} }
} }
@ -300,8 +281,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
it, it,
PreferencesActivity.NOTIFICATION_PREFERENCES PreferencesActivity.NOTIFICATION_PREFERENCES
) )
it.startActivity(intent) it.startActivityWithSlideInAnimation(intent)
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
} }
} }
} }
@ -368,8 +348,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
private fun launchFilterActivity() { private fun launchFilterActivity() {
val intent = Intent(context, FiltersActivity::class.java) val intent = Intent(context, FiltersActivity::class.java)
activity?.startActivity(intent) (activity as? BaseActivity)?.startActivityWithSlideInAnimation(intent)
activity?.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
} }
companion object { companion object {

View File

@ -38,6 +38,7 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.setAppNightMode import com.keylesspalace.tusky.util.setAppNightMode
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import javax.inject.Inject import javax.inject.Inject
@ -139,16 +140,14 @@ class PreferencesActivity :
).unregisterOnSharedPreferenceChangeListener(this) ).unregisterOnSharedPreferenceChangeListener(this)
} }
private fun saveInstanceState(outState: Bundle) {
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled) outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled)
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
sharedPreferences ?: return
key ?: return
when (key) { when (key) {
APP_THEME -> { APP_THEME -> {
val theme = sharedPreferences.getNonNullString(APP_THEME, AppTheme.DEFAULT.value) val theme = sharedPreferences.getNonNullString(APP_THEME, AppTheme.DEFAULT.value)
@ -156,11 +155,11 @@ class PreferencesActivity :
setAppNightMode(theme) setAppNightMode(theme)
restartActivitiesOnBackPressedCallback.isEnabled = true restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity() this.recreate()
} }
PrefKeys.UI_TEXT_SCALE_RATIO -> { PrefKeys.UI_TEXT_SCALE_RATIO -> {
restartActivitiesOnBackPressedCallback.isEnabled = true restartActivitiesOnBackPressedCallback.isEnabled = true
this.restartCurrentActivity() this.recreate()
} }
PrefKeys.STATUS_TEXT_SIZE, PrefKeys.ABSOLUTE_TIME_VIEW, PrefKeys.SHOW_BOT_OVERLAY, PrefKeys.ANIMATE_GIF_AVATARS, PrefKeys.USE_BLURHASH, PrefKeys.STATUS_TEXT_SIZE, PrefKeys.ABSOLUTE_TIME_VIEW, PrefKeys.SHOW_BOT_OVERLAY, PrefKeys.ANIMATE_GIF_AVATARS, PrefKeys.USE_BLURHASH,
PrefKeys.SHOW_SELF_USERNAME, PrefKeys.SHOW_CARDS_IN_TIMELINES, PrefKeys.CONFIRM_REBLOGS, PrefKeys.CONFIRM_FAVOURITES, PrefKeys.SHOW_SELF_USERNAME, PrefKeys.SHOW_CARDS_IN_TIMELINES, PrefKeys.CONFIRM_REBLOGS, PrefKeys.CONFIRM_FAVOURITES,
@ -173,16 +172,6 @@ class PreferencesActivity :
} }
} }
private fun restartCurrentActivity() {
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val savedInstanceState = Bundle()
saveInstanceState(savedInstanceState)
intent.putExtras(savedInstanceState)
startActivityWithSlideInAnimation(intent)
finish()
overridePendingTransition(R.anim.fade_in, R.anim.fade_out)
}
override fun androidInjector() = androidInjector override fun androidInjector() = androidInjector
companion object { companion object {

View File

@ -97,10 +97,6 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector, MenuProvider,
return false return false
} }
override fun finish() {
super.finishWithoutSlideOutAnimation()
}
private fun getPageTitle(position: Int): CharSequence { private fun getPageTitle(position: Int): CharSequence {
return when (position) { return when (position) {
0 -> getString(R.string.title_posts) 0 -> getString(R.string.title_posts)

View File

@ -28,6 +28,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable

View File

@ -58,6 +58,7 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData

View File

@ -39,7 +39,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.autoDispose import autodispose2.androidx.lifecycle.autoDispose
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
@ -66,6 +65,7 @@ import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.unsafeLazy import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
@ -463,13 +463,13 @@ class TimelineFragment :
override fun onShowReblogs(position: Int) { override fun onShowReblogs(position: Int) {
val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return
val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId)
(activity as BaseActivity).startActivityWithSlideInAnimation(intent) activity?.startActivityWithSlideInAnimation(intent)
} }
override fun onShowFavs(position: Int) { override fun onShowFavs(position: Int) {
val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return val statusId = adapter.peek(position)?.asStatusOrNull()?.id ?: return
val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId)
(activity as BaseActivity).startActivityWithSlideInAnimation(intent) activity?.startActivityWithSlideInAnimation(intent)
} }
override fun onLoadMore(position: Int) { override fun onLoadMore(position: Int) {

View File

@ -30,7 +30,6 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.components.trending.viewmodel.TrendingTagsViewModel import com.keylesspalace.tusky.components.trending.viewmodel.TrendingTagsViewModel
@ -42,6 +41,7 @@ import com.keylesspalace.tusky.interfaces.RefreshableFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.TrendingViewData import com.keylesspalace.tusky.viewdata.TrendingViewData
import javax.inject.Inject import javax.inject.Inject
@ -136,7 +136,7 @@ class TrendingTagsFragment :
} }
fun onViewTag(tag: String) { fun onViewTag(tag: String) {
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation( requireActivity().startActivityWithSlideInAnimation(
StatusListActivity.newHashtagIntent(requireContext(), tag) StatusListActivity.newHashtagIntent(requireContext(), tag)
) )
} }

View File

@ -36,7 +36,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.accountlist.AccountListActivity import com.keylesspalace.tusky.components.accountlist.AccountListActivity
import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Companion.newIntent import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Companion.newIntent
@ -53,6 +52,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.openLink import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewdata.AttachmentViewData.Companion.list import com.keylesspalace.tusky.viewdata.AttachmentViewData.Companion.list
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
@ -386,13 +386,13 @@ class ViewThreadFragment :
override fun onShowReblogs(position: Int) { override fun onShowReblogs(position: Int) {
val statusId = adapter.currentList[position].id val statusId = adapter.currentList[position].id
val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId)
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent) requireActivity().startActivityWithSlideInAnimation(intent)
} }
override fun onShowFavs(position: Int) { override fun onShowFavs(position: Int) {
val statusId = adapter.currentList[position].id val statusId = adapter.currentList[position].id
val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId)
(requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent) requireActivity().startActivityWithSlideInAnimation(intent)
} }
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {

View File

@ -46,6 +46,7 @@ import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.loadAvatar import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.unicodeWrap import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable

View File

@ -171,7 +171,7 @@ class ViewVideoFragment : ViewMediaFragment(), Injectable {
/** A fling up/down should dismiss the fragment */ /** A fling up/down should dismiss the fragment */
override fun onFling( override fun onFling(
e1: MotionEvent, e1: MotionEvent?,
e2: MotionEvent, e2: MotionEvent,
velocityX: Float, velocityX: Float,
velocityY: Float velocityY: Float

View File

@ -15,7 +15,9 @@
package com.keylesspalace.tusky.service package com.keylesspalace.tusky.service
import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.os.Build
import android.service.quicksettings.TileService import android.service.quicksettings.TileService
import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
@ -29,6 +31,13 @@ class TuskyTileService : TileService() {
override fun onClick() { override fun onClick() {
val intent = MainActivity.composeIntent(this, ComposeActivity.ComposeOptions()) val intent = MainActivity.composeIntent(this, ComposeActivity.ComposeOptions())
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivityAndCollapse(intent)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
val pendingIntent = PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_IMMUTABLE)
startActivityAndCollapse(pendingIntent)
} else {
@Suppress("DEPRECATION")
startActivityAndCollapse(intent)
}
} }
} }

View File

@ -0,0 +1,21 @@
@file:JvmName("ActivityExtensions")
package com.keylesspalace.tusky.util
import android.app.Activity
import android.content.Intent
import android.os.Build
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
fun Activity.startActivityWithSlideInAnimation(intent: Intent) {
// the new transition api needs to be called by the activity that is the result of the transition,
// so we pass a flag that BaseActivity will respect.
intent.putExtra(BaseActivity.OPEN_WITH_SLIDE_IN, true)
startActivity(intent)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
// the old api needs to be called by the activity that starts the transition
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
}

View File

@ -373,21 +373,21 @@ class ClickableSpanTextView @JvmOverloads constructor(
return firstDiff < secondDiff return firstDiff < secondDiff
} }
override fun onDraw(canvas: Canvas?) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
// Paint span boundaries. Optimised out on release builds, or debug builds where // Paint span boundaries. Optimised out on release builds, or debug builds where
// showSpanBoundaries is false. // showSpanBoundaries is false.
if (BuildConfig.DEBUG && showSpanBoundaries) { if (BuildConfig.DEBUG && showSpanBoundaries) {
canvas?.save() canvas.save()
for (entry in delegateRects) { for (entry in delegateRects) {
canvas?.drawRect(entry.key, paddingDebugPaint) canvas.drawRect(entry.key, paddingDebugPaint)
} }
for (entry in spanRects) { for (entry in spanRects) {
canvas?.drawRect(entry.key, spanDebugPaint) canvas.drawRect(entry.key, spanDebugPaint)
} }
canvas?.restore() canvas.restore()
} }
} }

View File

@ -265,14 +265,14 @@ class GraphView @JvmOverloads constructor(
private fun dataSpacing(data: List<Any>) = width.toFloat() / max(data.size - 1, 1).toFloat() private fun dataSpacing(data: List<Any>) = width.toFloat() / max(data.size - 1, 1).toFloat()
override fun onDraw(canvas: Canvas?) { override fun onDraw(canvas: Canvas) {
super.onDraw(canvas) super.onDraw(canvas)
if (primaryLinePath.isEmpty && width > 0) { if (primaryLinePath.isEmpty && width > 0) {
initializeVertices() initializeVertices()
} }
canvas?.apply { canvas.apply {
drawRect(sizeRect, graphPaint) drawRect(sizeRect, graphPaint)
val pointDistance = dataSpacing(primaryLineData) val pointDistance = dataSpacing(primaryLineData)

View File

@ -38,6 +38,7 @@ import com.keylesspalace.tusky.util.randomAlphanumericString
import java.io.File import java.io.File
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
@ -72,6 +73,8 @@ class EditProfileViewModel @Inject constructor(
val instanceData: Flow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow() val instanceData: Flow<InstanceInfo> = instanceInfoRepo::getInstanceInfo.asFlow()
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val isChanged = MutableStateFlow(false)
private var apiProfileAccount: Account? = null private var apiProfileAccount: Account? = null
fun obtainProfile() = viewModelScope.launch { fun obtainProfile() = viewModelScope.launch {
@ -102,6 +105,10 @@ class EditProfileViewModel @Inject constructor(
headerData.value = getHeaderUri() headerData.value = getHeaderUri()
} }
internal fun dataChanged(newProfileData: ProfileDataInUi) {
isChanged.value = getProfileDiff(apiProfileAccount, newProfileData).hasChanges()
}
internal fun save(newProfileData: ProfileDataInUi) { internal fun save(newProfileData: ProfileDataInUi) {
if (saveData.value is Loading || profileData.value !is Success) { if (saveData.value is Loading || profileData.value !is Success) {
return return
@ -178,12 +185,6 @@ class EditProfileViewModel @Inject constructor(
} }
} }
internal fun hasUnsavedChanges(newProfileData: ProfileDataInUi): Boolean {
val diff = getProfileDiff(apiProfileAccount, newProfileData)
return diff.hasChanges()
}
private fun getProfileDiff( private fun getProfileDiff(
oldProfileAccount: Account?, oldProfileAccount: Account?,
newProfileData: ProfileDataInUi newProfileData: ProfileDataInUi

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator"
android:fromAlpha="0"
android:toAlpha="1"
android:duration="300" />

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator"
android:fromAlpha="1"
android:toAlpha="0"
android:duration="300" />

View File

@ -23,8 +23,10 @@ import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(ParameterizedRobolectricTestRunner::class) @RunWith(ParameterizedRobolectricTestRunner::class)
@Config(sdk = [33])
class StatusLengthTest( class StatusLengthTest(
private val text: String, private val text: String,
private val expectedLength: Int private val expectedLength: Int