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