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:
Konrad Pozniak 2024-01-04 17:00:55 +01:00 committed by GitHub
parent 33cd6fdb98
commit 5192fb08a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
215 changed files with 2813 additions and 1177 deletions

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)
}
)

View File

@ -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() }
)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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))

View File

@ -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)
}

View File

@ -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

View File

@ -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) {

View File

@ -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)
}

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)
}

View File

@ -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")

View File

@ -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 {

View File

@ -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)
}

View File

@ -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,

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)

View File

@ -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
}

View File

@ -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()

View File

@ -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(

View File

@ -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)
}

View File

@ -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,

View File

@ -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 ->

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
)
}

View File

@ -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))

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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())
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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
)
)

View File

@ -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)
}
}

View File

@ -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
)
}
}

View File

@ -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) {

View File

@ -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,

View File

@ -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)
}

View File

@ -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()

View File

@ -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

View File

@ -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
)
}
}

View 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)

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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)

View File

@ -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)
}

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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) {

View File

@ -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()
}
}
)

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -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()

View File

@ -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)
}
)

View File

@ -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()

View File

@ -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>() {

View File

@ -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
)
}
}
)

View File

@ -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

View File

@ -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 ->

View File

@ -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)

View File

@ -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) {

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 ->

View File

@ -20,5 +20,5 @@ enum class Screen {
Note,
Done,
Back,
Finish,
Finish
}

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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,

View File

@ -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()
}

View File

@ -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)

View File

@ -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
)
}

View File

@ -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)
}

View File

@ -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

View File

@ -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