Merge tag 'v25.2' into develop

# Conflicts:
#	README.md
#	app/build.gradle
#	app/lint-baseline.xml
#	app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt
#	app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt
#	app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
#	app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt
#	app/src/main/res/layout/activity_about.xml
#	app/src/main/res/layout/item_emoji_pref.xml
#	app/src/main/res/values-ar/strings.xml
#	app/src/main/res/values-bg/strings.xml
#	app/src/main/res/values-cy/strings.xml
#	app/src/main/res/values-de/strings.xml
#	app/src/main/res/values-fa/strings.xml
#	app/src/main/res/values-gd/strings.xml
#	app/src/main/res/values-gl/strings.xml
#	app/src/main/res/values-hu/strings.xml
#	app/src/main/res/values-is/strings.xml
#	app/src/main/res/values-it/strings.xml
#	app/src/main/res/values-ja/strings.xml
#	app/src/main/res/values-nl/strings.xml
#	app/src/main/res/values-oc/strings.xml
#	app/src/main/res/values-pt-rBR/strings.xml
#	app/src/main/res/values-pt-rPT/strings.xml
#	app/src/main/res/values-ru/strings.xml
#	app/src/main/res/values-si/strings.xml
#	app/src/main/res/values-sv/strings.xml
#	app/src/main/res/values-tr/strings.xml
#	app/src/main/res/values-uk/strings.xml
#	app/src/main/res/values-vi/strings.xml
#	app/src/main/res/values-zh-rCN/strings.xml
#	app/src/main/res/values/strings.xml
#	fastlane/metadata/android/ru/full_description.txt
#	fastlane/metadata/android/zh-Hans/full_description.txt
This commit is contained in:
Mike Barnes 2026-01-02 18:27:41 +11:00
commit 875013e47f
630 changed files with 22153 additions and 18732 deletions

View file

@ -16,6 +16,8 @@
package com.keylesspalace.tusky
import android.Manifest
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
@ -26,6 +28,7 @@ import android.graphics.drawable.Animatable
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
@ -33,6 +36,7 @@ import android.view.KeyEvent
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
import android.view.View
import android.widget.ImageView
import androidx.activity.OnBackPressedCallback
@ -41,15 +45,18 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.IntentCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.view.GravityCompat
import androidx.core.view.MenuProvider
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.MarginPageTransformer
import at.connyduck.calladapter.networkresult.fold
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.target.FixedSizeDrawable
@ -60,8 +67,11 @@ import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
import com.keylesspalace.tusky.appstore.CacheUpdater
import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
import com.keylesspalace.tusky.appstore.NewNotificationsEvent
import com.keylesspalace.tusky.appstore.NotificationsLoadingEvent
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.components.accountlist.AccountListActivity
@ -81,8 +91,10 @@ import com.keylesspalace.tusky.components.trending.TrendingActivity
import com.keylesspalace.tusky.databinding.ActivityMainBinding
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.DraftsAlert
import com.keylesspalace.tusky.di.ApplicationScope
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import com.keylesspalace.tusky.interfaces.FabFragment
@ -91,16 +103,17 @@ import com.keylesspalace.tusky.pager.MainPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.usecase.DeveloperToolsUseCase
import com.keylesspalace.tusky.usecase.LogoutUsecase
import com.keylesspalace.tusky.util.ShareShortcutHelper
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.getDimension
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.reduceSwipeSensitivity
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation
import com.keylesspalace.tusky.util.supportsOverridingActivityTransitions
import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.updateShortcut
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
@ -131,9 +144,10 @@ import com.mikepenz.materialdrawer.widget.AccountHeaderView
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider {
@Inject
@ -154,21 +168,23 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
@Inject
lateinit var developerToolsUseCase: DeveloperToolsUseCase
@Inject
lateinit var shareShortcutHelper: ShareShortcutHelper
@Inject
@ApplicationScope
lateinit var externalScope: CoroutineScope
private val binding by viewBinding(ActivityMainBinding::inflate)
private lateinit var header: AccountHeaderView
private var notificationTabPosition = 0
private var onTabSelectedListener: OnTabSelectedListener? = null
private var unreadAnnouncementsCount = 0
private val preferences by unsafeLazy { PreferenceManager.getDefaultSharedPreferences(this) }
private lateinit var glide: RequestManager
private var accountLocked: Boolean = false
// We need to know if the emoji pack has been changed
private var selectedEmojiPack: String? = null
@ -178,37 +194,68 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
/** Adapter for the different timeline tabs */
private lateinit var tabAdapter: MainPagerAdapter
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")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activeAccount = accountManager.activeAccount
?: return // will be redirected to LoginActivity by BaseActivity
if (supportsOverridingActivityTransitions() && explodeAnimationWasRequested()) {
overrideActivityTransition(OVERRIDE_TRANSITION_OPEN, R.anim.explode, R.anim.activity_open_exit)
}
var showNotificationTab = false
if (intent != null) {
// check for savedInstanceState in order to not handle intent events more than once
if (intent != null && savedInstanceState == null) {
val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1)
if (notificationId != -1) {
// opened from a notification action, cancel the notification
val notificationManager = getSystemService(
NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.cancel(intent.getStringExtra(NOTIFICATION_TAG), notificationId)
}
/** there are two possibilities the accountId can be passed to MainActivity:
* - from our code as long 'account_id'
* - from our code as Long Intent Extra TUSKY_ACCOUNT_ID
* - from share shortcuts as String 'android.intent.extra.shortcut.ID'
*/
var accountId = intent.getLongExtra(NotificationHelper.ACCOUNT_ID, -1)
if (accountId == -1L) {
var tuskyAccountId = intent.getLongExtra(TUSKY_ACCOUNT_ID, -1)
if (tuskyAccountId == -1L) {
val accountIdString = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID)
if (accountIdString != null) {
accountId = accountIdString.toLong()
tuskyAccountId = accountIdString.toLong()
}
}
val accountRequested = accountId != -1L
if (accountRequested && accountId != activeAccount.id) {
accountManager.setActiveAccount(accountId)
val accountRequested = tuskyAccountId != -1L
if (accountRequested && tuskyAccountId != activeAccount.id) {
accountManager.setActiveAccount(tuskyAccountId)
}
val openDrafts = intent.getBooleanExtra(OPEN_DRAFTS, false)
if (canHandleMimeType(intent.type)) {
if (canHandleMimeType(intent.type) || intent.hasExtra(COMPOSE_OPTIONS)) {
// Sharing to Tusky from an external app
if (accountRequested) {
// The correct account is already active
forwardShare(intent)
forwardToComposeActivity(intent)
} else {
// No account was provided, show the chooser
showAccountChooserDialog(
@ -219,10 +266,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val requestedId = account.id
if (requestedId == activeAccount.id) {
// The correct account is already active
forwardShare(intent)
forwardToComposeActivity(intent)
} else {
// A different account was requested, restart the activity
intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId)
intent.putExtra(TUSKY_ACCOUNT_ID, requestedId)
changeAccount(requestedId, intent)
}
}
@ -232,11 +279,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} else if (openDrafts) {
val intent = DraftsActivity.newIntent(this)
startActivity(intent)
} else if (accountRequested && savedInstanceState == null) {
} else if (accountRequested && intent.hasExtra(NOTIFICATION_TYPE)) {
// user clicked a notification, show follow requests for type FOLLOW_REQUEST,
// otherwise show notification tab
if (intent.getStringExtra(NotificationHelper.TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
val intent = AccountListActivity.newIntent(this, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = true)
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
val intent = AccountListActivity.newIntent(
this,
AccountListActivity.Type.FOLLOW_REQUESTS
)
startActivityWithSlideInAnimation(intent)
} else {
showNotificationTab = true
@ -245,17 +295,27 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
setContentView(binding.root)
setSupportActionBar(binding.mainToolbar)
glide = Glide.with(this)
binding.composeButton.setOnClickListener {
val composeIntent = Intent(applicationContext, ComposeActivity::class.java)
startActivity(composeIntent)
}
// Determine which of the three toolbars should be the supportActionBar (which hosts
// the options menu).
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
binding.mainToolbar.visible(!hideTopToolbar)
if (hideTopToolbar) {
when (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top")) {
"top" -> setSupportActionBar(binding.topNav)
"bottom" -> setSupportActionBar(binding.bottomNav)
}
binding.mainToolbar.hide()
// There's not enough space in the top/bottom bars to show the title as well.
supportActionBar?.setDisplayShowTitleEnabled(false)
} else {
setSupportActionBar(binding.mainToolbar)
binding.mainToolbar.show()
}
loadDrawerAvatar(activeAccount.profilePictureUrl, true)
@ -266,7 +326,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setupDrawer(
savedInstanceState,
addSearchButton = hideTopToolbar,
addTrendingButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING)
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
TRENDING_TAGS
),
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
TRENDING_STATUSES
)
)
/* Fetch user info while we're doing other things. This has to be done after setting up the
@ -291,47 +356,57 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
is MainTabsChangedEvent -> {
refreshMainDrawerItems(
addSearchButton = hideTopToolbar,
addTrendingButton = !event.newTabs.hasTab(TRENDING)
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS),
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES)
)
setupTabs(false)
}
is AnnouncementReadEvent -> {
unreadAnnouncementsCount--
updateAnnouncementsBadge()
}
is NewNotificationsEvent -> {
directMessageTab?.let {
if (event.accountId == activeAccount.accountId) {
val hasDirectMessageNotification =
event.notifications.any {
it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT
}
if (hasDirectMessageNotification) {
showDirectMessageBadge(true)
}
}
}
}
is NotificationsLoadingEvent -> {
if (event.accountId == activeAccount.accountId) {
showDirectMessageBadge(false)
}
}
is ConversationsLoadingEvent -> {
if (event.accountId == activeAccount.accountId) {
showDirectMessageBadge(false)
}
}
}
}
}
Schedulers.io().scheduleDirect {
externalScope.launch(Dispatchers.IO) {
// Flush old media that was cached for sharing
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
}
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
onBackPressedDispatcher.addCallback(
this,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
when {
binding.mainDrawerLayout.isOpen -> {
binding.mainDrawerLayout.close()
}
binding.viewPager.currentItem != 0 -> {
binding.viewPager.currentItem = 0
}
else -> {
finish()
}
}
}
}
)
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
if (
Build.VERSION.SDK_INT >= 33 &&
ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
@ -343,6 +418,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
draftsAlert.observeInContext(this, true)
}
private fun showDirectMessageBadge(showBadge: Boolean) {
directMessageTab?.let { tab ->
tab.badge?.isVisible = showBadge
// TODO a bit cumbersome (also for resetting)
lifecycleScope.launch(Dispatchers.IO) {
accountManager.activeAccount?.let {
if (it.hasDirectMessageBadge != showBadge) {
it.hasDirectMessageBadge = showBadge
accountManager.saveAccount(it)
}
}
}
}
}
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_main, menu)
menu.findItem(R.id.action_search)?.apply {
@ -353,6 +444,20 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
override fun onPrepareMenu(menu: Menu) {
super.onPrepareMenu(menu)
// If the main toolbar is hidden then there's no space in the top/bottomNav to show
// the menu items as icons, so forceably disable them
if (!binding.mainToolbar.isVisible) {
menu.forEach {
it.setShowAsAction(
SHOW_AS_ACTION_NEVER
)
}
}
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_search -> {
@ -425,12 +530,23 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
private fun forwardShare(intent: Intent) {
val composeIntent = Intent(this, ComposeActivity::class.java)
composeIntent.action = intent.action
composeIntent.type = intent.type
composeIntent.putExtras(intent)
composeIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
private fun forwardToComposeActivity(intent: Intent) {
val composeOptions = IntentCompat.getParcelableExtra(
intent,
COMPOSE_OPTIONS,
ComposeActivity.ComposeOptions::class.java
)
val composeIntent = if (composeOptions != null) {
ComposeActivity.startIntent(this, composeOptions)
} else {
Intent(this, ComposeActivity::class.java).apply {
action = intent.action
type = intent.type
putExtras(intent)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
startActivity(composeIntent)
finish()
}
@ -438,13 +554,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupDrawer(
savedInstanceState: Bundle?,
addSearchButton: Boolean,
addTrendingButton: Boolean
addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean
) {
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
binding.mainToolbar.setNavigationOnClickListener(drawerOpenClickListener)
binding.topNavAvatar.setOnClickListener(drawerOpenClickListener)
binding.bottomNavAvatar.setOnClickListener(drawerOpenClickListener)
binding.topNav.setNavigationOnClickListener(drawerOpenClickListener)
binding.bottomNav.setNavigationOnClickListener(drawerOpenClickListener)
header = AccountHeaderView(this).apply {
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
@ -468,17 +585,21 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
header.currentProfileName.ellipsize = TextUtils.TruncateAt.END
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter))
header.accountHeaderBackground.setBackgroundColor(MaterialColors.getColor(header, R.attr.colorBackgroundAccent))
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
header.accountHeaderBackground.setBackgroundColor(
MaterialColors.getColor(header, R.attr.colorBackgroundAccent)
)
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
if (animateAvatars) {
glide.load(uri)
Glide.with(imageView)
.load(uri)
.placeholder(placeholder)
.into(imageView)
} else {
glide.asBitmap()
Glide.with(imageView)
.asBitmap()
.load(uri)
.placeholder(placeholder)
.into(imageView)
@ -486,12 +607,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
override fun cancel(imageView: ImageView) {
glide.clear(imageView)
// nothing to do, Glide already handles cancellation automatically
}
override fun placeholder(ctx: Context, tag: String?): Drawable {
if (tag == DrawerImageLoader.Tags.PROFILE.name || tag == DrawerImageLoader.Tags.PROFILE_DRAWER_ITEM.name) {
return ctx.getDrawable(R.drawable.avatar_default)!!
return AppCompatResources.getDrawable(ctx, R.drawable.avatar_default)!!
}
return super.placeholder(ctx, tag)
@ -499,12 +620,33 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
})
binding.mainDrawer.apply {
refreshMainDrawerItems(addSearchButton, addTrendingButton)
refreshMainDrawerItems(
addSearchButton = addSearchButton,
addTrendingTagsButton = addTrendingTagsButton,
addTrendingStatusesButton = addTrendingStatusesButton
)
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(addSearchButton: Boolean, addTrendingButton: Boolean) {
private fun refreshMainDrawerItems(
addSearchButton: Boolean,
addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean
) {
binding.mainDrawer.apply {
itemAdapter.clear()
tintStatusBar = true
@ -538,7 +680,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
nameRes = R.string.action_view_follow_requests
iconicsIcon = GoogleMaterial.Icon.gmd_person_add
onClick = {
val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked)
val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS)
startActivityWithSlideInAnimation(intent)
}
},
@ -621,7 +763,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
)
}
if (addTrendingButton) {
if (addTrendingTagsButton) {
binding.mainDrawer.addItemsAtPosition(
5,
primaryDrawerItem {
@ -633,6 +775,19 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
)
}
if (addTrendingStatusesButton) {
binding.mainDrawer.addItemsAtPosition(
6,
primaryDrawerItem {
nameRes = R.string.title_public_trending_statuses
iconicsIcon = GoogleMaterial.Icon.gmd_local_fire_department
onClick = {
startActivityWithSlideInAnimation(StatusListActivity.newTrendingIntent(context))
}
}
)
}
}
if (BuildConfig.DEBUG) {
@ -702,6 +857,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// Detach any existing mediator before changing tab contents and attaching a new mediator
tabLayoutMediator?.detach()
directMessageTab = null
tabAdapter.tabs = tabs
tabAdapter.notifyItemRangeChanged(0, tabs.size)
@ -712,6 +869,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
LIST -> tabs[position].arguments[1]
else -> getString(tabs[position].text)
}
if (tabs[position].id == DIRECT) {
val badge = tab.orCreateBadge
badge.isVisible = accountManager.activeAccount?.hasDirectMessageBadge ?: false
badge.backgroundColor = MaterialColors.getColor(binding.mainDrawer, com.google.android.material.R.attr.colorPrimary)
directMessageTab = tab
}
}.also { it.attach() }
// Selected tab is either
@ -737,9 +900,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
onTabSelectedListener = object : OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
onBackPressedCallback.isEnabled = tab.position > 0 || binding.mainDrawerLayout.isOpen
binding.mainToolbar.title = tab.contentDescription
refreshComposeButtonState(tabAdapter, tab.position)
if (tab == directMessageTab) {
tab.badge?.isVisible = false
accountManager.activeAccount?.let {
if (it.hasDirectMessageBadge) {
it.hasDirectMessageBadge = false
accountManager.saveAccount(it)
}
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {}
@ -756,10 +932,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
activeTabLayout.addOnTabSelectedListener(it)
}
val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0
supportActionBar?.title = tabs[activeTabPosition].title(this@MainActivity)
supportActionBar?.title = tabs[position].title(this@MainActivity)
binding.mainToolbar.setOnClickListener {
(tabAdapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
(
tabAdapter.getFragment(
activeTabLayout.selectedTabPosition
) as? ReselectableFragment
)?.onReselect()
}
updateProfiles()
@ -790,7 +969,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
// open LoginActivity to add new account
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN))
startActivityWithSlideInAnimation(
LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN)
)
return false
}
// change Account
@ -802,15 +983,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
cacheUpdater.stop()
accountManager.setActiveAccount(newSelectedId)
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
intent.putExtra(OPEN_WITH_EXPLODE_ANIMATION, true)
if (forward != null) {
intent.type = forward.type
intent.action = forward.action
intent.putExtras(forward)
}
startActivity(intent)
finishWithoutSlideOutAnimation()
overridePendingTransition(R.anim.explode, R.anim.explode)
finish()
if (!supportsOverridingActivityTransitions()) {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.explode, R.anim.activity_open_exit)
}
}
private fun logout() {
@ -833,7 +1017,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
}
startActivity(intent)
finishWithoutSlideOutAnimation()
finish()
}
}
.setNegativeButton(android.R.string.cancel, null)
@ -853,17 +1037,26 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun onFetchUserInfoSuccess(me: Account) {
glide.asBitmap()
Glide.with(header.accountHeaderBackground)
.asBitmap()
.load(me.header)
.into(header.accountHeaderBackground)
loadDrawerAvatar(me.avatar, false)
accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
NotificationHelper.createNotificationChannelsForAccount(
accountManager.activeAccount!!,
this
)
// Setup push notifications
showMigrationNoticeIfNecessary(this, binding.mainCoordinatorLayout, binding.composeButton, accountManager)
showMigrationNoticeIfNecessary(
this,
binding.mainCoordinatorLayout,
binding.composeButton,
accountManager
)
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
lifecycleScope.launch {
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
@ -872,122 +1065,94 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
disableAllNotifications(this, accountManager)
}
accountLocked = me.locked
updateProfiles()
updateShortcut(this, accountManager.activeAccount!!)
shareShortcutHelper.updateShortcuts()
}
@SuppressLint("CheckResult")
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
if (hideTopToolbar) {
val navOnBottom = preferences.getString("mainNavPosition", "top") == "bottom"
val avatarView = if (navOnBottom) {
binding.bottomNavAvatar.show()
binding.bottomNavAvatar
val activeToolbar = if (hideTopToolbar) {
val navOnBottom = preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom"
if (navOnBottom) {
binding.bottomNav
} else {
binding.topNavAvatar.show()
binding.topNavAvatar
}
if (animateAvatars) {
Glide.with(this)
.load(avatarUrl)
.placeholder(R.drawable.avatar_default)
.into(avatarView)
} else {
Glide.with(this)
.asBitmap()
.load(avatarUrl)
.placeholder(R.drawable.avatar_default)
.into(avatarView)
binding.topNav
}
} else {
binding.bottomNavAvatar.hide()
binding.topNavAvatar.hide()
binding.mainToolbar
}
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
if (animateAvatars) {
glide.asDrawable()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
if (animateAvatars) {
Glide.with(this)
.asDrawable()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) placeholder(R.drawable.avatar_default)
}
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
placeholder?.let {
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
}
}
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
if (resource is Animatable) resource.start()
activeToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
}
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
if (resource is Animatable) {
resource.start()
}
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(resource, navIconSize, navIconSize)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
}
})
} else {
glide.asBitmap()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) {
placeholder(R.drawable.avatar_default)
override fun onLoadCleared(placeholder: Drawable?) {
placeholder?.let {
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
}
}
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
})
} else {
Glide.with(this)
.asBitmap()
.load(avatarUrl)
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) placeholder(R.drawable.avatar_default)
}
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
override fun onLoadStarted(placeholder: Drawable?) {
placeholder?.let {
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
}
}
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
binding.mainToolbar.navigationIcon = FixedSizeDrawable(
BitmapDrawable(resources, resource),
navIconSize,
navIconSize
)
}
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
activeToolbar.navigationIcon = FixedSizeDrawable(
BitmapDrawable(resources, resource),
navIconSize,
navIconSize
)
}
override fun onLoadCleared(placeholder: Drawable?) {
if (placeholder != null) {
binding.mainToolbar.navigationIcon =
FixedSizeDrawable(placeholder, navIconSize, navIconSize)
}
override fun onLoadCleared(placeholder: Drawable?) {
placeholder?.let {
activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize)
}
})
}
}
})
}
}
@ -1007,7 +1172,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun updateAnnouncementsBadge() {
binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()))
binding.mainDrawer.updateBadge(
DRAWER_ITEM_ANNOUNCEMENTS,
StringHolder(
if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()
)
)
}
private fun updateProfiles() {
@ -1041,16 +1211,93 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
}
private fun explodeAnimationWasRequested(): Boolean {
return intent.getBooleanExtra(OPEN_WITH_EXPLODE_ANIMATION, false)
}
override fun getActionButton() = binding.composeButton
override fun androidInjector() = androidInjector
companion object {
const val OPEN_WITH_EXPLODE_ANIMATION = "explode"
private const val TAG = "MainActivity" // logging tag
private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13
private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14
const val REDIRECT_URL = "redirectUrl"
const val OPEN_DRAFTS = "draft"
private const val REDIRECT_URL = "redirectUrl"
private const val OPEN_DRAFTS = "draft"
private const val TUSKY_ACCOUNT_ID = "tuskyAccountId"
private const val COMPOSE_OPTIONS = "composeOptions"
private const val NOTIFICATION_TYPE = "notificationType"
private const val NOTIFICATION_TAG = "notificationTag"
private const val NOTIFICATION_ID = "notificationId"
/**
* Switches the active account to the provided accountId and then stays on MainActivity
*/
@JvmStatic
fun accountSwitchIntent(context: Context, tuskyAccountId: Long): Intent {
return Intent(context, MainActivity::class.java).apply {
putExtra(TUSKY_ACCOUNT_ID, tuskyAccountId)
}
}
/**
* Switches the active account to the accountId and takes the user to the correct place according to the notification they clicked
*/
@JvmStatic
fun openNotificationIntent(
context: Context,
tuskyAccountId: Long,
type: Notification.Type
): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(NOTIFICATION_TYPE, type.name)
}
}
/**
* Switches the active account to the accountId and then opens ComposeActivity with the provided options
* @param tuskyAccountId the id of the Tusky account to open the screen with. Set to -1 for current account.
* @param notificationId optional id of the notification that should be cancelled when this intent is opened
* @param notificationTag optional tag of the notification that should be cancelled when this intent is opened
*/
@JvmStatic
fun composeIntent(
context: Context,
options: ComposeActivity.ComposeOptions,
tuskyAccountId: Long = -1,
notificationTag: String? = null,
notificationId: Int = -1
): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
action = Intent.ACTION_SEND // so it can be opened via shortcuts
putExtra(COMPOSE_OPTIONS, options)
putExtra(NOTIFICATION_TAG, notificationTag)
putExtra(NOTIFICATION_ID, notificationId)
}
}
/**
* switches the active account to the accountId and then tries to resolve and show the provided url
*/
@JvmStatic
fun redirectIntent(context: Context, tuskyAccountId: Long, url: String): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(REDIRECT_URL, url)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
/**
* switches the active account to the provided accountId and then opens drafts
*/
fun draftIntent(context: Context, tuskyAccountId: Long): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(OPEN_DRAFTS, true)
}
}
}
}