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 trim_trailing_whitespace = true
[*.{java,kt}] [*.{java,kt}]
ij_kotlin_imports_layout = *
# Disable wildcard imports # Disable wildcard imports
ij_kotlin_name_count_to_use_star_import = 999 ij_kotlin_name_count_to_use_star_import = 999
ij_kotlin_name_count_to_use_star_import_for_members = 999 ij_kotlin_name_count_to_use_star_import_for_members = 999
ij_java_class_count_to_use_import_on_demand = 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}] [*.{yml,yaml}]
indent_size = 2 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.NoUnderlineURLSpan
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class AboutActivity : BottomSheetActivity(), Injectable { class AboutActivity : BottomSheetActivity(), Injectable {
@Inject @Inject
@ -70,9 +70,15 @@ class AboutActivity : BottomSheetActivity(), Injectable {
binding.aboutPoweredByTusky.hide() binding.aboutPoweredByTusky.hide()
} }
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license) binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site) R.string.about_tusky_license
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site) )
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(
R.string.about_project_site
)
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(
R.string.about_bug_feature_request_site
)
binding.tuskyProfileButton.setOnClickListener { binding.tuskyProfileButton.setOnClickListener {
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL) 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.util.viewBinding
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State import com.keylesspalace.tusky.viewmodel.State
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
private typealias AccountInfo = Pair<TimelineAccount, Boolean> private typealias AccountInfo = Pair<TimelineAccount, Boolean>
@ -82,11 +82,18 @@ class AccountsInListFragment : DialogFragment(), Injectable {
super.onStart() super.onStart()
dialog?.apply { dialog?.apply {
// Stretch dialog to the window // 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) return inflater.inflate(R.layout.fragment_accounts_in_list, container, false)
} }
@ -164,15 +171,27 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return oldItem.id == newItem.id return oldItem.id == newItem.id
} }
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean { override fun areContentsTheSame(
oldItem: TimelineAccount,
newItem: TimelineAccount
): Boolean {
return oldItem == newItem 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> { override fun onCreateViewHolder(
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding) val holder = BindingHolder(binding)
binding.notificationTextView.hide() binding.notificationTextView.hide()
@ -186,7 +205,10 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return holder return holder
} }
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) { override fun onBindViewHolder(
holder: BindingHolder<ItemFollowRequestBinding>,
position: Int
) {
val account = getItem(position) val account = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
holder.binding.usernameTextView.text = account.username 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> { override fun onCreateViewHolder(
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding) val holder = BindingHolder(binding)
binding.notificationTextView.hide() binding.notificationTextView.hide()
@ -224,7 +255,10 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return holder return holder
} }
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) { override fun onBindViewHolder(
holder: BindingHolder<ItemFollowRequestBinding>,
position: Int
) {
val (account, inAList) = getItem(position) val (account, inAList) = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) 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)) { if (!looksLikeMastodonUrl(url)) {
openLink(url) openLink(url)
return return
@ -121,10 +124,17 @@ abstract class BottomSheetActivity : BaseActivity() {
startActivityWithSlideInAnimation(intent) startActivityWithSlideInAnimation(intent)
} }
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) { protected open fun performUrlFallbackAction(
url: String,
fallbackBehavior: PostLookupFallbackBehavior
) {
when (fallbackBehavior) { when (fallbackBehavior) {
PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url) 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.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class EditProfileActivity : BaseActivity(), Injectable { class EditProfileActivity : BaseActivity(), Injectable {
@ -126,9 +126,17 @@ class EditProfileActivity : BaseActivity(), Injectable {
binding.fieldList.layoutManager = LinearLayoutManager(this) binding.fieldList.layoutManager = LinearLayoutManager(this)
binding.fieldList.adapter = accountFieldEditAdapter 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 { binding.addFieldButton.setOnClickListener {
accountFieldEditAdapter.addField() accountFieldEditAdapter.addField()
@ -162,7 +170,9 @@ class EditProfileActivity : BaseActivity(), Injectable {
.placeholder(R.drawable.avatar_default) .placeholder(R.drawable.avatar_default)
.transform( .transform(
FitCenter(), FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) RoundedCorners(
resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)
)
) )
.into(binding.avatarPreview) .into(binding.avatarPreview)
} }
@ -175,7 +185,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
} }
} }
is Error -> { 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) { .setAction(R.string.action_retry) {
viewModel.obtainProfile() viewModel.obtainProfile()
} }
@ -188,7 +202,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
lifecycleScope.launch { lifecycleScope.launch {
viewModel.instanceData.collect { instanceInfo -> viewModel.instanceData.collect { instanceInfo ->
maxAccountFields = instanceInfo.maxFields maxAccountFields = instanceInfo.maxFields
accountFieldEditAdapter.setFieldLimits(instanceInfo.maxFieldNameLength, instanceInfo.maxFieldValueLength) accountFieldEditAdapter.setFieldLimits(
instanceInfo.maxFieldNameLength,
instanceInfo.maxFieldValueLength
)
binding.addFieldButton.isVisible = binding.addFieldButton.isVisible =
accountFieldEditAdapter.itemCount < maxAccountFields accountFieldEditAdapter.itemCount < maxAccountFields
} }
@ -318,7 +335,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
private fun onPickFailure(throwable: Throwable?) { private fun onPickFailure(throwable: Throwable?) {
Log.w("EditProfileActivity", "failed to pick media", 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 { 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 com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch
import javax.inject.Inject 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 (?) // 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) { if (listId == null) {
viewModel.createNewList(name, exclusive, replyPolicy) viewModel.createNewList(name, exclusive, replyPolicy)
} else { } else {

View File

@ -141,9 +141,9 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider { class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider {
@Inject @Inject
@ -199,7 +199,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1) val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1)
if (notificationId != -1) { if (notificationId != -1) {
// opened from a notification action, cancel the notification // 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) 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, // user clicked a notification, show follow requests for type FOLLOW_REQUEST,
// otherwise show notification tab // otherwise show notification tab
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) { 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) startActivityWithSlideInAnimation(intent)
} else { } else {
showNotificationTab = true showNotificationTab = true
@ -293,8 +298,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setupDrawer( setupDrawer(
savedInstanceState, savedInstanceState,
addSearchButton = hideTopToolbar, addSearchButton = hideTopToolbar,
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_TAGS), addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_STATUSES), 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 /* 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( refreshMainDrawerItems(
addSearchButton = hideTopToolbar, addSearchButton = hideTopToolbar,
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS), addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS),
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES), addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES)
) )
setupTabs(false) setupTabs(false)
@ -333,7 +342,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
directMessageTab?.let { directMessageTab?.let {
if (event.accountId == activeAccount.accountId) { if (event.accountId == activeAccount.accountId) {
val hasDirectMessageNotification = 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) { if (hasDirectMessageNotification) {
showDirectMessageBadge(true) 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 // 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 // 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 { override fun onMenuItemSelected(item: MenuItem): Boolean {
@ -503,7 +520,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
private fun forwardToComposeActivity(intent: Intent) { 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) { val composeIntent = if (composeOptions != null) {
ComposeActivity.startIntent(this, composeOptions) ComposeActivity.startIntent(this, composeOptions)
@ -523,7 +544,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
savedInstanceState: Bundle?, savedInstanceState: Bundle?,
addSearchButton: Boolean, addSearchButton: Boolean,
addTrendingTagsButton: Boolean, addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean, addTrendingStatusesButton: Boolean
) { ) {
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() } val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
@ -553,7 +574,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
header.currentProfileName.ellipsize = TextUtils.TruncateAt.END header.currentProfileName.ellipsize = TextUtils.TruncateAt.END
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter)) 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) val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
DrawerImageLoader.init(object : AbstractDrawerImageLoader() { DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
@ -589,7 +612,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
refreshMainDrawerItems( refreshMainDrawerItems(
addSearchButton = addSearchButton, addSearchButton = addSearchButton,
addTrendingTagsButton = addTrendingTagsButton, addTrendingTagsButton = addTrendingTagsButton,
addTrendingStatusesButton = addTrendingStatusesButton, addTrendingStatusesButton = addTrendingStatusesButton
) )
setSavedInstance(savedInstanceState) setSavedInstance(savedInstanceState)
} }
@ -598,7 +621,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun refreshMainDrawerItems( private fun refreshMainDrawerItems(
addSearchButton: Boolean, addSearchButton: Boolean,
addTrendingTagsButton: Boolean, addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean, addTrendingStatusesButton: Boolean
) { ) {
binding.mainDrawer.apply { binding.mainDrawer.apply {
itemAdapter.clear() itemAdapter.clear()
@ -884,7 +907,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
supportActionBar?.title = tabs[position].title(this@MainActivity) supportActionBar?.title = tabs[position].title(this@MainActivity)
binding.mainToolbar.setOnClickListener { binding.mainToolbar.setOnClickListener {
(tabAdapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() (
tabAdapter.getFragment(
activeTabLayout.selectedTabPosition
) as? ReselectableFragment
)?.onReselect()
} }
updateProfiles() updateProfiles()
@ -915,7 +942,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
// open LoginActivity to add new account // open LoginActivity to add new account
if (profile.identifier == DRAWER_ITEM_ADD_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 return false
} }
// change Account // change Account
@ -986,10 +1015,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
loadDrawerAvatar(me.avatar, false) loadDrawerAvatar(me.avatar, false)
accountManager.updateActiveAccount(me) accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) NotificationHelper.createNotificationChannelsForAccount(
accountManager.activeAccount!!,
this
)
// Setup push notifications // Setup push notifications
showMigrationNoticeIfNecessary(this, binding.mainCoordinatorLayout, binding.composeButton, accountManager) showMigrationNoticeIfNecessary(
this,
binding.mainCoordinatorLayout,
binding.composeButton,
accountManager
)
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) { if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
lifecycleScope.launch { lifecycleScope.launch {
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager) enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
@ -1024,7 +1061,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
Glide.with(this) Glide.with(this)
.asDrawable() .asDrawable()
.load(avatarUrl) .load(avatarUrl)
.transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))) .transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply { .apply {
if (showPlaceholder) placeholder(R.drawable.avatar_default) if (showPlaceholder) placeholder(R.drawable.avatar_default)
} }
@ -1054,7 +1093,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
Glide.with(this) Glide.with(this)
.asBitmap() .asBitmap()
.load(avatarUrl) .load(avatarUrl)
.transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))) .transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply { .apply {
if (showPlaceholder) placeholder(R.drawable.avatar_default) if (showPlaceholder) placeholder(R.drawable.avatar_default)
} }
@ -1101,7 +1142,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
private fun updateAnnouncementsBadge() { 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() { 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 * Switches the active account to the accountId and takes the user to the correct place according to the notification they clicked
*/ */
@JvmStatic @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 { return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(NOTIFICATION_TYPE, type.name) putExtra(NOTIFICATION_TYPE, type.name)
} }

View File

@ -38,8 +38,8 @@ import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -49,7 +49,9 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject @Inject
lateinit var eventHub: EventHub 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 lateinit var kind: Kind
private var hashtag: String? = null private var hashtag: String? = null
private var followTagItem: MenuItem? = null private var followTagItem: MenuItem? = null
@ -136,10 +138,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
followTagItem?.isVisible = false followTagItem?.isVisible = false
unfollowTagItem?.isVisible = true 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) Log.e(TAG, "Failed to follow #$tag", it)
} }
) )
@ -158,10 +168,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
followTagItem?.isVisible = true followTagItem?.isVisible = true
unfollowTagItem?.isVisible = false 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) Log.e(TAG, "Failed to unfollow #$tag", it)
} }
) )
@ -238,7 +256,12 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
expiresInSeconds = null expiresInSeconds = null
).fold( ).fold(
{ filter -> { 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) // must be requested again; otherwise does not contain the keyword (but server does)
mutedFilter = mastodonApi.getFilter(filter.id).getOrNull() mutedFilter = mastodonApi.getFilter(filter.id).getOrNull()
@ -246,7 +269,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
eventHub.dispatch(PreferenceChangedEvent(filter.context[0])) eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
filterCreateSuccess = true filterCreateSuccess = true
} else { } 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") Log.e(TAG, "Failed to mute #$tag")
} }
}, },
@ -265,12 +292,20 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
filterCreateSuccess = true filterCreateSuccess = true
}, },
{ throwable -> { 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) Log.e(TAG, "Failed to mute #$tag", throwable)
} }
) )
} else { } 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) Log.e(TAG, "Failed to mute #$tag", throwable)
} }
} }
@ -278,7 +313,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
if (filterCreateSuccess) { if (filterCreateSuccess) {
updateTagMuteState(true) 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) { setAction(R.string.action_view_filter) {
val intent = if (mutedFilter != null) { val intent = if (mutedFilter != null) {
Intent(this@StatusListActivity, EditFilterActivity::class.java).apply { Intent(this@StatusListActivity, EditFilterActivity::class.java).apply {
@ -339,10 +378,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
mutedFilterV1 = null mutedFilterV1 = null
mutedFilter = 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 -> { 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) 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, id = TRENDING_STATUSES,
text = R.string.title_public_trending_statuses, text = R.string.title_public_trending_statuses,
icon = R.drawable.ic_hot_24dp, icon = R.drawable.ic_hot_24dp,
fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) } fragment = {
TimelineFragment.newInstance(
TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES
)
}
) )
HASHTAG -> TabData( HASHTAG -> TabData(
id = HASHTAG, id = HASHTAG,
@ -112,13 +116,22 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
icon = R.drawable.ic_hashtag, icon = R.drawable.ic_hashtag,
fragment = { args -> TimelineFragment.newHashtagInstance(args) }, fragment = { args -> TimelineFragment.newHashtagInstance(args) },
arguments = arguments, 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( LIST -> TabData(
id = LIST, id = LIST,
text = R.string.list, text = R.string.list,
icon = R.drawable.ic_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, arguments = arguments,
title = { arguments.getOrNull(1).orEmpty() } title = { arguments.getOrNull(1).orEmpty() }
) )

View File

@ -47,10 +47,10 @@ import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.regex.Pattern import java.util.regex.Pattern
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, ItemInteractionListener, ListSelectionFragment.ListSelectionListener { class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, ItemInteractionListener, ListSelectionFragment.ListSelectionListener {
@ -72,9 +72,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
private var tabsChanged = false 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) { private val onFabDismissedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() { override fun handleOnBackPressed() {
@ -99,14 +103,19 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT) currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
binding.currentTabsRecyclerView.adapter = currentTabsAdapter binding.currentTabsRecyclerView.adapter = currentTabsAdapter
binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) 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) addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this)
binding.addTabRecyclerView.adapter = addTabAdapter binding.addTabRecyclerView.adapter = addTabAdapter
binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this) binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { 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) 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 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] val temp = currentTabs[viewHolder.bindingAdapterPosition]
currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition] currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition]
currentTabs[target.bindingAdapterPosition] = temp 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) super.clearView(recyclerView, viewHolder)
viewHolder.itemView.elevation = 0f 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.EmojiPackHelper
import de.c1710.filemojicompat_ui.helpers.EmojiPreference import de.c1710.filemojicompat_ui.helpers.EmojiPreference
import io.reactivex.rxjava3.plugins.RxJavaPlugins import io.reactivex.rxjava3.plugins.RxJavaPlugins
import org.conscrypt.Conscrypt
import java.security.Security import java.security.Security
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
import org.conscrypt.Conscrypt
class TuskyApplication : Application(), HasAndroidInjector { class TuskyApplication : Application(), HasAndroidInjector {
@Inject @Inject
@ -78,7 +78,10 @@ class TuskyApplication : Application(), HasAndroidInjector {
AppInjector.init(this) AppInjector.init(this)
// Migrate shared preference keys and defaults from version to version. // 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) { if (oldVersion != SCHEMA_VERSION) {
upgradeSharedPreferences(oldVersion, SCHEMA_VERSION) upgradeSharedPreferences(oldVersion, SCHEMA_VERSION)
} }

View File

@ -74,7 +74,12 @@ import javax.inject.Inject
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener { class ViewMediaActivity :
BaseActivity(),
HasAndroidInjector,
ViewImageFragment.PhotoActionsListener,
ViewVideoFragment.VideoActionsListener {
@Inject @Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any> lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -103,7 +108,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
supportPostponeEnterTransition() supportPostponeEnterTransition()
// Gather the parameters. // 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) val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener // Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
@ -215,7 +224,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun downloadMedia() { private fun downloadMedia() {
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
val filename = Uri.parse(url).lastPathSegment 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 downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(Uri.parse(url)) val request = DownloadManager.Request(Uri.parse(url))
@ -225,8 +238,13 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun requestDownloadMedia() { private fun requestDownloadMedia() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults -> requestPermissions(
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) { _, grantResults ->
if (
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
downloadMedia() downloadMedia()
} else { } else {
showErrorDialog( showErrorDialog(
@ -243,7 +261,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun onOpenStatus() { private fun onOpenStatus() {
val attach = attachments!![binding.viewPager.currentItem] 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() { private fun copyLink() {
@ -276,7 +296,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun shareFile(file: File, mimeType: String?) { private fun shareFile(file: File, mimeType: String?) {
ShareCompat.IntentBuilder(this) ShareCompat.IntentBuilder(this)
.setType(mimeType) .setType(mimeType)
.addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)) .addStream(
FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)
)
.setChooserTitle(R.string.send_media_to) .setChooserTitle(R.string.send_media_to)
.startChooser() .startChooser()
} }
@ -366,7 +388,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private const val TAG = "ViewMediaActivity" private const val TAG = "ViewMediaActivity"
@JvmStatic @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) val intent = Intent(context, ViewMediaActivity::class.java)
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments))
intent.putExtra(EXTRA_ATTACHMENT_INDEX, index) 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 getItemCount() = fieldData.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEditFieldBinding> { override fun onCreateViewHolder(
val binding = ItemEditFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemEditFieldBinding> {
val binding = ItemEditFieldBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding) 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.emojify
import com.keylesspalace.tusky.util.loadAvatar 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 { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = if (convertView == null) { val binding = if (convertView == null) {

View File

@ -36,8 +36,15 @@ class EmojiAdapter(
override fun getItemCount() = emojiList.size override fun getItemCount() = emojiList.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEmojiButtonBinding> { override fun onCreateViewHolder(
val binding = ItemEmojiButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemEmojiButtonBinding> {
val binding = ItemEmojiButtonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding) return BindingHolder(binding)
} }

View File

@ -47,16 +47,26 @@ class FollowRequestViewHolder(
showBotOverlay: Boolean showBotOverlay: Boolean
) { ) {
val wrappedName = account.name.unicodeWrap() 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 binding.displayNameTextView.text = emojifiedName
if (showHeader) { 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 { binding.notificationTextView.text = SpannableStringBuilder(wholeMessage).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}.emojify(account.emojis, itemView, animateEmojis) }.emojify(account.emojis, itemView, animateEmojis)
} }
binding.notificationTextView.visible(showHeader) 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 binding.usernameTextView.text = formattedUsername
if (account.note.isEmpty()) { if (account.note.isEmpty()) {
binding.accountNote.hide() binding.accountNote.hide()
@ -67,7 +77,9 @@ class FollowRequestViewHolder(
.emojify(account.emojis, binding.accountNote, animateEmojis) .emojify(account.emojis, binding.accountNote, animateEmojis)
setClickableText(binding.accountNote, emojifiedNote, emptyList(), null, linkListener) 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) loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar)
binding.avatarBadge.visible(showBotOverlay && account.bot) 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 com.keylesspalace.tusky.util.modernLanguageCode
import java.util.Locale 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 { override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getView(position, convertView, parent) as TextView).apply { return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary)) setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))

View File

@ -67,7 +67,10 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
.map { pollOptions.indexOf(it) } .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) val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding) return BindingHolder(binding)
} }

View File

@ -40,7 +40,11 @@ class PreviewPollOptionsAdapter : RecyclerView.Adapter<PreviewViewHolder>() {
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): 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 override fun getItemCount() = options.size

View File

@ -31,12 +31,25 @@ import com.keylesspalace.tusky.util.unicodeWrap
import java.util.Date import java.util.Date
class ReportNotificationViewHolder( class ReportNotificationViewHolder(
private val binding: ItemReportNotificationBinding, private val binding: ItemReportNotificationBinding
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun setupWithReport(reporter: TimelineAccount, report: Report, animateAvatar: Boolean, animateEmojis: Boolean) { fun setupWithReport(
val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, itemView, animateEmojis) reporter: TimelineAccount,
val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, itemView, animateEmojis) 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) val icon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_flag_24dp)
binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null) binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
@ -52,17 +65,22 @@ class ReportNotificationViewHolder(
report.targetAccount.avatar, report.targetAccount.avatar,
binding.notificationReporteeAvatar, binding.notificationReporteeAvatar,
itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp), itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp),
animateAvatar, animateAvatar
) )
loadAvatar( loadAvatar(
reporter.avatar, reporter.avatar,
binding.notificationReporterAvatar, binding.notificationReporterAvatar,
itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp), 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 { binding.notificationReporteeAvatar.setOnClickListener {
val position = bindingAdapterPosition val position = bindingAdapterPosition
if (position != RecyclerView.NO_POSITION) { if (position != RecyclerView.NO_POSITION) {

View File

@ -56,7 +56,11 @@ class TabAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> {
val binding = if (small) { val binding = if (small) {
ItemTabPreferenceSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false) ItemTabPreferenceSmallBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
} else { } else {
ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false) 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.google.gson.Gson
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class CacheUpdater @Inject constructor( class CacheUpdater @Inject constructor(
eventHub: EventHub, 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 DomainMuteEvent(val instance: String) : Event
data class AnnouncementReadEvent(val announcementId: String) : Event data class AnnouncementReadEvent(val announcementId: String) : Event
data class FilterUpdatedEvent(val filterContext: List<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 ConversationsLoadingEvent(val accountId: String) : Event
data class NotificationsLoadingEvent(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.core.Observable
import io.reactivex.rxjava3.subjects.PublishSubject import io.reactivex.rxjava3.subjects.PublishSubject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
interface Event interface Event

View File

@ -267,9 +267,18 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
binding.accountFragmentViewPager.adapter = adapter binding.accountFragmentViewPager.adapter = adapter
binding.accountFragmentViewPager.offscreenPageLimit = 2 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] tab.text = pageTitles[position]
}.attach() }.attach()
@ -301,7 +310,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val right = insets.getInsets(systemBars()).right val right = insets.getInsets(systemBars()).right
val bottom = insets.getInsets(systemBars()).bottom val bottom = insets.getInsets(systemBars()).bottom
val left = insets.getInsets(systemBars()).left 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 WindowInsetsCompat.CONSUMED
} }
@ -318,7 +331,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation) 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) toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
binding.accountToolbar.background = toolbarBackground binding.accountToolbar.background = toolbarBackground
@ -341,7 +357,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) 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) fillColor = ColorStateList.valueOf(toolbarColor)
elevation = appBarElevation elevation = appBarElevation
shapeAppearanceModel = ShapeAppearanceModel.builder() shapeAppearanceModel = ShapeAppearanceModel.builder()
@ -381,11 +400,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
binding.accountAvatarImageView.visible(scaledAvatarSize > 0) 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 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) toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor)
@ -407,7 +432,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
when (it) { when (it) {
is Success -> onAccountChanged(it.data) is Success -> onAccountChanged(it.data)
is Error -> { 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() } .setAction(R.string.action_retry) { viewModel.refresh() }
.show() .show()
} }
@ -421,7 +450,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
} }
if (it is Error) { 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() } .setAction(R.string.action_retry) { viewModel.refresh() }
.show() .show()
} }
@ -466,14 +499,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val fullUsername = getFullUsername(loadedAccount) val fullUsername = getFullUsername(loadedAccount)
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(null, fullUsername)) 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() .show()
} }
true 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) setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
accountFieldAdapter.fields = account.fields.orEmpty() accountFieldAdapter.fields = account.fields.orEmpty()
@ -503,7 +544,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val isLight = resources.getBoolean(R.bool.lightNavigationBar) val isLight = resources.getBoolean(R.bool.lightNavigationBar)
if (loadedAccount?.bot == true) { 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) binding.accountBadgeContainer.addView(badgeView)
} }
@ -873,7 +920,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
} else { } else {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(getString(R.string.mute_domain_warning, instance)) .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) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
@ -966,7 +1015,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
sendIntent.action = Intent.ACTION_SEND sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, url) sendIntent.putExtra(Intent.EXTRA_TEXT, url)
sendIntent.type = "text/plain" 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 return true
} }
@ -978,7 +1032,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
sendIntent.action = Intent.ACTION_SEND sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, fullUsername) sendIntent.putExtra(Intent.EXTRA_TEXT, fullUsername)
sendIntent.type = "text/plain" 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 return true
} }
@ -1009,7 +1068,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
} }
R.id.action_report -> { R.id.action_report -> {
loadedAccount?.let { loadedAccount -> loadedAccount?.let { loadedAccount ->
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)) startActivity(
ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)
)
} }
return true return true
} }
@ -1047,7 +1108,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
// text color with maximum contrast // text color with maximum contrast
val textColor = if (isLight) Color.BLACK else Color.WHITE val textColor = if (isLight) Color.BLACK else Color.WHITE
// badge color with 50% transparency so it blends in with the theme background // 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 // a color between the text color and the badge color
val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f) val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f)

View File

@ -38,8 +38,15 @@ class AccountFieldAdapter(
override fun getItemCount() = fields.size override fun getItemCount() = fields.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountFieldBinding> { override fun onCreateViewHolder(
val binding = ItemAccountFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAccountFieldBinding> {
val binding = ItemAccountFieldBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding) return BindingHolder(binding)
} }
@ -51,11 +58,20 @@ class AccountFieldAdapter(
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
nameTextView.text = emojifiedName 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) setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
if (field.verifiedAt != null) { 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 { } else {
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
} }

View File

@ -33,7 +33,11 @@ class AccountPagerAdapter(
override fun createFragment(position: Int): Fragment { override fun createFragment(position: Int): Fragment {
return when (position) { return when (position) {
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false) 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) 2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
3 -> AccountMediaFragment.newInstance(accountId) 3 -> AccountMediaFragment.newInstance(accountId)
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds") 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.Resource
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.getDomain
import javax.inject.Inject
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class AccountViewModel @Inject constructor( class AccountViewModel @Inject constructor(
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
@ -97,7 +97,15 @@ class AccountViewModel @Inject constructor(
mastodonApi.relationships(listOf(accountId)) mastodonApi.relationships(listOf(accountId))
.fold( .fold(
{ relationships -> { relationships ->
relationshipData.postValue(if (relationships.isNotEmpty()) Success(relationships[0]) else Error()) relationshipData.postValue(
if (relationships.isNotEmpty()) {
Success(
relationships[0]
)
} else {
Error()
}
)
}, },
{ t -> { t ->
Log.w(TAG, "failed obtaining relationships", t) Log.w(TAG, "failed obtaining relationships", t)
@ -135,8 +143,8 @@ class AccountViewModel @Inject constructor(
fun changeSubscribingState() { fun changeSubscribingState() {
val relationship = relationshipData.value?.data val relationship = relationshipData.value?.data
if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */ if (relationship?.notifying == true || // Mastodon 3.3.0rc1
relationship?.subscribing == true /* Pleroma */ relationship?.subscribing == true // Pleroma
) { ) {
changeRelationship(RelationShipAction.UNSUBSCRIBE) changeRelationship(RelationShipAction.UNSUBSCRIBE)
} else { } else {
@ -315,7 +323,14 @@ class AccountViewModel @Inject constructor(
} }
enum class RelationShipAction { enum class RelationShipAction {
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE FOLLOW,
UNFOLLOW,
BLOCK,
UNBLOCK,
MUTE,
UNMUTE,
SUBSCRIBE,
UNSUBSCRIBE
} }
companion object { 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.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class ListSelectionFragment : DialogFragment(), Injectable { class ListSelectionFragment : DialogFragment(), Injectable {
@ -133,14 +133,22 @@ class ListSelectionFragment : DialogFragment(), Injectable {
viewModel.actionError.collectLatest { error -> viewModel.actionError.collectLatest { error ->
when (error.type) { when (error.type) {
ActionError.Type.ADD -> { 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) { .setAction(R.string.action_retry) {
viewModel.addAccountToList(accountId!!, error.listId) viewModel.addAccountToList(accountId!!, error.listId)
} }
.show() .show()
} }
ActionError.Type.REMOVE -> { 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) { .setAction(R.string.action_retry) {
viewModel.removeAccountFromList(accountId!!, error.listId) viewModel.removeAccountFromList(accountId!!, error.listId)
} }

View File

@ -24,12 +24,12 @@ import at.connyduck.calladapter.networkresult.onSuccess
import at.connyduck.calladapter.networkresult.runCatching import at.connyduck.calladapter.networkresult.runCatching
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
data class AccountListState( data class AccountListState(
val list: MastoList, 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.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
/** /**
* Fragment with multiple columns of media previews for the specified account. * 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 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.layoutManager = GridLayoutManager(view.context, columnCount)
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
@ -124,7 +128,11 @@ class AccountMediaFragment :
is LoadState.NotLoading -> { is LoadState.NotLoading -> {
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
binding.statusView.show() 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 -> { is LoadState.Error -> {
@ -175,11 +183,19 @@ class AccountMediaFragment :
Attachment.Type.GIFV, Attachment.Type.GIFV,
Attachment.Type.VIDEO, Attachment.Type.VIDEO,
Attachment.Type.AUDIO -> { Attachment.Type.AUDIO -> {
val intent = ViewMediaActivity.newIntent(context, attachmentsFromSameStatus, currentIndex) val intent = ViewMediaActivity.newIntent(
context,
attachmentsFromSameStatus,
currentIndex
)
if (activity != null) { if (activity != null) {
val url = selected.attachment.url val url = selected.attachment.url
ViewCompat.setTransitionName(view, url) ViewCompat.setTransitionName(view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url) val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
view,
url
)
startActivity(intent, options.toBundle()) startActivity(intent, options.toBundle())
} else { } else {
startActivity(intent) startActivity(intent)

View File

@ -29,25 +29,48 @@ class AccountMediaGridAdapter(
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>( ) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
object : DiffUtil.ItemCallback<AttachmentViewData>() { 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 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 return oldItem == newItem
} }
} }
) { ) {
private val baseItemBackgroundColor = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, Color.BLACK) private val baseItemBackgroundColor = MaterialColors.getColor(
private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator) context,
private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp) 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 itemBgBaseHSV = FloatArray(3)
private val random = Random() private val random = Random()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountMediaBinding> { override fun onCreateViewHolder(
val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAccountMediaBinding> {
val binding = ItemAccountMediaBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV) Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV)
itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f
binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV)) binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
@ -71,7 +94,11 @@ class AccountMediaGridAdapter(
if (item.attachment.type == Attachment.Type.AUDIO) { if (item.attachment.type == Attachment.Type.AUDIO) {
overlay.hide() 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) Glide.with(imageView)
.load(R.drawable.ic_music_box_preview_24dp) .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.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.view.EndlessOnScrollListener
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import retrofit2.Response import retrofit2.Response
import javax.inject.Inject
class AccountListFragment : class AccountListFragment :
Fragment(R.layout.fragment_account_list), Fragment(R.layout.fragment_account_list),
@ -96,7 +96,9 @@ class AccountListFragment :
val layoutManager = LinearLayoutManager(view.context) val layoutManager = LinearLayoutManager(view.context)
binding.recyclerView.layoutManager = layoutManager binding.recyclerView.layoutManager = layoutManager
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (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.setOnRefreshListener { fetchAccounts() }
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
@ -116,7 +118,8 @@ class AccountListFragment :
instanceName = activeAccount.domain, instanceName = activeAccount.domain,
accountLocked = activeAccount.locked 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) binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter)
followRequestsAdapter followRequestsAdapter
} }
@ -142,7 +145,9 @@ class AccountListFragment :
override fun onViewTag(tag: String) { override fun onViewTag(tag: String) {
(activity as BaseActivity?) (activity as BaseActivity?)
?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag)) ?.startActivityWithSlideInAnimation(
StatusListActivity.newHashtagIntent(requireContext(), tag)
)
} }
override fun onViewAccount(id: String) { override fun onViewAccount(id: String) {
@ -225,7 +230,11 @@ class AccountListFragment :
val unblockedUser = blocksAdapter.removeItem(position) val unblockedUser = blocksAdapter.removeItem(position)
if (unblockedUser != null) { 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) { .setAction(R.string.action_undo) {
blocksAdapter.addItem(unblockedUser, position) blocksAdapter.addItem(unblockedUser, position)
onBlock(true, id, position) onBlock(true, id, position)
@ -243,11 +252,7 @@ class AccountListFragment :
Log.e(TAG, "Failed to $verb account accountId $accountId") Log.e(TAG, "Failed to $verb account accountId $accountId")
} }
override fun onRespondToFollowRequest( override fun onRespondToFollowRequest(accept: Boolean, accountId: String, position: Int) {
accept: Boolean,
accountId: String,
position: Int
) {
if (accept) { if (accept) {
api.authorizeFollowRequest(accountId) api.authorizeFollowRequest(accountId)
} else { } else {

View File

@ -60,9 +60,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
} }
} }
private fun createFooterViewHolder( private fun createFooterViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {
parent: ViewGroup
): RecyclerView.ViewHolder {
val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding) return BindingHolder(binding)
} }

View File

@ -39,16 +39,27 @@ class BlocksAdapter(
) { ) {
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemBlockedUserBinding> { 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) return BindingHolder(binding)
} }
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemBlockedUserBinding>, position: Int) { override fun onBindAccountViewHolder(
viewHolder: BindingHolder<ItemBlockedUserBinding>,
position: Int
) {
val account = accountList[position] val account = accountList[position]
val binding = viewHolder.binding val binding = viewHolder.binding
val context = binding.root.context 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 binding.blockedUserDisplayName.text = emojifiedName
val formattedUsername = context.getString(R.string.post_username_format, account.username) val formattedUsername = context.getString(R.string.post_username_format, account.username)
binding.blockedUserUsername.text = formattedUsername binding.blockedUserUsername.text = formattedUsername

View File

@ -27,12 +27,22 @@ class FollowRequestsHeaderAdapter(
private val accountLocked: Boolean private val accountLocked: Boolean
) : RecyclerView.Adapter<BindingHolder<ItemFollowRequestsHeaderBinding>>() { ) : RecyclerView.Adapter<BindingHolder<ItemFollowRequestsHeaderBinding>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestsHeaderBinding> { override fun onCreateViewHolder(
val binding = ItemFollowRequestsHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestsHeaderBinding> {
val binding = ItemFollowRequestsHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding) 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) 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>() private val mutingNotificationsMap = HashMap<String, Boolean>()
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemMutedUserBinding> { 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) return BindingHolder(binding)
} }
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemMutedUserBinding>, position: Int) { override fun onBindAccountViewHolder(
viewHolder: BindingHolder<ItemMutedUserBinding>,
position: Int
) {
val account = accountList[position] val account = accountList[position]
val binding = viewHolder.binding val binding = viewHolder.binding
val context = binding.root.context val context = binding.root.context
val mutingNotifications = mutingNotificationsMap[account.id] 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 binding.mutedUserDisplayName.text = emojifiedName
val formattedUsername = context.getString(R.string.post_username_format, account.username) val formattedUsername = context.getString(R.string.post_username_format, account.username)

View File

@ -54,8 +54,15 @@ class AnnouncementAdapter(
private val absoluteTimeFormatter = AbsoluteTimeFormatter() private val absoluteTimeFormatter = AbsoluteTimeFormatter()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> { override fun onCreateViewHolder(
val binding = ItemAnnouncementBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAnnouncementBinding> {
val binding = ItemAnnouncementBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding) return BindingHolder(binding)
} }
@ -69,7 +76,11 @@ class AnnouncementAdapter(
val chips = holder.binding.chipGroup val chips = holder.binding.chipGroup
val addReactionChip = holder.binding.addReactionChip 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) setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
@ -107,7 +118,13 @@ class AnnouncementAdapter(
spanBuilder.setSpan(span, 0, 1, 0) spanBuilder.setSpan(span, 0, 1, 0)
Glide.with(this) Glide.with(this)
.asDrawable() .asDrawable()
.load(if (animateEmojis) { reaction.url } else { reaction.staticUrl }) .load(
if (animateEmojis) {
reaction.url
} else {
reaction.staticUrl
}
)
.into(span.getTarget(animateEmojis)) .into(span.getTarget(animateEmojis))
this.text = spanBuilder this.text = spanBuilder
} }

View File

@ -116,7 +116,10 @@ class AnnouncementsActivity :
binding.progressBar.hide() binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
if (it.data.isNullOrEmpty()) { 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() binding.errorMessageView.show()
} else { } else {
binding.errorMessageView.hide() binding.errorMessageView.hide()
@ -129,7 +132,10 @@ class AnnouncementsActivity :
is Error -> { is Error -> {
binding.progressBar.hide() binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false 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() refreshAnnouncements()
} }
binding.errorMessageView.show() 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.Loading
import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class AnnouncementsViewModel @Inject constructor( class AnnouncementsViewModel @Inject constructor(
private val instanceInfoRepo: InstanceInfoRepository, private val instanceInfoRepo: InstanceInfoRepository,
@ -64,7 +64,9 @@ class AnnouncementsViewModel @Inject constructor(
mastodonApi.dismissAnnouncement(announcement.id) mastodonApi.dismissAnnouncement(announcement.id)
.fold( .fold(
{ {
eventHub.dispatch(AnnouncementReadEvent(announcement.id)) eventHub.dispatch(
AnnouncementReadEvent(announcement.id)
)
}, },
{ throwable -> { throwable ->
Log.d( 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.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp 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.File
import java.io.IOException import java.io.IOException
import java.text.DecimalFormat import java.text.DecimalFormat
@ -127,6 +122,11 @@ import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
import kotlin.math.min 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 : class ComposeActivity :
BaseActivity(), BaseActivity(),
@ -163,14 +163,23 @@ class ComposeActivity :
private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success -> private val takePicture =
if (success) { registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
pickMedia(photoUploadUri!!) if (success) {
pickMedia(photoUploadUri!!)
}
} }
}
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris -> private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) { 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 { } else {
uris.forEach { uri -> uris.forEach { uri ->
pickMedia(uri) pickMedia(uri)
@ -191,7 +200,8 @@ class ComposeActivity :
uriNew, uriNew,
size, size,
itemOld.description, itemOld.description,
null, // Intentionally reset focus when cropping // Intentionally reset focus when cropping
null,
itemOld itemOld
) )
} }
@ -222,7 +232,11 @@ class ComposeActivity :
val mediaAdapter = MediaPreviewAdapter( val mediaAdapter = MediaPreviewAdapter(
this, this,
onAddCaption = { item -> 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 -> onAddFocus = { item ->
makeFocusDialog(item.focus, item.uri) { newFocus -> 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 /* 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. */ * 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) viewModel.setup(composeOptions)
setupButtons() setupButtons()
@ -303,12 +321,20 @@ class ComposeActivity :
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) { if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
when (intent.action) { when (intent.action) {
Intent.ACTION_SEND -> { 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) pickMedia(uri)
} }
} }
Intent.ACTION_SEND_MULTIPLE -> { 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) pickMedia(uri)
} }
} }
@ -328,7 +354,13 @@ class ComposeActivity :
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0) val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
val left = min(start, end) val left = min(start, end)
val right = max(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 // move edittext cursor to first when shareBody parsed
binding.composeEditField.text.insert(0, "\n") binding.composeEditField.text.insert(0, "\n")
binding.composeEditField.setSelection(0) binding.composeEditField.setSelection(0)
@ -341,23 +373,48 @@ class ComposeActivity :
if (replyingStatusAuthor != null) { if (replyingStatusAuthor != null) {
binding.composeReplyView.show() binding.composeReplyView.show()
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) 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) setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
arrowDownIcon,
null
)
binding.composeReplyView.setOnClickListener { binding.composeReplyView.setOnClickListener {
TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup) TransitionManager.beginDelayedTransition(
binding.composeReplyContentView.parent as ViewGroup
)
if (binding.composeReplyContentView.isVisible) { if (binding.composeReplyContentView.isVisible) {
binding.composeReplyContentView.hide() binding.composeReplyContentView.hide()
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null) binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
arrowDownIcon,
null
)
} else { } else {
binding.composeReplyContentView.show() 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) 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?) { private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
binding.composeEditField.setOnReceiveContentListener(this) 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( binding.composeEditField.setAdapter(
ComposeAutoCompleteAdapter( ComposeAutoCompleteAdapter(
@ -419,7 +481,9 @@ class ComposeActivity :
} }
lifecycleScope.launch { lifecycleScope.launch {
viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive -> viewModel.showContentWarning.combine(
viewModel.markMediaAsSensitive
) { showContentWarning, markSensitive ->
updateSensitiveMediaToggle(markSensitive, showContentWarning) updateSensitiveMediaToggle(markSensitive, showContentWarning)
showContentWarning(showContentWarning) showContentWarning(showContentWarning)
}.collect() }.collect()
@ -434,7 +498,10 @@ class ComposeActivity :
mediaAdapter.submitList(media) mediaAdapter.submitList(media)
binding.composeMediaPreviewBar.visible(media.isNotEmpty()) 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 textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 } val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply {
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null) colorInt = textColor
sizeDp = 18
}
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(
cameraIcon,
null,
null,
null
)
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 } val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply {
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null) colorInt = textColor
sizeDp = 18
}
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(
imageIcon,
null,
null,
null
)
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 } val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply {
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null) 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.actionPhotoTake.setOnClickListener { initiateCameraApp() }
binding.actionPhotoPick.setOnClickListener { onMediaPick() } binding.actionPhotoPick.setOnClickListener { onMediaPick() }
@ -549,7 +642,12 @@ class ComposeActivity :
private fun setupLanguageSpinner(initialLanguages: List<String>) { private fun setupLanguageSpinner(initialLanguages: List<String>) {
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { 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 viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
} }
@ -594,8 +692,12 @@ class ComposeActivity :
private fun replaceTextAtCaret(text: CharSequence) { private fun replaceTextAtCaret(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd // If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd) val start = binding.composeEditField.selectionStart.coerceAtMost(
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd) binding.composeEditField.selectionEnd
)
val end = binding.composeEditField.selectionStart.coerceAtLeast(
binding.composeEditField.selectionEnd
)
val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) { val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) {
" $text" " $text"
} else { } else {
@ -609,8 +711,12 @@ class ComposeActivity :
fun prependSelectedWordsWith(text: CharSequence) { fun prependSelectedWordsWith(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd // If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd) val start = binding.composeEditField.selectionStart.coerceAtMost(
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd) binding.composeEditField.selectionEnd
)
val end = binding.composeEditField.selectionStart.coerceAtLeast(
binding.composeEditField.selectionEnd
)
val editorText = binding.composeEditField.text val editorText = binding.composeEditField.text
if (start == end) { if (start == end) {
@ -678,7 +784,10 @@ class ComposeActivity :
this.viewModel.toggleMarkSensitive() this.viewModel.toggleMarkSensitive()
} }
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) { private fun updateSensitiveMediaToggle(
markMediaSensitive: Boolean,
contentWarningShown: Boolean
) {
if (viewModel.media.value.isEmpty()) { if (viewModel.media.value.isEmpty()) {
binding.composeHideMediaButton.hide() binding.composeHideMediaButton.hide()
binding.descriptionMissingWarningButton.hide() binding.descriptionMissingWarningButton.hide()
@ -695,7 +804,10 @@ class ComposeActivity :
getColor(R.color.tusky_blue) getColor(R.color.tusky_blue)
} else { } else {
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) 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) binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
@ -717,7 +829,10 @@ class ComposeActivity :
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false) enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
} else { } else {
@ColorInt val color = if (binding.composeScheduleView.time == null) { @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 { } else {
getColor(R.color.tusky_blue) getColor(R.color.tusky_blue)
} }
@ -748,7 +863,11 @@ class ComposeActivity :
binding.composeToggleVisibilityButton.setImageResource(iconRes) binding.composeToggleVisibilityButton.setImageResource(iconRes)
if (viewModel.editing) { if (viewModel.editing) {
// Can't update visibility on published status // 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() { private fun showEmojis() {
binding.emojiView.adapter?.let { binding.emojiView.adapter?.let {
if (it.itemCount == 0) { 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) displayTransientMessage(errorMessage)
} else { } else {
if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
@ -852,9 +975,14 @@ class ComposeActivity :
private fun setupPollView() { private fun setupPollView() {
val margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin) 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) layoutParams.setMargins(margin, margin, margin, marginBottom)
binding.pollPreview.layoutParams = layoutParams binding.pollPreview.layoutParams = layoutParams
@ -905,7 +1033,10 @@ class ComposeActivity :
val textColor = if (remainingLength < 0) { val textColor = if (remainingLength < 0) {
getColor(R.color.tusky_red) getColor(R.color.tusky_red)
} else { } else {
MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary) MaterialColors.getColor(
binding.composeCharactersLeftView,
android.R.attr.textColorTertiary
)
} }
binding.composeCharactersLeftView.setTextColor(textColor) binding.composeCharactersLeftView.setTextColor(textColor)
} }
@ -917,7 +1048,9 @@ class ComposeActivity :
} }
private fun verifyScheduledTime(): Boolean { 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() { 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) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
@ -1042,14 +1179,20 @@ class ComposeActivity :
val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg") val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
// "Authority" must be the same as the android:authorities string in AndroidManifest.xml // "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 viewModel.cropImageItemOld = item
cropImage.launch( cropImage.launch(
options(uri = item.uri) { options(uri = item.uri) {
setOutputUri(uriNew) 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) val formattedSize = decimalFormat.format(allowedSizeInMb)
getString(R.string.error_multimedia_size_limit, formattedSize) 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) else -> getString(R.string.error_media_upload_opening)
} }
displayTransientMessage(errorString) displayTransientMessage(errorString)
@ -1096,16 +1241,23 @@ class ComposeActivity :
} }
private fun showContentWarning(show: Boolean) { private fun showContentWarning(show: Boolean) {
TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup) TransitionManager.beginDelayedTransition(
binding.composeContentWarningBar.parent as ViewGroup
)
@ColorInt val color = if (show) { @ColorInt val color = if (show) {
binding.composeContentWarningBar.show() binding.composeContentWarningBar.show()
binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length) binding.composeContentWarningField.setSelection(
binding.composeContentWarningField.text.length
)
binding.composeContentWarningField.requestFocus() binding.composeContentWarningField.requestFocus()
getColor(R.color.tusky_blue) getColor(R.color.tusky_blue)
} else { } else {
binding.composeContentWarningBar.hide() binding.composeContentWarningBar.hide()
binding.composeEditField.requestFocus() 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) 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. * 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()) { val warning = if (viewModel.media.value.isNotEmpty()) {
R.string.compose_save_draft_loses_media R.string.compose_save_draft_loses_media
} else { } else {
@ -1182,7 +1337,10 @@ class ComposeActivity :
* User is editing an existing draft, and can either update the draft with the new changes or * User is editing an existing draft, and can either update the draft with the new changes or
* discard them. * 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()) { val warning = if (viewModel.media.value.isNotEmpty()) {
R.string.compose_save_draft_loses_media R.string.compose_save_draft_loses_media
} else { } else {
@ -1286,10 +1444,15 @@ class ComposeActivity :
val state: State val state: State
) { ) {
enum class Type { enum class Type {
IMAGE, VIDEO, AUDIO; IMAGE,
VIDEO,
AUDIO
} }
enum class State { enum class State {
UPLOADING, UNPROCESSED, PROCESSED, PUBLISHED UPLOADING,
UNPROCESSED,
PROCESSED,
PUBLISHED
} }
} }
@ -1370,10 +1533,7 @@ class ComposeActivity :
* @return an Intent to start the ComposeActivity * @return an Intent to start the ComposeActivity
*/ */
@JvmStatic @JvmStatic
fun startIntent( fun startIntent(context: Context, options: ComposeOptions): Intent {
context: Context,
options: ComposeOptions
): Intent {
return Intent(context, ComposeActivity::class.java).apply { return Intent(context, ComposeActivity::class.java).apply {
putExtra(COMPOSE_OPTIONS_EXTRA, options) putExtra(COMPOSE_OPTIONS_EXTRA, options)
} }

View File

@ -108,7 +108,9 @@ class ComposeAutoCompleteAdapter(
val account = accountResult.account val account = accountResult.account
binding.username.text = context.getString(R.string.post_username_format, account.username) binding.username.text = context.getString(R.string.post_username_format, account.username)
binding.displayName.text = account.name.emojify(account.emojis, binding.displayName, animateEmojis) 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( loadAvatar(
account.avatar, account.avatar,
binding.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.ServiceClient
import com.keylesspalace.tusky.service.StatusToSend import com.keylesspalace.tusky.service.StatusToSend
import com.keylesspalace.tusky.util.randomAlphanumericString import com.keylesspalace.tusky.util.randomAlphanumericString
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
@ -50,7 +51,6 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject
class ComposeViewModel @Inject constructor( class ComposeViewModel @Inject constructor(
private val api: MastodonApi, private val api: MastodonApi,
@ -85,13 +85,19 @@ class ComposeViewModel @Inject constructor(
val markMediaAsSensitive: MutableStateFlow<Boolean> = val markMediaAsSensitive: MutableStateFlow<Boolean> =
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false) 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 showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null) val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null) val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList()) 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 private lateinit var composeKind: ComposeKind
@ -100,7 +106,13 @@ class ComposeViewModel @Inject constructor(
private var setupComplete = false 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 { try {
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first()) val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
val mediaItems = media.value val mediaItems = media.value
@ -164,7 +176,11 @@ class ComposeViewModel @Inject constructor(
item.copy( item.copy(
id = event.mediaId, id = event.mediaId,
uploadPercent = -1, 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 -> { is UploadEvent.ErrorEvent -> {
media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } } media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
@ -186,7 +202,13 @@ class ComposeViewModel @Inject constructor(
return mediaItem 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 -> media.update { mediaList ->
val mediaItem = QueuedMedia( val mediaItem = QueuedMedia(
localId = mediaUploader.getNewLocalMediaId(), localId = mediaUploader.getNewLocalMediaId(),
@ -305,11 +327,7 @@ class ComposeViewModel @Inject constructor(
* Send status to the server. * Send status to the server.
* Uses current state plus provided arguments. * Uses current state plus provided arguments.
*/ */
suspend fun sendStatus( suspend fun sendStatus(content: String, spoilerText: String, accountId: Long) {
content: String,
spoilerText: String,
accountId: Long
) {
if (!scheduledTootId.isNullOrEmpty()) { if (!scheduledTootId.isNullOrEmpty()) {
api.deleteScheduledStatus(scheduledTootId!!) 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 -> .fold({ searchResult ->
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) } searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
}, { e -> }, { e ->

View File

@ -54,10 +54,10 @@ fun downsizeImage(
// Get EXIF data, for orientation info. // Get EXIF data, for orientation info.
val orientation = getImageOrientation(uri, contentResolver) val orientation = getImageOrientation(uri, contentResolver)
/* Unfortunately, there isn't a determined worst case compression ratio for image /* 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 * 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 * 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 * many cases, so it should only iterate once, but the loop is used to be absolutely
* sure it gets downsized to below the limit. */ * sure it gets downsized to below the limit. */
var scaledImageSize = 1024 var scaledImageSize = 1024
do { do {
val outputStream = try { val outputStream = try {

View File

@ -113,11 +113,17 @@ class MediaPreviewAdapter(
private val differ = AsyncListDiffer( private val differ = AsyncListDiffer(
this, this,
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() { 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 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 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.getMediaSize
import com.keylesspalace.tusky.util.getServerErrorMessage import com.keylesspalace.tusky.util.getServerErrorMessage
import com.keylesspalace.tusky.util.randomAlphanumericString 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -54,19 +61,15 @@ import kotlinx.coroutines.flow.shareIn
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody import okhttp3.MultipartBody
import retrofit2.HttpException 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 interface FinalUploadEvent
sealed class UploadEvent { sealed class UploadEvent {
data class ProgressEvent(val percentage: Int) : 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 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 randomId = randomAlphanumericString(12)
val imageFileName = "Tusky_${randomId}_" val imageFileName = "Tusky_${randomId}_"
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile( return File.createTempFile(imageFileName, suffix, storageDir)
imageFileName, /* prefix */
suffix, /* suffix */
storageDir /* directory */
)
} }
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long) 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. // .m4a files. See https://github.com/tuskyapp/Tusky/issues/3189 for details.
// Sniff the content of the file to determine the actual type. // Sniff the content of the file to determine the actual type.
if (mimeType != null && ( if (mimeType != null && (
mimeType.startsWith("audio/", ignoreCase = true) || mimeType.startsWith("audio/", ignoreCase = true) ||
mimeType.startsWith("video/", ignoreCase = true) mimeType.startsWith("video/", ignoreCase = true)
) )
) { ) {
val retriever = MediaMetadataRetriever() val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, media.uri) retriever.setDataSource(context, media.uri)

View File

@ -60,7 +60,9 @@ fun showAddPollDialog(
binding.pollChoices.adapter = adapter binding.pollChoices.adapter = adapter
var durations = context.resources.getIntArray(R.array.poll_duration_values).toList() 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 { binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply {
setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item) setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
} }
@ -75,8 +77,8 @@ fun showAddPollDialog(
} }
} }
val DAY_SECONDS = 60 * 60 * 24 val secondsInADay = 60 * 60 * 24
val desiredDuration = poll?.expiresIn ?: DAY_SECONDS val desiredDuration = poll?.expiresIn ?: secondsInADay
val pollDurationId = durations.indexOfLast { val pollDurationId = durations.indexOfLast {
it <= desiredDuration it <= desiredDuration
} }
@ -105,5 +107,7 @@ fun showAddPollDialog(
dialog.show() dialog.show()
// make the dialog focusable so the keyboard does not stay behind it // 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) notifyItemInserted(options.size - 1)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAddPollOptionBinding> { override fun onCreateViewHolder(
val binding = ItemAddPollOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAddPollOptionBinding> {
val binding = ItemAddPollOptionBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding) val holder = BindingHolder(binding)
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength)) binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))

View File

@ -133,17 +133,14 @@ class CaptionDialog : DialogFragment() {
} }
companion object { companion object {
fun newInstance( fun newInstance(localId: Int, existingDescription: String?, previewUri: Uri) =
localId: Int, CaptionDialog().apply {
existingDescription: String?, arguments = bundleOf(
previewUri: Uri LOCAL_ID_ARG to localId,
) = CaptionDialog().apply { EXISTING_DESCRIPTION_ARG to existingDescription,
arguments = bundleOf( PREVIEW_URI_ARG to previewUri
LOCAL_ID_ARG to localId, )
EXISTING_DESCRIPTION_ARG to existingDescription, }
PREVIEW_URI_ARG to previewUri
)
}
private const val DESCRIPTION_KEY = "description" private const val DESCRIPTION_KEY = "description"
private const val EXISTING_DESCRIPTION_ARG = "existing_description" private const val EXISTING_DESCRIPTION_ARG = "existing_description"

View File

@ -49,11 +49,22 @@ fun <T> T.makeFocusDialog(
.load(previewUri) .load(previewUri)
.downsample(DownsampleStrategy.CENTER_INSIDE) .downsample(DownsampleStrategy.CENTER_INSIDE)
.listener(object : RequestListener<Drawable> { .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 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 width = resource.intrinsicWidth
val height = resource.intrinsicHeight val height = resource.intrinsicHeight

View File

@ -21,7 +21,10 @@ import android.widget.RadioGroup
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Status 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 var listener: ComposeOptionsListener? = null

View File

@ -223,7 +223,8 @@ class ComposeScheduleView
} }
companion object { 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()) 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 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 { override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_CANCEL) { if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
return false return false
@ -112,7 +114,13 @@ class FocusIndicatorView
curtainPath.reset() // Draw a flood fill with a hole cut out of it curtainPath.reset() // Draw a flood fill with a hole cut out of it
curtainPath.fillType = Path.FillType.WINDING 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) curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW)
canvas.drawPath(curtainPath, curtainPaint) canvas.drawPath(curtainPath, curtainPaint)

View File

@ -60,7 +60,10 @@ class TootButton
Status.Visibility.PRIVATE, Status.Visibility.PRIVATE,
Status.Visibility.DIRECT -> { Status.Visibility.DIRECT -> {
setText(R.string.action_send) 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 -> { else -> {
null null

View File

@ -38,7 +38,9 @@ class ConversationAdapter(
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder { 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) return ConversationViewHolder(view, statusDisplayOptions, listener)
} }
@ -58,15 +60,24 @@ class ConversationAdapter(
companion object { companion object {
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() { 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 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 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) { return if (oldItem == newItem) {
// If items are equal - update timestamp only // If items are equal - update timestamp only
listOf(StatusBaseViewHolder.Key.KEY_CREATED) listOf(StatusBaseViewHolder.Key.KEY_CREATED)

View File

@ -140,21 +140,16 @@ data class ConversationStatusEntity(
} }
} }
fun TimelineAccount.toEntity() = fun TimelineAccount.toEntity() = ConversationAccountEntity(
ConversationAccountEntity( id = id,
id = id, localUsername = localUsername,
localUsername = localUsername, username = username,
username = username, displayName = name,
displayName = name, avatar = avatar,
avatar = avatar, emojis = emojis.orEmpty()
emojis = emojis.orEmpty() )
)
fun Status.toEntity( fun Status.toEntity(expanded: Boolean, contentShowing: Boolean, contentCollapsed: Boolean) =
expanded: Boolean,
contentShowing: Boolean,
contentCollapsed: Boolean
) =
ConversationStatusEntity( ConversationStatusEntity(
id = id, id = id,
url = url, url = url,
@ -188,16 +183,15 @@ fun Conversation.toEntity(
expanded: Boolean, expanded: Boolean,
contentShowing: Boolean, contentShowing: Boolean,
contentCollapsed: Boolean contentCollapsed: Boolean
) = ) = ConversationEntity(
ConversationEntity( accountId = accountId,
accountId = accountId, id = id,
id = id, order = order,
order = order, accounts = accounts.map { it.toEntity() },
accounts = accounts.map { it.toEntity() }, unread = unread,
unread = unread, lastStatus = lastStatus!!.toEntity(
lastStatus = lastStatus!!.toEntity( expanded = expanded,
expanded = expanded, contentShowing = contentShowing,
contentShowing = contentShowing, contentCollapsed = contentCollapsed
contentCollapsed = contentCollapsed
)
) )
)

View File

@ -27,7 +27,10 @@ class ConversationLoadStateAdapter(
private val retryCallback: () -> Unit private val retryCallback: () -> Unit
) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() { ) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() {
override fun onBindViewHolder(holder: BindingHolder<ItemNetworkStateBinding>, loadState: LoadState) { override fun onBindViewHolder(
holder: BindingHolder<ItemNetworkStateBinding>,
loadState: LoadState
) {
val binding = holder.binding val binding = holder.binding
binding.progressBar.visible(loadState == LoadState.Loading) binding.progressBar.visible(loadState == LoadState.Loading)
binding.retryButton.visible(loadState is LoadState.Error) binding.retryButton.visible(loadState is LoadState.Error)
@ -47,7 +50,11 @@ class ConversationLoadStateAdapter(
parent: ViewGroup, parent: ViewGroup,
loadState: LoadState loadState: LoadState
): BindingHolder<ItemNetworkStateBinding> { ): 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) 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.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.DurationUnit import kotlin.time.DurationUnit
import kotlin.time.toDuration import kotlin.time.toDuration
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class ConversationsFragment : class ConversationsFragment :
SFragment(), SFragment(),
@ -91,7 +91,11 @@ class ConversationsFragment :
private var hideFab = false 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) return inflater.inflate(R.layout.fragment_timeline, container, false)
} }
@ -141,13 +145,19 @@ class ConversationsFragment :
is LoadState.NotLoading -> { is LoadState.NotLoading -> {
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
binding.statusView.show() 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) binding.statusView.showHelp(R.string.help_empty_conversations)
} }
} }
is LoadState.Error -> { is LoadState.Error -> {
binding.statusView.show() 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 -> { is LoadState.Loading -> {
binding.progressBar.show() binding.progressBar.show()
@ -240,7 +250,9 @@ class ConversationsFragment :
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(context) 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 (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
@ -298,7 +310,11 @@ class ConversationsFragment :
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
adapter.peek(position)?.let { conversation -> 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 { 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() val conversations = conversationsResponse.body()
if (!conversationsResponse.isSuccessful || conversations == null) { 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.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.EmptyPagingSource import com.keylesspalace.tusky.util.EmptyPagingSource
import javax.inject.Inject
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class ConversationsViewModel @Inject constructor( class ConversationsViewModel @Inject constructor(
private val timelineCases: TimelineCases, private val timelineCases: TimelineCases,
@ -91,7 +91,11 @@ class ConversationsViewModel @Inject constructor(
fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) { fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) {
viewModelScope.launch { 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 -> .fold({ poll ->
val newConversation = conversation.toEntity( val newConversation = conversation.toEntity(
accountId = accountManager.activeAccount!!.id, accountId = accountManager.activeAccount!!.id,

View File

@ -11,8 +11,15 @@ class DomainBlocksAdapter(
private val onUnmute: (String) -> Unit private val onUnmute: (String) -> Unit
) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) { ) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemBlockedDomainBinding> { override fun onCreateViewHolder(
val binding = ItemBlockedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemBlockedDomainBinding> {
val binding = ItemBlockedDomainBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding) 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.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectable { 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) val adapter = DomainBlocksAdapter(viewModel::unblock)
binding.recyclerView.setHasFixedSize(true) 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.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(view.context) binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
@ -52,7 +54,9 @@ class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectab
} }
adapter.addLoadStateListener { loadState -> 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) { if (loadState.refresh is LoadState.Error) {
binding.recyclerView.hide() binding.recyclerView.hide()

View File

@ -8,9 +8,9 @@ import androidx.paging.cachedIn
import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.fold
import at.connyduck.calladapter.networkresult.onFailure import at.connyduck.calladapter.networkresult.onFailure
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class DomainBlocksViewModel @Inject constructor( class DomainBlocksViewModel @Inject constructor(
private val repo: DomainBlocksRepository 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.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.copyToFile 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.File
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import javax.inject.Inject 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( class DraftHelper @Inject constructor(
val context: Context, val context: Context,
@ -200,6 +200,10 @@ class DraftHelper @Inject constructor(
} else { } else {
this.copyToFile(contentResolver, file) 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 return oldItem == newItem
} }
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { override fun areContentsTheSame(
oldItem: DraftAttachment,
newItem: DraftAttachment
): Boolean {
return oldItem == newItem return oldItem == newItem
} }
} }
@ -75,7 +78,9 @@ class DraftMediaAdapter(
RecyclerView.ViewHolder(imageView) { RecyclerView.ViewHolder(imageView) {
init { init {
val thumbnailViewSize = 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 layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources val margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin) .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.isHttpNotFound
import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class DraftsActivity : BaseActivity(), DraftActionListener { class DraftsActivity : BaseActivity(), DraftActionListener {
@ -74,7 +74,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
binding.draftsRecyclerView.adapter = adapter binding.draftsRecyclerView.adapter = adapter
binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this) 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) bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
@ -134,10 +136,18 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
if (throwable.isHttpNotFound()) { if (throwable.isHttpNotFound()) {
// the original status to which a reply was drafted has been deleted // the original status to which a reply was drafted has been deleted
// let's open the ComposeActivity without reply information // 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) openDraftWithoutReply(draft)
} else { } 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() .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 binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val viewHolder = BindingHolder(binding) val viewHolder = BindingHolder(binding)
@ -77,7 +80,9 @@ class DraftsAdapter(
holder.binding.content.text = draft.content holder.binding.content.text = draft.content
holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty()) 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) { if (draft.poll != null) {
holder.binding.draftPoll.show() 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.db.DraftEntity
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class DraftsViewModel @Inject constructor( class DraftsViewModel @Inject constructor(
val database: AppDatabase, val database: AppDatabase,
@ -38,7 +38,11 @@ class DraftsViewModel @Inject constructor(
val drafts = Pager( val drafts = Pager(
config = PagingConfig(pageSize = 20), config = PagingConfig(pageSize = 20),
pagingSourceFactory = { database.draftDao().draftsPagingSource(accountManager.activeAccount?.id!!) } pagingSourceFactory = {
database.draftDao().draftsPagingSource(
accountManager.activeAccount?.id!!
)
}
).flow ).flow
.cachedIn(viewModelScope) .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.isHttpNotFound
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.launch
import java.util.Date import java.util.Date
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class EditFilterActivity : BaseActivity() { class EditFilterActivity : BaseActivity() {
@Inject @Inject
@ -115,7 +115,12 @@ class EditFilterActivity : BaseActivity() {
) )
} }
binding.filterDurationSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { 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( viewModel.setDuration(
if (originalFilter?.expiresAt == null) { if (originalFilter?.expiresAt == null) {
position position
@ -266,10 +271,16 @@ class EditFilterActivity : BaseActivity() {
if (viewModel.saveChanges(this@EditFilterActivity)) { if (viewModel.saveChanges(this@EditFilterActivity)) {
finish() finish()
// Possibly affected contexts: any context affected by the original filter OR any context affected by the updated filter // 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)) eventHub.dispatch(FilterUpdatedEvent(affectedContexts))
} else { } 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() 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 { } 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) // 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? { fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): Int? {
return when (index) { 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 0 -> null
else -> context?.resources?.getIntArray(R.array.filter_duration_values)?.get(index) 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.entity.FilterKeyword
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject
class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() { class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() {
private var originalFilter: Filter? = null 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) val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
api.createFilter( api.createFilter(
title = title, title = title,
@ -103,7 +109,11 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
{ newFilter -> { newFilter ->
// This is _terrible_, but the all-in-one update filter api Just Doesn't Work // This is _terrible_, but the all-in-one update filter api Just Doesn't Work
return keywords.value.map { keyword -> 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 } }.none { it.isFailure }
}, },
{ throwable -> { 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) val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
api.updateFilter( api.updateFilter(
id = originalFilter.id, id = originalFilter.id,

View File

@ -22,7 +22,9 @@ import androidx.appcompat.app.AlertDialog
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.util.await 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)) .setMessage(getString(R.string.dialog_delete_filter_text, filterTitle))
.setCancelable(true) .setCancelable(true)
.create() .create()

View File

@ -14,8 +14,8 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class FiltersActivity : BaseActivity(), FiltersListener { class FiltersActivity : BaseActivity(), FiltersListener {
@Inject @Inject
@ -54,20 +54,30 @@ class FiltersActivity : BaseActivity(), FiltersListener {
private fun observeViewModel() { private fun observeViewModel() {
lifecycleScope.launch { lifecycleScope.launch {
viewModel.state.collect { state -> 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.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) { when (state.loadingState) {
FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide() FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide()
FiltersViewModel.LoadingState.ERROR_NETWORK -> { 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() loadFilters()
} }
binding.messageView.show() binding.messageView.show()
} }
FiltersViewModel.LoadingState.ERROR_OTHER -> { 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() loadFilters()
} }
binding.messageView.show() 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 getItemCount(): Int = filters.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemRemovableBinding> { override fun onCreateViewHolder(
return BindingHolder(ItemRemovableBinding.inflate(LayoutInflater.from(parent.context), parent, false)) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemRemovableBinding> {
return BindingHolder(
ItemRemovableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
} }
override fun onBindViewHolder(holder: BindingHolder<ItemRemovableBinding>, position: Int) { 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.entity.Filter
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class FiltersViewModel @Inject constructor( class FiltersViewModel @Inject constructor(
private val api: MastodonApi, private val api: MastodonApi,
@ -21,7 +21,11 @@ class FiltersViewModel @Inject constructor(
) : ViewModel() { ) : ViewModel() {
enum class LoadingState { 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) data class State(val filters: List<Filter>, val loadingState: LoadingState)
@ -61,7 +65,12 @@ class FiltersViewModel @Inject constructor(
viewModelScope.launch { viewModelScope.launch {
api.deleteFilter(filter.id).fold( 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) { for (context in filter.context) {
eventHub.dispatch(PreferenceChangedEvent(context)) eventHub.dispatch(PreferenceChangedEvent(context))
} }
@ -70,14 +79,27 @@ class FiltersViewModel @Inject constructor(
if (throwable.isHttpNotFound()) { if (throwable.isHttpNotFound()) {
api.deleteFilterV1(filter.id).fold( 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 { } 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.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class FollowedTagsActivity : class FollowedTagsActivity :
BaseActivity(), BaseActivity(),
@ -81,7 +81,9 @@ class FollowedTagsActivity :
binding.followedTagsView.adapter = adapter binding.followedTagsView.adapter = adapter
binding.followedTagsView.setHasFixedSize(true) binding.followedTagsView.setHasFixedSize(true)
binding.followedTagsView.layoutManager = LinearLayoutManager(this) 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 (binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
val hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) val hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
@ -101,7 +103,9 @@ class FollowedTagsActivity :
private fun setupAdapter(): FollowedTagsAdapter { private fun setupAdapter(): FollowedTagsAdapter {
return FollowedTagsAdapter(this, viewModel).apply { return FollowedTagsAdapter(this, viewModel).apply {
addLoadStateListener { loadState -> 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) { if (loadState.refresh is LoadState.Error) {
binding.followedTagsView.hide() binding.followedTagsView.hide()

View File

@ -15,13 +15,22 @@ class FollowedTagsAdapter(
private val actionListener: HashtagActionListener, private val actionListener: HashtagActionListener,
private val viewModel: FollowedTagsViewModel private val viewModel: FollowedTagsViewModel
) : PagingDataAdapter<String, BindingHolder<ItemFollowedHashtagBinding>>(STRING_COMPARATOR) { ) : PagingDataAdapter<String, BindingHolder<ItemFollowedHashtagBinding>>(STRING_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowedHashtagBinding> = override fun onCreateViewHolder(
BindingHolder(ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)) 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 -> viewModel.tags[position].let { tag ->
holder.itemView.findViewById<TextView>(R.id.followed_tag).text = tag.name 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) actionListener.unfollow(tag.name, holder.bindingAdapterPosition)
} }
} }
@ -31,8 +40,10 @@ class FollowedTagsAdapter(
companion object { companion object {
val STRING_COMPARATOR = object : DiffUtil.ItemCallback<String>() { val STRING_COMPARATOR = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem 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) ).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) return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
.fold({ searchResult -> .fold({ searchResult ->
searchResult.hashtags.map { ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult(it.name) } searchResult.hashtags.map {
ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult(
it.name
)
}
}, { e -> }, { e ->
Log.e(TAG, "Autocomplete search for $token failed.", e) Log.e(TAG, "Autocomplete search for $token failed.", e)
emptyList() emptyList()

View File

@ -26,9 +26,9 @@ import com.keylesspalace.tusky.db.InstanceInfoEntity
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject
class InstanceInfoRepository @Inject constructor( class InstanceInfoRepository @Inject constructor(
private val api: MastodonApi, private val api: MastodonApi,
@ -77,7 +77,7 @@ class InstanceInfoRepository @Inject constructor(
maxMediaAttachments = instance.configuration.statuses?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS, maxMediaAttachments = instance.configuration.statuses?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS,
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields, maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength, maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength, maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength
) )
dao.upsert(instanceEntity) dao.upsert(instanceEntity)
instanceEntity instanceEntity
@ -86,7 +86,11 @@ class InstanceInfoRepository @Inject constructor(
if (throwable.isHttpNotFound()) { if (throwable.isHttpNotFound()) {
getInstanceInfoV1() getInstanceInfoV1()
} else { } 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) dao.getInstanceInfo(instanceName)
} }
} }
@ -105,7 +109,7 @@ class InstanceInfoRepository @Inject constructor(
maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS, maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS,
maxFieldNameLength = instanceInfo?.maxFieldNameLength, maxFieldNameLength = instanceInfo?.maxFieldNameLength,
maxFieldValueLength = instanceInfo?.maxFieldValueLength, maxFieldValueLength = instanceInfo?.maxFieldValueLength,
version = instanceInfo?.version, version = instanceInfo?.version
) )
} }
} }
@ -129,13 +133,17 @@ class InstanceInfoRepository @Inject constructor(
maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments, maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments,
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields, maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength, maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength, maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength
) )
dao.upsert(instanceEntity) dao.upsert(instanceEntity)
instanceEntity instanceEntity
}, },
{ throwable -> { 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) 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.rickRoll
import com.keylesspalace.tusky.util.shouldRickRoll import com.keylesspalace.tusky.util.shouldRickRoll
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.HttpUrl import okhttp3.HttpUrl
import javax.inject.Inject
/** Main login page, the first thing that users see. Has prompt for instance and login button. */ /** Main login page, the first thing that users see. Has prompt for instance and login button. */
class LoginActivity : BaseActivity(), Injectable { 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, // 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. // login there, and the server will redirect back to the app with its response.
val uri = HttpUrl.Builder() 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.hide
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import javax.inject.Inject
/** Contract for starting [LoginWebViewActivity]. */ /** Contract for starting [LoginWebViewActivity]. */
class OauthLogin : ActivityResultContract<LoginData, LoginResult>() { class OauthLogin : ActivityResultContract<LoginData, LoginResult>() {

View File

@ -21,9 +21,9 @@ import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class LoginWebViewViewModel @Inject constructor( class LoginWebViewViewModel @Inject constructor(
private val api: MastodonApi private val api: MastodonApi
@ -48,11 +48,19 @@ class LoginWebViewViewModel @Inject constructor(
instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty() instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty()
}, },
{ throwable -> { throwable ->
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable) Log.w(
"LoginWebViewViewModel",
"failed to load instance info",
throwable
)
} }
) )
} else { } 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.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.isLessThan import com.keylesspalace.tusky.util.isLessThan
import kotlinx.coroutines.delay
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.min import kotlin.math.min
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay
/** Models next/prev links from the "Links" header in an API response */ /** Models next/prev links from the "Links" header in an API response */
data class Links(val next: String?, val prev: String?) { data class Links(val next: String?, val prev: String?) {
@ -55,12 +55,16 @@ class NotificationFetcher @Inject constructor(
for (account in accountManager.getAllAccountsOrderedByActive()) { for (account in accountManager.getAllAccountsOrderedByActive()) {
if (account.notificationsEnabled) { if (account.notificationsEnabled) {
try { 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 // Create sorted list of new notifications
val notifications = fetchNewNotifications(account) val notifications = fetchNewNotifications(account)
.filter { filterNotification(notificationManager, account, it) } .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() .toMutableList()
// TODO do this before filter above? But one could argue that (for example) a tab badge is also a notification // 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 // Err on the side of removing *older* notifications to make room for newer
// notifications. // notifications.
val currentAndroidNotifications = notificationManager.activeNotifications 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 // Check to see if any notifications need to be removed
val toRemove = currentAndroidNotifications.size + notifications.size - MAX_NOTIFICATIONS val toRemove = currentAndroidNotifications.size + notifications.size - MAX_NOTIFICATIONS
if (toRemove > 0) { if (toRemove > 0) {
// Prefer to cancel old notifications first // 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) } .forEach { notificationManager.cancel(it.tag, it.id) }
// Still got notifications to remove? Trim the list of new notifications, // Still got notifications to remove? Trim the list of new notifications,
@ -106,7 +115,11 @@ class NotificationFetcher @Inject constructor(
account, account,
notificationsGroup.value.size == 1 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 // Android will rate limit / drop notifications if they're posted too
// quickly. There is no indication to the user that this happened. // 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}") Log.d(TAG, "getting notification marker for ${account.fullName}")
val remoteMarkerId = fetchMarker(authHeader, account)?.lastReadId ?: "0" val remoteMarkerId = fetchMarker(authHeader, account)?.lastReadId ?: "0"
val localMarkerId = account.notificationMarkerId 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 val readingPosition = account.lastNotificationId
var minId: String? = if (readingPosition.isLessThan(markerId)) markerId else readingPosition 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) Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE)
.setAnchorView(anchorView) .setAnchorView(anchorView)
.setAction(R.string.action_details) { showMigrationExplanationDialog(context, accountManager) } .setAction(
R.string.action_details
) { showMigrationExplanationDialog(context, accountManager) }
.show() .show()
} }
@ -75,7 +77,9 @@ private fun showMigrationExplanationDialog(context: Context, accountManager: Acc
if (currentAccountNeedsMigration(accountManager)) { if (currentAccountNeedsMigration(accountManager)) {
setMessage(R.string.dialog_push_notification_migration) setMessage(R.string.dialog_push_notification_migration)
setPositiveButton(R.string.title_migration_relogin) { _, _ -> setPositiveButton(R.string.title_migration_relogin) { _, _ ->
context.startActivity(LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)) context.startActivity(
LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)
)
} }
} else { } else {
setMessage(R.string.dialog_push_notification_migration_other_accounts) 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)) { if (isUnifiedPushNotificationEnabledForAccount(account)) {
// Already registered, update the subscription to match notification settings // Already registered, update the subscription to match notification settings
updateUnifiedPushSubscription(context, api, accountManager, account) updateUnifiedPushSubscription(context, api, accountManager, account)
} else { } 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 = fun canEnablePushNotifications(context: Context, accountManager: AccountManager): Boolean =
isUnifiedPushAvailable(context) && !anyAccountNeedsMigration(accountManager) 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)) { if (!canEnablePushNotifications(context, accountManager)) {
// No UP distributors // No UP distributors
NotificationHelper.enablePullNotifications(context) NotificationHelper.enablePullNotifications(context)
@ -151,9 +168,14 @@ fun disableAllNotifications(context: Context, accountManager: AccountManager) {
private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> = private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> =
buildMap { buildMap {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager val notificationManager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
Notification.Type.visibleTypes.forEach { 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 // 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) { withContext(Dispatchers.IO) {
api.updatePushNotificationSubscription( api.updatePushNotificationSubscription(
"Bearer ${account.accessToken}", "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) { withContext(Dispatchers.IO) {
api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain) api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain)
.onFailure { throwable -> .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.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeRes import com.mikepenz.iconics.utils.sizeRes
import javax.inject.Inject
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import javax.inject.Inject
class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable { class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject @Inject
@ -74,7 +74,11 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject @Inject
lateinit var accountPreferenceDataStore: AccountPreferenceDataStore 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?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = requireContext() val context = requireContext()
@ -198,14 +202,17 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
value = visibility.serverString() value = visibility.serverString()
setIcon(getIconForVisibility(visibility)) setIcon(getIconForVisibility(visibility))
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
setIcon(getIconForVisibility(Status.Visibility.byString(newValue as String))) setIcon(
getIconForVisibility(Status.Visibility.byString(newValue as String))
)
syncWithServer(visibility = newValue) syncWithServer(visibility = newValue)
true true
} }
} }
listPreference { listPreference {
val locales = getLocaleList(getInitialLanguages(null, accountManager.activeAccount)) val locales =
getLocaleList(getInitialLanguages(null, accountManager.activeAccount))
setTitle(R.string.pref_default_post_language) setTitle(R.string.pref_default_post_language)
// Explicitly add "System default" to the start of the list // Explicitly add "System default" to the start of the list
entries = ( entries = (
@ -289,14 +296,21 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
startActivity(intent) startActivity(intent)
} else { } else {
activity?.let { activity?.let {
val intent = PreferencesActivity.newIntent(it, PreferencesActivity.NOTIFICATION_PREFERENCES) val intent = PreferencesActivity.newIntent(
it,
PreferencesActivity.NOTIFICATION_PREFERENCES
)
it.startActivity(intent) it.startActivity(intent)
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) 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 // TODO these could also be "datastore backed" preferences (a ServerPreferenceDataStore); follow-up of issue #3204
mastodonApi.accountUpdateSource(visibility, sensitive, language) mastodonApi.accountUpdateSource(visibility, sensitive, language)

View File

@ -40,8 +40,8 @@ import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.setAppNightMode import com.keylesspalace.tusky.util.setAppNightMode
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class PreferencesActivity : class PreferencesActivity :
BaseActivity(), BaseActivity(),
@ -127,12 +127,16 @@ class PreferencesActivity :
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
PreferenceManager.getDefaultSharedPreferences(this).registerOnSharedPreferenceChangeListener(this) PreferenceManager.getDefaultSharedPreferences(
this
).registerOnSharedPreferenceChangeListener(this)
} }
override fun onPause() { override fun onPause() {
super.onPause() super.onPause()
PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this) PreferenceManager.getDefaultSharedPreferences(
this
).unregisterOnSharedPreferenceChangeListener(this)
} }
private fun saveInstanceState(outState: Bundle) { private fun saveInstanceState(outState: Bundle) {

View File

@ -49,7 +49,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject @Inject
lateinit var localeManager: LocaleManager 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 { enum class ReadingOrder {
/** User scrolls up, reading statuses oldest to newest */ /** User scrolls up, reading statuses oldest to newest */
@ -253,7 +257,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS
setOnPreferenceChangeListener { _, value -> setOnPreferenceChangeListener { _, value ->
for (account in accountManager.accounts) { for (account in accountManager.accounts) {
val notificationFilter = deserialize(account.notificationsFilter).toMutableSet() val notificationFilter = deserialize(
account.notificationsFilter
).toMutableSet()
if (value == true) { if (value == true) {
notificationFilter.add(Notification.Type.FAVOURITE) notificationFilter.add(Notification.Type.FAVOURITE)

View File

@ -59,7 +59,10 @@ class ProxyPreferencesFragment : PreferenceFragmentCompat() {
MAX_PROXY_PORT MAX_PROXY_PORT
) )
validatedEditTextPreference(portErrorMessage, ProxyConfiguration::isValidProxyPort) { validatedEditTextPreference(
portErrorMessage,
ProxyConfiguration::isValidProxyPort
) {
setTitle(R.string.pref_title_http_proxy_port) setTitle(R.string.pref_title_http_proxy_port)
key = PrefKeys.HTTP_PROXY_PORT key = PrefKeys.HTTP_PROXY_PORT
isIconSpaceReserved = false isIconSpaceReserved = false

View File

@ -46,7 +46,9 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
val accountId = intent?.getStringExtra(ACCOUNT_ID) val accountId = intent?.getStringExtra(ACCOUNT_ID)
val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME) val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME)
if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) { 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)) viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID))
@ -130,13 +132,17 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
private const val STATUS_ID = "status_id" private const val STATUS_ID = "status_id"
@JvmStatic @JvmStatic
fun getIntent(context: Context, accountId: String, userName: String, statusId: String? = null) = fun getIntent(
Intent(context, ReportActivity::class.java) context: Context,
.apply { accountId: String,
putExtra(ACCOUNT_ID, accountId) userName: String,
putExtra(ACCOUNT_USERNAME, userName) statusId: String? = null
putExtra(STATUS_ID, statusId) ) = Intent(context, ReportActivity::class.java)
} .apply {
putExtra(ACCOUNT_ID, accountId)
putExtra(ACCOUNT_USERNAME, userName)
putExtra(STATUS_ID, statusId)
}
} }
override fun androidInjector() = androidInjector 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.Resource
import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.util.toViewData
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class ReportViewModel @Inject constructor( class ReportViewModel @Inject constructor(
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
@ -196,7 +196,12 @@ class ReportViewModel @Inject constructor(
fun doReport() { fun doReport() {
reportingStateMutable.value = Loading() reportingStateMutable.value = Loading()
viewModelScope.launch { 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({ .fold({
reportingStateMutable.value = Success(true) reportingStateMutable.value = Success(true)
}, { error -> }, { error ->

View File

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

View File

@ -50,7 +50,9 @@ class StatusViewHolder(
private val getStatusForPosition: (Int) -> StatusViewData.Concrete? private val getStatusForPosition: (Int) -> StatusViewData.Concrete?
) : RecyclerView.ViewHolder(binding.root) { ) : 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 statusViewHelper = StatusViewHelper(itemView)
private val absoluteTimeFormatter = AbsoluteTimeFormatter() private val absoluteTimeFormatter = AbsoluteTimeFormatter()
@ -93,7 +95,11 @@ class StatusViewHolder(
mediaViewHeight 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) setCreatedAt(viewData.status.createdAt)
} }
@ -107,11 +113,22 @@ class StatusViewHolder(
) )
if (viewdata.status.spoilerText.isBlank()) { 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.statusContentWarningButton.hide()
binding.statusContentWarningDescription.hide() binding.statusContentWarningDescription.hide()
} else { } 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.text = emojiSpoiler
binding.statusContentWarningDescription.show() binding.statusContentWarningDescription.show()
binding.statusContentWarningButton.show() binding.statusContentWarningButton.show()
@ -121,11 +138,25 @@ class StatusViewHolder(
val contentShown = viewState.isContentShow(viewdata.id, true) val contentShown = viewState.isContentShow(viewdata.id, true)
binding.statusContentWarningDescription.invalidate() binding.statusContentWarningDescription.invalidate()
viewState.setContentShow(viewdata.id, !contentShown) 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) 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 listener: LinkListener
) { ) {
if (expanded) { 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) setClickableText(binding.statusContent, emojifiedText, mentions, tags, listener)
} else { } else {
setClickableMentions(binding.statusContent, mentions, listener) 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 */ /* input filter for TextViews have to be set before text */
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
binding.buttonToggleContent.setOnClickListener { binding.buttonToggleContent.setOnClickListener {

View File

@ -36,7 +36,11 @@ class StatusesAdapter(
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { 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( return StatusViewHolder(
binding, binding,
statusDisplayOptions, statusDisplayOptions,
@ -54,11 +58,15 @@ class StatusesAdapter(
companion object { companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() { val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = override fun areContentsTheSame(
oldItem == newItem oldItem: StatusViewData.Concrete,
newItem: StatusViewData.Concrete
): Boolean = oldItem == newItem
override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = override fun areItemsTheSame(
oldItem.id == newItem.id 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) { val result = if (params is LoadParams.Refresh && key != null) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val initialStatus = async { getSingleStatus(key) } 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() listOf(initialStatus.await()) + additionalStatuses.await()
} }
} else { } else {
@ -75,7 +76,11 @@ class StatusesPagingSource(
return mastodonApi.statusObservable(statusId).await() 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( return mastodonApi.accountStatusesObservable(
accountId = accountId, accountId = accountId,
maxId = maxId, maxId = maxId,

View File

@ -95,7 +95,11 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
binding.buttonBack.isEnabled = true binding.buttonBack.isEnabled = true
binding.progressBar.hide() 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) { .setAction(R.string.action_retry) {
sendReport() sendReport()
} }

View File

@ -59,9 +59,9 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class ReportStatusesFragment : class ReportStatusesFragment :
Fragment(R.layout.fragment_report_statuses), Fragment(R.layout.fragment_report_statuses),
@ -93,7 +93,11 @@ class ReportStatusesFragment :
if (v != null) { if (v != null) {
val url = actionable.attachments[idx].url val url = actionable.attachments[idx].url
ViewCompat.setTransitionName(v, url) ViewCompat.setTransitionName(v, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), v, url) val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
v,
url
)
startActivity(intent, options.toBundle()) startActivity(intent, options.toBundle())
} else { } else {
startActivity(intent) startActivity(intent)
@ -164,7 +168,9 @@ class ReportStatusesFragment :
adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this) 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.layoutManager = LinearLayoutManager(requireContext())
binding.recyclerView.adapter = adapter binding.recyclerView.adapter = adapter
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
@ -185,7 +191,9 @@ class ReportStatusesFragment :
binding.progressBarBottom.visible(loadState.append == LoadState.Loading) binding.progressBarBottom.visible(loadState.append == LoadState.Loading)
binding.progressBarTop.visible(loadState.prepend == 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) { if (loadState.refresh != LoadState.Loading) {
binding.swipeRefreshLayout.isRefreshing = false binding.swipeRefreshLayout.isRefreshing = false
@ -221,9 +229,13 @@ class ReportStatusesFragment :
return viewModel.isStatusChecked(id) 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) override fun onViewUrl(url: String) = viewModel.checkClickedUrl(url)

View File

@ -20,17 +20,35 @@ class StatusViewState {
private val contentShownState = HashMap<String, Boolean>() private val contentShownState = HashMap<String, Boolean>()
private val longContentCollapsedState = 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 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 setContentShow(id: String, isShow: Boolean) = setStateEnabled(contentShownState, id, isShow)
fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(longContentCollapsedState, id, isCollapsed) fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(
fun setCollapsed(id: String, isCollapsed: Boolean) = setStateEnabled(longContentCollapsedState, id, isCollapsed) 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] private fun isStateEnabled(map: Map<String, Boolean>, id: String, def: Boolean): Boolean =
?: def 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.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject
class ScheduledStatusActivity : class ScheduledStatusActivity :
BaseActivity(), BaseActivity(),
@ -109,7 +109,10 @@ class ScheduledStatusActivity :
if (loadState.refresh is LoadState.NotLoading) { if (loadState.refresh is LoadState.NotLoading) {
binding.progressBar.hide() binding.progressBar.hide()
if (adapter.itemCount == 0) { 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() binding.errorMessageView.show()
} else { } else {
binding.errorMessageView.hide() binding.errorMessageView.hide()
@ -163,8 +166,8 @@ class ScheduledStatusActivity :
visibility = item.params.visibility, visibility = item.params.visibility,
scheduledAt = item.scheduledAt, scheduledAt = item.scheduledAt,
sensitive = item.params.sensitive, sensitive = item.params.sensitive,
kind = ComposeActivity.ComposeKind.EDIT_SCHEDULED, kind = ComposeActivity.ComposeKind.EDIT_SCHEDULED
), )
) )
startActivity(intent) startActivity(intent)
} }

View File

@ -36,18 +36,31 @@ class ScheduledStatusAdapter(
return oldItem.id == newItem.id return oldItem.id == newItem.id
} }
override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { override fun areContentsTheSame(
oldItem: ScheduledStatus,
newItem: ScheduledStatus
): Boolean {
return oldItem == newItem return oldItem == newItem
} }
} }
) { ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledStatusBinding> { override fun onCreateViewHolder(
val binding = ItemScheduledStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int
): BindingHolder<ItemScheduledStatusBinding> {
val binding = ItemScheduledStatusBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding) return BindingHolder(binding)
} }
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledStatusBinding>, position: Int) { override fun onBindViewHolder(
holder: BindingHolder<ItemScheduledStatusBinding>,
position: Int
) {
getItem(position)?.let { item -> getItem(position)?.let { item ->
holder.binding.edit.isEnabled = true holder.binding.edit.isEnabled = true
holder.binding.delete.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.appstore.EventHub
import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
import kotlinx.coroutines.launch
class ScheduledStatusViewModel @Inject constructor( class ScheduledStatusViewModel @Inject constructor(
val mastodonApi: MastodonApi, val mastodonApi: MastodonApi,

Some files were not shown because too many files have changed in this diff Show More