Browse Source

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>
pull/4207/head
Konrad Pozniak 2 years ago committed by GitHub
parent
commit
5192fb08a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      .editorconfig
  2. 14
      app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt
  3. 62
      app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt
  4. 16
      app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt
  5. 35
      app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt
  6. 9
      app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt
  7. 92
      app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
  8. 73
      app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt
  9. 19
      app/src/main/java/com/keylesspalace/tusky/TabData.kt
  10. 32
      app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt
  11. 7
      app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
  12. 42
      app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
  13. 11
      app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt
  14. 5
      app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt
  15. 11
      app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt
  16. 20
      app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt
  17. 6
      app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt
  18. 5
      app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt
  19. 6
      app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt
  20. 32
      app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt
  21. 6
      app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt
  22. 2
      app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt
  23. 5
      app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
  24. 4
      app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt
  25. 100
      app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt
  26. 24
      app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt
  27. 6
      app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt
  28. 25
      app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt
  29. 14
      app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt
  30. 2
      app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt
  31. 28
      app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt
  32. 43
      app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt
  33. 25
      app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt
  34. 4
      app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt
  35. 17
      app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt
  36. 16
      app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsHeaderAdapter.kt
  37. 17
      app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt
  38. 25
      app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt
  39. 10
      app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt
  40. 6
      app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt
  41. 282
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
  42. 4
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt
  43. 46
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
  44. 8
      app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt
  45. 10
      app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt
  46. 31
      app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt
  47. 12
      app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt
  48. 11
      app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt
  49. 19
      app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt
  50. 15
      app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt
  51. 5
      app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt
  52. 3
      app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt
  53. 12
      app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt
  54. 5
      app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt
  55. 19
      app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt
  56. 46
      app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt
  57. 11
      app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt
  58. 32
      app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt
  59. 5
      app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt
  60. 8
      app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt
  61. 11
      app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksAdapter.kt
  62. 10
      app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt
  63. 2
      app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksViewModel.kt
  64. 18
      app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt
  65. 9
      app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt
  66. 18
      app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt
  67. 9
      app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt
  68. 8
      app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt
  69. 37
      app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt
  70. 25
      app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt
  71. 4
      app/src/main/java/com/keylesspalace/tusky/components/filters/FilterExtensions.kt
  72. 20
      app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt
  73. 9
      app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt
  74. 34
      app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt
  75. 10
      app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt
  76. 23
      app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsAdapter.kt
  77. 10
      app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt
  78. 20
      app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt
  79. 8
      app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt
  80. 2
      app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt
  81. 14
      app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt
  82. 34
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt
  83. 0
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt
  84. 49
      app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt
  85. 26
      app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
  86. 10
      app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt
  87. 10
      app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
  88. 5
      app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt
  89. 22
      app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt
  90. 9
      app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
  91. 2
      app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt
  92. 56
      app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt
  93. 18
      app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt
  94. 9
      app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt
  95. 6
      app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt
  96. 24
      app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt
  97. 32
      app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt
  98. 11
      app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt
  99. 21
      app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusAdapter.kt
  100. 2
      app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt
  101. Some files were not shown because too many files have changed in this diff Show More

12
.editorconfig

@ -8,12 +8,20 @@ insert_final_newline = true
trim_trailing_whitespace = true
[*.{java,kt}]
ij_kotlin_imports_layout = *
# Disable wildcard imports
ij_kotlin_name_count_to_use_star_import = 999
ij_kotlin_name_count_to_use_star_import_for_members = 999
ij_java_class_count_to_use_import_on_demand = 999
# Enable trailing comma
ktlint_disabled_rules=trailing-comma-on-call-site,trailing-comma-on-declaration-site
ktlint_code_style = android_studio
# Disable trailing comma
ktlint_standard_trailing-comma-on-call-site = disabled
ktlint_standard_trailing-comma-on-declaration-site = disabled
max_line_length = off
[*.{yml,yaml}]
indent_size = 2

14
app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt

@ -21,8 +21,8 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
class AboutActivity : BottomSheetActivity(), Injectable {
@Inject
@ -70,9 +70,15 @@ class AboutActivity : BottomSheetActivity(), Injectable {
binding.aboutPoweredByTusky.hide()
}
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(R.string.about_tusky_license)
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(R.string.about_project_site)
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(R.string.about_bug_feature_request_site)
binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines(
R.string.about_tusky_license
)
binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines(
R.string.about_project_site
)
binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines(
R.string.about_bug_feature_request_site
)
binding.tuskyProfileButton.setOnClickListener {
viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL)

62
app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt

@ -45,8 +45,8 @@ import com.keylesspalace.tusky.util.unsafeLazy
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
@ -82,11 +82,18 @@ class AccountsInListFragment : DialogFragment(), Injectable {
super.onStart()
dialog?.apply {
// Stretch dialog to the window
window?.setLayout(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)
window?.setLayout(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_accounts_in_list, container, false)
}
@ -164,15 +171,27 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
override fun areContentsTheSame(
oldItem: TimelineAccount,
newItem: TimelineAccount
): Boolean {
return oldItem == newItem
}
}
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(
AccountDiffer
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding)
binding.notificationTextView.hide()
@ -186,7 +205,10 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return holder
}
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
override fun onBindViewHolder(
holder: BindingHolder<ItemFollowRequestBinding>,
position: Int
) {
val account = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)
holder.binding.usernameTextView.text = account.username
@ -204,10 +226,19 @@ class AccountsInListFragment : DialogFragment(), Injectable {
}
}
inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(SearchDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>(
SearchDiffer
) {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestBinding> {
val binding = ItemFollowRequestBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding)
binding.notificationTextView.hide()
@ -224,7 +255,10 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return holder
}
override fun onBindViewHolder(holder: BindingHolder<ItemFollowRequestBinding>, position: Int) {
override fun onBindViewHolder(
holder: BindingHolder<ItemFollowRequestBinding>,
position: Int
) {
val (account, inAList) = getItem(position)
holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis)

16
app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt

@ -64,7 +64,10 @@ abstract class BottomSheetActivity : BaseActivity() {
})
}
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
open fun viewUrl(
url: String,
lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER
) {
if (!looksLikeMastodonUrl(url)) {
openLink(url)
return
@ -121,10 +124,17 @@ abstract class BottomSheetActivity : BaseActivity() {
startActivityWithSlideInAnimation(intent)
}
protected open fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) {
protected open fun performUrlFallbackAction(
url: String,
fallbackBehavior: PostLookupFallbackBehavior
) {
when (fallbackBehavior) {
PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url)
PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText(this, getString(R.string.post_lookup_error_format, url), Toast.LENGTH_SHORT).show()
PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText(
this,
getString(R.string.post_lookup_error_format, url),
Toast.LENGTH_SHORT
).show()
}
}

35
app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt

@ -57,8 +57,8 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
class EditProfileActivity : BaseActivity(), Injectable {
@ -126,9 +126,17 @@ class EditProfileActivity : BaseActivity(), Injectable {
binding.fieldList.layoutManager = LinearLayoutManager(this)
binding.fieldList.adapter = accountFieldEditAdapter
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply { sizeDp = 12; colorInt = Color.WHITE }
val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply {
sizeDp = 12
colorInt = Color.WHITE
}
binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(plusDrawable, null, null, null)
binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds(
plusDrawable,
null,
null,
null
)
binding.addFieldButton.setOnClickListener {
accountFieldEditAdapter.addField()
@ -162,7 +170,9 @@ class EditProfileActivity : BaseActivity(), Injectable {
.placeholder(R.drawable.avatar_default)
.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
RoundedCorners(
resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)
)
)
.into(binding.avatarPreview)
}
@ -175,7 +185,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
}
is Error -> {
Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
Snackbar.make(
binding.avatarButton,
R.string.error_generic,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_retry) {
viewModel.obtainProfile()
}
@ -188,7 +202,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
lifecycleScope.launch {
viewModel.instanceData.collect { instanceInfo ->
maxAccountFields = instanceInfo.maxFields
accountFieldEditAdapter.setFieldLimits(instanceInfo.maxFieldNameLength, instanceInfo.maxFieldValueLength)
accountFieldEditAdapter.setFieldLimits(
instanceInfo.maxFieldNameLength,
instanceInfo.maxFieldValueLength
)
binding.addFieldButton.isVisible =
accountFieldEditAdapter.itemCount < maxAccountFields
}
@ -318,7 +335,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
private fun onPickFailure(throwable: Throwable?) {
Log.w("EditProfileActivity", "failed to pick media", throwable)
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
Snackbar.make(
binding.avatarButton,
R.string.error_media_upload_sending,
Snackbar.LENGTH_LONG
).show()
}
private fun showUnsavedChangesDialog() = lifecycleScope.launch {

9
app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt

@ -54,8 +54,8 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
// TODO use the ListSelectionFragment (and/or its adapter or binding) here; but keep the LoadingState from here (?)
@ -273,7 +273,12 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
}
}
private fun onPickedDialogName(name: String, listId: String?, exclusive: Boolean, replyPolicy: String) {
private fun onPickedDialogName(
name: String,
listId: String?,
exclusive: Boolean,
replyPolicy: String
) {
if (listId == null) {
viewModel.createNewList(name, exclusive, replyPolicy)
} else {

92
app/src/main/java/com/keylesspalace/tusky/MainActivity.kt

@ -141,9 +141,9 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
import io.reactivex.rxjava3.schedulers.Schedulers
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import javax.inject.Inject
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, MenuProvider {
@Inject
@ -199,7 +199,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1)
if (notificationId != -1) {
// opened from a notification action, cancel the notification
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val notificationManager = getSystemService(
NOTIFICATION_SERVICE
) as NotificationManager
notificationManager.cancel(intent.getStringExtra(NOTIFICATION_TAG), notificationId)
}
@ -253,7 +255,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// user clicked a notification, show follow requests for type FOLLOW_REQUEST,
// otherwise show notification tab
if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) {
val intent = AccountListActivity.newIntent(this, AccountListActivity.Type.FOLLOW_REQUESTS)
val intent = AccountListActivity.newIntent(
this,
AccountListActivity.Type.FOLLOW_REQUESTS
)
startActivityWithSlideInAnimation(intent)
} else {
showNotificationTab = true
@ -293,8 +298,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
setupDrawer(
savedInstanceState,
addSearchButton = hideTopToolbar,
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_TAGS),
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(TRENDING_STATUSES),
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
TRENDING_TAGS
),
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
TRENDING_STATUSES
)
)
/* Fetch user info while we're doing other things. This has to be done after setting up the
@ -320,7 +329,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
refreshMainDrawerItems(
addSearchButton = hideTopToolbar,
addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS),
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES),
addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES)
)
setupTabs(false)
@ -333,7 +342,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
directMessageTab?.let {
if (event.accountId == activeAccount.accountId) {
val hasDirectMessageNotification =
event.notifications.any { it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT }
event.notifications.any {
it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT
}
if (hasDirectMessageNotification) {
showDirectMessageBadge(true)
@ -427,7 +438,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// If the main toolbar is hidden then there's no space in the top/bottomNav to show
// the menu items as icons, so forceably disable them
if (!binding.mainToolbar.isVisible) menu.forEach { it.setShowAsAction(SHOW_AS_ACTION_NEVER) }
if (!binding.mainToolbar.isVisible) {
menu.forEach {
it.setShowAsAction(
SHOW_AS_ACTION_NEVER
)
}
}
}
override fun onMenuItemSelected(item: MenuItem): Boolean {
@ -503,7 +520,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun forwardToComposeActivity(intent: Intent) {
val composeOptions = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS, ComposeActivity.ComposeOptions::class.java)
val composeOptions = IntentCompat.getParcelableExtra(
intent,
COMPOSE_OPTIONS,
ComposeActivity.ComposeOptions::class.java
)
val composeIntent = if (composeOptions != null) {
ComposeActivity.startIntent(this, composeOptions)
@ -523,7 +544,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
savedInstanceState: Bundle?,
addSearchButton: Boolean,
addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean,
addTrendingStatusesButton: Boolean
) {
val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
@ -553,7 +574,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
header.currentProfileName.ellipsize = TextUtils.TruncateAt.END
header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter))
header.accountHeaderBackground.setBackgroundColor(MaterialColors.getColor(header, R.attr.colorBackgroundAccent))
header.accountHeaderBackground.setBackgroundColor(
MaterialColors.getColor(header, R.attr.colorBackgroundAccent)
)
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
@ -589,7 +612,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
refreshMainDrawerItems(
addSearchButton = addSearchButton,
addTrendingTagsButton = addTrendingTagsButton,
addTrendingStatusesButton = addTrendingStatusesButton,
addTrendingStatusesButton = addTrendingStatusesButton
)
setSavedInstance(savedInstanceState)
}
@ -598,7 +621,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun refreshMainDrawerItems(
addSearchButton: Boolean,
addTrendingTagsButton: Boolean,
addTrendingStatusesButton: Boolean,
addTrendingStatusesButton: Boolean
) {
binding.mainDrawer.apply {
itemAdapter.clear()
@ -884,7 +907,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
supportActionBar?.title = tabs[position].title(this@MainActivity)
binding.mainToolbar.setOnClickListener {
(tabAdapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
(
tabAdapter.getFragment(
activeTabLayout.selectedTabPosition
) as? ReselectableFragment
)?.onReselect()
}
updateProfiles()
@ -915,7 +942,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
// open LoginActivity to add new account
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN))
startActivityWithSlideInAnimation(
LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN)
)
return false
}
// change Account
@ -986,10 +1015,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
loadDrawerAvatar(me.avatar, false)
accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this)
NotificationHelper.createNotificationChannelsForAccount(
accountManager.activeAccount!!,
this
)
// Setup push notifications
showMigrationNoticeIfNecessary(this, binding.mainCoordinatorLayout, binding.composeButton, accountManager)
showMigrationNoticeIfNecessary(
this,
binding.mainCoordinatorLayout,
binding.composeButton,
accountManager
)
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
lifecycleScope.launch {
enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager)
@ -1024,7 +1061,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
Glide.with(this)
.asDrawable()
.load(avatarUrl)
.transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)))
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) placeholder(R.drawable.avatar_default)
}
@ -1054,7 +1093,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
Glide.with(this)
.asBitmap()
.load(avatarUrl)
.transform(RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)))
.transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
)
.apply {
if (showPlaceholder) placeholder(R.drawable.avatar_default)
}
@ -1101,7 +1142,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun updateAnnouncementsBadge() {
binding.mainDrawer.updateBadge(DRAWER_ITEM_ANNOUNCEMENTS, StringHolder(if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()))
binding.mainDrawer.updateBadge(
DRAWER_ITEM_ANNOUNCEMENTS,
StringHolder(
if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString()
)
)
}
private fun updateProfiles() {
@ -1165,7 +1211,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
* Switches the active account to the accountId and takes the user to the correct place according to the notification they clicked
*/
@JvmStatic
fun openNotificationIntent(context: Context, tuskyAccountId: Long, type: Notification.Type): Intent {
fun openNotificationIntent(
context: Context,
tuskyAccountId: Long,
type: Notification.Type
): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(NOTIFICATION_TYPE, type.name)
}

73
app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt

@ -38,8 +38,8 @@ import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -49,7 +49,9 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@Inject
lateinit var eventHub: EventHub
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
private val binding: ActivityStatuslistBinding by viewBinding(
ActivityStatuslistBinding::inflate
)
private lateinit var kind: Kind
private var hashtag: String? = null
private var followTagItem: MenuItem? = null
@ -136,10 +138,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
followTagItem?.isVisible = false
unfollowTagItem?.isVisible = true
Snackbar.make(binding.root, getString(R.string.following_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.following_hashtag_success_format, tag),
Snackbar.LENGTH_SHORT
).show()
},
{
Snackbar.make(binding.root, getString(R.string.error_following_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.error_following_hashtag_format, tag),
Snackbar.LENGTH_SHORT
).show()
Log.e(TAG, "Failed to follow #$tag", it)
}
)
@ -158,10 +168,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
followTagItem?.isVisible = true
unfollowTagItem?.isVisible = false
Snackbar.make(binding.root, getString(R.string.unfollowing_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.unfollowing_hashtag_success_format, tag),
Snackbar.LENGTH_SHORT
).show()
},
{
Snackbar.make(binding.root, getString(R.string.error_unfollowing_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.error_unfollowing_hashtag_format, tag),
Snackbar.LENGTH_SHORT
).show()
Log.e(TAG, "Failed to unfollow #$tag", it)
}
)
@ -238,7 +256,12 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
expiresInSeconds = null
).fold(
{ filter ->
if (mastodonApi.addFilterKeyword(filterId = filter.id, keyword = hashedTag, wholeWord = true).isSuccess) {
if (mastodonApi.addFilterKeyword(
filterId = filter.id,
keyword = hashedTag,
wholeWord = true
).isSuccess
) {
// must be requested again; otherwise does not contain the keyword (but server does)
mutedFilter = mastodonApi.getFilter(filter.id).getOrNull()
@ -246,7 +269,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
filterCreateSuccess = true
} else {
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.error_muting_hashtag_format, tag),
Snackbar.LENGTH_SHORT
).show()
Log.e(TAG, "Failed to mute #$tag")
}
},
@ -265,12 +292,20 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
filterCreateSuccess = true
},
{ throwable ->
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.error_muting_hashtag_format, tag),
Snackbar.LENGTH_SHORT
).show()
Log.e(TAG, "Failed to mute #$tag", throwable)
}
)
} else {
Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.error_muting_hashtag_format, tag),
Snackbar.LENGTH_SHORT
).show()
Log.e(TAG, "Failed to mute #$tag", throwable)
}
}
@ -278,7 +313,11 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
if (filterCreateSuccess) {
updateTagMuteState(true)
Snackbar.make(binding.root, getString(R.string.muting_hashtag_success_format, tag), Snackbar.LENGTH_LONG).apply {
Snackbar.make(
binding.root,
getString(R.string.muting_hashtag_success_format, tag),
Snackbar.LENGTH_LONG
).apply {
setAction(R.string.action_view_filter) {
val intent = if (mutedFilter != null) {
Intent(this@StatusListActivity, EditFilterActivity::class.java).apply {
@ -339,10 +378,18 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
mutedFilterV1 = null
mutedFilter = null
Snackbar.make(binding.root, getString(R.string.unmuting_hashtag_success_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.unmuting_hashtag_success_format, tag),
Snackbar.LENGTH_SHORT
).show()
},
{ throwable ->
Snackbar.make(binding.root, getString(R.string.error_unmuting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
getString(R.string.error_unmuting_hashtag_format, tag),
Snackbar.LENGTH_SHORT
).show()
Log.e(TAG, "Failed to unmute #$tag", throwable)
}
)

19
app/src/main/java/com/keylesspalace/tusky/TabData.kt

@ -104,7 +104,11 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
id = TRENDING_STATUSES,
text = R.string.title_public_trending_statuses,
icon = R.drawable.ic_hot_24dp,
fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) }
fragment = {
TimelineFragment.newInstance(
TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES
)
}
)
HASHTAG -> TabData(
id = HASHTAG,
@ -112,13 +116,22 @@ fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabD
icon = R.drawable.ic_hashtag,
fragment = { args -> TimelineFragment.newHashtagInstance(args) },
arguments = arguments,
title = { context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) } }
title = { context ->
arguments.joinToString(separator = " ") {
context.getString(R.string.title_tag, it)
}
}
)
LIST -> TabData(
id = LIST,
text = R.string.list,
icon = R.drawable.ic_list,
fragment = { args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) },
fragment = { args ->
TimelineFragment.newInstance(
TimelineViewModel.Kind.LIST,
args.getOrNull(0).orEmpty()
)
},
arguments = arguments,
title = { arguments.getOrNull(1).orEmpty() }
)

32
app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt

@ -47,10 +47,10 @@ import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.regex.Pattern
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, ItemInteractionListener, ListSelectionFragment.ListSelectionListener {
@ -72,9 +72,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
private var tabsChanged = false
private val selectedItemElevation by unsafeLazy { resources.getDimension(R.dimen.selected_drag_item_elevation) }
private val selectedItemElevation by unsafeLazy {
resources.getDimension(R.dimen.selected_drag_item_elevation)
}
private val hashtagRegex by unsafeLazy { Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE) }
private val hashtagRegex by unsafeLazy {
Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE)
}
private val onFabDismissedCallback = object : OnBackPressedCallback(false) {
override fun handleOnBackPressed() {
@ -99,14 +103,19 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
binding.currentTabsRecyclerView.adapter = currentTabsAdapter
binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
binding.currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
binding.currentTabsRecyclerView.addItemDecoration(
DividerItemDecoration(this, LinearLayoutManager.VERTICAL)
)
addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this)
binding.addTabRecyclerView.adapter = addTabAdapter
binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this)
touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() {
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END)
}
@ -118,7 +127,11 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
return MIN_TAB_COUNT < currentTabs.size
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val temp = currentTabs[viewHolder.bindingAdapterPosition]
currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition]
currentTabs[target.bindingAdapterPosition] = temp
@ -138,7 +151,10 @@ class TabPreferenceActivity : BaseActivity(), Injectable, HasAndroidInjector, It
}
}
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
viewHolder.itemView.elevation = 0f
}

7
app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt

@ -40,10 +40,10 @@ import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
import de.c1710.filemojicompat_ui.helpers.EmojiPreference
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import org.conscrypt.Conscrypt
import java.security.Security
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import org.conscrypt.Conscrypt
class TuskyApplication : Application(), HasAndroidInjector {
@Inject
@ -78,7 +78,10 @@ class TuskyApplication : Application(), HasAndroidInjector {
AppInjector.init(this)
// Migrate shared preference keys and defaults from version to version.
val oldVersion = sharedPreferences.getInt(PrefKeys.SCHEMA_VERSION, NEW_INSTALL_SCHEMA_VERSION)
val oldVersion = sharedPreferences.getInt(
PrefKeys.SCHEMA_VERSION,
NEW_INSTALL_SCHEMA_VERSION
)
if (oldVersion != SCHEMA_VERSION) {
upgradeSharedPreferences(oldVersion, SCHEMA_VERSION)
}

42
app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt

@ -74,7 +74,12 @@ import javax.inject.Inject
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
class ViewMediaActivity :
BaseActivity(),
HasAndroidInjector,
ViewImageFragment.PhotoActionsListener,
ViewVideoFragment.VideoActionsListener {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -103,7 +108,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
supportPostponeEnterTransition()
// Gather the parameters.
attachments = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java)
attachments = IntentCompat.getParcelableArrayListExtra(
intent,
EXTRA_ATTACHMENTS,
AttachmentViewData::class.java
)
val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0)
// Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener
@ -215,7 +224,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun downloadMedia() {
val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url
val filename = Uri.parse(url).lastPathSegment
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()
Toast.makeText(
applicationContext,
resources.getString(R.string.download_image, filename),
Toast.LENGTH_SHORT
).show()
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(Uri.parse(url))
@ -225,8 +238,13 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun requestDownloadMedia() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
requestPermissions(
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
) { _, grantResults ->
if (
grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
downloadMedia()
} else {
showErrorDialog(
@ -243,7 +261,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun onOpenStatus() {
val attach = attachments!![binding.viewPager.currentItem]
startActivityWithSlideInAnimation(ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl))
startActivityWithSlideInAnimation(
ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl)
)
}
private fun copyLink() {
@ -276,7 +296,9 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private fun shareFile(file: File, mimeType: String?) {
ShareCompat.IntentBuilder(this)
.setType(mimeType)
.addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
.addStream(
FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file)
)
.setChooserTitle(R.string.send_media_to)
.startChooser()
}
@ -366,7 +388,11 @@ class ViewMediaActivity : BaseActivity(), HasAndroidInjector, ViewImageFragment.
private const val TAG = "ViewMediaActivity"
@JvmStatic
fun newIntent(context: Context?, attachments: List<AttachmentViewData>, index: Int): Intent {
fun newIntent(
context: Context?,
attachments: List<AttachmentViewData>,
index: Int
): Intent {
val intent = Intent(context, ViewMediaActivity::class.java)
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments))
intent.putExtra(EXTRA_ATTACHMENT_INDEX, index)

11
app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt

@ -62,8 +62,15 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
override fun getItemCount() = fieldData.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemEditFieldBinding> {
val binding = ItemEditFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemEditFieldBinding> {
val binding = ItemEditFieldBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}

5
app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt

@ -28,7 +28,10 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(
context,
R.layout.item_autocomplete_account
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val binding = if (convertView == null) {

11
app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt

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

20
app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt

@ -47,16 +47,26 @@ class FollowRequestViewHolder(
showBotOverlay: Boolean
) {
val wrappedName = account.name.unicodeWrap()
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
val emojifiedName: CharSequence = wrappedName.emojify(
account.emojis,
itemView,
animateEmojis
)
binding.displayNameTextView.text = emojifiedName
if (showHeader) {
val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName)
val wholeMessage: String = itemView.context.getString(
R.string.notification_follow_request_format,
wrappedName
)
binding.notificationTextView.text = SpannableStringBuilder(wholeMessage).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}.emojify(account.emojis, itemView, animateEmojis)
}
binding.notificationTextView.visible(showHeader)
val formattedUsername = itemView.context.getString(R.string.post_username_format, account.username)
val formattedUsername = itemView.context.getString(
R.string.post_username_format,
account.username
)
binding.usernameTextView.text = formattedUsername
if (account.note.isEmpty()) {
binding.accountNote.hide()
@ -67,7 +77,9 @@ class FollowRequestViewHolder(
.emojify(account.emojis, binding.accountNote, animateEmojis)
setClickableText(binding.accountNote, emojifiedNote, emptyList(), null, linkListener)
}
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(
R.dimen.avatar_radius_48dp
)
loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar)
binding.avatarBadge.visible(showBotOverlay && account.bot)
}

6
app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt

@ -26,7 +26,11 @@ import com.keylesspalace.tusky.util.getTuskyDisplayName
import com.keylesspalace.tusky.util.modernLanguageCode
import java.util.Locale
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(context, resource, locales) {
class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>(
context,
resource,
locales
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary))

5
app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt

@ -67,7 +67,10 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
.map { pollOptions.indexOf(it) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemPollBinding> {
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
}

6
app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt

@ -40,7 +40,11 @@ class PreviewPollOptionsAdapter : RecyclerView.Adapter<PreviewViewHolder>() {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder {
return PreviewViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_poll_preview_option, parent, false))
return PreviewViewHolder(
LayoutInflater.from(
parent.context
).inflate(R.layout.item_poll_preview_option, parent, false)
)
}
override fun getItemCount() = options.size

32
app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt

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

6
app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt

@ -56,7 +56,11 @@ class TabAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> {
val binding = if (small) {
ItemTabPreferenceSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false)
ItemTabPreferenceSmallBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
} else {
ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false)
}

2
app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt

@ -3,12 +3,12 @@ package com.keylesspalace.tusky.appstore
import com.google.gson.Gson
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
class CacheUpdater @Inject constructor(
eventHub: EventHub,

5
app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt

@ -21,6 +21,9 @@ data class PollVoteEvent(val statusId: String, val poll: Poll) : Event
data class DomainMuteEvent(val instance: String) : Event
data class AnnouncementReadEvent(val announcementId: String) : Event
data class FilterUpdatedEvent(val filterContext: List<String>) : Event
data class NewNotificationsEvent(val accountId: String, val notifications: List<Notification>) : Event
data class NewNotificationsEvent(
val accountId: String,
val notifications: List<Notification>
) : Event
data class ConversationsLoadingEvent(val accountId: String) : Event
data class NotificationsLoadingEvent(val accountId: String) : Event

4
app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt

@ -2,10 +2,10 @@ package com.keylesspalace.tusky.appstore
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.subjects.PublishSubject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
interface Event

100
app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt

@ -267,9 +267,18 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
binding.accountFragmentViewPager.adapter = adapter
binding.accountFragmentViewPager.offscreenPageLimit = 2
val pageTitles = arrayOf(getString(R.string.title_posts), getString(R.string.title_posts_with_replies), getString(R.string.title_posts_pinned), getString(R.string.title_media))
val pageTitles =
arrayOf(
getString(R.string.title_posts),
getString(R.string.title_posts_with_replies),
getString(R.string.title_posts_pinned),
getString(R.string.title_media)
)
TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position ->
TabLayoutMediator(
binding.accountTabLayout,
binding.accountFragmentViewPager
) { tab, position ->
tab.text = pageTitles[position]
}.attach()
@ -301,7 +310,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val right = insets.getInsets(systemBars()).right
val bottom = insets.getInsets(systemBars()).bottom
val left = insets.getInsets(systemBars()).left
binding.accountCoordinatorLayout.updatePadding(right = right, bottom = bottom, left = left)
binding.accountCoordinatorLayout.updatePadding(
right = right,
bottom = bottom,
left = left
)
WindowInsetsCompat.CONSUMED
}
@ -318,7 +331,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation)
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(
this,
appBarElevation
)
toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
binding.accountToolbar.background = toolbarBackground
@ -341,7 +357,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply {
val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(
this,
appBarElevation
).apply {
fillColor = ColorStateList.valueOf(toolbarColor)
elevation = appBarElevation
shapeAppearanceModel = ShapeAppearanceModel.builder()
@ -381,11 +400,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
binding.accountAvatarImageView.visible(scaledAvatarSize > 0)
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f)
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(
1f
)
window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int
val evaluatedToolbarColor = argbEvaluator.evaluate(transparencyPercent, Color.TRANSPARENT, toolbarColor) as Int
val evaluatedToolbarColor = argbEvaluator.evaluate(
transparencyPercent,
Color.TRANSPARENT,
toolbarColor
) as Int
toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor)
@ -407,7 +432,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
when (it) {
is Success -> onAccountChanged(it.data)
is Error -> {
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
Snackbar.make(
binding.accountCoordinatorLayout,
R.string.error_generic,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
}
@ -421,7 +450,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
}
if (it is Error) {
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
Snackbar.make(
binding.accountCoordinatorLayout,
R.string.error_generic,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
}
@ -466,14 +499,22 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val fullUsername = getFullUsername(loadedAccount)
val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.setPrimaryClip(ClipData.newPlainText(null, fullUsername))
Snackbar.make(binding.root, getString(R.string.account_username_copied), Snackbar.LENGTH_SHORT)
Snackbar.make(
binding.root,
getString(R.string.account_username_copied),
Snackbar.LENGTH_SHORT
)
.show()
}
true
}
}
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(
account.emojis,
binding.accountNoteTextView,
animateEmojis
)
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
accountFieldAdapter.fields = account.fields.orEmpty()
@ -503,7 +544,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
val isLight = resources.getBoolean(R.bool.lightNavigationBar)
if (loadedAccount?.bot == true) {
val badgeView = getBadge(getColor(R.color.tusky_grey_50), R.drawable.ic_bot_24dp, getString(R.string.profile_badge_bot_text), isLight)
val badgeView =
getBadge(
getColor(R.color.tusky_grey_50),
R.drawable.ic_bot_24dp,
getString(R.string.profile_badge_bot_text),
isLight
)
binding.accountBadgeContainer.addView(badgeView)
}
@ -873,7 +920,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
} else {
AlertDialog.Builder(this)
.setMessage(getString(R.string.mute_domain_warning, instance))
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) }
.setPositiveButton(
getString(R.string.mute_domain_warning_dialog_ok)
) { _, _ -> viewModel.blockDomain(instance) }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
@ -966,7 +1015,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, url)
sendIntent.type = "text/plain"
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_account_link_to)))
startActivity(
Intent.createChooser(
sendIntent,
resources.getText(R.string.send_account_link_to)
)
)
}
return true
}
@ -978,7 +1032,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(Intent.EXTRA_TEXT, fullUsername)
sendIntent.type = "text/plain"
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_account_username_to)))
startActivity(
Intent.createChooser(
sendIntent,
resources.getText(R.string.send_account_username_to)
)
)
}
return true
}
@ -1009,7 +1068,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
}
R.id.action_report -> {
loadedAccount?.let { loadedAccount ->
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username))
startActivity(
ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username)
)
}
return true
}
@ -1047,7 +1108,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvide
// text color with maximum contrast
val textColor = if (isLight) Color.BLACK else Color.WHITE
// badge color with 50% transparency so it blends in with the theme background
val backgroundColor = Color.argb(128, Color.red(baseColor), Color.green(baseColor), Color.blue(baseColor))
val backgroundColor = Color.argb(
128,
Color.red(baseColor),
Color.green(baseColor),
Color.blue(baseColor)
)
// a color between the text color and the badge color
val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f)

24
app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt

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

6
app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt

@ -33,7 +33,11 @@ class AccountPagerAdapter(
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false)
1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false)
1 -> TimelineFragment.newInstance(
TimelineViewModel.Kind.USER_WITH_REPLIES,
accountId,
false
)
2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
3 -> AccountMediaFragment.newInstance(accountId)
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")

25
app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt

@ -20,10 +20,10 @@ import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.getDomain
import javax.inject.Inject
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import javax.inject.Inject
class AccountViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
@ -97,7 +97,15 @@ class AccountViewModel @Inject constructor(
mastodonApi.relationships(listOf(accountId))
.fold(
{ relationships ->
relationshipData.postValue(if (relationships.isNotEmpty()) Success(relationships[0]) else Error())
relationshipData.postValue(
if (relationships.isNotEmpty()) {
Success(
relationships[0]
)
} else {
Error()
}
)
},
{ t ->
Log.w(TAG, "failed obtaining relationships", t)
@ -135,8 +143,8 @@ class AccountViewModel @Inject constructor(
fun changeSubscribingState() {
val relationship = relationshipData.value?.data
if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */
relationship?.subscribing == true /* Pleroma */
if (relationship?.notifying == true || // Mastodon 3.3.0rc1
relationship?.subscribing == true // Pleroma
) {
changeRelationship(RelationShipAction.UNSUBSCRIBE)
} else {
@ -315,7 +323,14 @@ class AccountViewModel @Inject constructor(
}
enum class RelationShipAction {
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE
FOLLOW,
UNFOLLOW,
BLOCK,
UNBLOCK,
MUTE,
UNMUTE,
SUBSCRIBE,
UNSUBSCRIBE
}
companion object {

14
app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt

@ -42,12 +42,12 @@ import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
class ListSelectionFragment : DialogFragment(), Injectable {
@ -133,14 +133,22 @@ class ListSelectionFragment : DialogFragment(), Injectable {
viewModel.actionError.collectLatest { error ->
when (error.type) {
ActionError.Type.ADD -> {
Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_LONG)
Snackbar.make(
binding.root,
R.string.failed_to_add_to_list,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_retry) {
viewModel.addAccountToList(accountId!!, error.listId)
}
.show()
}
ActionError.Type.REMOVE -> {
Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_LONG)
Snackbar.make(
binding.root,
R.string.failed_to_remove_from_list,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_retry) {
viewModel.removeAccountFromList(accountId!!, error.listId)
}

2
app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt

@ -24,12 +24,12 @@ import at.connyduck.calladapter.networkresult.onSuccess
import at.connyduck.calladapter.networkresult.runCatching
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.network.MastodonApi
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
data class AccountListState(
val list: MastoList,

28
app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt

@ -49,9 +49,9 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* Fragment with multiple columns of media previews for the specified account.
@ -92,9 +92,13 @@ class AccountMediaFragment :
)
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
val imageSpacing = view.context.resources.getDimensionPixelSize(R.dimen.profile_media_spacing)
val imageSpacing = view.context.resources.getDimensionPixelSize(
R.dimen.profile_media_spacing
)
binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(columnCount, imageSpacing, 0))
binding.recyclerView.addItemDecoration(
GridSpacingItemDecoration(columnCount, imageSpacing, 0)
)
binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount)
binding.recyclerView.adapter = adapter
@ -124,7 +128,11 @@ class AccountMediaFragment :
is LoadState.NotLoading -> {
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
binding.statusView.show()
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
binding.statusView.setup(
R.drawable.elephant_friend_empty,
R.string.message_empty,
null
)
}
}
is LoadState.Error -> {
@ -175,11 +183,19 @@ class AccountMediaFragment :
Attachment.Type.GIFV,
Attachment.Type.VIDEO,
Attachment.Type.AUDIO -> {
val intent = ViewMediaActivity.newIntent(context, attachmentsFromSameStatus, currentIndex)
val intent = ViewMediaActivity.newIntent(
context,
attachmentsFromSameStatus,
currentIndex
)
if (activity != null) {
val url = selected.attachment.url
ViewCompat.setTransitionName(view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
view,
url
)
startActivity(intent, options.toBundle())
} else {
startActivity(intent)

43
app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt

@ -29,25 +29,48 @@ class AccountMediaGridAdapter(
private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit
) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>(
object : DiffUtil.ItemCallback<AttachmentViewData>() {
override fun areItemsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
override fun areItemsTheSame(
oldItem: AttachmentViewData,
newItem: AttachmentViewData
): Boolean {
return oldItem.attachment.id == newItem.attachment.id
}
override fun areContentsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean {
override fun areContentsTheSame(
oldItem: AttachmentViewData,
newItem: AttachmentViewData
): Boolean {
return oldItem == newItem
}
}
) {
private val baseItemBackgroundColor = MaterialColors.getColor(context, com.google.android.material.R.attr.colorSurface, Color.BLACK)
private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator)
private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp)
private val baseItemBackgroundColor = MaterialColors.getColor(
context,
com.google.android.material.R.attr.colorSurface,
Color.BLACK
)
private val videoIndicator = AppCompatResources.getDrawable(
context,
R.drawable.ic_play_indicator
)
private val mediaHiddenDrawable = AppCompatResources.getDrawable(
context,
R.drawable.ic_hide_media_24dp
)
private val itemBgBaseHSV = FloatArray(3)
private val random = Random()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountMediaBinding> {
val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAccountMediaBinding> {
val binding = ItemAccountMediaBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV)
itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f
binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
@ -71,7 +94,11 @@ class AccountMediaGridAdapter(
if (item.attachment.type == Attachment.Type.AUDIO) {
overlay.hide()
imageView.setPadding(context.resources.getDimensionPixelSize(R.dimen.profile_media_audio_icon_padding))
imageView.setPadding(
context.resources.getDimensionPixelSize(
R.dimen.profile_media_audio_icon_padding
)
)
Glide.with(imageView)
.load(R.drawable.ic_music_box_preview_24dp)

25
app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt

@ -59,9 +59,9 @@ import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EndlessOnScrollListener
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import javax.inject.Inject
import kotlinx.coroutines.launch
import retrofit2.Response
import javax.inject.Inject
class AccountListFragment :
Fragment(R.layout.fragment_account_list),
@ -96,7 +96,9 @@ class AccountListFragment :
val layoutManager = LinearLayoutManager(view.context)
binding.recyclerView.layoutManager = layoutManager
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
binding.recyclerView.addItemDecoration(
DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
)
binding.swipeRefreshLayout.setOnRefreshListener { fetchAccounts() }
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
@ -116,7 +118,8 @@ class AccountListFragment :
instanceName = activeAccount.domain,
accountLocked = activeAccount.locked
)
val followRequestsAdapter = FollowRequestsAdapter(this, this, animateAvatar, animateEmojis, showBotOverlay)
val followRequestsAdapter =
FollowRequestsAdapter(this, this, animateAvatar, animateEmojis, showBotOverlay)
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter)
followRequestsAdapter
}
@ -142,7 +145,9 @@ class AccountListFragment :
override fun onViewTag(tag: String) {
(activity as BaseActivity?)
?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
?.startActivityWithSlideInAnimation(
StatusListActivity.newHashtagIntent(requireContext(), tag)
)
}
override fun onViewAccount(id: String) {
@ -225,7 +230,11 @@ class AccountListFragment :
val unblockedUser = blocksAdapter.removeItem(position)
if (unblockedUser != null) {
Snackbar.make(binding.recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG)
Snackbar.make(
binding.recyclerView,
R.string.confirmation_unblocked,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_undo) {
blocksAdapter.addItem(unblockedUser, position)
onBlock(true, id, position)
@ -243,11 +252,7 @@ class AccountListFragment :
Log.e(TAG, "Failed to $verb account accountId $accountId")
}
override fun onRespondToFollowRequest(
accept: Boolean,
accountId: String,
position: Int
) {
override fun onRespondToFollowRequest(accept: Boolean, accountId: String, position: Int) {
if (accept) {
api.authorizeFollowRequest(accountId)
} else {

4
app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt

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

17
app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt

@ -39,16 +39,27 @@ class BlocksAdapter(
) {
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemBlockedUserBinding> {
val binding = ItemBlockedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val binding = ItemBlockedUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemBlockedUserBinding>, position: Int) {
override fun onBindAccountViewHolder(
viewHolder: BindingHolder<ItemBlockedUserBinding>,
position: Int
) {
val account = accountList[position]
val binding = viewHolder.binding
val context = binding.root.context
val emojifiedName = account.name.emojify(account.emojis, binding.blockedUserDisplayName, animateEmojis)
val emojifiedName = account.name.emojify(
account.emojis,
binding.blockedUserDisplayName,
animateEmojis
)
binding.blockedUserDisplayName.text = emojifiedName
val formattedUsername = context.getString(R.string.post_username_format, account.username)
binding.blockedUserUsername.text = formattedUsername

16
app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsHeaderAdapter.kt

@ -27,12 +27,22 @@ class FollowRequestsHeaderAdapter(
private val accountLocked: Boolean
) : RecyclerView.Adapter<BindingHolder<ItemFollowRequestsHeaderBinding>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestsHeaderBinding> {
val binding = ItemFollowRequestsHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowRequestsHeaderBinding> {
val binding = ItemFollowRequestsHeaderBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
override fun onBindViewHolder(viewHolder: BindingHolder<ItemFollowRequestsHeaderBinding>, position: Int) {
override fun onBindViewHolder(
viewHolder: BindingHolder<ItemFollowRequestsHeaderBinding>,
position: Int
) {
viewHolder.binding.root.text = viewHolder.binding.root.context.getString(R.string.follow_requests_info, instanceName)
}

17
app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt

@ -42,18 +42,29 @@ class MutesAdapter(
private val mutingNotificationsMap = HashMap<String, Boolean>()
override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemMutedUserBinding> {
val binding = ItemMutedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val binding = ItemMutedUserBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
override fun onBindAccountViewHolder(viewHolder: BindingHolder<ItemMutedUserBinding>, position: Int) {
override fun onBindAccountViewHolder(
viewHolder: BindingHolder<ItemMutedUserBinding>,
position: Int
) {
val account = accountList[position]
val binding = viewHolder.binding
val context = binding.root.context
val mutingNotifications = mutingNotificationsMap[account.id]
val emojifiedName = account.name.emojify(account.emojis, binding.mutedUserDisplayName, animateEmojis)
val emojifiedName = account.name.emojify(
account.emojis,
binding.mutedUserDisplayName,
animateEmojis
)
binding.mutedUserDisplayName.text = emojifiedName
val formattedUsername = context.getString(R.string.post_username_format, account.username)

25
app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt

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

10
app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt

@ -116,7 +116,10 @@ class AnnouncementsActivity :
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
if (it.data.isNullOrEmpty()) {
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_announcements)
binding.errorMessageView.setup(
R.drawable.elephant_friend_empty,
R.string.no_announcements
)
binding.errorMessageView.show()
} else {
binding.errorMessageView.hide()
@ -129,7 +132,10 @@ class AnnouncementsActivity :
is Error -> {
binding.progressBar.hide()
binding.swipeRefreshLayout.isRefreshing = false
binding.errorMessageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
binding.errorMessageView.setup(
R.drawable.errorphant_error,
R.string.error_generic
) {
refreshAnnouncements()
}
binding.errorMessageView.show()

6
app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt

@ -31,8 +31,8 @@ import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
class AnnouncementsViewModel @Inject constructor(
private val instanceInfoRepo: InstanceInfoRepository,
@ -64,7 +64,9 @@ class AnnouncementsViewModel @Inject constructor(
mastodonApi.dismissAnnouncement(announcement.id)
.fold(
{
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
eventHub.dispatch(
AnnouncementReadEvent(announcement.id)
)
},
{ throwable ->
Log.d(

282
app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt

@ -115,11 +115,6 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import java.io.File
import java.io.IOException
import java.text.DecimalFormat
@ -127,6 +122,11 @@ import java.util.Locale
import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
class ComposeActivity :
BaseActivity(),
@ -163,14 +163,23 @@ class ComposeActivity :
private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
pickMedia(photoUploadUri!!)
private val takePicture =
registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
if (success) {
pickMedia(photoUploadUri!!)
}
}
}
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) {
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
Toast.makeText(
this,
resources.getQuantityString(
R.plurals.error_upload_max_media_reached,
maxUploadMediaNumber,
maxUploadMediaNumber
),
Toast.LENGTH_SHORT
).show()
} else {
uris.forEach { uri ->
pickMedia(uri)
@ -191,7 +200,8 @@ class ComposeActivity :
uriNew,
size,
itemOld.description,
null, // Intentionally reset focus when cropping
// Intentionally reset focus when cropping
null,
itemOld
)
}
@ -222,7 +232,11 @@ class ComposeActivity :
val mediaAdapter = MediaPreviewAdapter(
this,
onAddCaption = { item ->
CaptionDialog.newInstance(item.localId, item.description, item.uri).show(supportFragmentManager, "caption_dialog")
CaptionDialog.newInstance(
item.localId,
item.description,
item.uri
).show(supportFragmentManager, "caption_dialog")
},
onAddFocus = { item ->
makeFocusDialog(item.focus, item.uri) { newFocus ->
@ -240,7 +254,11 @@ class ComposeActivity :
/* If the composer is started up as a reply to another post, override the "starting" state
* based on what the intent from the reply request passes. */
val composeOptions: ComposeOptions? = IntentCompat.getParcelableExtra(intent, COMPOSE_OPTIONS_EXTRA, ComposeOptions::class.java)
val composeOptions: ComposeOptions? = IntentCompat.getParcelableExtra(
intent,
COMPOSE_OPTIONS_EXTRA,
ComposeOptions::class.java
)
viewModel.setup(composeOptions)
setupButtons()
@ -303,12 +321,20 @@ class ComposeActivity :
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
when (intent.action) {
Intent.ACTION_SEND -> {
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri ->
IntentCompat.getParcelableExtra(
intent,
Intent.EXTRA_STREAM,
Uri::class.java
)?.let { uri ->
pickMedia(uri)
}
}
Intent.ACTION_SEND_MULTIPLE -> {
IntentCompat.getParcelableArrayListExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.forEach { uri ->
IntentCompat.getParcelableArrayListExtra(
intent,
Intent.EXTRA_STREAM,
Uri::class.java
)?.forEach { uri ->
pickMedia(uri)
}
}
@ -328,7 +354,13 @@ class ComposeActivity :
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
val left = min(start, end)
val right = max(start, end)
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
binding.composeEditField.text.replace(
left,
right,
shareBody,
0,
shareBody.length
)
// move edittext cursor to first when shareBody parsed
binding.composeEditField.text.insert(0, "\n")
binding.composeEditField.setSelection(0)
@ -341,23 +373,48 @@ class ComposeActivity :
if (replyingStatusAuthor != null) {
binding.composeReplyView.show()
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 }
val arrowDownIcon = IconicsDrawable(
this,
GoogleMaterial.Icon.gmd_arrow_drop_down
).apply {
sizeDp = 12
}
setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
arrowDownIcon,
null
)
binding.composeReplyView.setOnClickListener {
TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup)
TransitionManager.beginDelayedTransition(
binding.composeReplyContentView.parent as ViewGroup
)
if (binding.composeReplyContentView.isVisible) {
binding.composeReplyContentView.hide()
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
arrowDownIcon,
null
)
} else {
binding.composeReplyContentView.show()
val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 }
val arrowUpIcon = IconicsDrawable(
this,
GoogleMaterial.Icon.gmd_arrow_drop_up
).apply { sizeDp = 12 }
setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null)
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(
null,
null,
arrowUpIcon,
null
)
}
}
}
@ -374,7 +431,12 @@ class ComposeActivity :
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
binding.composeEditField.setOnReceiveContentListener(this)
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
binding.composeEditField.setOnKeyListener { _, keyCode, event ->
this.onKeyDown(
keyCode,
event
)
}
binding.composeEditField.setAdapter(
ComposeAutoCompleteAdapter(
@ -419,7 +481,9 @@ class ComposeActivity :
}
lifecycleScope.launch {
viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive ->
viewModel.showContentWarning.combine(
viewModel.markMediaAsSensitive
) { showContentWarning, markSensitive ->
updateSensitiveMediaToggle(markSensitive, showContentWarning)
showContentWarning(showContentWarning)
}.collect()
@ -434,7 +498,10 @@ class ComposeActivity :
mediaAdapter.submitList(media)
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value, viewModel.showContentWarning.value)
updateSensitiveMediaToggle(
viewModel.markMediaAsSensitive.value,
viewModel.showContentWarning.value
)
}
}
@ -510,16 +577,42 @@ class ComposeActivity :
val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 }
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null)
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply {
colorInt = textColor
sizeDp = 18
}
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(
cameraIcon,
null,
null,
null
)
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 }
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null)
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply {
colorInt = textColor
sizeDp = 18
}
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(
imageIcon,
null,
null,
null
)
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 }
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null)
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply {
colorInt = textColor
sizeDp = 18
}
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(
pollIcon,
null,
null,
null
)
binding.actionPhotoTake.visible(Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null)
binding.actionPhotoTake.visible(
Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null
)
binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
@ -549,7 +642,12 @@ class ComposeActivity :
private fun setupLanguageSpinner(initialLanguages: List<String>) {
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
override fun onItemSelected(
parent: AdapterView<*>,
view: View?,
position: Int,
id: Long
) {
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
}
@ -594,8 +692,12 @@ class ComposeActivity :
private fun replaceTextAtCaret(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
val start = binding.composeEditField.selectionStart.coerceAtMost(
binding.composeEditField.selectionEnd
)
val end = binding.composeEditField.selectionStart.coerceAtLeast(
binding.composeEditField.selectionEnd
)
val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) {
" $text"
} else {
@ -609,8 +711,12 @@ class ComposeActivity :
fun prependSelectedWordsWith(text: CharSequence) {
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
val start = binding.composeEditField.selectionStart.coerceAtMost(
binding.composeEditField.selectionEnd
)
val end = binding.composeEditField.selectionStart.coerceAtLeast(
binding.composeEditField.selectionEnd
)
val editorText = binding.composeEditField.text
if (start == end) {
@ -678,7 +784,10 @@ class ComposeActivity :
this.viewModel.toggleMarkSensitive()
}
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
private fun updateSensitiveMediaToggle(
markMediaSensitive: Boolean,
contentWarningShown: Boolean
) {
if (viewModel.media.value.isEmpty()) {
binding.composeHideMediaButton.hide()
binding.descriptionMissingWarningButton.hide()
@ -695,7 +804,10 @@ class ComposeActivity :
getColor(R.color.tusky_blue)
} else {
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
MaterialColors.getColor(binding.composeHideMediaButton, android.R.attr.textColorTertiary)
MaterialColors.getColor(
binding.composeHideMediaButton,
android.R.attr.textColorTertiary
)
}
}
binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
@ -717,7 +829,10 @@ class ComposeActivity :
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
} else {
@ColorInt val color = if (binding.composeScheduleView.time == null) {
MaterialColors.getColor(binding.composeScheduleButton, android.R.attr.textColorTertiary)
MaterialColors.getColor(
binding.composeScheduleButton,
android.R.attr.textColorTertiary
)
} else {
getColor(R.color.tusky_blue)
}
@ -748,7 +863,11 @@ class ComposeActivity :
binding.composeToggleVisibilityButton.setImageResource(iconRes)
if (viewModel.editing) {
// Can't update visibility on published status
enableButton(binding.composeToggleVisibilityButton, clickable = false, colorActive = false)
enableButton(
binding.composeToggleVisibilityButton,
clickable = false,
colorActive = false
)
}
}
@ -785,7 +904,11 @@ class ComposeActivity :
private fun showEmojis() {
binding.emojiView.adapter?.let {
if (it.itemCount == 0) {
val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain)
val errorMessage =
getString(
R.string.error_no_custom_emojis,
accountManager.activeAccount!!.domain
)
displayTransientMessage(errorMessage)
} else {
if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
@ -852,9 +975,14 @@ class ComposeActivity :
private fun setupPollView() {
val margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
val marginBottom = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
val marginBottom = resources.getDimensionPixelSize(
R.dimen.compose_media_preview_margin_bottom
)
val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
val layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
layoutParams.setMargins(margin, margin, margin, marginBottom)
binding.pollPreview.layoutParams = layoutParams
@ -905,7 +1033,10 @@ class ComposeActivity :
val textColor = if (remainingLength < 0) {
getColor(R.color.tusky_red)
} else {
MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary)
MaterialColors.getColor(
binding.composeCharactersLeftView,
android.R.attr.textColorTertiary
)
}
binding.composeCharactersLeftView.setTextColor(textColor)
}
@ -917,7 +1048,9 @@ class ComposeActivity :
}
private fun verifyScheduledTime(): Boolean {
return binding.composeScheduleView.verifyScheduledTime(binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value))
return binding.composeScheduleView.verifyScheduledTime(
binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value)
)
}
private fun onSendClicked() {
@ -967,7 +1100,11 @@ class ComposeActivity :
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
@ -1042,14 +1179,20 @@ class ComposeActivity :
val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
// "Authority" must be the same as the android:authorities string in AndroidManifest.xml
val uriNew = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile)
val uriNew = FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID + ".fileprovider",
tempFile
)
viewModel.cropImageItemOld = item
cropImage.launch(
options(uri = item.uri) {
setOutputUri(uriNew)
setOutputCompressFormat(if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG)
setOutputCompressFormat(
if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG
)
}
)
}
@ -1087,7 +1230,9 @@ class ComposeActivity :
val formattedSize = decimalFormat.format(allowedSizeInMb)
getString(R.string.error_multimedia_size_limit, formattedSize)
}
is VideoOrImageException -> getString(R.string.error_media_upload_image_or_video)
is VideoOrImageException -> getString(
R.string.error_media_upload_image_or_video
)
else -> getString(R.string.error_media_upload_opening)
}
displayTransientMessage(errorString)
@ -1096,16 +1241,23 @@ class ComposeActivity :
}
private fun showContentWarning(show: Boolean) {
TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup)
TransitionManager.beginDelayedTransition(
binding.composeContentWarningBar.parent as ViewGroup
)
@ColorInt val color = if (show) {
binding.composeContentWarningBar.show()
binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length)
binding.composeContentWarningField.setSelection(
binding.composeContentWarningField.text.length
)
binding.composeContentWarningField.requestFocus()
getColor(R.color.tusky_blue)
} else {
binding.composeContentWarningBar.hide()
binding.composeEditField.requestFocus()
MaterialColors.getColor(binding.composeContentWarningButton, android.R.attr.textColorTertiary)
MaterialColors.getColor(
binding.composeContentWarningButton,
android.R.attr.textColorTertiary
)
}
binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
}
@ -1159,7 +1311,10 @@ class ComposeActivity :
/**
* User is editing a new post, and can either save the changes as a draft or discard them.
*/
private fun getSaveAsDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
private fun getSaveAsDraftOrDiscardDialog(
contentText: String,
contentWarning: String
): AlertDialog.Builder {
val warning = if (viewModel.media.value.isNotEmpty()) {
R.string.compose_save_draft_loses_media
} else {
@ -1182,7 +1337,10 @@ class ComposeActivity :
* User is editing an existing draft, and can either update the draft with the new changes or
* discard them.
*/
private fun getUpdateDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
private fun getUpdateDraftOrDiscardDialog(
contentText: String,
contentWarning: String
): AlertDialog.Builder {
val warning = if (viewModel.media.value.isNotEmpty()) {
R.string.compose_save_draft_loses_media
} else {
@ -1286,10 +1444,15 @@ class ComposeActivity :
val state: State
) {
enum class Type {
IMAGE, VIDEO, AUDIO;
IMAGE,
VIDEO,
AUDIO
}
enum class State {
UPLOADING, UNPROCESSED, PROCESSED, PUBLISHED
UPLOADING,
UNPROCESSED,
PROCESSED,
PUBLISHED
}
}
@ -1370,10 +1533,7 @@ class ComposeActivity :
* @return an Intent to start the ComposeActivity
*/
@JvmStatic
fun startIntent(
context: Context,
options: ComposeOptions
): Intent {
fun startIntent(context: Context, options: ComposeOptions): Intent {
return Intent(context, ComposeActivity::class.java).apply {
putExtra(COMPOSE_OPTIONS_EXTRA, options)
}

4
app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt

@ -108,7 +108,9 @@ class ComposeAutoCompleteAdapter(
val account = accountResult.account
binding.username.text = context.getString(R.string.post_username_format, account.username)
binding.displayName.text = account.name.emojify(account.emojis, binding.displayName, animateEmojis)
val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp)
val avatarRadius = context.resources.getDimensionPixelSize(
R.dimen.avatar_radius_42dp
)
loadAvatar(
account.avatar,
binding.avatar,

46
app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt

@ -38,6 +38,7 @@ import com.keylesspalace.tusky.service.MediaToSend
import com.keylesspalace.tusky.service.ServiceClient
import com.keylesspalace.tusky.service.StatusToSend
import com.keylesspalace.tusky.util.randomAlphanumericString
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
@ -50,7 +51,6 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
class ComposeViewModel @Inject constructor(
private val api: MastodonApi,
@ -85,13 +85,19 @@ class ComposeViewModel @Inject constructor(
val markMediaAsSensitive: MutableStateFlow<Boolean> =
MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
val statusVisibility: MutableStateFlow<Status.Visibility> = MutableStateFlow(Status.Visibility.UNKNOWN)
val statusVisibility: MutableStateFlow<Status.Visibility> =
MutableStateFlow(Status.Visibility.UNKNOWN)
val showContentWarning: MutableStateFlow<Boolean> = MutableStateFlow(false)
val poll: MutableStateFlow<NewPoll?> = MutableStateFlow(null)
val scheduledAt: MutableStateFlow<String?> = MutableStateFlow(null)
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
val uploadError = MutableSharedFlow<Throwable>(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
val uploadError =
MutableSharedFlow<Throwable>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
private lateinit var composeKind: ComposeKind
@ -100,7 +106,13 @@ class ComposeViewModel @Inject constructor(
private var setupComplete = false
suspend fun pickMedia(mediaUri: Uri, description: String? = null, focus: Attachment.Focus? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
suspend fun pickMedia(
mediaUri: Uri,
description: String? = null,
focus: Attachment.Focus? = null
): Result<QueuedMedia> = withContext(
Dispatchers.IO
) {
try {
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first())
val mediaItems = media.value
@ -164,7 +176,11 @@ class ComposeViewModel @Inject constructor(
item.copy(
id = event.mediaId,
uploadPercent = -1,
state = if (event.processed) { QueuedMedia.State.PROCESSED } else { QueuedMedia.State.UNPROCESSED }
state = if (event.processed) {
QueuedMedia.State.PROCESSED
} else {
QueuedMedia.State.UNPROCESSED
}
)
is UploadEvent.ErrorEvent -> {
media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } }
@ -186,7 +202,13 @@ class ComposeViewModel @Inject constructor(
return mediaItem
}
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?, focus: Attachment.Focus?) {
private fun addUploadedMedia(
id: String,
type: QueuedMedia.Type,
uri: Uri,
description: String?,
focus: Attachment.Focus?
) {
media.update { mediaList ->
val mediaItem = QueuedMedia(
localId = mediaUploader.getNewLocalMediaId(),
@ -305,11 +327,7 @@ class ComposeViewModel @Inject constructor(
* Send status to the server.
* Uses current state plus provided arguments.
*/
suspend fun sendStatus(
content: String,
spoilerText: String,
accountId: Long
) {
suspend fun sendStatus(content: String, spoilerText: String, accountId: Long) {
if (!scheduledTootId.isNullOrEmpty()) {
api.deleteScheduledStatus(scheduledTootId!!)
}
@ -382,7 +400,11 @@ class ComposeViewModel @Inject constructor(
})
}
'#' -> {
return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
return api.searchSync(
query = token,
type = SearchType.Hashtag.apiParameter,
limit = 10
)
.fold({ searchResult ->
searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) }
}, { e ->

8
app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt

@ -54,10 +54,10 @@ fun downsizeImage(
// Get EXIF data, for orientation info.
val orientation = getImageOrientation(uri, contentResolver)
/* Unfortunately, there isn't a determined worst case compression ratio for image
* formats. So, the only way to tell if they're too big is to compress them and
* test, and keep trying at smaller sizes. The initial estimate should be good for
* many cases, so it should only iterate once, but the loop is used to be absolutely
* sure it gets downsized to below the limit. */
* formats. So, the only way to tell if they're too big is to compress them and
* test, and keep trying at smaller sizes. The initial estimate should be good for
* many cases, so it should only iterate once, but the loop is used to be absolutely
* sure it gets downsized to below the limit. */
var scaledImageSize = 1024
do {
val outputStream = try {

10
app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt

@ -113,11 +113,17 @@ class MediaPreviewAdapter(
private val differ = AsyncListDiffer(
this,
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
override fun areItemsTheSame(
oldItem: ComposeActivity.QueuedMedia,
newItem: ComposeActivity.QueuedMedia
): Boolean {
return oldItem.localId == newItem.localId
}
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
override fun areContentsTheSame(
oldItem: ComposeActivity.QueuedMedia,
newItem: ComposeActivity.QueuedMedia
): Boolean {
return oldItem == newItem
}
}

31
app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt

@ -37,6 +37,13 @@ import com.keylesspalace.tusky.util.getImageSquarePixels
import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.getServerErrorMessage
import com.keylesspalace.tusky.util.randomAlphanumericString
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -54,19 +61,15 @@ import kotlinx.coroutines.flow.shareIn
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import retrofit2.HttpException
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.IOException
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
sealed interface FinalUploadEvent
sealed class UploadEvent {
data class ProgressEvent(val percentage: Int) : UploadEvent()
data class FinishedEvent(val mediaId: String, val processed: Boolean) : UploadEvent(), FinalUploadEvent
data class FinishedEvent(
val mediaId: String,
val processed: Boolean
) : UploadEvent(), FinalUploadEvent
data class ErrorEvent(val error: Throwable) : UploadEvent(), FinalUploadEvent
}
@ -80,11 +83,7 @@ fun createNewImageFile(context: Context, suffix: String = ".jpg"): File {
val randomId = randomAlphanumericString(12)
val imageFileName = "Tusky_${randomId}_"
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(
imageFileName, /* prefix */
suffix, /* suffix */
storageDir /* directory */
)
return File.createTempFile(imageFileName, suffix, storageDir)
}
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
@ -256,9 +255,9 @@ class MediaUploader @Inject constructor(
// .m4a files. See https://github.com/tuskyapp/Tusky/issues/3189 for details.
// Sniff the content of the file to determine the actual type.
if (mimeType != null && (
mimeType.startsWith("audio/", ignoreCase = true) ||
mimeType.startsWith("video/", ignoreCase = true)
)
mimeType.startsWith("audio/", ignoreCase = true) ||
mimeType.startsWith("video/", ignoreCase = true)
)
) {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, media.uri)

12
app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt

@ -60,7 +60,9 @@ fun showAddPollDialog(
binding.pollChoices.adapter = adapter
var durations = context.resources.getIntArray(R.array.poll_duration_values).toList()
val durationLabels = context.resources.getStringArray(R.array.poll_duration_names).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration }
val durationLabels = context.resources.getStringArray(
R.array.poll_duration_names
).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration }
binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply {
setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item)
}
@ -75,8 +77,8 @@ fun showAddPollDialog(
}
}
val DAY_SECONDS = 60 * 60 * 24
val desiredDuration = poll?.expiresIn ?: DAY_SECONDS
val secondsInADay = 60 * 60 * 24
val desiredDuration = poll?.expiresIn ?: secondsInADay
val pollDurationId = durations.indexOfLast {
it <= desiredDuration
}
@ -105,5 +107,7 @@ fun showAddPollDialog(
dialog.show()
// make the dialog focusable so the keyboard does not stay behind it
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
dialog.window?.clearFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
)
}

11
app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt

@ -41,8 +41,15 @@ class AddPollOptionsAdapter(
notifyItemInserted(options.size - 1)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAddPollOptionBinding> {
val binding = ItemAddPollOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemAddPollOptionBinding> {
val binding = ItemAddPollOptionBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
val holder = BindingHolder(binding)
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))

19
app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt

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

15
app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt

@ -49,11 +49,22 @@ fun <T> T.makeFocusDialog(
.load(previewUri)
.downsample(DownsampleStrategy.CENTER_INSIDE)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(p0: GlideException?, p1: Any?, p2: Target<Drawable?>, p3: Boolean): Boolean {
override fun onLoadFailed(
p0: GlideException?,
p1: Any?,
p2: Target<Drawable?>,
p3: Boolean
): Boolean {
return false
}
override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable?>?, dataSource: DataSource, isFirstResource: Boolean): Boolean {
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable?>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
val width = resource.intrinsicWidth
val height = resource.intrinsicHeight

5
app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt

@ -21,7 +21,10 @@ import android.widget.RadioGroup
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Status
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(context, attrs) {
class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup(
context,
attrs
) {
var listener: ComposeOptionsListener? = null

3
app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt

@ -223,7 +223,8 @@ class ComposeScheduleView
}
companion object {
var MINIMUM_SCHEDULED_SECONDS = 330 // Minimum is 5 minutes, pad 30 seconds for posting
// Minimum is 5 minutes, pad 30 seconds for posting
private const val MINIMUM_SCHEDULED_SECONDS = 330
fun calendar(): Calendar = Calendar.getInstance(TimeZone.getDefault())
}
}

12
app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt

@ -68,7 +68,9 @@ class FocusIndicatorView
return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1
}
@SuppressLint("ClickableViewAccessibility") // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget.
@SuppressLint(
"ClickableViewAccessibility"
) // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget.
override fun onTouchEvent(event: MotionEvent): Boolean {
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
return false
@ -112,7 +114,13 @@ class FocusIndicatorView
curtainPath.reset() // Draw a flood fill with a hole cut out of it
curtainPath.fillType = Path.FillType.WINDING
curtainPath.addRect(0.0f, 0.0f, this.width.toFloat(), this.height.toFloat(), Path.Direction.CW)
curtainPath.addRect(
0.0f,
0.0f,
this.width.toFloat(),
this.height.toFloat(),
Path.Direction.CW
)
curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW)
canvas.drawPath(curtainPath, curtainPaint)

5
app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt

@ -60,7 +60,10 @@ class TootButton
Status.Visibility.PRIVATE,
Status.Visibility.DIRECT -> {
setText(R.string.action_send)
IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).apply { sizeDp = 18; colorInt = Color.WHITE }
IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).apply {
sizeDp = 18
colorInt = Color.WHITE
}
}
else -> {
null

19
app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt

@ -38,7 +38,9 @@ class ConversationAdapter(
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
val view = LayoutInflater.from(
parent.context
).inflate(R.layout.item_conversation, parent, false)
return ConversationViewHolder(view, statusDisplayOptions, listener)
}
@ -58,15 +60,24 @@ class ConversationAdapter(
companion object {
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
override fun areItemsTheSame(
oldItem: ConversationViewData,
newItem: ConversationViewData
): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
override fun areContentsTheSame(
oldItem: ConversationViewData,
newItem: ConversationViewData
): Boolean {
return false // Items are different always. It allows to refresh timestamp on every view holder update
}
override fun getChangePayload(oldItem: ConversationViewData, newItem: ConversationViewData): Any? {
override fun getChangePayload(
oldItem: ConversationViewData,
newItem: ConversationViewData
): Any? {
return if (oldItem == newItem) {
// If items are equal - update timestamp only
listOf(StatusBaseViewHolder.Key.KEY_CREATED)

46
app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt

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

11
app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt

@ -27,7 +27,10 @@ class ConversationLoadStateAdapter(
private val retryCallback: () -> Unit
) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() {
override fun onBindViewHolder(holder: BindingHolder<ItemNetworkStateBinding>, loadState: LoadState) {
override fun onBindViewHolder(
holder: BindingHolder<ItemNetworkStateBinding>,
loadState: LoadState
) {
val binding = holder.binding
binding.progressBar.visible(loadState == LoadState.Loading)
binding.retryButton.visible(loadState is LoadState.Error)
@ -47,7 +50,11 @@ class ConversationLoadStateAdapter(
parent: ViewGroup,
loadState: LoadState
): BindingHolder<ItemNetworkStateBinding> {
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val binding = ItemNetworkStateBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
}

32
app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt

@ -63,12 +63,12 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
class ConversationsFragment :
SFragment(),
@ -91,7 +91,11 @@ class ConversationsFragment :
private var hideFab = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_timeline, container, false)
}
@ -141,13 +145,19 @@ class ConversationsFragment :
is LoadState.NotLoading -> {
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
binding.statusView.show()
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
binding.statusView.setup(
R.drawable.elephant_friend_empty,
R.string.message_empty,
null
)
binding.statusView.showHelp(R.string.help_empty_conversations)
}
}
is LoadState.Error -> {
binding.statusView.show()
binding.statusView.setup((loadState.refresh as LoadState.Error).error) { refreshContent() }
binding.statusView.setup(
(loadState.refresh as LoadState.Error).error
) { refreshContent() }
}
is LoadState.Loading -> {
binding.progressBar.show()
@ -240,7 +250,9 @@ class ConversationsFragment :
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.layoutManager = LinearLayoutManager(context)
binding.recyclerView.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
binding.recyclerView.addItemDecoration(
DividerItemDecoration(context, DividerItemDecoration.VERTICAL)
)
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
@ -298,7 +310,11 @@ class ConversationsFragment :
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
adapter.peek(position)?.let { conversation ->
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view)
viewMedia(
attachmentIndex,
AttachmentViewData.list(conversation.lastStatus.status),
view
)
}
}

5
app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt

@ -38,7 +38,10 @@ class ConversationsRemoteMediator(
}
try {
val conversationsResponse = api.getConversations(maxId = nextKey, limit = state.config.pageSize)
val conversationsResponse = api.getConversations(
maxId = nextKey,
limit = state.config.pageSize
)
val conversations = conversationsResponse.body()
if (!conversationsResponse.isSuccessful || conversations == null) {

8
app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt

@ -29,9 +29,9 @@ import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases
import com.keylesspalace.tusky.util.EmptyPagingSource
import javax.inject.Inject
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
class ConversationsViewModel @Inject constructor(
private val timelineCases: TimelineCases,
@ -91,7 +91,11 @@ class ConversationsViewModel @Inject constructor(
fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) {
viewModelScope.launch {
timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices)
timelineCases.voteInPoll(
conversation.lastStatus.id,
conversation.lastStatus.status.poll?.id!!,
choices
)
.fold({ poll ->
val newConversation = conversation.toEntity(
accountId = accountManager.activeAccount!!.id,

11
app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksAdapter.kt

@ -11,8 +11,15 @@ class DomainBlocksAdapter(
private val onUnmute: (String) -> Unit
) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemBlockedDomainBinding> {
val binding = ItemBlockedDomainBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemBlockedDomainBinding> {
val binding = ItemBlockedDomainBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}

10
app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt

@ -18,9 +18,9 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectable {
@ -35,7 +35,9 @@ class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectab
val adapter = DomainBlocksAdapter(viewModel::unblock)
binding.recyclerView.setHasFixedSize(true)
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
binding.recyclerView.addItemDecoration(
DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)
)
binding.recyclerView.adapter = adapter
binding.recyclerView.layoutManager = LinearLayoutManager(view.context)
@ -52,7 +54,9 @@ class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks), Injectab
}
adapter.addLoadStateListener { loadState ->
binding.progressBar.visible(loadState.refresh == LoadState.Loading && adapter.itemCount == 0)
binding.progressBar.visible(
loadState.refresh == LoadState.Loading && adapter.itemCount == 0
)
if (loadState.refresh is LoadState.Error) {
binding.recyclerView.hide()

2
app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksViewModel.kt

@ -8,9 +8,9 @@ import androidx.paging.cachedIn
import at.connyduck.calladapter.networkresult.fold
import at.connyduck.calladapter.networkresult.onFailure
import com.keylesspalace.tusky.R
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class DomainBlocksViewModel @Inject constructor(
private val repo: DomainBlocksRepository

18
app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt

@ -29,18 +29,18 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.copyToFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.buffer
import okio.sink
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.buffer
import okio.sink
class DraftHelper @Inject constructor(
val context: Context,
@ -200,6 +200,10 @@ class DraftHelper @Inject constructor(
} else {
this.copyToFile(contentResolver, file)
}
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
return FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
}
}

9
app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt

@ -35,7 +35,10 @@ class DraftMediaAdapter(
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
override fun areContentsTheSame(
oldItem: DraftAttachment,
newItem: DraftAttachment
): Boolean {
return oldItem == newItem
}
}
@ -75,7 +78,9 @@ class DraftMediaAdapter(
RecyclerView.ViewHolder(imageView) {
init {
val thumbnailViewSize =
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
imageView.context.resources.getDimensionPixelSize(
R.dimen.compose_media_preview_size
)
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)

18
app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt

@ -38,9 +38,9 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
class DraftsActivity : BaseActivity(), DraftActionListener {
@ -74,7 +74,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
binding.draftsRecyclerView.adapter = adapter
binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this)
binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
binding.draftsRecyclerView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
@ -134,10 +136,18 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
if (throwable.isHttpNotFound()) {
// the original status to which a reply was drafted has been deleted
// let's open the ComposeActivity without reply information
Toast.makeText(context, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show()
Toast.makeText(
context,
getString(R.string.drafts_post_reply_removed),
Toast.LENGTH_LONG
).show()
openDraftWithoutReply(draft)
} else {
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
Snackbar.make(
binding.root,
getString(R.string.drafts_failed_loading_reply),
Snackbar.LENGTH_SHORT
)
.show()
}
}

9
app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt

@ -47,7 +47,10 @@ class DraftsAdapter(
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemDraftBinding> {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemDraftBinding> {
val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val viewHolder = BindingHolder(binding)
@ -77,7 +80,9 @@ class DraftsAdapter(
holder.binding.content.text = draft.content
holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty())
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments)
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(
draft.attachments
)
if (draft.poll != null) {
holder.binding.draftPoll.show()

8
app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt

@ -26,8 +26,8 @@ import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
class DraftsViewModel @Inject constructor(
val database: AppDatabase,
@ -38,7 +38,11 @@ class DraftsViewModel @Inject constructor(
val drafts = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { database.draftDao().draftsPagingSource(accountManager.activeAccount?.id!!) }
pagingSourceFactory = {
database.draftDao().draftsPagingSource(
accountManager.activeAccount?.id!!
)
}
).flow
.cachedIn(viewModelScope)

37
app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt

@ -29,9 +29,9 @@ import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.launch
import java.util.Date
import javax.inject.Inject
import kotlinx.coroutines.launch
class EditFilterActivity : BaseActivity() {
@Inject
@ -115,7 +115,12 @@ class EditFilterActivity : BaseActivity() {
)
}
binding.filterDurationSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
viewModel.setDuration(
if (originalFilter?.expiresAt == null) {
position
@ -266,10 +271,16 @@ class EditFilterActivity : BaseActivity() {
if (viewModel.saveChanges(this@EditFilterActivity)) {
finish()
// Possibly affected contexts: any context affected by the original filter OR any context affected by the updated filter
val affectedContexts = viewModel.contexts.value.map { it.kind }.union(originalFilter?.context ?: listOf()).distinct()
val affectedContexts = viewModel.contexts.value.map {
it.kind
}.union(originalFilter?.context ?: listOf()).distinct()
eventHub.dispatch(FilterUpdatedEvent(affectedContexts))
} else {
Snackbar.make(binding.root, "Error saving filter '${viewModel.title.value}'", Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
"Error saving filter '${viewModel.title.value}'",
Snackbar.LENGTH_SHORT
).show()
}
}
}
@ -288,11 +299,19 @@ class EditFilterActivity : BaseActivity() {
finish()
},
{
Snackbar.make(binding.root, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
"Error deleting filter '${filter.title}'",
Snackbar.LENGTH_SHORT
).show()
}
)
} else {
Snackbar.make(binding.root, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
Snackbar.make(
binding.root,
"Error deleting filter '${filter.title}'",
Snackbar.LENGTH_SHORT
).show()
}
}
)
@ -307,7 +326,11 @@ class EditFilterActivity : BaseActivity() {
// but create/edit take a number of seconds (relative to the time the operation is posted)
fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): Int? {
return when (index) {
-1 -> if (default == null) { default } else { ((default.time - System.currentTimeMillis()) / 1000).toInt() }
-1 -> if (default == null) {
default
} else {
((default.time - System.currentTimeMillis()) / 1000).toInt()
}
0 -> null
else -> context?.resources?.getIntArray(R.array.filter_duration_values)?.get(index)
}

25
app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt

@ -9,9 +9,9 @@ import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.FilterKeyword
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.withContext
import javax.inject.Inject
class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub: EventHub) : ViewModel() {
private var originalFilter: Filter? = null
@ -92,7 +92,13 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
}
}
private suspend fun createFilter(title: String, contexts: List<String>, action: String, durationIndex: Int, context: Context): Boolean {
private suspend fun createFilter(
title: String,
contexts: List<String>,
action: String,
durationIndex: Int,
context: Context
): Boolean {
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
api.createFilter(
title = title,
@ -103,7 +109,11 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
{ newFilter ->
// This is _terrible_, but the all-in-one update filter api Just Doesn't Work
return keywords.value.map { keyword ->
api.addFilterKeyword(filterId = newFilter.id, keyword = keyword.keyword, wholeWord = keyword.wholeWord)
api.addFilterKeyword(
filterId = newFilter.id,
keyword = keyword.keyword,
wholeWord = keyword.wholeWord
)
}.none { it.isFailure }
},
{ throwable ->
@ -116,7 +126,14 @@ class EditFilterViewModel @Inject constructor(val api: MastodonApi, val eventHub
)
}
private suspend fun updateFilter(originalFilter: Filter, title: String, contexts: List<String>, action: String, durationIndex: Int, context: Context): Boolean {
private suspend fun updateFilter(
originalFilter: Filter,
title: String,
contexts: List<String>,
action: String,
durationIndex: Int,
context: Context
): Boolean {
val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context)
api.updateFilter(
id = originalFilter.id,

4
app/src/main/java/com/keylesspalace/tusky/components/filters/FilterExtensions.kt

@ -22,7 +22,9 @@ import androidx.appcompat.app.AlertDialog
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.util.await
internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder(this)
internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder(
this
)
.setMessage(getString(R.string.dialog_delete_filter_text, filterTitle))
.setCancelable(true)
.create()

20
app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt

@ -14,8 +14,8 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
class FiltersActivity : BaseActivity(), FiltersListener {
@Inject
@ -54,20 +54,30 @@ class FiltersActivity : BaseActivity(), FiltersListener {
private fun observeViewModel() {
lifecycleScope.launch {
viewModel.state.collect { state ->
binding.progressBar.visible(state.loadingState == FiltersViewModel.LoadingState.LOADING)
binding.progressBar.visible(
state.loadingState == FiltersViewModel.LoadingState.LOADING
)
binding.swipeRefreshLayout.isRefreshing = state.loadingState == FiltersViewModel.LoadingState.LOADING
binding.addFilterButton.visible(state.loadingState == FiltersViewModel.LoadingState.LOADED)
binding.addFilterButton.visible(
state.loadingState == FiltersViewModel.LoadingState.LOADED
)
when (state.loadingState) {
FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide()
FiltersViewModel.LoadingState.ERROR_NETWORK -> {
binding.messageView.setup(R.drawable.errorphant_offline, R.string.error_network) {
binding.messageView.setup(
R.drawable.errorphant_offline,
R.string.error_network
) {
loadFilters()
}
binding.messageView.show()
}
FiltersViewModel.LoadingState.ERROR_OTHER -> {
binding.messageView.setup(R.drawable.errorphant_error, R.string.error_generic) {
binding.messageView.setup(
R.drawable.errorphant_error,
R.string.error_generic
) {
loadFilters()
}
binding.messageView.show()

9
app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt

@ -14,8 +14,13 @@ class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) :
override fun getItemCount(): Int = filters.size
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemRemovableBinding> {
return BindingHolder(ItemRemovableBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemRemovableBinding> {
return BindingHolder(
ItemRemovableBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
override fun onBindViewHolder(holder: BindingHolder<ItemRemovableBinding>, position: Int) {

34
app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt

@ -10,10 +10,10 @@ import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class FiltersViewModel @Inject constructor(
private val api: MastodonApi,
@ -21,7 +21,11 @@ class FiltersViewModel @Inject constructor(
) : ViewModel() {
enum class LoadingState {
INITIAL, LOADING, LOADED, ERROR_NETWORK, ERROR_OTHER
INITIAL,
LOADING,
LOADED,
ERROR_NETWORK,
ERROR_OTHER
}
data class State(val filters: List<Filter>, val loadingState: LoadingState)
@ -61,7 +65,12 @@ class FiltersViewModel @Inject constructor(
viewModelScope.launch {
api.deleteFilter(filter.id).fold(
{
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
this@FiltersViewModel._state.value = State(
this@FiltersViewModel._state.value.filters.filter {
it.id != filter.id
},
LoadingState.LOADED
)
for (context in filter.context) {
eventHub.dispatch(PreferenceChangedEvent(context))
}
@ -70,14 +79,27 @@ class FiltersViewModel @Inject constructor(
if (throwable.isHttpNotFound()) {
api.deleteFilterV1(filter.id).fold(
{
this@FiltersViewModel._state.value = State(this@FiltersViewModel._state.value.filters.filter { it.id != filter.id }, LoadingState.LOADED)
this@FiltersViewModel._state.value = State(
this@FiltersViewModel._state.value.filters.filter {
it.id != filter.id
},
LoadingState.LOADED
)
},
{
Snackbar.make(parent, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
Snackbar.make(
parent,
"Error deleting filter '${filter.title}'",
Snackbar.LENGTH_SHORT
).show()
}
)
} else {
Snackbar.make(parent, "Error deleting filter '${filter.title}'", Snackbar.LENGTH_SHORT).show()
Snackbar.make(
parent,
"Error deleting filter '${filter.title}'",
Snackbar.LENGTH_SHORT
).show()
}
}
)

10
app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt

@ -29,9 +29,9 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
class FollowedTagsActivity :
BaseActivity(),
@ -81,7 +81,9 @@ class FollowedTagsActivity :
binding.followedTagsView.adapter = adapter
binding.followedTagsView.setHasFixedSize(true)
binding.followedTagsView.layoutManager = LinearLayoutManager(this)
binding.followedTagsView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
binding.followedTagsView.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
(binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
val hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
@ -101,7 +103,9 @@ class FollowedTagsActivity :
private fun setupAdapter(): FollowedTagsAdapter {
return FollowedTagsAdapter(this, viewModel).apply {
addLoadStateListener { loadState ->
binding.followedTagsProgressBar.visible(loadState.refresh == LoadState.Loading && itemCount == 0)
binding.followedTagsProgressBar.visible(
loadState.refresh == LoadState.Loading && itemCount == 0
)
if (loadState.refresh is LoadState.Error) {
binding.followedTagsView.hide()

23
app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsAdapter.kt

@ -15,13 +15,22 @@ class FollowedTagsAdapter(
private val actionListener: HashtagActionListener,
private val viewModel: FollowedTagsViewModel
) : PagingDataAdapter<String, BindingHolder<ItemFollowedHashtagBinding>>(STRING_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowedHashtagBinding> =
BindingHolder(ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemFollowedHashtagBinding> = BindingHolder(
ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
override fun onBindViewHolder(holder: BindingHolder<ItemFollowedHashtagBinding>, position: Int) {
override fun onBindViewHolder(
holder: BindingHolder<ItemFollowedHashtagBinding>,
position: Int
) {
viewModel.tags[position].let { tag ->
holder.itemView.findViewById<TextView>(R.id.followed_tag).text = tag.name
holder.itemView.findViewById<ImageButton>(R.id.followed_tag_unfollow).setOnClickListener {
holder.itemView.findViewById<ImageButton>(
R.id.followed_tag_unfollow
).setOnClickListener {
actionListener.unfollow(tag.name, holder.bindingAdapterPosition)
}
}
@ -31,8 +40,10 @@ class FollowedTagsAdapter(
companion object {
val STRING_COMPARATOR = object : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
override fun areItemsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String): Boolean =
oldItem == newItem
}
}
}

10
app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt

@ -35,10 +35,16 @@ class FollowedTagsViewModel @Inject constructor(
}
).flow.cachedIn(viewModelScope)
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
fun searchAutocompleteSuggestions(
token: String
): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
.fold({ searchResult ->
searchResult.hashtags.map { ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult(it.name) }
searchResult.hashtags.map {
ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult(
it.name
)
}
}, { e ->
Log.e(TAG, "Autocomplete search for $token failed.", e)
emptyList()

20
app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt

@ -26,9 +26,9 @@ import com.keylesspalace.tusky.db.InstanceInfoEntity
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject
class InstanceInfoRepository @Inject constructor(
private val api: MastodonApi,
@ -77,7 +77,7 @@ class InstanceInfoRepository @Inject constructor(
maxMediaAttachments = instance.configuration.statuses?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS,
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength,
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength
)
dao.upsert(instanceEntity)
instanceEntity
@ -86,7 +86,11 @@ class InstanceInfoRepository @Inject constructor(
if (throwable.isHttpNotFound()) {
getInstanceInfoV1()
} else {
Log.w(TAG, "failed to instance, falling back to cache and default values", throwable)
Log.w(
TAG,
"failed to instance, falling back to cache and default values",
throwable
)
dao.getInstanceInfo(instanceName)
}
}
@ -105,7 +109,7 @@ class InstanceInfoRepository @Inject constructor(
maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS,
maxFieldNameLength = instanceInfo?.maxFieldNameLength,
maxFieldValueLength = instanceInfo?.maxFieldValueLength,
version = instanceInfo?.version,
version = instanceInfo?.version
)
}
}
@ -129,13 +133,17 @@ class InstanceInfoRepository @Inject constructor(
maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments,
maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields,
maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength,
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength,
maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength
)
dao.upsert(instanceEntity)
instanceEntity
},
{ throwable ->
Log.w(TAG, "failed to instance, falling back to cache and default values", throwable)
Log.w(
TAG,
"failed to instance, falling back to cache and default values",
throwable
)
dao.getInstanceInfo(instanceName)
}
)

8
app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt

@ -42,9 +42,9 @@ import com.keylesspalace.tusky.util.openLinkInCustomTab
import com.keylesspalace.tusky.util.rickRoll
import com.keylesspalace.tusky.util.shouldRickRoll
import com.keylesspalace.tusky.util.viewBinding
import javax.inject.Inject
import kotlinx.coroutines.launch
import okhttp3.HttpUrl
import javax.inject.Inject
/** Main login page, the first thing that users see. Has prompt for instance and login button. */
class LoginActivity : BaseActivity(), Injectable {
@ -201,7 +201,11 @@ class LoginActivity : BaseActivity(), Injectable {
}
}
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String, openInWebView: Boolean) {
private fun redirectUserToAuthorizeAndLogin(
domain: String,
clientId: String,
openInWebView: Boolean
) {
// To authorize this app and log in it's necessary to redirect to the domain given,
// login there, and the server will redirect back to the app with its response.
val uri = HttpUrl.Builder()

2
app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt

@ -45,9 +45,9 @@ import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import javax.inject.Inject
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
/** Contract for starting [LoginWebViewActivity]. */
class OauthLogin : ActivityResultContract<LoginData, LoginResult>() {

14
app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt

@ -21,9 +21,9 @@ import androidx.lifecycle.viewModelScope
import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isHttpNotFound
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
class LoginWebViewViewModel @Inject constructor(
private val api: MastodonApi
@ -48,11 +48,19 @@ class LoginWebViewViewModel @Inject constructor(
instanceRules.value = instance.rules?.map { rule -> rule.text }.orEmpty()
},
{ throwable ->
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable)
Log.w(
"LoginWebViewViewModel",
"failed to load instance info",
throwable
)
}
)
} else {
Log.w("LoginWebViewViewModel", "failed to load instance info", throwable)
Log.w(
"LoginWebViewViewModel",
"failed to load instance info",
throwable
)
}
}
)

34
app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt

@ -14,10 +14,10 @@ import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.isLessThan
import kotlinx.coroutines.delay
import javax.inject.Inject
import kotlin.math.min
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.delay
/** Models next/prev links from the "Links" header in an API response */
data class Links(val next: String?, val prev: String?) {
@ -55,12 +55,16 @@ class NotificationFetcher @Inject constructor(
for (account in accountManager.getAllAccountsOrderedByActive()) {
if (account.notificationsEnabled) {
try {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationManager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
// Create sorted list of new notifications
val notifications = fetchNewNotifications(account)
.filter { filterNotification(notificationManager, account, it) }
.sortedWith(compareBy({ it.id.length }, { it.id })) // oldest notifications first
.sortedWith(
compareBy({ it.id.length }, { it.id })
) // oldest notifications first
.toMutableList()
// TODO do this before filter above? But one could argue that (for example) a tab badge is also a notification
@ -74,13 +78,18 @@ class NotificationFetcher @Inject constructor(
// Err on the side of removing *older* notifications to make room for newer
// notifications.
val currentAndroidNotifications = notificationManager.activeNotifications
.sortedWith(compareBy({ it.tag.length }, { it.tag })) // oldest notifications first
.sortedWith(
compareBy({ it.tag.length }, { it.tag })
) // oldest notifications first
// Check to see if any notifications need to be removed
val toRemove = currentAndroidNotifications.size + notifications.size - MAX_NOTIFICATIONS
if (toRemove > 0) {
// Prefer to cancel old notifications first
currentAndroidNotifications.subList(0, min(toRemove, currentAndroidNotifications.size))
currentAndroidNotifications.subList(
0,
min(toRemove, currentAndroidNotifications.size)
)
.forEach { notificationManager.cancel(it.tag, it.id) }
// Still got notifications to remove? Trim the list of new notifications,
@ -106,7 +115,11 @@ class NotificationFetcher @Inject constructor(
account,
notificationsGroup.value.size == 1
)
notificationManager.notify(notification.id, account.id.toInt(), androidNotification)
notificationManager.notify(
notification.id,
account.id.toInt(),
androidNotification
)
// Android will rate limit / drop notifications if they're posted too
// quickly. There is no indication to the user that this happened.
@ -158,7 +171,14 @@ class NotificationFetcher @Inject constructor(
Log.d(TAG, "getting notification marker for ${account.fullName}")
val remoteMarkerId = fetchMarker(authHeader, account)?.lastReadId ?: "0"
val localMarkerId = account.notificationMarkerId
val markerId = if (remoteMarkerId.isLessThan(localMarkerId)) localMarkerId else remoteMarkerId
val markerId = if (remoteMarkerId.isLessThan(
localMarkerId
)
) {
localMarkerId
} else {
remoteMarkerId
}
val readingPosition = account.lastNotificationId
var minId: String? = if (readingPosition.isLessThan(markerId)) markerId else readingPosition

0
app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt

49
app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt

@ -66,7 +66,9 @@ fun showMigrationNoticeIfNecessary(
Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE)
.setAnchorView(anchorView)
.setAction(R.string.action_details) { showMigrationExplanationDialog(context, accountManager) }
.setAction(
R.string.action_details
) { showMigrationExplanationDialog(context, accountManager) }
.show()
}
@ -75,7 +77,9 @@ private fun showMigrationExplanationDialog(context: Context, accountManager: Acc
if (currentAccountNeedsMigration(accountManager)) {
setMessage(R.string.dialog_push_notification_migration)
setPositiveButton(R.string.title_migration_relogin) { _, _ ->
context.startActivity(LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION))
context.startActivity(
LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION)
)
}
} else {
setMessage(R.string.dialog_push_notification_migration_other_accounts)
@ -89,12 +93,21 @@ private fun showMigrationExplanationDialog(context: Context, accountManager: Acc
}
}
private suspend fun enableUnifiedPushNotificationsForAccount(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
private suspend fun enableUnifiedPushNotificationsForAccount(
context: Context,
api: MastodonApi,
accountManager: AccountManager,
account: AccountEntity
) {
if (isUnifiedPushNotificationEnabledForAccount(account)) {
// Already registered, update the subscription to match notification settings
updateUnifiedPushSubscription(context, api, accountManager, account)
} else {
UnifiedPush.registerAppWithDialog(context, account.id.toString(), features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE))
UnifiedPush.registerAppWithDialog(
context,
account.id.toString(),
features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE)
)
}
}
@ -116,7 +129,11 @@ private fun isUnifiedPushAvailable(context: Context): Boolean =
fun canEnablePushNotifications(context: Context, accountManager: AccountManager): Boolean =
isUnifiedPushAvailable(context) && !anyAccountNeedsMigration(accountManager)
suspend fun enablePushNotificationsWithFallback(context: Context, api: MastodonApi, accountManager: AccountManager) {
suspend fun enablePushNotificationsWithFallback(
context: Context,
api: MastodonApi,
accountManager: AccountManager
) {
if (!canEnablePushNotifications(context, accountManager)) {
// No UP distributors
NotificationHelper.enablePullNotifications(context)
@ -151,9 +168,14 @@ fun disableAllNotifications(context: Context, accountManager: AccountManager) {
private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> =
buildMap {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notificationManager = context.getSystemService(
Context.NOTIFICATION_SERVICE
) as NotificationManager
Notification.Type.visibleTypes.forEach {
put("data[alerts][${it.presentation}]", NotificationHelper.filterNotification(notificationManager, account, it))
put(
"data[alerts][${it.presentation}]",
NotificationHelper.filterNotification(notificationManager, account, it)
)
}
}
@ -196,7 +218,12 @@ suspend fun registerUnifiedPushEndpoint(
}
// Synchronize the enabled / disabled state of notifications with server-side subscription
suspend fun updateUnifiedPushSubscription(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
suspend fun updateUnifiedPushSubscription(
context: Context,
api: MastodonApi,
accountManager: AccountManager,
account: AccountEntity
) {
withContext(Dispatchers.IO) {
api.updatePushNotificationSubscription(
"Bearer ${account.accessToken}",
@ -211,7 +238,11 @@ suspend fun updateUnifiedPushSubscription(context: Context, api: MastodonApi, ac
}
}
suspend fun unregisterUnifiedPushEndpoint(api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
suspend fun unregisterUnifiedPushEndpoint(
api: MastodonApi,
accountManager: AccountManager,
account: AccountEntity
) {
withContext(Dispatchers.IO) {
api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain)
.onFailure { throwable ->

26
app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt

@ -56,10 +56,10 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeRes
import javax.inject.Inject
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import javax.inject.Inject
class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
@ -74,7 +74,11 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var accountPreferenceDataStore: AccountPreferenceDataStore
private val iconSize by unsafeLazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private val iconSize by unsafeLazy {
resources.getDimensionPixelSize(
R.dimen.preference_icon_size
)
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = requireContext()
@ -198,14 +202,17 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
value = visibility.serverString()
setIcon(getIconForVisibility(visibility))
setOnPreferenceChangeListener { _, newValue ->
setIcon(getIconForVisibility(Status.Visibility.byString(newValue as String)))
setIcon(
getIconForVisibility(Status.Visibility.byString(newValue as String))
)
syncWithServer(visibility = newValue)
true
}
}
listPreference {
val locales = getLocaleList(getInitialLanguages(null, accountManager.activeAccount))
val locales =
getLocaleList(getInitialLanguages(null, accountManager.activeAccount))
setTitle(R.string.pref_default_post_language)
// Explicitly add "System default" to the start of the list
entries = (
@ -289,14 +296,21 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
startActivity(intent)
} else {
activity?.let {
val intent = PreferencesActivity.newIntent(it, PreferencesActivity.NOTIFICATION_PREFERENCES)
val intent = PreferencesActivity.newIntent(
it,
PreferencesActivity.NOTIFICATION_PREFERENCES
)
it.startActivity(intent)
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
}
}
private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null, language: String? = null) {
private fun syncWithServer(
visibility: String? = null,
sensitive: Boolean? = null,
language: String? = null
) {
// TODO these could also be "datastore backed" preferences (a ServerPreferenceDataStore); follow-up of issue #3204
mastodonApi.accountUpdateSource(visibility, sensitive, language)

10
app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt

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

10
app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt

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

5
app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt

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

22
app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt

@ -46,7 +46,9 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
val accountId = intent?.getStringExtra(ACCOUNT_ID)
val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME)
if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) {
throw IllegalStateException("accountId ($accountId) or accountUserName ($accountUserName) is null")
throw IllegalStateException(
"accountId ($accountId) or accountUserName ($accountUserName) is null"
)
}
viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID))
@ -130,13 +132,17 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
private const val STATUS_ID = "status_id"
@JvmStatic
fun getIntent(context: Context, accountId: String, userName: String, statusId: String? = null) =
Intent(context, ReportActivity::class.java)
.apply {
putExtra(ACCOUNT_ID, accountId)
putExtra(ACCOUNT_USERNAME, userName)
putExtra(STATUS_ID, statusId)
}
fun getIntent(
context: Context,
accountId: String,
userName: String,
statusId: String? = null
) = Intent(context, ReportActivity::class.java)
.apply {
putExtra(ACCOUNT_ID, accountId)
putExtra(ACCOUNT_USERNAME, userName)
putExtra(STATUS_ID, statusId)
}
}
override fun androidInjector() = androidInjector

9
app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt

@ -37,12 +37,12 @@ import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.toViewData
import javax.inject.Inject
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import javax.inject.Inject
class ReportViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
@ -196,7 +196,12 @@ class ReportViewModel @Inject constructor(
fun doReport() {
reportingStateMutable.value = Loading()
viewModelScope.launch {
mastodonApi.report(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null)
mastodonApi.report(
accountId,
selectedIds.toList(),
reportNote,
if (isRemoteAccount) isRemoteNotify else null
)
.fold({
reportingStateMutable.value = Success(true)
}, { error ->

2
app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt

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

56
app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt

@ -50,7 +50,9 @@ class StatusViewHolder(
private val getStatusForPosition: (Int) -> StatusViewData.Concrete?
) : RecyclerView.ViewHolder(binding.root) {
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(
R.dimen.status_media_preview_height
)
private val statusViewHelper = StatusViewHelper(itemView)
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
@ -93,7 +95,11 @@ class StatusViewHolder(
mediaViewHeight
)
statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions)
statusViewHelper.setupPollReadonly(
viewData.status.poll.toViewData(),
viewData.status.emojis,
statusDisplayOptions
)
setCreatedAt(viewData.status.createdAt)
}
@ -107,11 +113,22 @@ class StatusViewHolder(
)
if (viewdata.status.spoilerText.isBlank()) {
setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
setTextVisible(
true,
viewdata.content,
viewdata.status.mentions,
viewdata.status.tags,
viewdata.status.emojis,
adapterHandler
)
binding.statusContentWarningButton.hide()
binding.statusContentWarningDescription.hide()
} else {
val emojiSpoiler = viewdata.status.spoilerText.emojify(viewdata.status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
val emojiSpoiler = viewdata.status.spoilerText.emojify(
viewdata.status.emojis,
binding.statusContentWarningDescription,
statusDisplayOptions.animateEmojis
)
binding.statusContentWarningDescription.text = emojiSpoiler
binding.statusContentWarningDescription.show()
binding.statusContentWarningButton.show()
@ -121,11 +138,25 @@ class StatusViewHolder(
val contentShown = viewState.isContentShow(viewdata.id, true)
binding.statusContentWarningDescription.invalidate()
viewState.setContentShow(viewdata.id, !contentShown)
setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
setTextVisible(
!contentShown,
viewdata.content,
viewdata.status.mentions,
viewdata.status.tags,
viewdata.status.emojis,
adapterHandler
)
setContentWarningButtonText(!contentShown)
}
}
setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
setTextVisible(
viewState.isContentShow(viewdata.id, true),
viewdata.content,
viewdata.status.mentions,
viewdata.status.tags,
viewdata.status.emojis,
adapterHandler
)
}
}
}
@ -147,7 +178,11 @@ class StatusViewHolder(
listener: LinkListener
) {
if (expanded) {
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
val emojifiedText = content.emojify(
emojis,
binding.statusContent,
statusDisplayOptions.animateEmojis
)
setClickableText(binding.statusContent, emojifiedText, mentions, tags, listener)
} else {
setClickableMentions(binding.statusContent, mentions, listener)
@ -174,7 +209,12 @@ class StatusViewHolder(
}
}
private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) {
private fun setupCollapsedState(
collapsible: Boolean,
collapsed: Boolean,
expanded: Boolean,
spoilerText: String
) {
/* input filter for TextViews have to be set before text */
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
binding.buttonToggleContent.setOnClickListener {

18
app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt

@ -36,7 +36,11 @@ class StatusesAdapter(
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val binding = ItemReportStatusBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return StatusViewHolder(
binding,
statusDisplayOptions,
@ -54,11 +58,15 @@ class StatusesAdapter(
companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
oldItem == newItem
override fun areContentsTheSame(
oldItem: StatusViewData.Concrete,
newItem: StatusViewData.Concrete
): Boolean = oldItem == newItem
override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
oldItem.id == newItem.id
override fun areItemsTheSame(
oldItem: StatusViewData.Concrete,
newItem: StatusViewData.Concrete
): Boolean = oldItem.id == newItem.id
}
}
}

9
app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt

@ -42,7 +42,8 @@ class StatusesPagingSource(
val result = if (params is LoadParams.Refresh && key != null) {
withContext(Dispatchers.IO) {
val initialStatus = async { getSingleStatus(key) }
val additionalStatuses = async { getStatusList(maxId = key, limit = params.loadSize - 1) }
val additionalStatuses =
async { getStatusList(maxId = key, limit = params.loadSize - 1) }
listOf(initialStatus.await()) + additionalStatuses.await()
}
} else {
@ -75,7 +76,11 @@ class StatusesPagingSource(
return mastodonApi.statusObservable(statusId).await()
}
private suspend fun getStatusList(minId: String? = null, maxId: String? = null, limit: Int): List<Status> {
private suspend fun getStatusList(
minId: String? = null,
maxId: String? = null,
limit: Int
): List<Status> {
return mastodonApi.accountStatusesObservable(
accountId = accountId,
maxId = maxId,

6
app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt

@ -95,7 +95,11 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
binding.buttonBack.isEnabled = true
binding.progressBar.hide()
Snackbar.make(binding.buttonBack, if (error is IOException) R.string.error_network else R.string.error_generic, Snackbar.LENGTH_LONG)
Snackbar.make(
binding.buttonBack,
if (error is IOException) R.string.error_network else R.string.error_generic,
Snackbar.LENGTH_LONG
)
.setAction(R.string.action_retry) {
sendReport()
}

24
app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt

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

32
app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt

@ -20,17 +20,35 @@ class StatusViewState {
private val contentShownState = HashMap<String, Boolean>()
private val longContentCollapsedState = HashMap<String, Boolean>()
fun isMediaShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(mediaShownState, id, !isSensitive)
fun isMediaShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(
mediaShownState,
id,
!isSensitive
)
fun setMediaShow(id: String, isShow: Boolean) = setStateEnabled(mediaShownState, id, isShow)
fun isContentShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(contentShownState, id, !isSensitive)
fun isContentShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled(
contentShownState,
id,
!isSensitive
)
fun setContentShow(id: String, isShow: Boolean) = setStateEnabled(contentShownState, id, isShow)
fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(longContentCollapsedState, id, isCollapsed)
fun setCollapsed(id: String, isCollapsed: Boolean) = setStateEnabled(longContentCollapsedState, id, isCollapsed)
fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled(
longContentCollapsedState,
id,
isCollapsed
)
fun setCollapsed(id: String, isCollapsed: Boolean) =
setStateEnabled(longContentCollapsedState, id, isCollapsed)
private fun isStateEnabled(map: Map<String, Boolean>, id: String, def: Boolean): Boolean = map[id]
?: def
private fun isStateEnabled(map: Map<String, Boolean>, id: String, def: Boolean): Boolean =
map[id]
?: def
private fun setStateEnabled(map: MutableMap<String, Boolean>, id: String, state: Boolean) = map.put(id, state)
private fun setStateEnabled(map: MutableMap<String, Boolean>, id: String, state: Boolean) =
map.put(
id,
state
)
}

11
app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt

@ -45,9 +45,9 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import javax.inject.Inject
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
class ScheduledStatusActivity :
BaseActivity(),
@ -109,7 +109,10 @@ class ScheduledStatusActivity :
if (loadState.refresh is LoadState.NotLoading) {
binding.progressBar.hide()
if (adapter.itemCount == 0) {
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_posts)
binding.errorMessageView.setup(
R.drawable.elephant_friend_empty,
R.string.no_scheduled_posts
)
binding.errorMessageView.show()
} else {
binding.errorMessageView.hide()
@ -163,8 +166,8 @@ class ScheduledStatusActivity :
visibility = item.params.visibility,
scheduledAt = item.scheduledAt,
sensitive = item.params.sensitive,
kind = ComposeActivity.ComposeKind.EDIT_SCHEDULED,
),
kind = ComposeActivity.ComposeKind.EDIT_SCHEDULED
)
)
startActivity(intent)
}

21
app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusAdapter.kt

@ -36,18 +36,31 @@ class ScheduledStatusAdapter(
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
override fun areContentsTheSame(
oldItem: ScheduledStatus,
newItem: ScheduledStatus
): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledStatusBinding> {
val binding = ItemScheduledStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): BindingHolder<ItemScheduledStatusBinding> {
val binding = ItemScheduledStatusBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return BindingHolder(binding)
}
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledStatusBinding>, position: Int) {
override fun onBindViewHolder(
holder: BindingHolder<ItemScheduledStatusBinding>,
position: Int
) {
getItem(position)?.let { item ->
holder.binding.edit.isEnabled = true
holder.binding.delete.isEnabled = true

2
app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt

@ -25,8 +25,8 @@ import at.connyduck.calladapter.networkresult.fold
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.entity.ScheduledStatus
import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlinx.coroutines.launch
class ScheduledStatusViewModel @Inject constructor(
val mastodonApi: MastodonApi,

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

Loading…
Cancel
Save