upgrade ktlint plugin to 12.0.3 (#4169)
There are some new rules, I think they mostly make sense, except for the max line length which I had to disable because we are over it in a lot of places. --------- Co-authored-by: Goooler <wangzongler@gmail.com>
This commit is contained in:
parent
33cd6fdb98
commit
5192fb08a5
215 changed files with 2813 additions and 1177 deletions
|
@ -8,12 +8,20 @@ insert_final_newline = true
|
|||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{java,kt}]
|
||||
ij_kotlin_imports_layout = *
|
||||
|
||||
# Disable wildcard imports
|
||||
ij_kotlin_name_count_to_use_star_import = 999
|
||||
ij_kotlin_name_count_to_use_star_import_for_members = 999
|
||||
ij_java_class_count_to_use_import_on_demand = 999
|
||||
# Enable trailing comma
|
||||
ktlint_disabled_rules=trailing-comma-on-call-site,trailing-comma-on-declaration-site
|
||||
|
||||
ktlint_code_style = android_studio
|
||||
|
||||
# Disable trailing comma
|
||||
ktlint_standard_trailing-comma-on-call-site = disabled
|
||||
ktlint_standard_trailing-comma-on-declaration-site = disabled
|
||||
|
||||
max_line_length = off
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_size = 2
|
||||
|
|
|
@ -21,8 +21,8 @@ import com.keylesspalace.tusky.di.Injectable
|
|||
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AboutActivity : BottomSheetActivity(), Injectable {
|
||||
@Inject
|
||||
|
@ -70,9 +70,15 @@ class AboutActivity : BottomSheetActivity(), Injectable {
|
|||
binding.aboutPoweredByTusky.hide()
|
||||
}
|
||||
|
||||
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license)
|
||||
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site)
|
||||
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site)
|
||||
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(
|
||||
R.string.about_tusky_license
|
||||
)
|
||||
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(
|
||||
R.string.about_project_site
|
||||
)
|
||||
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(
|
||||
R.string.about_bug_feature_request_site
|
||||
)
|
||||
|
||||
binding.tuskyProfileButton.setOnClickListener {
|
||||
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL)
|
||||
|
|
|
@ -45,8 +45,8 @@ import com.keylesspalace.tusky.util.unsafeLazy
|
|||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.State
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
||||
|
||||
|
@ -82,11 +82,18 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
super.onStart()
|
||||
dialog?.apply {
|
||||
// Stretch dialog to the window
|
||||
window?.setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)
|
||||
window?.setLayout(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_accounts_in_list, container, false)
|
||||
}
|
||||
|
||||
|
@ -164,15 +171,27 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||
override fun areContentsTheSame(
|
||||
oldItem: TimelineAccount,
|
||||
newItem: TimelineAccount
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
||||
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(
|
||||
AccountDiffer
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemFollowRequestBinding> {
|
||||
val binding = ItemFollowRequestBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
val holder = BindingHolder(binding)
|
||||
|
||||
binding.notificationTextView.hide()
|
||||
|
@ -186,7 +205,10 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
return holder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
|
||||
override fun onBindViewHolder(
|
||||
holder: BindingHolder<ItemFollowRequestBinding>,
|
||||
position: Int
|
||||
) {
|
||||
val account = getItem(position)
|
||||
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
|
||||
holder.binding.usernameTextView.text = account.username
|
||||
|
@ -204,10 +226,19 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(SearchDiffer) {
|
||||
inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(
|
||||
SearchDiffer
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemFollowRequestBinding> {
|
||||
val binding = ItemFollowRequestBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
val holder = BindingHolder(binding)
|
||||
|
||||
binding.notificationTextView.hide()
|
||||
|
@ -224,7 +255,10 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
return holder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
|
||||
override fun onBindViewHolder(
|
||||
holder: BindingHolder<ItemFollowRequestBinding>,
|
||||
position: Int
|
||||
) {
|
||||
val (account, inAList) = getItem(position)
|
||||
|
||||
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
|
||||
|
|
|
@ -64,7 +64,10 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
})
|
||||
}
|
||||
|
||||
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
|
||||
open fun viewUrl(
|
||||
url: String,
|
||||
lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER
|
||||
) {
|
||||
if (!looksLikeMastodonUrl(url)) {
|
||||
openLink(url)
|
||||
return
|
||||
|
@ -121,10 +124,17 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
|
||||
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
|
||||
protected open fun performUrlFallbackAction(
|
||||
url: String,
|
||||
fallbackBehavior: PostLookupFallbackBehavior
|
||||
) {
|
||||
when (fallbackBehavior) {
|
||||
PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url)
|
||||
PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText(this, getString(R.string.post_lookup_error_format, url), Toast.LENGTH_SHORT).show()
|
||||
PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText(
|
||||
this,
|
||||
getString(R.string.post_lookup_error_format, url),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,8 +57,8 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EditProfileActivity : BaseActivity(), Injectable {
|
||||
|
||||
|
@ -126,9 +126,17 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
||||
binding.fieldList.adapter = accountFieldEditAdapter
|
||||
|
||||
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply { sizeDp = 12; colorInt = Color.WHITE }
|
||||
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply {
|
||||
sizeDp = 12
|
||||
colorInt = Color.WHITE
|
||||
}
|
||||
|
||||
binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(plusDrawable, null, null, null)
|
||||
binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
plusDrawable,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
binding.addFieldButton.setOnClickListener {
|
||||
accountFieldEditAdapter.addField()
|
||||
|
@ -162,7 +170,9 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
.placeholder(R.drawable.avatar_default)
|
||||
.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
RoundedCorners(
|
||||
resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)
|
||||
)
|
||||
)
|
||||
.into(binding.avatarPreview)
|
||||
}
|
||||
|
@ -175,7 +185,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
is Error -> {
|
||||
Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(
|
||||
binding.avatarButton,
|
||||
R.string.error_generic,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.obtainProfile()
|
||||
}
|
||||
|
@ -188,7 +202,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
lifecycleScope.launch {
|
||||
viewModel.instanceData.collect { instanceInfo ->
|
||||
maxAccountFields = instanceInfo.maxFields
|
||||
accountFieldEditAdapter.setFieldLimits(instanceInfo.maxFieldNameLength, instanceInfo.maxFieldValueLength)
|
||||
accountFieldEditAdapter.setFieldLimits(
|
||||
instanceInfo.maxFieldNameLength,
|
||||
instanceInfo.maxFieldValueLength
|
||||
)
|
||||
binding.addFieldButton.isVisible =
|
||||
accountFieldEditAdapter.itemCount < maxAccountFields
|
||||
}
|
||||
|
@ -318,7 +335,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
private fun onPickFailure(throwable: Throwable?) {
|
||||
Log.w("EditProfileActivity", "failed to pick media", throwable)
|
||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||
Snackbar.make(
|
||||
binding.avatarButton,
|
||||
R.string.error_media_upload_sending,
|
||||
Snackbar.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun showUnsavedChangesDialog() = lifecycleScope.launch {
|
||||
|
|
|
@ -54,8 +54,8 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
|
|||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
// TODO use the ListSelectionFragment (and/or its adapter or binding) here; but keep the LoadingState from here (?)
|
||||
|
||||
|
@ -273,7 +273,12 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
}
|
||||
}
|
||||
|
||||
private fun onPickedDialogName(name: String, listId: String?, exclusive: Boolean, replyPolicy: String) {
|
||||
private fun onPickedDialogName(
|
||||
name: String,
|
||||
listId: String?,
|
||||
exclusive: Boolean,
|
||||
replyPolicy: String
|
||||
) {
|
||||
if (listId == null) {
|
||||
viewModel.createNewList(name, exclusive, replyPolicy)
|
||||
} else {
|
||||
|
|
|
@ -141,9 +141,9 @@ import dagger.android.DispatchingAndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider {
|
||||
@Inject
|
||||
|
@ -199,7 +199,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
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
|
||||
val notificationManager = getSystemService(
|
||||
NOTIFICATION_SERVICE
|
||||
) as NotificationManager
|
||||
notificationManager.cancel(intent.getStringExtra(NOTIFICATION_TAG), notificationId)
|
||||
}
|
||||
|
||||
|
@ -253,7 +255,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
// user clicked a notification, show follow requests for type FOLLOW_REQUEST,
|
||||
// otherwise show notification tab
|
||||
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
|
||||
val intent = AccountListActivity.newIntent(this, AccountListActivity.Type.FOLLOW_REQUESTS)
|
||||
val intent = AccountListActivity.newIntent(
|
||||
this,
|
||||
AccountListActivity.Type.FOLLOW_REQUESTS
|
||||
)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
} else {
|
||||
showNotificationTab = true
|
||||
|
@ -293,8 +298,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
setupDrawer(
|
||||
savedInstanceState,
|
||||
addSearchButton = hideTopToolbar,
|
||||
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_TAGS),
|
||||
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_STATUSES),
|
||||
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
|
||||
|
@ -320,7 +329,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
refreshMainDrawerItems(
|
||||
addSearchButton = hideTopToolbar,
|
||||
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS),
|
||||
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES),
|
||||
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES)
|
||||
)
|
||||
|
||||
setupTabs(false)
|
||||
|
@ -333,7 +342,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
directMessageTab?.let {
|
||||
if (event.accountId == activeAccount.accountId) {
|
||||
val hasDirectMessageNotification =
|
||||
event.notifications.any { it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT }
|
||||
event.notifications.any {
|
||||
it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT
|
||||
}
|
||||
|
||||
if (hasDirectMessageNotification) {
|
||||
showDirectMessageBadge(true)
|
||||
|
@ -427,7 +438,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
|
||||
// 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) }
|
||||
if (!binding.mainToolbar.isVisible) {
|
||||
menu.forEach {
|
||||
it.setShowAsAction(
|
||||
SHOW_AS_ACTION_NEVER
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemSelected(item: MenuItem): Boolean {
|
||||
|
@ -503,7 +520,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
}
|
||||
|
||||
private fun forwardToComposeActivity(intent: Intent) {
|
||||
val composeOptions = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS, ComposeActivity.ComposeOptions::class.java)
|
||||
val composeOptions = IntentCompat.getParcelableExtra(
|
||||
intent,
|
||||
COMPOSE_OPTIONS,
|
||||
ComposeActivity.ComposeOptions::class.java
|
||||
)
|
||||
|
||||
val composeIntent = if (composeOptions != null) {
|
||||
ComposeActivity.startIntent(this, composeOptions)
|
||||
|
@ -523,7 +544,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
savedInstanceState: Bundle?,
|
||||
addSearchButton: Boolean,
|
||||
addTrendingTagsButton: Boolean,
|
||||
addTrendingStatusesButton: Boolean,
|
||||
addTrendingStatusesButton: Boolean
|
||||
) {
|
||||
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
|
||||
|
||||
|
@ -553,7 +574,9 @@ 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))
|
||||
header.accountHeaderBackground.setBackgroundColor(
|
||||
MaterialColors.getColor(header, R.attr.colorBackgroundAccent)
|
||||
)
|
||||
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
||||
|
||||
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
|
||||
|
@ -589,7 +612,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
refreshMainDrawerItems(
|
||||
addSearchButton = addSearchButton,
|
||||
addTrendingTagsButton = addTrendingTagsButton,
|
||||
addTrendingStatusesButton = addTrendingStatusesButton,
|
||||
addTrendingStatusesButton = addTrendingStatusesButton
|
||||
)
|
||||
setSavedInstance(savedInstanceState)
|
||||
}
|
||||
|
@ -598,7 +621,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private fun refreshMainDrawerItems(
|
||||
addSearchButton: Boolean,
|
||||
addTrendingTagsButton: Boolean,
|
||||
addTrendingStatusesButton: Boolean,
|
||||
addTrendingStatusesButton: Boolean
|
||||
) {
|
||||
binding.mainDrawer.apply {
|
||||
itemAdapter.clear()
|
||||
|
@ -884,7 +907,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
|
||||
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()
|
||||
|
@ -915,7 +942,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
|
||||
|
@ -986,10 +1015,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
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)
|
||||
|
@ -1024,7 +1061,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(avatarUrl)
|
||||
.transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)))
|
||||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
|
@ -1054,7 +1093,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(avatarUrl)
|
||||
.transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)))
|
||||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
|
@ -1101,7 +1142,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() {
|
||||
|
@ -1165,7 +1211,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
* 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 {
|
||||
fun openNotificationIntent(
|
||||
context: Context,
|
||||
tuskyAccountId: Long,
|
||||
type: Notification.Type
|
||||
): Intent {
|
||||
return accountSwitchIntent(context, tuskyAccountId).apply {
|
||||
putExtra(NOTIFICATION_TYPE, type.name)
|
||||
}
|
||||
|
|
|
@ -38,8 +38,8 @@ import com.keylesspalace.tusky.util.isHttpNotFound
|
|||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||
|
||||
|
@ -49,7 +49,9 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
@Inject
|
||||
lateinit var eventHub: EventHub
|
||||
|
||||
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
|
||||
private val binding: ActivityStatuslistBinding by viewBinding(
|
||||
ActivityStatuslistBinding::inflate
|
||||
)
|
||||
private lateinit var kind: Kind
|
||||
private var hashtag: String? = null
|
||||
private var followTagItem: MenuItem? = null
|
||||
|
@ -136,10 +138,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
followTagItem?.isVisible = false
|
||||
unfollowTagItem?.isVisible = true
|
||||
|
||||
Snackbar.make(binding.root, getString(R.string.following_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.following_hashtag_success_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
},
|
||||
{
|
||||
Snackbar.make(binding.root, getString(R.string.error_following_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.error_following_hashtag_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
Log.e(TAG, "Failed to follow #$tag", it)
|
||||
}
|
||||
)
|
||||
|
@ -158,10 +168,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
followTagItem?.isVisible = true
|
||||
unfollowTagItem?.isVisible = false
|
||||
|
||||
Snackbar.make(binding.root, getString(R.string.unfollowing_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.unfollowing_hashtag_success_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
},
|
||||
{
|
||||
Snackbar.make(binding.root, getString(R.string.error_unfollowing_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.error_unfollowing_hashtag_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
Log.e(TAG, "Failed to unfollow #$tag", it)
|
||||
}
|
||||
)
|
||||
|
@ -238,7 +256,12 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
expiresInSeconds = null
|
||||
).fold(
|
||||
{ filter ->
|
||||
if (mastodonApi.addFilterKeyword(filterId = filter.id, keyword = hashedTag, wholeWord = true).isSuccess) {
|
||||
if (mastodonApi.addFilterKeyword(
|
||||
filterId = filter.id,
|
||||
keyword = hashedTag,
|
||||
wholeWord = true
|
||||
).isSuccess
|
||||
) {
|
||||
// must be requested again; otherwise does not contain the keyword (but server does)
|
||||
mutedFilter = mastodonApi.getFilter(filter.id).getOrNull()
|
||||
|
||||
|
@ -246,7 +269,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
|
||||
filterCreateSuccess = true
|
||||
} else {
|
||||
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.error_muting_hashtag_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
Log.e(TAG, "Failed to mute #$tag")
|
||||
}
|
||||
},
|
||||
|
@ -265,12 +292,20 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
filterCreateSuccess = true
|
||||
},
|
||||
{ throwable ->
|
||||
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.error_muting_hashtag_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
Log.e(TAG, "Failed to mute #$tag", throwable)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.error_muting_hashtag_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
Log.e(TAG, "Failed to mute #$tag", throwable)
|
||||
}
|
||||
}
|
||||
|
@ -278,7 +313,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
if (filterCreateSuccess) {
|
||||
updateTagMuteState(true)
|
||||
Snackbar.make(binding.root, getString(R.string.muting_hashtag_success_format, tag), Snackbar.LENGTH_LONG).apply {
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.muting_hashtag_success_format, tag),
|
||||
Snackbar.LENGTH_LONG
|
||||
).apply {
|
||||
setAction(R.string.action_view_filter) {
|
||||
val intent = if (mutedFilter != null) {
|
||||
Intent(this@StatusListActivity, EditFilterActivity::class.java).apply {
|
||||
|
@ -339,10 +378,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
mutedFilterV1 = null
|
||||
mutedFilter = null
|
||||
|
||||
Snackbar.make(binding.root, getString(R.string.unmuting_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.unmuting_hashtag_success_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
},
|
||||
{ throwable ->
|
||||
Snackbar.make(binding.root, getString(R.string.error_unmuting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.error_unmuting_hashtag_format, tag),
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
Log.e(TAG, "Failed to unmute #$tag", throwable)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -104,7 +104,11 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
|||
id = TRENDING_STATUSES,
|
||||
text = R.string.title_public_trending_statuses,
|
||||
icon = R.drawable.ic_hot_24dp,
|
||||
fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) }
|
||||
fragment = {
|
||||
TimelineFragment.newInstance(
|
||||
TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES
|
||||
)
|
||||
}
|
||||
)
|
||||
HASHTAG -> TabData(
|
||||
id = HASHTAG,
|
||||
|
@ -112,13 +116,22 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
|
|||
icon = R.drawable.ic_hashtag,
|
||||
fragment = { args -> TimelineFragment.newHashtagInstance(args) },
|
||||
arguments = arguments,
|
||||
title = { context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) } }
|
||||
title = { context ->
|
||||
arguments.joinToString(separator = " ") {
|
||||
context.getString(R.string.title_tag, it)
|
||||
}
|
||||
}
|
||||
)
|
||||
LIST -> TabData(
|
||||
id = LIST,
|
||||
text = R.string.list,
|
||||
icon = R.drawable.ic_list,
|
||||
fragment = { args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) },
|
||||
fragment = { args ->
|
||||
TimelineFragment.newInstance(
|
||||
TimelineViewModel.Kind.LIST,
|
||||
args.getOrNull(0).orEmpty()
|
||||
)
|
||||
},
|
||||
arguments = arguments,
|
||||
title = { arguments.getOrNull(1).orEmpty() }
|
||||
)
|
||||
|
|
|
@ -47,10 +47,10 @@ import com.keylesspalace.tusky.util.viewBinding
|
|||
import com.keylesspalace.tusky.util.visible
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, ItemInteractionListener, ListSelectionFragment.ListSelectionListener {
|
||||
|
||||
|
@ -72,9 +72,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
|
|||
|
||||
private var tabsChanged = false
|
||||
|
||||
private val selectedItemElevation by unsafeLazy { resources.getDimension(R.dimen.selected_drag_item_elevation) }
|
||||
private val selectedItemElevation by unsafeLazy {
|
||||
resources.getDimension(R.dimen.selected_drag_item_elevation)
|
||||
}
|
||||
|
||||
private val hashtagRegex by unsafeLazy { Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE) }
|
||||
private val hashtagRegex by unsafeLazy {
|
||||
Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE)
|
||||
}
|
||||
|
||||
private val onFabDismissedCallback = object : OnBackPressedCallback(false) {
|
||||
override fun handleOnBackPressed() {
|
||||
|
@ -99,14 +103,19 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
|
|||
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
|
||||
binding.currentTabsRecyclerView.adapter = currentTabsAdapter
|
||||
binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
binding.currentTabsRecyclerView.addItemDecoration(
|
||||
DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
|
||||
)
|
||||
|
||||
addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this)
|
||||
binding.addTabRecyclerView.adapter = addTabAdapter
|
||||
binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
|
||||
touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
|
||||
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
|
||||
override fun getMovementFlags(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
): Int {
|
||||
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END)
|
||||
}
|
||||
|
||||
|
@ -118,7 +127,11 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
|
|||
return MIN_TAB_COUNT < currentTabs.size
|
||||
}
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
override fun onMove(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder,
|
||||
target: RecyclerView.ViewHolder
|
||||
): Boolean {
|
||||
val temp = currentTabs[viewHolder.bindingAdapterPosition]
|
||||
currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition]
|
||||
currentTabs[target.bindingAdapterPosition] = temp
|
||||
|
@ -138,7 +151,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
|
|||
}
|
||||
}
|
||||
|
||||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
override fun clearView(
|
||||
recyclerView: RecyclerView,
|
||||
viewHolder: RecyclerView.ViewHolder
|
||||
) {
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
viewHolder.itemView.elevation = 0f
|
||||
}
|
||||
|
|
|
@ -40,10 +40,10 @@ import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
|
|||
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
|
||||
import de.c1710.filemojicompat_ui.helpers.EmojiPreference
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import org.conscrypt.Conscrypt
|
||||
import java.security.Security
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import org.conscrypt.Conscrypt
|
||||
|
||||
class TuskyApplication : Application(), HasAndroidInjector {
|
||||
@Inject
|
||||
|
@ -78,7 +78,10 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
AppInjector.init(this)
|
||||
|
||||
// Migrate shared preference keys and defaults from version to version.
|
||||
val oldVersion = sharedPreferences.getInt(PrefKeys.SCHEMA_VERSION, NEW_INSTALL_SCHEMA_VERSION)
|
||||
val oldVersion = sharedPreferences.getInt(
|
||||
PrefKeys.SCHEMA_VERSION,
|
||||
NEW_INSTALL_SCHEMA_VERSION
|
||||
)
|
||||
if (oldVersion != SCHEMA_VERSION) {
|
||||
upgradeSharedPreferences(oldVersion, SCHEMA_VERSION)
|
||||
}
|
||||
|
|
|
@ -74,7 +74,12 @@ import javax.inject.Inject
|
|||
|
||||
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
|
||||
|
||||
class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
|
||||
class ViewMediaActivity :
|
||||
BaseActivity(),
|
||||
HasAndroidInjector,
|
||||
ViewImageFragment.PhotoActionsListener,
|
||||
ViewVideoFragment.VideoActionsListener {
|
||||
|
||||
@Inject
|
||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
||||
|
||||
|
@ -103,7 +108,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
|||
supportPostponeEnterTransition()
|
||||
|
||||
// Gather the parameters.
|
||||
attachments = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java)
|
||||
attachments = IntentCompat.getParcelableArrayListExtra(
|
||||
intent,
|
||||
EXTRA_ATTACHMENTS,
|
||||
AttachmentViewData::class.java
|
||||
)
|
||||
val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
|
||||
|
||||
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
|
||||
|
@ -215,7 +224,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
|||
private fun downloadMedia() {
|
||||
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
|
||||
val filename = Uri.parse(url).lastPathSegment
|
||||
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
resources.getString(R.string.download_image, filename),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
|
||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
|
@ -225,8 +238,13 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
|||
|
||||
private fun requestDownloadMedia() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermissions(
|
||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
) { _, grantResults ->
|
||||
if (
|
||||
grantResults.isNotEmpty() &&
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
downloadMedia()
|
||||
} else {
|
||||
showErrorDialog(
|
||||
|
@ -243,7 +261,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
|||
|
||||
private fun onOpenStatus() {
|
||||
val attach = attachments!![binding.viewPager.currentItem]
|
||||
startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl))
|
||||
startActivityWithSlideInAnimation(
|
||||
ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)
|
||||
)
|
||||
}
|
||||
|
||||
private fun copyLink() {
|
||||
|
@ -276,7 +296,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
|||
private fun shareFile(file: File, mimeType: String?) {
|
||||
ShareCompat.IntentBuilder(this)
|
||||
.setType(mimeType)
|
||||
.addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
|
||||
.addStream(
|
||||
FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)
|
||||
)
|
||||
.setChooserTitle(R.string.send_media_to)
|
||||
.startChooser()
|
||||
}
|
||||
|
@ -366,7 +388,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
|
|||
private const val TAG = "ViewMediaActivity"
|
||||
|
||||
@JvmStatic
|
||||
fun newIntent(context: Context?, attachments: List<AttachmentViewData>, index: Int): Intent {
|
||||
fun newIntent(
|
||||
context: Context?,
|
||||
attachments: List<AttachmentViewData>,
|
||||
index: Int
|
||||
): Intent {
|
||||
val intent = Intent(context, ViewMediaActivity::class.java)
|
||||
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments))
|
||||
intent.putExtra(EXTRA_ATTACHMENT_INDEX, index)
|
||||
|
|
|
@ -62,8 +62,15 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
|
||||
override fun getItemCount() = fieldData.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEditFieldBinding> {
|
||||
val binding = ItemEditFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemEditFieldBinding> {
|
||||
val binding = ItemEditFieldBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,10 @@ import com.keylesspalace.tusky.settings.PrefKeys
|
|||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
||||
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
|
||||
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(
|
||||
context,
|
||||
R.layout.item_autocomplete_account
|
||||
) {
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val binding = if (convertView == null) {
|
||||
|
|
|
@ -36,8 +36,15 @@ class EmojiAdapter(
|
|||
|
||||
override fun getItemCount() = emojiList.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEmojiButtonBinding> {
|
||||
val binding = ItemEmojiButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemEmojiButtonBinding> {
|
||||
val binding = ItemEmojiButtonBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
|
|
|
@ -47,16 +47,26 @@ class FollowRequestViewHolder(
|
|||
showBotOverlay: Boolean
|
||||
) {
|
||||
val wrappedName = account.name.unicodeWrap()
|
||||
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
|
||||
val emojifiedName: CharSequence = wrappedName.emojify(
|
||||
account.emojis,
|
||||
itemView,
|
||||
animateEmojis
|
||||
)
|
||||
binding.displayNameTextView.text = emojifiedName
|
||||
if (showHeader) {
|
||||
val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName)
|
||||
val wholeMessage: String = itemView.context.getString(
|
||||
R.string.notification_follow_request_format,
|
||||
wrappedName
|
||||
)
|
||||
binding.notificationTextView.text = SpannableStringBuilder(wholeMessage).apply {
|
||||
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}.emojify(account.emojis, itemView, animateEmojis)
|
||||
}
|
||||
binding.notificationTextView.visible(showHeader)
|
||||
val formattedUsername = itemView.context.getString(R.string.post_username_format, account.username)
|
||||
val formattedUsername = itemView.context.getString(
|
||||
R.string.post_username_format,
|
||||
account.username
|
||||
)
|
||||
binding.usernameTextView.text = formattedUsername
|
||||
if (account.note.isEmpty()) {
|
||||
binding.accountNote.hide()
|
||||
|
@ -67,7 +77,9 @@ class FollowRequestViewHolder(
|
|||
.emojify(account.emojis, binding.accountNote, animateEmojis)
|
||||
setClickableText(binding.accountNote, emojifiedNote, emptyList(), null, linkListener)
|
||||
}
|
||||
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(
|
||||
R.dimen.avatar_radius_48dp
|
||||
)
|
||||
loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar)
|
||||
binding.avatarBadge.visible(showBotOverlay && account.bot)
|
||||
}
|
||||
|
|
|
@ -26,7 +26,11 @@ import com.keylesspalace.tusky.util.getTuskyDisplayName
|
|||
import com.keylesspalace.tusky.util.modernLanguageCode
|
||||
import java.util.Locale
|
||||
|
||||
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
|
||||
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(
|
||||
context,
|
||||
resource,
|
||||
locales
|
||||
) {
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
return (super.getView(position, convertView, parent) as TextView).apply {
|
||||
setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))
|
||||
|
|
|
@ -67,7 +67,10 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
.map { pollOptions.indexOf(it) }
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemPollBinding> {
|
||||
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,11 @@ class PreviewPollOptionsAdapter : RecyclerView.Adapter<PreviewViewHolder>() {
|
|||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder {
|
||||
return PreviewViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll_preview_option, parent, false))
|
||||
return PreviewViewHolder(
|
||||
LayoutInflater.from(
|
||||
parent.context
|
||||
).inflate(R.layout.item_poll_preview_option, parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun getItemCount() = options.size
|
||||
|
|
|
@ -31,12 +31,25 @@ import com.keylesspalace.tusky.util.unicodeWrap
|
|||
import java.util.Date
|
||||
|
||||
class ReportNotificationViewHolder(
|
||||
private val binding: ItemReportNotificationBinding,
|
||||
private val binding: ItemReportNotificationBinding
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun setupWithReport(reporter: TimelineAccount, report: Report, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, itemView, animateEmojis)
|
||||
val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, itemView, animateEmojis)
|
||||
fun setupWithReport(
|
||||
reporter: TimelineAccount,
|
||||
report: Report,
|
||||
animateAvatar: Boolean,
|
||||
animateEmojis: Boolean
|
||||
) {
|
||||
val reporterName = reporter.name.unicodeWrap().emojify(
|
||||
reporter.emojis,
|
||||
itemView,
|
||||
animateEmojis
|
||||
)
|
||||
val reporteeName = report.targetAccount.name.unicodeWrap().emojify(
|
||||
report.targetAccount.emojis,
|
||||
itemView,
|
||||
animateEmojis
|
||||
)
|
||||
val icon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_flag_24dp)
|
||||
|
||||
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
|
||||
|
@ -52,17 +65,22 @@ class ReportNotificationViewHolder(
|
|||
report.targetAccount.avatar,
|
||||
binding.notificationReporteeAvatar,
|
||||
itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp),
|
||||
animateAvatar,
|
||||
animateAvatar
|
||||
)
|
||||
loadAvatar(
|
||||
reporter.avatar,
|
||||
binding.notificationReporterAvatar,
|
||||
itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp),
|
||||
animateAvatar,
|
||||
animateAvatar
|
||||
)
|
||||
}
|
||||
|
||||
fun setupActionListener(listener: NotificationActionListener, reporteeId: String, reporterId: String, reportId: String) {
|
||||
fun setupActionListener(
|
||||
listener: NotificationActionListener,
|
||||
reporteeId: String,
|
||||
reporterId: String,
|
||||
reportId: String
|
||||
) {
|
||||
binding.notificationReporteeAvatar.setOnClickListener {
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
|
|
|
@ -56,7 +56,11 @@ class TabAdapter(
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> {
|
||||
val binding = if (small) {
|
||||
ItemTabPreferenceSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
ItemTabPreferenceSmallBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
} else {
|
||||
ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
}
|
||||
|
|
|
@ -3,12 +3,12 @@ package com.keylesspalace.tusky.appstore
|
|||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class CacheUpdater @Inject constructor(
|
||||
eventHub: EventHub,
|
||||
|
|
|
@ -21,6 +21,9 @@ data class PollVoteEvent(val statusId: String, val poll: Poll) : Event
|
|||
data class DomainMuteEvent(val instance: String) : Event
|
||||
data class AnnouncementReadEvent(val announcementId: String) : Event
|
||||
data class FilterUpdatedEvent(val filterContext: List<String>) : Event
|
||||
data class NewNotificationsEvent(val accountId: String, val notifications: List<Notification>) : Event
|
||||
data class NewNotificationsEvent(
|
||||
val accountId: String,
|
||||
val notifications: List<Notification>
|
||||
) : Event
|
||||
data class ConversationsLoadingEvent(val accountId: String) : Event
|
||||
data class NotificationsLoadingEvent(val accountId: String) : Event
|
||||
|
|
|
@ -2,10 +2,10 @@ package com.keylesspalace.tusky.appstore
|
|||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
||||
interface Event
|
||||
|
||||
|
|
|
@ -267,9 +267,18 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
binding.accountFragmentViewPager.adapter = adapter
|
||||
binding.accountFragmentViewPager.offscreenPageLimit = 2
|
||||
|
||||
val pageTitles = arrayOf(getString(R.string.title_posts), getString(R.string.title_posts_with_replies), getString(R.string.title_posts_pinned), getString(R.string.title_media))
|
||||
val pageTitles =
|
||||
arrayOf(
|
||||
getString(R.string.title_posts),
|
||||
getString(R.string.title_posts_with_replies),
|
||||
getString(R.string.title_posts_pinned),
|
||||
getString(R.string.title_media)
|
||||
)
|
||||
|
||||
TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position ->
|
||||
TabLayoutMediator(
|
||||
binding.accountTabLayout,
|
||||
binding.accountFragmentViewPager
|
||||
) { tab, position ->
|
||||
tab.text = pageTitles[position]
|
||||
}.attach()
|
||||
|
||||
|
@ -301,7 +310,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
val right = insets.getInsets(systemBars()).right
|
||||
val bottom = insets.getInsets(systemBars()).bottom
|
||||
val left = insets.getInsets(systemBars()).left
|
||||
binding.accountCoordinatorLayout.updatePadding(right = right, bottom = bottom, left = left)
|
||||
binding.accountCoordinatorLayout.updatePadding(
|
||||
right = right,
|
||||
bottom = bottom,
|
||||
left = left
|
||||
)
|
||||
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
|
@ -318,7 +331,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
|
||||
val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation)
|
||||
|
||||
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
|
||||
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(
|
||||
this,
|
||||
appBarElevation
|
||||
)
|
||||
toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
binding.accountToolbar.background = toolbarBackground
|
||||
|
||||
|
@ -341,7 +357,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
|
||||
binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
|
||||
|
||||
val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply {
|
||||
val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(
|
||||
this,
|
||||
appBarElevation
|
||||
).apply {
|
||||
fillColor = ColorStateList.valueOf(toolbarColor)
|
||||
elevation = appBarElevation
|
||||
shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
|
@ -381,11 +400,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
|
||||
binding.accountAvatarImageView.visible(scaledAvatarSize > 0)
|
||||
|
||||
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f)
|
||||
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(
|
||||
1f
|
||||
)
|
||||
|
||||
window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int
|
||||
|
||||
val evaluatedToolbarColor = argbEvaluator.evaluate(transparencyPercent, Color.TRANSPARENT, toolbarColor) as Int
|
||||
val evaluatedToolbarColor = argbEvaluator.evaluate(
|
||||
transparencyPercent,
|
||||
Color.TRANSPARENT,
|
||||
toolbarColor
|
||||
) as Int
|
||||
|
||||
toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor)
|
||||
|
||||
|
@ -407,7 +432,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
when (it) {
|
||||
is Success -> onAccountChanged(it.data)
|
||||
is Error -> {
|
||||
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(
|
||||
binding.accountCoordinatorLayout,
|
||||
R.string.error_generic,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
}
|
||||
|
@ -421,7 +450,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
}
|
||||
|
||||
if (it is Error) {
|
||||
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(
|
||||
binding.accountCoordinatorLayout,
|
||||
R.string.error_generic,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
}
|
||||
|
@ -466,14 +499,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
val fullUsername = getFullUsername(loadedAccount)
|
||||
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, fullUsername))
|
||||
Snackbar.make(binding.root, getString(R.string.account_username_copied), Snackbar.LENGTH_SHORT)
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.account_username_copied),
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(
|
||||
account.emojis,
|
||||
binding.accountNoteTextView,
|
||||
animateEmojis
|
||||
)
|
||||
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||
|
||||
accountFieldAdapter.fields = account.fields.orEmpty()
|
||||
|
@ -503,7 +544,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
val isLight = resources.getBoolean(R.bool.lightNavigationBar)
|
||||
|
||||
if (loadedAccount?.bot == true) {
|
||||
val badgeView = getBadge(getColor(R.color.tusky_grey_50), R.drawable.ic_bot_24dp, getString(R.string.profile_badge_bot_text), isLight)
|
||||
val badgeView =
|
||||
getBadge(
|
||||
getColor(R.color.tusky_grey_50),
|
||||
R.drawable.ic_bot_24dp,
|
||||
getString(R.string.profile_badge_bot_text),
|
||||
isLight
|
||||
)
|
||||
binding.accountBadgeContainer.addView(badgeView)
|
||||
}
|
||||
|
||||
|
@ -873,7 +920,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
} else {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.mute_domain_warning, instance))
|
||||
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) }
|
||||
.setPositiveButton(
|
||||
getString(R.string.mute_domain_warning_dialog_ok)
|
||||
) { _, _ -> viewModel.blockDomain(instance) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
@ -966,7 +1015,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
sendIntent.action = Intent.ACTION_SEND
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, url)
|
||||
sendIntent.type = "text/plain"
|
||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_account_link_to)))
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
resources.getText(R.string.send_account_link_to)
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -978,7 +1032,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
sendIntent.action = Intent.ACTION_SEND
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, fullUsername)
|
||||
sendIntent.type = "text/plain"
|
||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_account_username_to)))
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
resources.getText(R.string.send_account_username_to)
|
||||
)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -1009,7 +1068,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
}
|
||||
R.id.action_report -> {
|
||||
loadedAccount?.let { loadedAccount ->
|
||||
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username))
|
||||
startActivity(
|
||||
ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -1047,7 +1108,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
|
|||
// text color with maximum contrast
|
||||
val textColor = if (isLight) Color.BLACK else Color.WHITE
|
||||
// badge color with 50% transparency so it blends in with the theme background
|
||||
val backgroundColor = Color.argb(128, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor))
|
||||
val backgroundColor = Color.argb(
|
||||
128,
|
||||
Color.red(baseColor),
|
||||
Color.green(baseColor),
|
||||
Color.blue(baseColor)
|
||||
)
|
||||
// a color between the text color and the badge color
|
||||
val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f)
|
||||
|
||||
|
|
|
@ -38,8 +38,15 @@ class AccountFieldAdapter(
|
|||
|
||||
override fun getItemCount() = fields.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountFieldBinding> {
|
||||
val binding = ItemAccountFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemAccountFieldBinding> {
|
||||
val binding = ItemAccountFieldBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
|
@ -51,11 +58,20 @@ class AccountFieldAdapter(
|
|||
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
||||
nameTextView.text = emojifiedName
|
||||
|
||||
val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis)
|
||||
val emojifiedValue = field.value.parseAsMastodonHtml().emojify(
|
||||
emojis,
|
||||
valueTextView,
|
||||
animateEmojis
|
||||
)
|
||||
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
||||
|
||||
if (field.verifiedAt != null) {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
0,
|
||||
0,
|
||||
R.drawable.ic_check_circle,
|
||||
0
|
||||
)
|
||||
} else {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
|
|
|
@ -33,7 +33,11 @@ class AccountPagerAdapter(
|
|||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false)
|
||||
1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false)
|
||||
1 -> TimelineFragment.newInstance(
|
||||
TimelineViewModel.Kind.USER_WITH_REPLIES,
|
||||
accountId,
|
||||
false
|
||||
)
|
||||
2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
|
||||
3 -> AccountMediaFragment.newInstance(accountId)
|
||||
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
|
||||
|
|
|
@ -20,10 +20,10 @@ import com.keylesspalace.tusky.util.Loading
|
|||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.getDomain
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
|
@ -97,7 +97,15 @@ class AccountViewModel @Inject constructor(
|
|||
mastodonApi.relationships(listOf(accountId))
|
||||
.fold(
|
||||
{ relationships ->
|
||||
relationshipData.postValue(if (relationships.isNotEmpty()) Success(relationships[0]) else Error())
|
||||
relationshipData.postValue(
|
||||
if (relationships.isNotEmpty()) {
|
||||
Success(
|
||||
relationships[0]
|
||||
)
|
||||
} else {
|
||||
Error()
|
||||
}
|
||||
)
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining relationships", t)
|
||||
|
@ -135,8 +143,8 @@ class AccountViewModel @Inject constructor(
|
|||
|
||||
fun changeSubscribingState() {
|
||||
val relationship = relationshipData.value?.data
|
||||
if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */
|
||||
relationship?.subscribing == true /* Pleroma */
|
||||
if (relationship?.notifying == true || // Mastodon 3.3.0rc1
|
||||
relationship?.subscribing == true // Pleroma
|
||||
) {
|
||||
changeRelationship(RelationShipAction.UNSUBSCRIBE)
|
||||
} else {
|
||||
|
@ -315,7 +323,14 @@ class AccountViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
enum class RelationShipAction {
|
||||
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE
|
||||
FOLLOW,
|
||||
UNFOLLOW,
|
||||
BLOCK,
|
||||
UNBLOCK,
|
||||
MUTE,
|
||||
UNMUTE,
|
||||
SUBSCRIBE,
|
||||
UNSUBSCRIBE
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -42,12 +42,12 @@ import com.keylesspalace.tusky.util.BindingHolder
|
|||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ListSelectionFragment : DialogFragment(), Injectable {
|
||||
|
||||
|
@ -133,14 +133,22 @@ class ListSelectionFragment : DialogFragment(), Injectable {
|
|||
viewModel.actionError.collectLatest { error ->
|
||||
when (error.type) {
|
||||
ActionError.Type.ADD -> {
|
||||
Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
R.string.failed_to_add_to_list,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.addAccountToList(accountId!!, error.listId)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
ActionError.Type.REMOVE -> {
|
||||
Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
R.string.failed_to_remove_from_list,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.action_retry) {
|
||||
viewModel.removeAccountFromList(accountId!!, error.listId)
|
||||
}
|
||||
|
|
|
@ -24,12 +24,12 @@ import at.connyduck.calladapter.networkresult.onSuccess
|
|||
import at.connyduck.calladapter.networkresult.runCatching
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
data class AccountListState(
|
||||
val list: MastoList,
|
||||
|
|
|
@ -49,9 +49,9 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Fragment with multiple columns of media previews for the specified account.
|
||||
|
@ -92,9 +92,13 @@ class AccountMediaFragment :
|
|||
)
|
||||
|
||||
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
||||
val imageSpacing = view.context.resources.getDimensionPixelSize(R.dimen.profile_media_spacing)
|
||||
val imageSpacing = view.context.resources.getDimensionPixelSize(
|
||||
R.dimen.profile_media_spacing
|
||||
)
|
||||
|
||||
binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(columnCount, imageSpacing, 0))
|
||||
binding.recyclerView.addItemDecoration(
|
||||
GridSpacingItemDecoration(columnCount, imageSpacing, 0)
|
||||
)
|
||||
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
@ -124,7 +128,11 @@ class AccountMediaFragment :
|
|||
is LoadState.NotLoading -> {
|
||||
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||
binding.statusView.show()
|
||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
||||
binding.statusView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.message_empty,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
is LoadState.Error -> {
|
||||
|
@ -175,11 +183,19 @@ class AccountMediaFragment :
|
|||
Attachment.Type.GIFV,
|
||||
Attachment.Type.VIDEO,
|
||||
Attachment.Type.AUDIO -> {
|
||||
val intent = ViewMediaActivity.newIntent(context, attachmentsFromSameStatus, currentIndex)
|
||||
val intent = ViewMediaActivity.newIntent(
|
||||
context,
|
||||
attachmentsFromSameStatus,
|
||||
currentIndex
|
||||
)
|
||||
if (activity != null) {
|
||||
val url = selected.attachment.url
|
||||
ViewCompat.setTransitionName(view, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
requireActivity(),
|
||||
view,
|
||||
url
|
||||
)
|
||||
startActivity(intent, options.toBundle())
|
||||
} else {
|
||||
startActivity(intent)
|
||||
|
|
|
@ -29,25 +29,48 @@ class AccountMediaGridAdapter(
|
|||
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit
|
||||
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
|
||||
object : DiffUtil.ItemCallback<AttachmentViewData>() {
|
||||
override fun areItemsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: AttachmentViewData,
|
||||
newItem: AttachmentViewData
|
||||
): Boolean {
|
||||
return oldItem.attachment.id == newItem.attachment.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
|
||||
override fun areContentsTheSame(
|
||||
oldItem: AttachmentViewData,
|
||||
newItem: AttachmentViewData
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
private val baseItemBackgroundColor = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, Color.BLACK)
|
||||
private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator)
|
||||
private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp)
|
||||
private val baseItemBackgroundColor = MaterialColors.getColor(
|
||||
context,
|
||||
com.google.android.material.R.attr.colorSurface,
|
||||
Color.BLACK
|
||||
)
|
||||
private val videoIndicator = AppCompatResources.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_play_indicator
|
||||
)
|
||||
private val mediaHiddenDrawable = AppCompatResources.getDrawable(
|
||||
context,
|
||||
R.drawable.ic_hide_media_24dp
|
||||
)
|
||||
|
||||
private val itemBgBaseHSV = FloatArray(3)
|
||||
private val random = Random()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountMediaBinding> {
|
||||
val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemAccountMediaBinding> {
|
||||
val binding = ItemAccountMediaBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV)
|
||||
itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f
|
||||
binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
||||
|
@ -71,7 +94,11 @@ class AccountMediaGridAdapter(
|
|||
if (item.attachment.type == Attachment.Type.AUDIO) {
|
||||
overlay.hide()
|
||||
|
||||
imageView.setPadding(context.resources.getDimensionPixelSize(R.dimen.profile_media_audio_icon_padding))
|
||||
imageView.setPadding(
|
||||
context.resources.getDimensionPixelSize(
|
||||
R.dimen.profile_media_audio_icon_padding
|
||||
)
|
||||
)
|
||||
|
||||
Glide.with(imageView)
|
||||
.load(R.drawable.ic_music_box_preview_24dp)
|
||||
|
|
|
@ -59,9 +59,9 @@ import com.keylesspalace.tusky.util.show
|
|||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountListFragment :
|
||||
Fragment(R.layout.fragment_account_list),
|
||||
|
@ -96,7 +96,9 @@ class AccountListFragment :
|
|||
val layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
binding.recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
|
||||
binding.swipeRefreshLayout.setOnRefreshListener { fetchAccounts() }
|
||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
|
@ -116,7 +118,8 @@ class AccountListFragment :
|
|||
instanceName = activeAccount.domain,
|
||||
accountLocked = activeAccount.locked
|
||||
)
|
||||
val followRequestsAdapter = FollowRequestsAdapter(this, this, animateAvatar, animateEmojis, showBotOverlay)
|
||||
val followRequestsAdapter =
|
||||
FollowRequestsAdapter(this, this, animateAvatar, animateEmojis, showBotOverlay)
|
||||
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter)
|
||||
followRequestsAdapter
|
||||
}
|
||||
|
@ -142,7 +145,9 @@ class AccountListFragment :
|
|||
|
||||
override fun onViewTag(tag: String) {
|
||||
(activity as BaseActivity?)
|
||||
?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||
?.startActivityWithSlideInAnimation(
|
||||
StatusListActivity.newHashtagIntent(requireContext(), tag)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
|
@ -225,7 +230,11 @@ class AccountListFragment :
|
|||
val unblockedUser = blocksAdapter.removeItem(position)
|
||||
|
||||
if (unblockedUser != null) {
|
||||
Snackbar.make(binding.recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(
|
||||
binding.recyclerView,
|
||||
R.string.confirmation_unblocked,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.action_undo) {
|
||||
blocksAdapter.addItem(unblockedUser, position)
|
||||
onBlock(true, id, position)
|
||||
|
@ -243,11 +252,7 @@ class AccountListFragment :
|
|||
Log.e(TAG, "Failed to $verb account accountId $accountId")
|
||||
}
|
||||
|
||||
override fun onRespondToFollowRequest(
|
||||
accept: Boolean,
|
||||
accountId: String,
|
||||
position: Int
|
||||
) {
|
||||
override fun onRespondToFollowRequest(accept: Boolean, accountId: String, position: Int) {
|
||||
if (accept) {
|
||||
api.authorizeFollowRequest(accountId)
|
||||
} else {
|
||||
|
|
|
@ -60,9 +60,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
|||
}
|
||||
}
|
||||
|
||||
private fun createFooterViewHolder(
|
||||
parent: ViewGroup
|
||||
): RecyclerView.ViewHolder {
|
||||
private fun createFooterViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {
|
||||
val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
|
|
@ -39,16 +39,27 @@ class BlocksAdapter(
|
|||
) {
|
||||
|
||||
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemBlockedUserBinding> {
|
||||
val binding = ItemBlockedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding = ItemBlockedUserBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemBlockedUserBinding>, position: Int) {
|
||||
override fun onBindAccountViewHolder(
|
||||
viewHolder: BindingHolder<ItemBlockedUserBinding>,
|
||||
position: Int
|
||||
) {
|
||||
val account = accountList[position]
|
||||
val binding = viewHolder.binding
|
||||
val context = binding.root.context
|
||||
|
||||
val emojifiedName = account.name.emojify(account.emojis, binding.blockedUserDisplayName, animateEmojis)
|
||||
val emojifiedName = account.name.emojify(
|
||||
account.emojis,
|
||||
binding.blockedUserDisplayName,
|
||||
animateEmojis
|
||||
)
|
||||
binding.blockedUserDisplayName.text = emojifiedName
|
||||
val formattedUsername = context.getString(R.string.post_username_format, account.username)
|
||||
binding.blockedUserUsername.text = formattedUsername
|
||||
|
|
|
@ -27,12 +27,22 @@ class FollowRequestsHeaderAdapter(
|
|||
private val accountLocked: Boolean
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemFollowRequestsHeaderBinding>>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestsHeaderBinding> {
|
||||
val binding = ItemFollowRequestsHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemFollowRequestsHeaderBinding> {
|
||||
val binding = ItemFollowRequestsHeaderBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: BindingHolder<ItemFollowRequestsHeaderBinding>, position: Int) {
|
||||
override fun onBindViewHolder(
|
||||
viewHolder: BindingHolder<ItemFollowRequestsHeaderBinding>,
|
||||
position: Int
|
||||
) {
|
||||
viewHolder.binding.root.text = viewHolder.binding.root.context.getString(R.string.follow_requests_info, instanceName)
|
||||
}
|
||||
|
||||
|
|
|
@ -42,18 +42,29 @@ class MutesAdapter(
|
|||
private val mutingNotificationsMap = HashMap<String, Boolean>()
|
||||
|
||||
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemMutedUserBinding> {
|
||||
val binding = ItemMutedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding = ItemMutedUserBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemMutedUserBinding>, position: Int) {
|
||||
override fun onBindAccountViewHolder(
|
||||
viewHolder: BindingHolder<ItemMutedUserBinding>,
|
||||
position: Int
|
||||
) {
|
||||
val account = accountList[position]
|
||||
val binding = viewHolder.binding
|
||||
val context = binding.root.context
|
||||
|
||||
val mutingNotifications = mutingNotificationsMap[account.id]
|
||||
|
||||
val emojifiedName = account.name.emojify(account.emojis, binding.mutedUserDisplayName, animateEmojis)
|
||||
val emojifiedName = account.name.emojify(
|
||||
account.emojis,
|
||||
binding.mutedUserDisplayName,
|
||||
animateEmojis
|
||||
)
|
||||
binding.mutedUserDisplayName.text = emojifiedName
|
||||
|
||||
val formattedUsername = context.getString(R.string.post_username_format, account.username)
|
||||
|
|
|
@ -54,8 +54,15 @@ class AnnouncementAdapter(
|
|||
|
||||
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> {
|
||||
val binding = ItemAnnouncementBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemAnnouncementBinding> {
|
||||
val binding = ItemAnnouncementBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
|
@ -69,7 +76,11 @@ class AnnouncementAdapter(
|
|||
val chips = holder.binding.chipGroup
|
||||
val addReactionChip = holder.binding.addReactionChip
|
||||
|
||||
val emojifiedText: CharSequence = item.content.parseAsMastodonHtml().emojify(item.emojis, text, animateEmojis)
|
||||
val emojifiedText: CharSequence = item.content.parseAsMastodonHtml().emojify(
|
||||
item.emojis,
|
||||
text,
|
||||
animateEmojis
|
||||
)
|
||||
|
||||
setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
|
||||
|
||||
|
@ -107,7 +118,13 @@ class AnnouncementAdapter(
|
|||
spanBuilder.setSpan(span, 0, 1, 0)
|
||||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(if (animateEmojis) { reaction.url } else { reaction.staticUrl })
|
||||
.load(
|
||||
if (animateEmojis) {
|
||||
reaction.url
|
||||
} else {
|
||||
reaction.staticUrl
|
||||
}
|
||||
)
|
||||
.into(span.getTarget(animateEmojis))
|
||||
this.text = spanBuilder
|
||||
}
|
||||
|
|
|
@ -116,7 +116,10 @@ class AnnouncementsActivity :
|
|||
binding.progressBar.hide()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
if (it.data.isNullOrEmpty()) {
|
||||
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
|
||||
binding.errorMessageView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.no_announcements
|
||||
)
|
||||
binding.errorMessageView.show()
|
||||
} else {
|
||||
binding.errorMessageView.hide()
|
||||
|
@ -129,7 +132,10 @@ class AnnouncementsActivity :
|
|||
is Error -> {
|
||||
binding.progressBar.hide()
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
binding.errorMessageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
|
||||
binding.errorMessageView.setup(
|
||||
R.drawable.errorphant_error,
|
||||
R.string.error_generic
|
||||
) {
|
||||
refreshAnnouncements()
|
||||
}
|
||||
binding.errorMessageView.show()
|
||||
|
|
|
@ -31,8 +31,8 @@ import com.keylesspalace.tusky.util.Error
|
|||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AnnouncementsViewModel @Inject constructor(
|
||||
private val instanceInfoRepo: InstanceInfoRepository,
|
||||
|
@ -64,7 +64,9 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
mastodonApi.dismissAnnouncement(announcement.id)
|
||||
.fold(
|
||||
{
|
||||
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
|
||||
eventHub.dispatch(
|
||||
AnnouncementReadEvent(announcement.id)
|
||||
)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.d(
|
||||
|
|
|
@ -115,11 +115,6 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.DecimalFormat
|
||||
|
@ -127,6 +122,11 @@ import java.util.Locale
|
|||
import javax.inject.Inject
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
class ComposeActivity :
|
||||
BaseActivity(),
|
||||
|
@ -163,14 +163,23 @@ class ComposeActivity :
|
|||
|
||||
private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS
|
||||
|
||||
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||
if (success) {
|
||||
pickMedia(photoUploadUri!!)
|
||||
private val takePicture =
|
||||
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||
if (success) {
|
||||
pickMedia(photoUploadUri!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
|
||||
if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) {
|
||||
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(
|
||||
this,
|
||||
resources.getQuantityString(
|
||||
R.plurals.error_upload_max_media_reached,
|
||||
maxUploadMediaNumber,
|
||||
maxUploadMediaNumber
|
||||
),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
uris.forEach { uri ->
|
||||
pickMedia(uri)
|
||||
|
@ -191,7 +200,8 @@ class ComposeActivity :
|
|||
uriNew,
|
||||
size,
|
||||
itemOld.description,
|
||||
null, // Intentionally reset focus when cropping
|
||||
// Intentionally reset focus when cropping
|
||||
null,
|
||||
itemOld
|
||||
)
|
||||
}
|
||||
|
@ -222,7 +232,11 @@ class ComposeActivity :
|
|||
val mediaAdapter = MediaPreviewAdapter(
|
||||
this,
|
||||
onAddCaption = { item ->
|
||||
CaptionDialog.newInstance(item.localId, item.description, item.uri).show(supportFragmentManager, "caption_dialog")
|
||||
CaptionDialog.newInstance(
|
||||
item.localId,
|
||||
item.description,
|
||||
item.uri
|
||||
).show(supportFragmentManager, "caption_dialog")
|
||||
},
|
||||
onAddFocus = { item ->
|
||||
makeFocusDialog(item.focus, item.uri) { newFocus ->
|
||||
|
@ -240,7 +254,11 @@ class ComposeActivity :
|
|||
|
||||
/* If the composer is started up as a reply to another post, override the "starting" state
|
||||
* based on what the intent from the reply request passes. */
|
||||
val composeOptions: ComposeOptions? = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS_EXTRA, ComposeOptions::class.java)
|
||||
val composeOptions: ComposeOptions? = IntentCompat.getParcelableExtra(
|
||||
intent,
|
||||
COMPOSE_OPTIONS_EXTRA,
|
||||
ComposeOptions::class.java
|
||||
)
|
||||
viewModel.setup(composeOptions)
|
||||
|
||||
setupButtons()
|
||||
|
@ -303,12 +321,20 @@ class ComposeActivity :
|
|||
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri ->
|
||||
IntentCompat.getParcelableExtra(
|
||||
intent,
|
||||
Intent.EXTRA_STREAM,
|
||||
Uri::class.java
|
||||
)?.let { uri ->
|
||||
pickMedia(uri)
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
IntentCompat.getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.forEach { uri ->
|
||||
IntentCompat.getParcelableArrayListExtra(
|
||||
intent,
|
||||
Intent.EXTRA_STREAM,
|
||||
Uri::class.java
|
||||
)?.forEach { uri ->
|
||||
pickMedia(uri)
|
||||
}
|
||||
}
|
||||
|
@ -328,7 +354,13 @@ class ComposeActivity :
|
|||
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
||||
val left = min(start, end)
|
||||
val right = max(start, end)
|
||||
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
||||
binding.composeEditField.text.replace(
|
||||
left,
|
||||
right,
|
||||
shareBody,
|
||||
0,
|
||||
shareBody.length
|
||||
)
|
||||
// move edittext cursor to first when shareBody parsed
|
||||
binding.composeEditField.text.insert(0, "\n")
|
||||
binding.composeEditField.setSelection(0)
|
||||
|
@ -341,23 +373,48 @@ class ComposeActivity :
|
|||
if (replyingStatusAuthor != null) {
|
||||
binding.composeReplyView.show()
|
||||
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
|
||||
val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 }
|
||||
val arrowDownIcon = IconicsDrawable(
|
||||
this,
|
||||
GoogleMaterial.Icon.gmd_arrow_drop_down
|
||||
).apply {
|
||||
sizeDp = 12
|
||||
}
|
||||
|
||||
setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
|
||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
|
||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
arrowDownIcon,
|
||||
null
|
||||
)
|
||||
|
||||
binding.composeReplyView.setOnClickListener {
|
||||
TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup)
|
||||
TransitionManager.beginDelayedTransition(
|
||||
binding.composeReplyContentView.parent as ViewGroup
|
||||
)
|
||||
|
||||
if (binding.composeReplyContentView.isVisible) {
|
||||
binding.composeReplyContentView.hide()
|
||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
|
||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
arrowDownIcon,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
binding.composeReplyContentView.show()
|
||||
val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 }
|
||||
val arrowUpIcon = IconicsDrawable(
|
||||
this,
|
||||
GoogleMaterial.Icon.gmd_arrow_drop_up
|
||||
).apply { sizeDp = 12 }
|
||||
|
||||
setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
|
||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null)
|
||||
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
null,
|
||||
null,
|
||||
arrowUpIcon,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -374,7 +431,12 @@ class ComposeActivity :
|
|||
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
||||
binding.composeEditField.setOnReceiveContentListener(this)
|
||||
|
||||
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
||||
binding.composeEditField.setOnKeyListener { _, keyCode, event ->
|
||||
this.onKeyDown(
|
||||
keyCode,
|
||||
event
|
||||
)
|
||||
}
|
||||
|
||||
binding.composeEditField.setAdapter(
|
||||
ComposeAutoCompleteAdapter(
|
||||
|
@ -419,7 +481,9 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive ->
|
||||
viewModel.showContentWarning.combine(
|
||||
viewModel.markMediaAsSensitive
|
||||
) { showContentWarning, markSensitive ->
|
||||
updateSensitiveMediaToggle(markSensitive, showContentWarning)
|
||||
showContentWarning(showContentWarning)
|
||||
}.collect()
|
||||
|
@ -434,7 +498,10 @@ class ComposeActivity :
|
|||
mediaAdapter.submitList(media)
|
||||
|
||||
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
|
||||
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value, viewModel.showContentWarning.value)
|
||||
updateSensitiveMediaToggle(
|
||||
viewModel.markMediaAsSensitive.value,
|
||||
viewModel.showContentWarning.value
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -510,16 +577,42 @@ class ComposeActivity :
|
|||
|
||||
val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
|
||||
|
||||
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 }
|
||||
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null)
|
||||
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply {
|
||||
colorInt = textColor
|
||||
sizeDp = 18
|
||||
}
|
||||
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
cameraIcon,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 }
|
||||
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null)
|
||||
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply {
|
||||
colorInt = textColor
|
||||
sizeDp = 18
|
||||
}
|
||||
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
imageIcon,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 }
|
||||
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null)
|
||||
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply {
|
||||
colorInt = textColor
|
||||
sizeDp = 18
|
||||
}
|
||||
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
|
||||
pollIcon,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
|
||||
binding.actionPhotoTake.visible(Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null)
|
||||
binding.actionPhotoTake.visible(
|
||||
Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null
|
||||
)
|
||||
|
||||
binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
|
||||
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
||||
|
@ -549,7 +642,12 @@ class ComposeActivity :
|
|||
|
||||
private fun setupLanguageSpinner(initialLanguages: List<String>) {
|
||||
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
|
||||
}
|
||||
|
||||
|
@ -594,8 +692,12 @@ class ComposeActivity :
|
|||
|
||||
private fun replaceTextAtCaret(text: CharSequence) {
|
||||
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
|
||||
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
|
||||
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
|
||||
val start = binding.composeEditField.selectionStart.coerceAtMost(
|
||||
binding.composeEditField.selectionEnd
|
||||
)
|
||||
val end = binding.composeEditField.selectionStart.coerceAtLeast(
|
||||
binding.composeEditField.selectionEnd
|
||||
)
|
||||
val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) {
|
||||
" $text"
|
||||
} else {
|
||||
|
@ -609,8 +711,12 @@ class ComposeActivity :
|
|||
|
||||
fun prependSelectedWordsWith(text: CharSequence) {
|
||||
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
|
||||
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
|
||||
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
|
||||
val start = binding.composeEditField.selectionStart.coerceAtMost(
|
||||
binding.composeEditField.selectionEnd
|
||||
)
|
||||
val end = binding.composeEditField.selectionStart.coerceAtLeast(
|
||||
binding.composeEditField.selectionEnd
|
||||
)
|
||||
val editorText = binding.composeEditField.text
|
||||
|
||||
if (start == end) {
|
||||
|
@ -678,7 +784,10 @@ class ComposeActivity :
|
|||
this.viewModel.toggleMarkSensitive()
|
||||
}
|
||||
|
||||
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
|
||||
private fun updateSensitiveMediaToggle(
|
||||
markMediaSensitive: Boolean,
|
||||
contentWarningShown: Boolean
|
||||
) {
|
||||
if (viewModel.media.value.isEmpty()) {
|
||||
binding.composeHideMediaButton.hide()
|
||||
binding.descriptionMissingWarningButton.hide()
|
||||
|
@ -695,7 +804,10 @@ class ComposeActivity :
|
|||
getColor(R.color.tusky_blue)
|
||||
} else {
|
||||
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
|
||||
MaterialColors.getColor(binding.composeHideMediaButton, android.R.attr.textColorTertiary)
|
||||
MaterialColors.getColor(
|
||||
binding.composeHideMediaButton,
|
||||
android.R.attr.textColorTertiary
|
||||
)
|
||||
}
|
||||
}
|
||||
binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
|
@ -717,7 +829,10 @@ class ComposeActivity :
|
|||
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
|
||||
} else {
|
||||
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
||||
MaterialColors.getColor(binding.composeScheduleButton, android.R.attr.textColorTertiary)
|
||||
MaterialColors.getColor(
|
||||
binding.composeScheduleButton,
|
||||
android.R.attr.textColorTertiary
|
||||
)
|
||||
} else {
|
||||
getColor(R.color.tusky_blue)
|
||||
}
|
||||
|
@ -748,7 +863,11 @@ class ComposeActivity :
|
|||
binding.composeToggleVisibilityButton.setImageResource(iconRes)
|
||||
if (viewModel.editing) {
|
||||
// Can't update visibility on published status
|
||||
enableButton(binding.composeToggleVisibilityButton, clickable = false, colorActive = false)
|
||||
enableButton(
|
||||
binding.composeToggleVisibilityButton,
|
||||
clickable = false,
|
||||
colorActive = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -785,7 +904,11 @@ class ComposeActivity :
|
|||
private fun showEmojis() {
|
||||
binding.emojiView.adapter?.let {
|
||||
if (it.itemCount == 0) {
|
||||
val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain)
|
||||
val errorMessage =
|
||||
getString(
|
||||
R.string.error_no_custom_emojis,
|
||||
accountManager.activeAccount!!.domain
|
||||
)
|
||||
displayTransientMessage(errorMessage)
|
||||
} else {
|
||||
if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
|
@ -852,9 +975,14 @@ class ComposeActivity :
|
|||
|
||||
private fun setupPollView() {
|
||||
val margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||
val marginBottom = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
||||
val marginBottom = resources.getDimensionPixelSize(
|
||||
R.dimen.compose_media_preview_margin_bottom
|
||||
)
|
||||
|
||||
val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
val layoutParams = LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
layoutParams.setMargins(margin, margin, margin, marginBottom)
|
||||
binding.pollPreview.layoutParams = layoutParams
|
||||
|
||||
|
@ -905,7 +1033,10 @@ class ComposeActivity :
|
|||
val textColor = if (remainingLength < 0) {
|
||||
getColor(R.color.tusky_red)
|
||||
} else {
|
||||
MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary)
|
||||
MaterialColors.getColor(
|
||||
binding.composeCharactersLeftView,
|
||||
android.R.attr.textColorTertiary
|
||||
)
|
||||
}
|
||||
binding.composeCharactersLeftView.setTextColor(textColor)
|
||||
}
|
||||
|
@ -917,7 +1048,9 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
private fun verifyScheduledTime(): Boolean {
|
||||
return binding.composeScheduleView.verifyScheduledTime(binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value))
|
||||
return binding.composeScheduleView.verifyScheduledTime(
|
||||
binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value)
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSendClicked() {
|
||||
|
@ -967,7 +1100,11 @@ class ComposeActivity :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
|
||||
|
@ -1042,14 +1179,20 @@ class ComposeActivity :
|
|||
val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
|
||||
|
||||
// "Authority" must be the same as the android:authorities string in AndroidManifest.xml
|
||||
val uriNew = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile)
|
||||
val uriNew = FileProvider.getUriForFile(
|
||||
this,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
tempFile
|
||||
)
|
||||
|
||||
viewModel.cropImageItemOld = item
|
||||
|
||||
cropImage.launch(
|
||||
options(uri = item.uri) {
|
||||
setOutputUri(uriNew)
|
||||
setOutputCompressFormat(if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG)
|
||||
setOutputCompressFormat(
|
||||
if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -1087,7 +1230,9 @@ class ComposeActivity :
|
|||
val formattedSize = decimalFormat.format(allowedSizeInMb)
|
||||
getString(R.string.error_multimedia_size_limit, formattedSize)
|
||||
}
|
||||
is VideoOrImageException -> getString(R.string.error_media_upload_image_or_video)
|
||||
is VideoOrImageException -> getString(
|
||||
R.string.error_media_upload_image_or_video
|
||||
)
|
||||
else -> getString(R.string.error_media_upload_opening)
|
||||
}
|
||||
displayTransientMessage(errorString)
|
||||
|
@ -1096,16 +1241,23 @@ class ComposeActivity :
|
|||
}
|
||||
|
||||
private fun showContentWarning(show: Boolean) {
|
||||
TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup)
|
||||
TransitionManager.beginDelayedTransition(
|
||||
binding.composeContentWarningBar.parent as ViewGroup
|
||||
)
|
||||
@ColorInt val color = if (show) {
|
||||
binding.composeContentWarningBar.show()
|
||||
binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length)
|
||||
binding.composeContentWarningField.setSelection(
|
||||
binding.composeContentWarningField.text.length
|
||||
)
|
||||
binding.composeContentWarningField.requestFocus()
|
||||
getColor(R.color.tusky_blue)
|
||||
} else {
|
||||
binding.composeContentWarningBar.hide()
|
||||
binding.composeEditField.requestFocus()
|
||||
MaterialColors.getColor(binding.composeContentWarningButton, android.R.attr.textColorTertiary)
|
||||
MaterialColors.getColor(
|
||||
binding.composeContentWarningButton,
|
||||
android.R.attr.textColorTertiary
|
||||
)
|
||||
}
|
||||
binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
@ -1159,7 +1311,10 @@ class ComposeActivity :
|
|||
/**
|
||||
* User is editing a new post, and can either save the changes as a draft or discard them.
|
||||
*/
|
||||
private fun getSaveAsDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
||||
private fun getSaveAsDraftOrDiscardDialog(
|
||||
contentText: String,
|
||||
contentWarning: String
|
||||
): AlertDialog.Builder {
|
||||
val warning = if (viewModel.media.value.isNotEmpty()) {
|
||||
R.string.compose_save_draft_loses_media
|
||||
} else {
|
||||
|
@ -1182,7 +1337,10 @@ class ComposeActivity :
|
|||
* User is editing an existing draft, and can either update the draft with the new changes or
|
||||
* discard them.
|
||||
*/
|
||||
private fun getUpdateDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
||||
private fun getUpdateDraftOrDiscardDialog(
|
||||
contentText: String,
|
||||
contentWarning: String
|
||||
): AlertDialog.Builder {
|
||||
val warning = if (viewModel.media.value.isNotEmpty()) {
|
||||
R.string.compose_save_draft_loses_media
|
||||
} else {
|
||||
|
@ -1286,10 +1444,15 @@ class ComposeActivity :
|
|||
val state: State
|
||||
) {
|
||||
enum class Type {
|
||||
IMAGE, VIDEO, AUDIO;
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
AUDIO
|
||||
}
|
||||
enum class State {
|
||||
UPLOADING, UNPROCESSED, PROCESSED, PUBLISHED
|
||||
UPLOADING,
|
||||
UNPROCESSED,
|
||||
PROCESSED,
|
||||
PUBLISHED
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1370,10 +1533,7 @@ class ComposeActivity :
|
|||
* @return an Intent to start the ComposeActivity
|
||||
*/
|
||||
@JvmStatic
|
||||
fun startIntent(
|
||||
context: Context,
|
||||
options: ComposeOptions
|
||||
): Intent {
|
||||
fun startIntent(context: Context, options: ComposeOptions): Intent {
|
||||
return Intent(context, ComposeActivity::class.java).apply {
|
||||
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
||||
}
|
||||
|
|
|
@ -108,7 +108,9 @@ class ComposeAutoCompleteAdapter(
|
|||
val account = accountResult.account
|
||||
binding.username.text = context.getString(R.string.post_username_format, account.username)
|
||||
binding.displayName.text = account.name.emojify(account.emojis, binding.displayName, animateEmojis)
|
||||
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
|
||||
val avatarRadius = context.resources.getDimensionPixelSize(
|
||||
R.dimen.avatar_radius_42dp
|
||||
)
|
||||
loadAvatar(
|
||||
account.avatar,
|
||||
binding.avatar,
|
||||
|
|
|
@ -38,6 +38,7 @@ import com.keylesspalace.tusky.service.MediaToSend
|
|||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.keylesspalace.tusky.service.StatusToSend
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
@ -50,7 +51,6 @@ import kotlinx.coroutines.flow.shareIn
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class ComposeViewModel @Inject constructor(
|
||||
private val api: MastodonApi,
|
||||
|
@ -85,13 +85,19 @@ class ComposeViewModel @Inject constructor(
|
|||
val markMediaAsSensitive: MutableStateFlow<Boolean> =
|
||||
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||
|
||||
val statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
|
||||
val statusVisibility: MutableStateFlow<Status.Visibility> =
|
||||
MutableStateFlow(Status.Visibility.UNKNOWN)
|
||||
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
|
||||
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
|
||||
|
||||
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
val uploadError =
|
||||
MutableSharedFlow<Throwable>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
private lateinit var composeKind: ComposeKind
|
||||
|
||||
|
@ -100,7 +106,13 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
private var setupComplete = false
|
||||
|
||||
suspend fun pickMedia(mediaUri: Uri, description: String? = null, focus: Attachment.Focus? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
|
||||
suspend fun pickMedia(
|
||||
mediaUri: Uri,
|
||||
description: String? = null,
|
||||
focus: Attachment.Focus? = null
|
||||
): Result<QueuedMedia> = withContext(
|
||||
Dispatchers.IO
|
||||
) {
|
||||
try {
|
||||
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
|
||||
val mediaItems = media.value
|
||||
|
@ -164,7 +176,11 @@ class ComposeViewModel @Inject constructor(
|
|||
item.copy(
|
||||
id = event.mediaId,
|
||||
uploadPercent = -1,
|
||||
state = if (event.processed) { QueuedMedia.State.PROCESSED } else { QueuedMedia.State.UNPROCESSED }
|
||||
state = if (event.processed) {
|
||||
QueuedMedia.State.PROCESSED
|
||||
} else {
|
||||
QueuedMedia.State.UNPROCESSED
|
||||
}
|
||||
)
|
||||
is UploadEvent.ErrorEvent -> {
|
||||
media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
|
||||
|
@ -186,7 +202,13 @@ class ComposeViewModel @Inject constructor(
|
|||
return mediaItem
|
||||
}
|
||||
|
||||
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) {
|
||||
private fun addUploadedMedia(
|
||||
id: String,
|
||||
type: QueuedMedia.Type,
|
||||
uri: Uri,
|
||||
description: String?,
|
||||
focus: Attachment.Focus?
|
||||
) {
|
||||
media.update { mediaList ->
|
||||
val mediaItem = QueuedMedia(
|
||||
localId = mediaUploader.getNewLocalMediaId(),
|
||||
|
@ -305,11 +327,7 @@ class ComposeViewModel @Inject constructor(
|
|||
* Send status to the server.
|
||||
* Uses current state plus provided arguments.
|
||||
*/
|
||||
suspend fun sendStatus(
|
||||
content: String,
|
||||
spoilerText: String,
|
||||
accountId: Long
|
||||
) {
|
||||
suspend fun sendStatus(content: String, spoilerText: String, accountId: Long) {
|
||||
if (!scheduledTootId.isNullOrEmpty()) {
|
||||
api.deleteScheduledStatus(scheduledTootId!!)
|
||||
}
|
||||
|
@ -382,7 +400,11 @@ class ComposeViewModel @Inject constructor(
|
|||
})
|
||||
}
|
||||
'#' -> {
|
||||
return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||
return api.searchSync(
|
||||
query = token,
|
||||
type = SearchType.Hashtag.apiParameter,
|
||||
limit = 10
|
||||
)
|
||||
.fold({ searchResult ->
|
||||
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
|
||||
}, { e ->
|
||||
|
|
|
@ -54,10 +54,10 @@ fun downsizeImage(
|
|||
// Get EXIF data, for orientation info.
|
||||
val orientation = getImageOrientation(uri, contentResolver)
|
||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||
* formats. So, the only way to tell if they're too big is to compress them and
|
||||
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
||||
* many cases, so it should only iterate once, but the loop is used to be absolutely
|
||||
* sure it gets downsized to below the limit. */
|
||||
* formats. So, the only way to tell if they're too big is to compress them and
|
||||
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
||||
* many cases, so it should only iterate once, but the loop is used to be absolutely
|
||||
* sure it gets downsized to below the limit. */
|
||||
var scaledImageSize = 1024
|
||||
do {
|
||||
val outputStream = try {
|
||||
|
|
|
@ -113,11 +113,17 @@ class MediaPreviewAdapter(
|
|||
private val differ = AsyncListDiffer(
|
||||
this,
|
||||
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
||||
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ComposeActivity.QueuedMedia,
|
||||
newItem: ComposeActivity.QueuedMedia
|
||||
): Boolean {
|
||||
return oldItem.localId == newItem.localId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ComposeActivity.QueuedMedia,
|
||||
newItem: ComposeActivity.QueuedMedia
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,13 @@ import com.keylesspalace.tusky.util.getImageSquarePixels
|
|||
import com.keylesspalace.tusky.util.getMediaSize
|
||||
import com.keylesspalace.tusky.util.getServerErrorMessage
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
@ -54,19 +61,15 @@ import kotlinx.coroutines.flow.shareIn
|
|||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import retrofit2.HttpException
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
sealed interface FinalUploadEvent
|
||||
|
||||
sealed class UploadEvent {
|
||||
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
||||
data class FinishedEvent(val mediaId: String, val processed: Boolean) : UploadEvent(), FinalUploadEvent
|
||||
data class FinishedEvent(
|
||||
val mediaId: String,
|
||||
val processed: Boolean
|
||||
) : UploadEvent(), FinalUploadEvent
|
||||
data class ErrorEvent(val error: Throwable) : UploadEvent(), FinalUploadEvent
|
||||
}
|
||||
|
||||
|
@ -80,11 +83,7 @@ fun createNewImageFile(context: Context, suffix: String = ".jpg"): File {
|
|||
val randomId = randomAlphanumericString(12)
|
||||
val imageFileName = "Tusky_${randomId}_"
|
||||
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||
return File.createTempFile(
|
||||
imageFileName, /* prefix */
|
||||
suffix, /* suffix */
|
||||
storageDir /* directory */
|
||||
)
|
||||
return File.createTempFile(imageFileName, suffix, storageDir)
|
||||
}
|
||||
|
||||
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
||||
|
@ -256,9 +255,9 @@ class MediaUploader @Inject constructor(
|
|||
// .m4a files. See https://github.com/tuskyapp/Tusky/issues/3189 for details.
|
||||
// Sniff the content of the file to determine the actual type.
|
||||
if (mimeType != null && (
|
||||
mimeType.startsWith("audio/", ignoreCase = true) ||
|
||||
mimeType.startsWith("video/", ignoreCase = true)
|
||||
)
|
||||
mimeType.startsWith("audio/", ignoreCase = true) ||
|
||||
mimeType.startsWith("video/", ignoreCase = true)
|
||||
)
|
||||
) {
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(context, media.uri)
|
||||
|
|
|
@ -60,7 +60,9 @@ fun showAddPollDialog(
|
|||
binding.pollChoices.adapter = adapter
|
||||
|
||||
var durations = context.resources.getIntArray(R.array.poll_duration_values).toList()
|
||||
val durationLabels = context.resources.getStringArray(R.array.poll_duration_names).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration }
|
||||
val durationLabels = context.resources.getStringArray(
|
||||
R.array.poll_duration_names
|
||||
).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration }
|
||||
binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply {
|
||||
setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
|
||||
}
|
||||
|
@ -75,8 +77,8 @@ fun showAddPollDialog(
|
|||
}
|
||||
}
|
||||
|
||||
val DAY_SECONDS = 60 * 60 * 24
|
||||
val desiredDuration = poll?.expiresIn ?: DAY_SECONDS
|
||||
val secondsInADay = 60 * 60 * 24
|
||||
val desiredDuration = poll?.expiresIn ?: secondsInADay
|
||||
val pollDurationId = durations.indexOfLast {
|
||||
it <= desiredDuration
|
||||
}
|
||||
|
@ -105,5 +107,7 @@ fun showAddPollDialog(
|
|||
dialog.show()
|
||||
|
||||
// make the dialog focusable so the keyboard does not stay behind it
|
||||
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
|
||||
dialog.window?.clearFlags(
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
|
||||
)
|
||||
}
|
||||
|
|
|
@ -41,8 +41,15 @@ class AddPollOptionsAdapter(
|
|||
notifyItemInserted(options.size - 1)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAddPollOptionBinding> {
|
||||
val binding = ItemAddPollOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemAddPollOptionBinding> {
|
||||
val binding = ItemAddPollOptionBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
val holder = BindingHolder(binding)
|
||||
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
|
||||
|
||||
|
|
|
@ -133,17 +133,14 @@ class CaptionDialog : DialogFragment() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(
|
||||
localId: Int,
|
||||
existingDescription: String?,
|
||||
previewUri: Uri
|
||||
) = CaptionDialog().apply {
|
||||
arguments = bundleOf(
|
||||
LOCAL_ID_ARG to localId,
|
||||
EXISTING_DESCRIPTION_ARG to existingDescription,
|
||||
PREVIEW_URI_ARG to previewUri
|
||||
)
|
||||
}
|
||||
fun newInstance(localId: Int, existingDescription: String?, previewUri: Uri) =
|
||||
CaptionDialog().apply {
|
||||
arguments = bundleOf(
|
||||
LOCAL_ID_ARG to localId,
|
||||
EXISTING_DESCRIPTION_ARG to existingDescription,
|
||||
PREVIEW_URI_ARG to previewUri
|
||||
)
|
||||
}
|
||||
|
||||
private const val DESCRIPTION_KEY = "description"
|
||||
private const val EXISTING_DESCRIPTION_ARG = "existing_description"
|
||||
|
|
|
@ -49,11 +49,22 @@ fun <T> T.makeFocusDialog(
|
|||
.load(previewUri)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.listener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target<Drawable?>, p3: Boolean): Boolean {
|
||||
override fun onLoadFailed(
|
||||
p0: GlideException?,
|
||||
p1: Any?,
|
||||
p2: Target<Drawable?>,
|
||||
p3: Boolean
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable?>?, dataSource: DataSource, isFirstResource: Boolean): Boolean {
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable?>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean
|
||||
): Boolean {
|
||||
val width = resource.intrinsicWidth
|
||||
val height = resource.intrinsicHeight
|
||||
|
||||
|
|
|
@ -21,7 +21,10 @@ import android.widget.RadioGroup
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
|
||||
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(context, attrs) {
|
||||
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(
|
||||
context,
|
||||
attrs
|
||||
) {
|
||||
|
||||
var listener: ComposeOptionsListener? = null
|
||||
|
||||
|
|
|
@ -223,7 +223,8 @@ class ComposeScheduleView
|
|||
}
|
||||
|
||||
companion object {
|
||||
var MINIMUM_SCHEDULED_SECONDS = 330 // Minimum is 5 minutes, pad 30 seconds for posting
|
||||
// Minimum is 5 minutes, pad 30 seconds for posting
|
||||
private const val MINIMUM_SCHEDULED_SECONDS = 330
|
||||
fun calendar(): Calendar = Calendar.getInstance(TimeZone.getDefault())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,9 @@ class FocusIndicatorView
|
|||
return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility") // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget.
|
||||
@SuppressLint(
|
||||
"ClickableViewAccessibility"
|
||||
) // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget.
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
return false
|
||||
|
@ -112,7 +114,13 @@ class FocusIndicatorView
|
|||
|
||||
curtainPath.reset() // Draw a flood fill with a hole cut out of it
|
||||
curtainPath.fillType = Path.FillType.WINDING
|
||||
curtainPath.addRect(0.0f, 0.0f, this.width.toFloat(), this.height.toFloat(), Path.Direction.CW)
|
||||
curtainPath.addRect(
|
||||
0.0f,
|
||||
0.0f,
|
||||
this.width.toFloat(),
|
||||
this.height.toFloat(),
|
||||
Path.Direction.CW
|
||||
)
|
||||
curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW)
|
||||
canvas.drawPath(curtainPath, curtainPaint)
|
||||
|
||||
|
|
|
@ -60,7 +60,10 @@ class TootButton
|
|||
Status.Visibility.PRIVATE,
|
||||
Status.Visibility.DIRECT -> {
|
||||
setText(R.string.action_send)
|
||||
IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).apply { sizeDp = 18; colorInt = Color.WHITE }
|
||||
IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).apply {
|
||||
sizeDp = 18
|
||||
colorInt = Color.WHITE
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
null
|
||||
|
|
|
@ -38,7 +38,9 @@ class ConversationAdapter(
|
|||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
||||
val view = LayoutInflater.from(
|
||||
parent.context
|
||||
).inflate(R.layout.item_conversation, parent, false)
|
||||
return ConversationViewHolder(view, statusDisplayOptions, listener)
|
||||
}
|
||||
|
||||
|
@ -58,15 +60,24 @@ class ConversationAdapter(
|
|||
|
||||
companion object {
|
||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
|
||||
override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ConversationViewData,
|
||||
newItem: ConversationViewData
|
||||
): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ConversationViewData,
|
||||
newItem: ConversationViewData
|
||||
): Boolean {
|
||||
return false // Items are different always. It allows to refresh timestamp on every view holder update
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: ConversationViewData, newItem: ConversationViewData): Any? {
|
||||
override fun getChangePayload(
|
||||
oldItem: ConversationViewData,
|
||||
newItem: ConversationViewData
|
||||
): Any? {
|
||||
return if (oldItem == newItem) {
|
||||
// If items are equal - update timestamp only
|
||||
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
||||
|
|
|
@ -140,21 +140,16 @@ data class ConversationStatusEntity(
|
|||
}
|
||||
}
|
||||
|
||||
fun TimelineAccount.toEntity() =
|
||||
ConversationAccountEntity(
|
||||
id = id,
|
||||
localUsername = localUsername,
|
||||
username = username,
|
||||
displayName = name,
|
||||
avatar = avatar,
|
||||
emojis = emojis.orEmpty()
|
||||
)
|
||||
fun TimelineAccount.toEntity() = ConversationAccountEntity(
|
||||
id = id,
|
||||
localUsername = localUsername,
|
||||
username = username,
|
||||
displayName = name,
|
||||
avatar = avatar,
|
||||
emojis = emojis.orEmpty()
|
||||
)
|
||||
|
||||
fun Status.toEntity(
|
||||
expanded: Boolean,
|
||||
contentShowing: Boolean,
|
||||
contentCollapsed: Boolean
|
||||
) =
|
||||
fun Status.toEntity(expanded: Boolean, contentShowing: Boolean, contentCollapsed: Boolean) =
|
||||
ConversationStatusEntity(
|
||||
id = id,
|
||||
url = url,
|
||||
|
@ -188,16 +183,15 @@ fun Conversation.toEntity(
|
|||
expanded: Boolean,
|
||||
contentShowing: Boolean,
|
||||
contentCollapsed: Boolean
|
||||
) =
|
||||
ConversationEntity(
|
||||
accountId = accountId,
|
||||
id = id,
|
||||
order = order,
|
||||
accounts = accounts.map { it.toEntity() },
|
||||
unread = unread,
|
||||
lastStatus = lastStatus!!.toEntity(
|
||||
expanded = expanded,
|
||||
contentShowing = contentShowing,
|
||||
contentCollapsed = contentCollapsed
|
||||
)
|
||||
) = ConversationEntity(
|
||||
accountId = accountId,
|
||||
id = id,
|
||||
order = order,
|
||||
accounts = accounts.map { it.toEntity() },
|
||||
unread = unread,
|
||||
lastStatus = lastStatus!!.toEntity(
|
||||
expanded = expanded,
|
||||
contentShowing = contentShowing,
|
||||
contentCollapsed = contentCollapsed
|
||||
)
|
||||
)
|
||||
|
|
|
@ -27,7 +27,10 @@ class ConversationLoadStateAdapter(
|
|||
private val retryCallback: () -> Unit
|
||||
) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() {
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemNetworkStateBinding>, loadState: LoadState) {
|
||||
override fun onBindViewHolder(
|
||||
holder: BindingHolder<ItemNetworkStateBinding>,
|
||||
loadState: LoadState
|
||||
) {
|
||||
val binding = holder.binding
|
||||
binding.progressBar.visible(loadState == LoadState.Loading)
|
||||
binding.retryButton.visible(loadState is LoadState.Error)
|
||||
|
@ -47,7 +50,11 @@ class ConversationLoadStateAdapter(
|
|||
parent: ViewGroup,
|
||||
loadState: LoadState
|
||||
): BindingHolder<ItemNetworkStateBinding> {
|
||||
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding = ItemNetworkStateBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,12 +63,12 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ConversationsFragment :
|
||||
SFragment(),
|
||||
|
@ -91,7 +91,11 @@ class ConversationsFragment :
|
|||
|
||||
private var hideFab = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
||||
}
|
||||
|
||||
|
@ -141,13 +145,19 @@ class ConversationsFragment :
|
|||
is LoadState.NotLoading -> {
|
||||
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||
binding.statusView.show()
|
||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
||||
binding.statusView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.message_empty,
|
||||
null
|
||||
)
|
||||
binding.statusView.showHelp(R.string.help_empty_conversations)
|
||||
}
|
||||
}
|
||||
is LoadState.Error -> {
|
||||
binding.statusView.show()
|
||||
binding.statusView.setup((loadState.refresh as LoadState.Error).error) { refreshContent() }
|
||||
binding.statusView.setup(
|
||||
(loadState.refresh as LoadState.Error).error
|
||||
) { refreshContent() }
|
||||
}
|
||||
is LoadState.Loading -> {
|
||||
binding.progressBar.show()
|
||||
|
@ -240,7 +250,9 @@ class ConversationsFragment :
|
|||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
|
||||
binding.recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
|
@ -298,7 +310,11 @@ class ConversationsFragment :
|
|||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||
adapter.peek(position)?.let { conversation ->
|
||||
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view)
|
||||
viewMedia(
|
||||
attachmentIndex,
|
||||
AttachmentViewData.list(conversation.lastStatus.status),
|
||||
view
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,10 @@ class ConversationsRemoteMediator(
|
|||
}
|
||||
|
||||
try {
|
||||
val conversationsResponse = api.getConversations(maxId = nextKey, limit = state.config.pageSize)
|
||||
val conversationsResponse = api.getConversations(
|
||||
maxId = nextKey,
|
||||
limit = state.config.pageSize
|
||||
)
|
||||
|
||||
val conversations = conversationsResponse.body()
|
||||
if (!conversationsResponse.isSuccessful || conversations == null) {
|
||||
|
|
|
@ -29,9 +29,9 @@ import com.keylesspalace.tusky.db.AppDatabase
|
|||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||
import com.keylesspalace.tusky.util.EmptyPagingSource
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConversationsViewModel @Inject constructor(
|
||||
private val timelineCases: TimelineCases,
|
||||
|
@ -91,7 +91,11 @@ class ConversationsViewModel @Inject constructor(
|
|||
|
||||
fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) {
|
||||
viewModelScope.launch {
|
||||
timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices)
|
||||
timelineCases.voteInPoll(
|
||||
conversation.lastStatus.id,
|
||||
conversation.lastStatus.status.poll?.id!!,
|
||||
choices
|
||||
)
|
||||
.fold({ poll ->
|
||||
val newConversation = conversation.toEntity(
|
||||
accountId = accountManager.activeAccount!!.id,
|
||||
|
|
|
@ -11,8 +11,15 @@ class DomainBlocksAdapter(
|
|||
private val onUnmute: (String) -> Unit
|
||||
) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemBlockedDomainBinding> {
|
||||
val binding = ItemBlockedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemBlockedDomainBinding> {
|
||||
val binding = ItemBlockedDomainBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
|
|
|
@ -18,9 +18,9 @@ import com.keylesspalace.tusky.util.hide
|
|||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectable {
|
||||
|
||||
|
@ -35,7 +35,9 @@ class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectab
|
|||
val adapter = DomainBlocksAdapter(viewModel::unblock)
|
||||
|
||||
binding.recyclerView.setHasFixedSize(true)
|
||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
binding.recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
|
||||
|
||||
|
@ -52,7 +54,9 @@ class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectab
|
|||
}
|
||||
|
||||
adapter.addLoadStateListener { loadState ->
|
||||
binding.progressBar.visible(loadState.refresh == LoadState.Loading && adapter.itemCount == 0)
|
||||
binding.progressBar.visible(
|
||||
loadState.refresh == LoadState.Loading && adapter.itemCount == 0
|
||||
)
|
||||
|
||||
if (loadState.refresh is LoadState.Error) {
|
||||
binding.recyclerView.hide()
|
||||
|
|
|
@ -8,9 +8,9 @@ import androidx.paging.cachedIn
|
|||
import at.connyduck.calladapter.networkresult.fold
|
||||
import at.connyduck.calladapter.networkresult.onFailure
|
||||
import com.keylesspalace.tusky.R
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class DomainBlocksViewModel @Inject constructor(
|
||||
private val repo: DomainBlocksRepository
|
||||
|
|
|
@ -29,18 +29,18 @@ import com.keylesspalace.tusky.entity.Attachment
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.copyToFile
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
|
||||
class DraftHelper @Inject constructor(
|
||||
val context: Context,
|
||||
|
@ -200,6 +200,10 @@ class DraftHelper @Inject constructor(
|
|||
} else {
|
||||
this.copyToFile(contentResolver, file)
|
||||
}
|
||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
||||
return FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
file
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,10 @@ class DraftMediaAdapter(
|
|||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||
override fun areContentsTheSame(
|
||||
oldItem: DraftAttachment,
|
||||
newItem: DraftAttachment
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +78,9 @@ class DraftMediaAdapter(
|
|||
RecyclerView.ViewHolder(imageView) {
|
||||
init {
|
||||
val thumbnailViewSize =
|
||||
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||
imageView.context.resources.getDimensionPixelSize(
|
||||
R.dimen.compose_media_preview_size
|
||||
)
|
||||
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
||||
val margin = itemView.context.resources
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||
|
|
|
@ -38,9 +38,9 @@ import com.keylesspalace.tusky.di.ViewModelFactory
|
|||
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||
|
||||
|
@ -74,7 +74,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
|
||||
binding.draftsRecyclerView.adapter = adapter
|
||||
binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||
binding.draftsRecyclerView.addItemDecoration(
|
||||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
|
||||
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
||||
|
||||
|
@ -134,10 +136,18 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
if (throwable.isHttpNotFound()) {
|
||||
// the original status to which a reply was drafted has been deleted
|
||||
// let's open the ComposeActivity without reply information
|
||||
Toast.makeText(context, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(
|
||||
context,
|
||||
getString(R.string.drafts_post_reply_removed),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
openDraftWithoutReply(draft)
|
||||
} else {
|
||||
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
getString(R.string.drafts_failed_loading_reply),
|
||||
Snackbar.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,10 @@ class DraftsAdapter(
|
|||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemDraftBinding> {
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemDraftBinding> {
|
||||
val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
|
||||
val viewHolder = BindingHolder(binding)
|
||||
|
@ -77,7 +80,9 @@ class DraftsAdapter(
|
|||
holder.binding.content.text = draft.content
|
||||
|
||||
holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty())
|
||||
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments)
|
||||
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(
|
||||
draft.attachments
|
||||
)
|
||||
|
||||
if (draft.poll != null) {
|
||||
holder.binding.draftPoll.show()
|
||||
|
|
|
@ -26,8 +26,8 @@ import com.keylesspalace.tusky.db.AppDatabase
|
|||
import com.keylesspalace.tusky.db.DraftEntity
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class DraftsViewModel @Inject constructor(
|
||||
val database: AppDatabase,
|
||||
|
@ -38,7 +38,11 @@ class DraftsViewModel @Inject constructor(
|
|||
|
||||
val drafts = Pager(
|
||||
config = PagingConfig(pageSize = 20),
|
||||
pagingSourceFactory = { database.draftDao().draftsPagingSource(accountManager.activeAccount?.id!!) }
|
||||
pagingSourceFactory = {
|
||||
database.draftDao().draftsPagingSource(
|
||||
accountManager.activeAccount?.id!!
|
||||
)
|
||||
}
|
||||
).flow
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
|
|
|
@ -29,9 +29,9 @@ import com.keylesspalace.tusky.network.MastodonApi
|
|||
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class EditFilterActivity : BaseActivity() {
|
||||
@Inject
|
||||
|
@ -115,7 +115,12 @@ class EditFilterActivity : BaseActivity() {
|
|||
)
|
||||
}
|
||||
binding.filterDurationSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
override fun onItemSelected(
|
||||
parent: AdapterView<*>?,
|
||||
view: View?,
|
||||
position: Int,
|
||||
id: Long
|
||||
) {
|
||||
viewModel.setDuration(
|
||||
if (originalFilter?.expiresAt == null) {
|
||||
position
|
||||
|
@ -266,10 +271,16 @@ class EditFilterActivity : BaseActivity() {
|
|||
if (viewModel.saveChanges(this@EditFilterActivity)) {
|
||||
finish()
|
||||
// Possibly affected contexts: any context affected by the original filter OR any context affected by the updated filter
|
||||
val affectedContexts = viewModel.contexts.value.map { it.kind }.union(originalFilter?.context ?: listOf()).distinct()
|
||||
val affectedContexts = viewModel.contexts.value.map {
|
||||
it.kind
|
||||
}.union(originalFilter?.context ?: listOf()).distinct()
|
||||
eventHub.dispatch(FilterUpdatedEvent(affectedContexts))
|
||||
} else {
|
||||
Snackbar.make(binding.root, "Error saving filter '${viewModel.title.value}'", Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
"Error saving filter '${viewModel.title.value}'",
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -288,11 +299,19 @@ class EditFilterActivity : BaseActivity() {
|
|||
finish()
|
||||
},
|
||||
{
|
||||
Snackbar.make(binding.root, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
"Error deleting filter '${filter.title}'",
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Snackbar.make(binding.root, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
binding.root,
|
||||
"Error deleting filter '${filter.title}'",
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -307,7 +326,11 @@ class EditFilterActivity : BaseActivity() {
|
|||
// but create/edit take a number of seconds (relative to the time the operation is posted)
|
||||
fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): Int? {
|
||||
return when (index) {
|
||||
-1 -> if (default == null) { default } else { ((default.time - System.currentTimeMillis()) / 1000).toInt() }
|
||||
-1 -> if (default == null) {
|
||||
default
|
||||
} else {
|
||||
((default.time - System.currentTimeMillis()) / 1000).toInt()
|
||||
}
|
||||
0 -> null
|
||||
else -> context?.resources?.getIntArray(R.array.filter_duration_values)?.get(index)
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ import com.keylesspalace.tusky.entity.Filter
|
|||
import com.keylesspalace.tusky.entity.FilterKeyword
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() {
|
||||
private var originalFilter: Filter? = null
|
||||
|
@ -92,7 +92,13 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun createFilter(title: String, contexts: List<String>, action: String, durationIndex: Int, context: Context): Boolean {
|
||||
private suspend fun createFilter(
|
||||
title: String,
|
||||
contexts: List<String>,
|
||||
action: String,
|
||||
durationIndex: Int,
|
||||
context: Context
|
||||
): Boolean {
|
||||
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
||||
api.createFilter(
|
||||
title = title,
|
||||
|
@ -103,7 +109,11 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
{ newFilter ->
|
||||
// This is _terrible_, but the all-in-one update filter api Just Doesn't Work
|
||||
return keywords.value.map { keyword ->
|
||||
api.addFilterKeyword(filterId = newFilter.id, keyword = keyword.keyword, wholeWord = keyword.wholeWord)
|
||||
api.addFilterKeyword(
|
||||
filterId = newFilter.id,
|
||||
keyword = keyword.keyword,
|
||||
wholeWord = keyword.wholeWord
|
||||
)
|
||||
}.none { it.isFailure }
|
||||
},
|
||||
{ throwable ->
|
||||
|
@ -116,7 +126,14 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun updateFilter(originalFilter: Filter, title: String, contexts: List<String>, action: String, durationIndex: Int, context: Context): Boolean {
|
||||
private suspend fun updateFilter(
|
||||
originalFilter: Filter,
|
||||
title: String,
|
||||
contexts: List<String>,
|
||||
action: String,
|
||||
durationIndex: Int,
|
||||
context: Context
|
||||
): Boolean {
|
||||
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
|
||||
api.updateFilter(
|
||||
id = originalFilter.id,
|
||||
|
|
|
@ -22,7 +22,9 @@ import androidx.appcompat.app.AlertDialog
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.util.await
|
||||
|
||||
internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder(this)
|
||||
internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder(
|
||||
this
|
||||
)
|
||||
.setMessage(getString(R.string.dialog_delete_filter_text, filterTitle))
|
||||
.setCancelable(true)
|
||||
.create()
|
||||
|
|
|
@ -14,8 +14,8 @@ import com.keylesspalace.tusky.util.hide
|
|||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class FiltersActivity : BaseActivity(), FiltersListener {
|
||||
@Inject
|
||||
|
@ -54,20 +54,30 @@ class FiltersActivity : BaseActivity(), FiltersListener {
|
|||
private fun observeViewModel() {
|
||||
lifecycleScope.launch {
|
||||
viewModel.state.collect { state ->
|
||||
binding.progressBar.visible(state.loadingState == FiltersViewModel.LoadingState.LOADING)
|
||||
binding.progressBar.visible(
|
||||
state.loadingState == FiltersViewModel.LoadingState.LOADING
|
||||
)
|
||||
binding.swipeRefreshLayout.isRefreshing = state.loadingState == FiltersViewModel.LoadingState.LOADING
|
||||
binding.addFilterButton.visible(state.loadingState == FiltersViewModel.LoadingState.LOADED)
|
||||
binding.addFilterButton.visible(
|
||||
state.loadingState == FiltersViewModel.LoadingState.LOADED
|
||||
)
|
||||
|
||||
when (state.loadingState) {
|
||||
FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide()
|
||||
FiltersViewModel.LoadingState.ERROR_NETWORK -> {
|
||||
binding.messageView.setup(R.drawable.errorphant_offline, R.string.error_network) {
|
||||
binding.messageView.setup(
|
||||
R.drawable.errorphant_offline,
|
||||
R.string.error_network
|
||||
) {
|
||||
loadFilters()
|
||||
}
|
||||
binding.messageView.show()
|
||||
}
|
||||
FiltersViewModel.LoadingState.ERROR_OTHER -> {
|
||||
binding.messageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
|
||||
binding.messageView.setup(
|
||||
R.drawable.errorphant_error,
|
||||
R.string.error_generic
|
||||
) {
|
||||
loadFilters()
|
||||
}
|
||||
binding.messageView.show()
|
||||
|
|
|
@ -14,8 +14,13 @@ class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
|
|||
|
||||
override fun getItemCount(): Int = filters.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemRemovableBinding> {
|
||||
return BindingHolder(ItemRemovableBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemRemovableBinding> {
|
||||
return BindingHolder(
|
||||
ItemRemovableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemRemovableBinding>, position: Int) {
|
||||
|
|
|
@ -10,10 +10,10 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
|||
import com.keylesspalace.tusky.entity.Filter
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class FiltersViewModel @Inject constructor(
|
||||
private val api: MastodonApi,
|
||||
|
@ -21,7 +21,11 @@ class FiltersViewModel @Inject constructor(
|
|||
) : ViewModel() {
|
||||
|
||||
enum class LoadingState {
|
||||
INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER
|
||||
INITIAL,
|
||||
LOADING,
|
||||
LOADED,
|
||||
ERROR_NETWORK,
|
||||
ERROR_OTHER
|
||||
}
|
||||
|
||||
data class State(val filters: List<Filter>, val loadingState: LoadingState)
|
||||
|
@ -61,7 +65,12 @@ class FiltersViewModel @Inject constructor(
|
|||
viewModelScope.launch {
|
||||
api.deleteFilter(filter.id).fold(
|
||||
{
|
||||
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
|
||||
this@FiltersViewModel._state.value = State(
|
||||
this@FiltersViewModel._state.value.filters.filter {
|
||||
it.id != filter.id
|
||||
},
|
||||
LoadingState.LOADED
|
||||
)
|
||||
for (context in filter.context) {
|
||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||
}
|
||||
|
@ -70,14 +79,27 @@ class FiltersViewModel @Inject constructor(
|
|||
if (throwable.isHttpNotFound()) {
|
||||
api.deleteFilterV1(filter.id).fold(
|
||||
{
|
||||
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
|
||||
this@FiltersViewModel._state.value = State(
|
||||
this@FiltersViewModel._state.value.filters.filter {
|
||||
it.id != filter.id
|
||||
},
|
||||
LoadingState.LOADED
|
||||
)
|
||||
},
|
||||
{
|
||||
Snackbar.make(parent, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
parent,
|
||||
"Error deleting filter '${filter.title}'",
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Snackbar.make(parent, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
|
||||
Snackbar.make(
|
||||
parent,
|
||||
"Error deleting filter '${filter.title}'",
|
||||
Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -29,9 +29,9 @@ import com.keylesspalace.tusky.util.hide
|
|||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class FollowedTagsActivity :
|
||||
BaseActivity(),
|
||||
|
@ -81,7 +81,9 @@ class FollowedTagsActivity :
|
|||
binding.followedTagsView.adapter = adapter
|
||||
binding.followedTagsView.setHasFixedSize(true)
|
||||
binding.followedTagsView.layoutManager = LinearLayoutManager(this)
|
||||
binding.followedTagsView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||
binding.followedTagsView.addItemDecoration(
|
||||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
(binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
val hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||
|
@ -101,7 +103,9 @@ class FollowedTagsActivity :
|
|||
private fun setupAdapter(): FollowedTagsAdapter {
|
||||
return FollowedTagsAdapter(this, viewModel).apply {
|
||||
addLoadStateListener { loadState ->
|
||||
binding.followedTagsProgressBar.visible(loadState.refresh == LoadState.Loading && itemCount == 0)
|
||||
binding.followedTagsProgressBar.visible(
|
||||
loadState.refresh == LoadState.Loading && itemCount == 0
|
||||
)
|
||||
|
||||
if (loadState.refresh is LoadState.Error) {
|
||||
binding.followedTagsView.hide()
|
||||
|
|
|
@ -15,13 +15,22 @@ class FollowedTagsAdapter(
|
|||
private val actionListener: HashtagActionListener,
|
||||
private val viewModel: FollowedTagsViewModel
|
||||
) : PagingDataAdapter<String, BindingHolder<ItemFollowedHashtagBinding>>(STRING_COMPARATOR) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowedHashtagBinding> =
|
||||
BindingHolder(ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false))
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemFollowedHashtagBinding> = BindingHolder(
|
||||
ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemFollowedHashtagBinding>, position: Int) {
|
||||
override fun onBindViewHolder(
|
||||
holder: BindingHolder<ItemFollowedHashtagBinding>,
|
||||
position: Int
|
||||
) {
|
||||
viewModel.tags[position].let { tag ->
|
||||
holder.itemView.findViewById<TextView>(R.id.followed_tag).text = tag.name
|
||||
holder.itemView.findViewById<ImageButton>(R.id.followed_tag_unfollow).setOnClickListener {
|
||||
holder.itemView.findViewById<ImageButton>(
|
||||
R.id.followed_tag_unfollow
|
||||
).setOnClickListener {
|
||||
actionListener.unfollow(tag.name, holder.bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
|
@ -31,8 +40,10 @@ class FollowedTagsAdapter(
|
|||
|
||||
companion object {
|
||||
val STRING_COMPARATOR = object : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
|
||||
oldItem == newItem
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean =
|
||||
oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,10 +35,16 @@ class FollowedTagsViewModel @Inject constructor(
|
|||
}
|
||||
).flow.cachedIn(viewModelScope)
|
||||
|
||||
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||
fun searchAutocompleteSuggestions(
|
||||
token: String
|
||||
): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||
return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||
.fold({ searchResult ->
|
||||
searchResult.hashtags.map { ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult(it.name) }
|
||||
searchResult.hashtags.map {
|
||||
ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult(
|
||||
it.name
|
||||
)
|
||||
}
|
||||
}, { e ->
|
||||
Log.e(TAG, "Autocomplete search for $token failed.", e)
|
||||
emptyList()
|
||||
|
|
|
@ -26,9 +26,9 @@ import com.keylesspalace.tusky.db.InstanceInfoEntity
|
|||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class InstanceInfoRepository @Inject constructor(
|
||||
private val api: MastodonApi,
|
||||
|
@ -77,7 +77,7 @@ class InstanceInfoRepository @Inject constructor(
|
|||
maxMediaAttachments = instance.configuration.statuses?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS,
|
||||
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
|
||||
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
|
||||
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength,
|
||||
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength
|
||||
)
|
||||
dao.upsert(instanceEntity)
|
||||
instanceEntity
|
||||
|
@ -86,7 +86,11 @@ class InstanceInfoRepository @Inject constructor(
|
|||
if (throwable.isHttpNotFound()) {
|
||||
getInstanceInfoV1()
|
||||
} else {
|
||||
Log.w(TAG, "failed to instance, falling back to cache and default values", throwable)
|
||||
Log.w(
|
||||
TAG,
|
||||
"failed to instance, falling back to cache and default values",
|
||||
throwable
|
||||
)
|
||||
dao.getInstanceInfo(instanceName)
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +109,7 @@ class InstanceInfoRepository @Inject constructor(
|
|||
maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS,
|
||||
maxFieldNameLength = instanceInfo?.maxFieldNameLength,
|
||||
maxFieldValueLength = instanceInfo?.maxFieldValueLength,
|
||||
version = instanceInfo?.version,
|
||||
version = instanceInfo?.version
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -129,13 +133,17 @@ class InstanceInfoRepository @Inject constructor(
|
|||
maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments,
|
||||
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
|
||||
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
|
||||
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength,
|
||||
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength
|
||||
)
|
||||
dao.upsert(instanceEntity)
|
||||
instanceEntity
|
||||
},
|
||||
{ throwable ->
|
||||
Log.w(TAG, "failed to instance, falling back to cache and default values", throwable)
|
||||
Log.w(
|
||||
TAG,
|
||||
"failed to instance, falling back to cache and default values",
|
||||
throwable
|
||||
)
|
||||
dao.getInstanceInfo(instanceName)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -42,9 +42,9 @@ import com.keylesspalace.tusky.util.openLinkInCustomTab
|
|||
import com.keylesspalace.tusky.util.rickRoll
|
||||
import com.keylesspalace.tusky.util.shouldRickRoll
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.HttpUrl
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Main login page, the first thing that users see. Has prompt for instance and login button. */
|
||||
class LoginActivity : BaseActivity(), Injectable {
|
||||
|
@ -201,7 +201,11 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String, openInWebView: Boolean) {
|
||||
private fun redirectUserToAuthorizeAndLogin(
|
||||
domain: String,
|
||||
clientId: String,
|
||||
openInWebView: Boolean
|
||||
) {
|
||||
// To authorize this app and log in it's necessary to redirect to the domain given,
|
||||
// login there, and the server will redirect back to the app with its response.
|
||||
val uri = HttpUrl.Builder()
|
||||
|
|
|
@ -45,9 +45,9 @@ import com.keylesspalace.tusky.di.ViewModelFactory
|
|||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
/** Contract for starting [LoginWebViewActivity]. */
|
||||
class OauthLogin : ActivityResultContract<LoginData, LoginResult>() {
|
||||
|
|
|
@ -21,9 +21,9 @@ import androidx.lifecycle.viewModelScope
|
|||
import at.connyduck.calladapter.networkresult.fold
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.isHttpNotFound
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class LoginWebViewViewModel @Inject constructor(
|
||||
private val api: MastodonApi
|
||||
|
@ -48,11 +48,19 @@ class LoginWebViewViewModel @Inject constructor(
|
|||
instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty()
|
||||
},
|
||||
{ throwable ->
|
||||
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable)
|
||||
Log.w(
|
||||
"LoginWebViewViewModel",
|
||||
"failed to load instance info",
|
||||
throwable
|
||||
)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable)
|
||||
Log.w(
|
||||
"LoginWebViewViewModel",
|
||||
"failed to load instance info",
|
||||
throwable
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -14,10 +14,10 @@ import com.keylesspalace.tusky.entity.Notification
|
|||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||
import com.keylesspalace.tusky.util.isLessThan
|
||||
import kotlinx.coroutines.delay
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.min
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/** Models next/prev links from the "Links" header in an API response */
|
||||
data class Links(val next: String?, val prev: String?) {
|
||||
|
@ -55,12 +55,16 @@ class NotificationFetcher @Inject constructor(
|
|||
for (account in accountManager.getAllAccountsOrderedByActive()) {
|
||||
if (account.notificationsEnabled) {
|
||||
try {
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val notificationManager = context.getSystemService(
|
||||
Context.NOTIFICATION_SERVICE
|
||||
) as NotificationManager
|
||||
|
||||
// Create sorted list of new notifications
|
||||
val notifications = fetchNewNotifications(account)
|
||||
.filter { filterNotification(notificationManager, account, it) }
|
||||
.sortedWith(compareBy({ it.id.length }, { it.id })) // oldest notifications first
|
||||
.sortedWith(
|
||||
compareBy({ it.id.length }, { it.id })
|
||||
) // oldest notifications first
|
||||
.toMutableList()
|
||||
|
||||
// TODO do this before filter above? But one could argue that (for example) a tab badge is also a notification
|
||||
|
@ -74,13 +78,18 @@ class NotificationFetcher @Inject constructor(
|
|||
// Err on the side of removing *older* notifications to make room for newer
|
||||
// notifications.
|
||||
val currentAndroidNotifications = notificationManager.activeNotifications
|
||||
.sortedWith(compareBy({ it.tag.length }, { it.tag })) // oldest notifications first
|
||||
.sortedWith(
|
||||
compareBy({ it.tag.length }, { it.tag })
|
||||
) // oldest notifications first
|
||||
|
||||
// Check to see if any notifications need to be removed
|
||||
val toRemove = currentAndroidNotifications.size + notifications.size - MAX_NOTIFICATIONS
|
||||
if (toRemove > 0) {
|
||||
// Prefer to cancel old notifications first
|
||||
currentAndroidNotifications.subList(0, min(toRemove, currentAndroidNotifications.size))
|
||||
currentAndroidNotifications.subList(
|
||||
0,
|
||||
min(toRemove, currentAndroidNotifications.size)
|
||||
)
|
||||
.forEach { notificationManager.cancel(it.tag, it.id) }
|
||||
|
||||
// Still got notifications to remove? Trim the list of new notifications,
|
||||
|
@ -106,7 +115,11 @@ class NotificationFetcher @Inject constructor(
|
|||
account,
|
||||
notificationsGroup.value.size == 1
|
||||
)
|
||||
notificationManager.notify(notification.id, account.id.toInt(), androidNotification)
|
||||
notificationManager.notify(
|
||||
notification.id,
|
||||
account.id.toInt(),
|
||||
androidNotification
|
||||
)
|
||||
|
||||
// Android will rate limit / drop notifications if they're posted too
|
||||
// quickly. There is no indication to the user that this happened.
|
||||
|
@ -158,7 +171,14 @@ class NotificationFetcher @Inject constructor(
|
|||
Log.d(TAG, "getting notification marker for ${account.fullName}")
|
||||
val remoteMarkerId = fetchMarker(authHeader, account)?.lastReadId ?: "0"
|
||||
val localMarkerId = account.notificationMarkerId
|
||||
val markerId = if (remoteMarkerId.isLessThan(localMarkerId)) localMarkerId else remoteMarkerId
|
||||
val markerId = if (remoteMarkerId.isLessThan(
|
||||
localMarkerId
|
||||
)
|
||||
) {
|
||||
localMarkerId
|
||||
} else {
|
||||
remoteMarkerId
|
||||
}
|
||||
val readingPosition = account.lastNotificationId
|
||||
|
||||
var minId: String? = if (readingPosition.isLessThan(markerId)) markerId else readingPosition
|
||||
|
|
|
@ -66,7 +66,9 @@ fun showMigrationNoticeIfNecessary(
|
|||
|
||||
Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE)
|
||||
.setAnchorView(anchorView)
|
||||
.setAction(R.string.action_details) { showMigrationExplanationDialog(context, accountManager) }
|
||||
.setAction(
|
||||
R.string.action_details
|
||||
) { showMigrationExplanationDialog(context, accountManager) }
|
||||
.show()
|
||||
}
|
||||
|
||||
|
@ -75,7 +77,9 @@ private fun showMigrationExplanationDialog(context: Context, accountManager: Acc
|
|||
if (currentAccountNeedsMigration(accountManager)) {
|
||||
setMessage(R.string.dialog_push_notification_migration)
|
||||
setPositiveButton(R.string.title_migration_relogin) { _, _ ->
|
||||
context.startActivity(LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION))
|
||||
context.startActivity(
|
||||
LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setMessage(R.string.dialog_push_notification_migration_other_accounts)
|
||||
|
@ -89,12 +93,21 @@ private fun showMigrationExplanationDialog(context: Context, accountManager: Acc
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun enableUnifiedPushNotificationsForAccount(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
|
||||
private suspend fun enableUnifiedPushNotificationsForAccount(
|
||||
context: Context,
|
||||
api: MastodonApi,
|
||||
accountManager: AccountManager,
|
||||
account: AccountEntity
|
||||
) {
|
||||
if (isUnifiedPushNotificationEnabledForAccount(account)) {
|
||||
// Already registered, update the subscription to match notification settings
|
||||
updateUnifiedPushSubscription(context, api, accountManager, account)
|
||||
} else {
|
||||
UnifiedPush.registerAppWithDialog(context, account.id.toString(), features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE))
|
||||
UnifiedPush.registerAppWithDialog(
|
||||
context,
|
||||
account.id.toString(),
|
||||
features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -116,7 +129,11 @@ private fun isUnifiedPushAvailable(context: Context): Boolean =
|
|||
fun canEnablePushNotifications(context: Context, accountManager: AccountManager): Boolean =
|
||||
isUnifiedPushAvailable(context) && !anyAccountNeedsMigration(accountManager)
|
||||
|
||||
suspend fun enablePushNotificationsWithFallback(context: Context, api: MastodonApi, accountManager: AccountManager) {
|
||||
suspend fun enablePushNotificationsWithFallback(
|
||||
context: Context,
|
||||
api: MastodonApi,
|
||||
accountManager: AccountManager
|
||||
) {
|
||||
if (!canEnablePushNotifications(context, accountManager)) {
|
||||
// No UP distributors
|
||||
NotificationHelper.enablePullNotifications(context)
|
||||
|
@ -151,9 +168,14 @@ fun disableAllNotifications(context: Context, accountManager: AccountManager) {
|
|||
|
||||
private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> =
|
||||
buildMap {
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val notificationManager = context.getSystemService(
|
||||
Context.NOTIFICATION_SERVICE
|
||||
) as NotificationManager
|
||||
Notification.Type.visibleTypes.forEach {
|
||||
put("data[alerts][${it.presentation}]", NotificationHelper.filterNotification(notificationManager, account, it))
|
||||
put(
|
||||
"data[alerts][${it.presentation}]",
|
||||
NotificationHelper.filterNotification(notificationManager, account, it)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -196,7 +218,12 @@ suspend fun registerUnifiedPushEndpoint(
|
|||
}
|
||||
|
||||
// Synchronize the enabled / disabled state of notifications with server-side subscription
|
||||
suspend fun updateUnifiedPushSubscription(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
|
||||
suspend fun updateUnifiedPushSubscription(
|
||||
context: Context,
|
||||
api: MastodonApi,
|
||||
accountManager: AccountManager,
|
||||
account: AccountEntity
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.updatePushNotificationSubscription(
|
||||
"Bearer ${account.accessToken}",
|
||||
|
@ -211,7 +238,11 @@ suspend fun updateUnifiedPushSubscription(context: Context, api: MastodonApi, ac
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun unregisterUnifiedPushEndpoint(api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
|
||||
suspend fun unregisterUnifiedPushEndpoint(
|
||||
api: MastodonApi,
|
||||
accountManager: AccountManager,
|
||||
account: AccountEntity
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain)
|
||||
.onFailure { throwable ->
|
||||
|
|
|
@ -56,10 +56,10 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeRes
|
||||
import javax.inject.Inject
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
|
||||
class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||
@Inject
|
||||
|
@ -74,7 +74,11 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
@Inject
|
||||
lateinit var accountPreferenceDataStore: AccountPreferenceDataStore
|
||||
|
||||
private val iconSize by unsafeLazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
|
||||
private val iconSize by unsafeLazy {
|
||||
resources.getDimensionPixelSize(
|
||||
R.dimen.preference_icon_size
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
val context = requireContext()
|
||||
|
@ -198,14 +202,17 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
value = visibility.serverString()
|
||||
setIcon(getIconForVisibility(visibility))
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
setIcon(getIconForVisibility(Status.Visibility.byString(newValue as String)))
|
||||
setIcon(
|
||||
getIconForVisibility(Status.Visibility.byString(newValue as String))
|
||||
)
|
||||
syncWithServer(visibility = newValue)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
listPreference {
|
||||
val locales = getLocaleList(getInitialLanguages(null, accountManager.activeAccount))
|
||||
val locales =
|
||||
getLocaleList(getInitialLanguages(null, accountManager.activeAccount))
|
||||
setTitle(R.string.pref_default_post_language)
|
||||
// Explicitly add "System default" to the start of the list
|
||||
entries = (
|
||||
|
@ -289,14 +296,21 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
startActivity(intent)
|
||||
} else {
|
||||
activity?.let {
|
||||
val intent = PreferencesActivity.newIntent(it, PreferencesActivity.NOTIFICATION_PREFERENCES)
|
||||
val intent = PreferencesActivity.newIntent(
|
||||
it,
|
||||
PreferencesActivity.NOTIFICATION_PREFERENCES
|
||||
)
|
||||
it.startActivity(intent)
|
||||
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null, language: String? = null) {
|
||||
private fun syncWithServer(
|
||||
visibility: String? = null,
|
||||
sensitive: Boolean? = null,
|
||||
language: String? = null
|
||||
) {
|
||||
// TODO these could also be "datastore backed" preferences (a ServerPreferenceDataStore); follow-up of issue #3204
|
||||
|
||||
mastodonApi.accountUpdateSource(visibility, sensitive, language)
|
||||
|
|
|
@ -40,8 +40,8 @@ import com.keylesspalace.tusky.util.getNonNullString
|
|||
import com.keylesspalace.tusky.util.setAppNightMode
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PreferencesActivity :
|
||||
BaseActivity(),
|
||||
|
@ -127,12 +127,16 @@ class PreferencesActivity :
|
|||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this)
|
||||
PreferenceManager.getDefaultSharedPreferences(
|
||||
this
|
||||
).registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this)
|
||||
PreferenceManager.getDefaultSharedPreferences(
|
||||
this
|
||||
).unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
private fun saveInstanceState(outState: Bundle) {
|
||||
|
|
|
@ -49,7 +49,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
@Inject
|
||||
lateinit var localeManager: LocaleManager
|
||||
|
||||
private val iconSize by unsafeLazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
|
||||
private val iconSize by unsafeLazy {
|
||||
resources.getDimensionPixelSize(
|
||||
R.dimen.preference_icon_size
|
||||
)
|
||||
}
|
||||
|
||||
enum class ReadingOrder {
|
||||
/** User scrolls up, reading statuses oldest to newest */
|
||||
|
@ -253,7 +257,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS
|
||||
setOnPreferenceChangeListener { _, value ->
|
||||
for (account in accountManager.accounts) {
|
||||
val notificationFilter = deserialize(account.notificationsFilter).toMutableSet()
|
||||
val notificationFilter = deserialize(
|
||||
account.notificationsFilter
|
||||
).toMutableSet()
|
||||
|
||||
if (value == true) {
|
||||
notificationFilter.add(Notification.Type.FAVOURITE)
|
||||
|
|
|
@ -59,7 +59,10 @@ class ProxyPreferencesFragment : PreferenceFragmentCompat() {
|
|||
MAX_PROXY_PORT
|
||||
)
|
||||
|
||||
validatedEditTextPreference(portErrorMessage, ProxyConfiguration::isValidProxyPort) {
|
||||
validatedEditTextPreference(
|
||||
portErrorMessage,
|
||||
ProxyConfiguration::isValidProxyPort
|
||||
) {
|
||||
setTitle(R.string.pref_title_http_proxy_port)
|
||||
key = PrefKeys.HTTP_PROXY_PORT
|
||||
isIconSpaceReserved = false
|
||||
|
|
|
@ -46,7 +46,9 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
val accountId = intent?.getStringExtra(ACCOUNT_ID)
|
||||
val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME)
|
||||
if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) {
|
||||
throw IllegalStateException("accountId ($accountId) or accountUserName ($accountUserName) is null")
|
||||
throw IllegalStateException(
|
||||
"accountId ($accountId) or accountUserName ($accountUserName) is null"
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID))
|
||||
|
@ -130,13 +132,17 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
private const val STATUS_ID = "status_id"
|
||||
|
||||
@JvmStatic
|
||||
fun getIntent(context: Context, accountId: String, userName: String, statusId: String? = null) =
|
||||
Intent(context, ReportActivity::class.java)
|
||||
.apply {
|
||||
putExtra(ACCOUNT_ID, accountId)
|
||||
putExtra(ACCOUNT_USERNAME, userName)
|
||||
putExtra(STATUS_ID, statusId)
|
||||
}
|
||||
fun getIntent(
|
||||
context: Context,
|
||||
accountId: String,
|
||||
userName: String,
|
||||
statusId: String? = null
|
||||
) = Intent(context, ReportActivity::class.java)
|
||||
.apply {
|
||||
putExtra(ACCOUNT_ID, accountId)
|
||||
putExtra(ACCOUNT_USERNAME, userName)
|
||||
putExtra(STATUS_ID, statusId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun androidInjector() = androidInjector
|
||||
|
|
|
@ -37,12 +37,12 @@ import com.keylesspalace.tusky.util.Loading
|
|||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.toViewData
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReportViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
|
@ -196,7 +196,12 @@ class ReportViewModel @Inject constructor(
|
|||
fun doReport() {
|
||||
reportingStateMutable.value = Loading()
|
||||
viewModelScope.launch {
|
||||
mastodonApi.report(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null)
|
||||
mastodonApi.report(
|
||||
accountId,
|
||||
selectedIds.toList(),
|
||||
reportNote,
|
||||
if (isRemoteAccount) isRemoteNotify else null
|
||||
)
|
||||
.fold({
|
||||
reportingStateMutable.value = Success(true)
|
||||
}, { error ->
|
||||
|
|
|
@ -20,5 +20,5 @@ enum class Screen {
|
|||
Note,
|
||||
Done,
|
||||
Back,
|
||||
Finish,
|
||||
Finish
|
||||
}
|
||||
|
|
|
@ -50,7 +50,9 @@ class StatusViewHolder(
|
|||
private val getStatusForPosition: (Int) -> StatusViewData.Concrete?
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
|
||||
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(
|
||||
R.dimen.status_media_preview_height
|
||||
)
|
||||
private val statusViewHelper = StatusViewHelper(itemView)
|
||||
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
||||
|
||||
|
@ -93,7 +95,11 @@ class StatusViewHolder(
|
|||
mediaViewHeight
|
||||
)
|
||||
|
||||
statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions)
|
||||
statusViewHelper.setupPollReadonly(
|
||||
viewData.status.poll.toViewData(),
|
||||
viewData.status.emojis,
|
||||
statusDisplayOptions
|
||||
)
|
||||
setCreatedAt(viewData.status.createdAt)
|
||||
}
|
||||
|
||||
|
@ -107,11 +113,22 @@ class StatusViewHolder(
|
|||
)
|
||||
|
||||
if (viewdata.status.spoilerText.isBlank()) {
|
||||
setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||
setTextVisible(
|
||||
true,
|
||||
viewdata.content,
|
||||
viewdata.status.mentions,
|
||||
viewdata.status.tags,
|
||||
viewdata.status.emojis,
|
||||
adapterHandler
|
||||
)
|
||||
binding.statusContentWarningButton.hide()
|
||||
binding.statusContentWarningDescription.hide()
|
||||
} else {
|
||||
val emojiSpoiler = viewdata.status.spoilerText.emojify(viewdata.status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
|
||||
val emojiSpoiler = viewdata.status.spoilerText.emojify(
|
||||
viewdata.status.emojis,
|
||||
binding.statusContentWarningDescription,
|
||||
statusDisplayOptions.animateEmojis
|
||||
)
|
||||
binding.statusContentWarningDescription.text = emojiSpoiler
|
||||
binding.statusContentWarningDescription.show()
|
||||
binding.statusContentWarningButton.show()
|
||||
|
@ -121,11 +138,25 @@ class StatusViewHolder(
|
|||
val contentShown = viewState.isContentShow(viewdata.id, true)
|
||||
binding.statusContentWarningDescription.invalidate()
|
||||
viewState.setContentShow(viewdata.id, !contentShown)
|
||||
setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||
setTextVisible(
|
||||
!contentShown,
|
||||
viewdata.content,
|
||||
viewdata.status.mentions,
|
||||
viewdata.status.tags,
|
||||
viewdata.status.emojis,
|
||||
adapterHandler
|
||||
)
|
||||
setContentWarningButtonText(!contentShown)
|
||||
}
|
||||
}
|
||||
setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||
setTextVisible(
|
||||
viewState.isContentShow(viewdata.id, true),
|
||||
viewdata.content,
|
||||
viewdata.status.mentions,
|
||||
viewdata.status.tags,
|
||||
viewdata.status.emojis,
|
||||
adapterHandler
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +178,11 @@ class StatusViewHolder(
|
|||
listener: LinkListener
|
||||
) {
|
||||
if (expanded) {
|
||||
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
|
||||
val emojifiedText = content.emojify(
|
||||
emojis,
|
||||
binding.statusContent,
|
||||
statusDisplayOptions.animateEmojis
|
||||
)
|
||||
setClickableText(binding.statusContent, emojifiedText, mentions, tags, listener)
|
||||
} else {
|
||||
setClickableMentions(binding.statusContent, mentions, listener)
|
||||
|
@ -174,7 +209,12 @@ class StatusViewHolder(
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) {
|
||||
private fun setupCollapsedState(
|
||||
collapsible: Boolean,
|
||||
collapsed: Boolean,
|
||||
expanded: Boolean,
|
||||
spoilerText: String
|
||||
) {
|
||||
/* input filter for TextViews have to be set before text */
|
||||
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
||||
binding.buttonToggleContent.setOnClickListener {
|
||||
|
|
|
@ -36,7 +36,11 @@ class StatusesAdapter(
|
|||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
|
||||
val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
val binding = ItemReportStatusBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return StatusViewHolder(
|
||||
binding,
|
||||
statusDisplayOptions,
|
||||
|
@ -54,11 +58,15 @@ class StatusesAdapter(
|
|||
|
||||
companion object {
|
||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
|
||||
override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||
oldItem == newItem
|
||||
override fun areContentsTheSame(
|
||||
oldItem: StatusViewData.Concrete,
|
||||
newItem: StatusViewData.Concrete
|
||||
): Boolean = oldItem == newItem
|
||||
|
||||
override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
override fun areItemsTheSame(
|
||||
oldItem: StatusViewData.Concrete,
|
||||
newItem: StatusViewData.Concrete
|
||||
): Boolean = oldItem.id == newItem.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,8 @@ class StatusesPagingSource(
|
|||
val result = if (params is LoadParams.Refresh && key != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val initialStatus = async { getSingleStatus(key) }
|
||||
val additionalStatuses = async { getStatusList(maxId = key, limit = params.loadSize - 1) }
|
||||
val additionalStatuses =
|
||||
async { getStatusList(maxId = key, limit = params.loadSize - 1) }
|
||||
listOf(initialStatus.await()) + additionalStatuses.await()
|
||||
}
|
||||
} else {
|
||||
|
@ -75,7 +76,11 @@ class StatusesPagingSource(
|
|||
return mastodonApi.statusObservable(statusId).await()
|
||||
}
|
||||
|
||||
private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List<Status> {
|
||||
private suspend fun getStatusList(
|
||||
minId: String? = null,
|
||||
maxId: String? = null,
|
||||
limit: Int
|
||||
): List<Status> {
|
||||
return mastodonApi.accountStatusesObservable(
|
||||
accountId = accountId,
|
||||
maxId = maxId,
|
||||
|
|
|
@ -95,7 +95,11 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
|
|||
binding.buttonBack.isEnabled = true
|
||||
binding.progressBar.hide()
|
||||
|
||||
Snackbar.make(binding.buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
Snackbar.make(
|
||||
binding.buttonBack,
|
||||
if (error is IOException) R.string.error_network else R.string.error_generic,
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setAction(R.string.action_retry) {
|
||||
sendReport()
|
||||
}
|
||||
|
|
|
@ -59,9 +59,9 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReportStatusesFragment :
|
||||
Fragment(R.layout.fragment_report_statuses),
|
||||
|
@ -93,7 +93,11 @@ class ReportStatusesFragment :
|
|||
if (v != null) {
|
||||
val url = actionable.attachments[idx].url
|
||||
ViewCompat.setTransitionName(v, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), v, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
requireActivity(),
|
||||
v,
|
||||
url
|
||||
)
|
||||
startActivity(intent, options.toBundle())
|
||||
} else {
|
||||
startActivity(intent)
|
||||
|
@ -164,7 +168,9 @@ class ReportStatusesFragment :
|
|||
|
||||
adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this)
|
||||
|
||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL))
|
||||
binding.recyclerView.addItemDecoration(
|
||||
DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerView.adapter = adapter
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
@ -185,7 +191,9 @@ class ReportStatusesFragment :
|
|||
|
||||
binding.progressBarBottom.visible(loadState.append == LoadState.Loading)
|
||||
binding.progressBarTop.visible(loadState.prepend == LoadState.Loading)
|
||||
binding.progressBarLoading.visible(loadState.refresh == LoadState.Loading && !binding.swipeRefreshLayout.isRefreshing)
|
||||
binding.progressBarLoading.visible(
|
||||
loadState.refresh == LoadState.Loading && !binding.swipeRefreshLayout.isRefreshing
|
||||
)
|
||||
|
||||
if (loadState.refresh != LoadState.Loading) {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
|
@ -221,9 +229,13 @@ class ReportStatusesFragment :
|
|||
return viewModel.isStatusChecked(id)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
||||
override fun onViewAccount(id: String) = startActivity(
|
||||
AccountActivity.getIntent(requireContext(), id)
|
||||
)
|
||||
|
||||
override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||
override fun onViewTag(tag: String) = startActivity(
|
||||
StatusListActivity.newHashtagIntent(requireContext(), tag)
|
||||
)
|
||||
|
||||
override fun onViewUrl(url: String) = viewModel.checkClickedUrl(url)
|
||||
|
||||
|
|
|
@ -20,17 +20,35 @@ class StatusViewState {
|
|||
private val contentShownState = HashMap<String, Boolean>()
|
||||
private val longContentCollapsedState = HashMap<String, Boolean>()
|
||||
|
||||
fun isMediaShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(mediaShownState, id, !isSensitive)
|
||||
fun isMediaShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(
|
||||
mediaShownState,
|
||||
id,
|
||||
!isSensitive
|
||||
)
|
||||
fun setMediaShow(id: String, isShow: Boolean) = setStateEnabled(mediaShownState, id, isShow)
|
||||
|
||||
fun isContentShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(contentShownState, id, !isSensitive)
|
||||
fun isContentShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(
|
||||
contentShownState,
|
||||
id,
|
||||
!isSensitive
|
||||
)
|
||||
fun setContentShow(id: String, isShow: Boolean) = setStateEnabled(contentShownState, id, isShow)
|
||||
|
||||
fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(longContentCollapsedState, id, isCollapsed)
|
||||
fun setCollapsed(id: String, isCollapsed: Boolean) = setStateEnabled(longContentCollapsedState, id, isCollapsed)
|
||||
fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(
|
||||
longContentCollapsedState,
|
||||
id,
|
||||
isCollapsed
|
||||
)
|
||||
fun setCollapsed(id: String, isCollapsed: Boolean) =
|
||||
setStateEnabled(longContentCollapsedState, id, isCollapsed)
|
||||
|
||||
private fun isStateEnabled(map: Map<String, Boolean>, id: String, def: Boolean): Boolean = map[id]
|
||||
?: def
|
||||
private fun isStateEnabled(map: Map<String, Boolean>, id: String, def: Boolean): Boolean =
|
||||
map[id]
|
||||
?: def
|
||||
|
||||
private fun setStateEnabled(map: MutableMap<String, Boolean>, id: String, state: Boolean) = map.put(id, state)
|
||||
private fun setStateEnabled(map: MutableMap<String, Boolean>, id: String, state: Boolean) =
|
||||
map.put(
|
||||
id,
|
||||
state
|
||||
)
|
||||
}
|
||||
|
|
|
@ -45,9 +45,9 @@ import com.mikepenz.iconics.IconicsDrawable
|
|||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScheduledStatusActivity :
|
||||
BaseActivity(),
|
||||
|
@ -109,7 +109,10 @@ class ScheduledStatusActivity :
|
|||
if (loadState.refresh is LoadState.NotLoading) {
|
||||
binding.progressBar.hide()
|
||||
if (adapter.itemCount == 0) {
|
||||
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_posts)
|
||||
binding.errorMessageView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.no_scheduled_posts
|
||||
)
|
||||
binding.errorMessageView.show()
|
||||
} else {
|
||||
binding.errorMessageView.hide()
|
||||
|
@ -163,8 +166,8 @@ class ScheduledStatusActivity :
|
|||
visibility = item.params.visibility,
|
||||
scheduledAt = item.scheduledAt,
|
||||
sensitive = item.params.sensitive,
|
||||
kind = ComposeActivity.ComposeKind.EDIT_SCHEDULED,
|
||||
),
|
||||
kind = ComposeActivity.ComposeKind.EDIT_SCHEDULED
|
||||
)
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
|
|
@ -36,18 +36,31 @@ class ScheduledStatusAdapter(
|
|||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ScheduledStatus,
|
||||
newItem: ScheduledStatus
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledStatusBinding> {
|
||||
val binding = ItemScheduledStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemScheduledStatusBinding> {
|
||||
val binding = ItemScheduledStatusBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledStatusBinding>, position: Int) {
|
||||
override fun onBindViewHolder(
|
||||
holder: BindingHolder<ItemScheduledStatusBinding>,
|
||||
position: Int
|
||||
) {
|
||||
getItem(position)?.let { item ->
|
||||
holder.binding.edit.isEnabled = true
|
||||
holder.binding.delete.isEnabled = true
|
||||
|
|
|
@ -25,8 +25,8 @@ import at.connyduck.calladapter.networkresult.fold
|
|||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ScheduledStatusViewModel @Inject constructor(
|
||||
val mastodonApi: MastodonApi,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue