add ktlint plugin to project and apply default code style (#2209)

* add ktlint plugin to project and apply default code style

* some manual adjustments, fix wildcard imports

* update CONTRIBUTING.md

* fix formatting
This commit is contained in:
Konrad Pozniak 2021-06-28 21:13:24 +02:00 committed by GitHub
parent 955267199e
commit 16ffcca748
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
227 changed files with 3933 additions and 3371 deletions

View File

@ -11,17 +11,23 @@
All English text that will be visible to users should be put in ```app/src/main/res/values/strings.xml```. Any text that is missing in a translation will fall back to the version in this file. Be aware that anything added to this file will need to be translated, so be very concise with wording and try to add as few things as possible. Look for existing strings to use first. If there is untranslatable text that you don't want to keep as a string constant in a Java class, you can use the string resource file ```app/src/main/res/values/donottranslate.xml```.
### Translation
Translations are done through https://weblate.tusky.app/projects/tusky/tusky/ .
To add a new language, clic on the 'Start a new translation' button on at the bottom of the page.
Translations are done through our [Weblate](https://weblate.tusky.app/projects/tusky/tusky/).
To add a new language, click on the 'Start a new translation' button on at the bottom of the page.
### Kotlin
This project is in the process of migrating to Kotlin, we prefer new code to be written in Kotlin. We try to follow the [Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html) and make use of the [Kotlin Android Extensions](https://kotlinlang.org/docs/tutorials/android-plugin.html).
This project is in the process of migrating to Kotlin, all new code must be written in Kotlin.
We try to follow the [Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) and make format the code according to the default [ktlint codestyle](https://github.com/pinterest/ktlint).
You can check the codestyle by running `./gradlew ktlintCheck`.
### Java
Existing code in Java should follow the [Android Style Guide](https://source.android.com/source/code-style), which is what Android uses for their own source code. ```@Nullable``` and ```@NotNull``` annotations are really helpful for Kotlin interoperability.
Existing code in Java should follow the [Android Style Guide](https://source.android.com/source/code-style), which is what Android uses for their own source code. ```@Nullable``` and ```@NotNull``` annotations are really helpful for Kotlin interoperability. Please don't submit new features written in Kotlin.
### Viewbinding
We use [Viewbinding](https://developer.android.com/topic/libraries/view-binding) to reference views. No contribution using another mechanism will be accepted.
There are useful extensions in `src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt` that make working with viewbinding easier.
### Visuals
There are three themes in the app, so any visual changes should be checked with each of them to ensure they look appropriate no matter which theme is selected. Usually, you can use existing color attributes like ```?attr/colorPrimary``` and ```?attr/textColorSecondary```. For icons and drawables, use a white drawable and tint it at runtime using ```ThemeUtils``` and specify an attribute that references different colours depending on the theme.
There are three themes in the app, so any visual changes should be checked with each of them to ensure they look appropriate no matter which theme is selected. Usually, you can use existing color attributes like ```?attr/colorPrimary``` and ```?attr/textColorSecondary```.
### Saving
Any time you get a good chunk of work done it's good to make a commit. You can either uses Android Studio's built-in UI for doing this or running the commands:

View File

@ -2,8 +2,8 @@ package com.keylesspalace.tusky
import androidx.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.db.AppDatabase
import org.junit.Assert.assertEquals
import org.junit.Rule
@ -33,12 +33,15 @@ class MigrationsTest {
val active = true
val accountId = "accountId"
val username = "username"
val values = arrayOf(id, domain, token, active, accountId, username, "Display Name",
val values = arrayOf(
id, domain, token, active, accountId, username, "Display Name",
"https://picture.url", true, true, true, true, true, true, true,
true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false,
false, true)
false, true
)
db.execSQL("INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
db.execSQL(
"INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," +
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
@ -46,7 +49,8 @@ class MigrationsTest {
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
"`mediaPreviewEnabled`) " +
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
values)
values
)
db.close()

View File

@ -3,9 +3,13 @@ package com.keylesspalace.tusky
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.db.*
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.components.timeline.TimelineRepository
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.TimelineAccountEntity
import com.keylesspalace.tusky.db.TimelineDao
import com.keylesspalace.tusky.db.TimelineStatusEntity
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
import com.keylesspalace.tusky.entity.Status
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@ -41,8 +45,10 @@ class TimelineDAOTest {
timelineDao.insertInTransaction(status, author, reblogger)
}
val resultsFromDb = timelineDao.getStatusesForAccount(setOne.first.timelineUserId,
maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10)
val resultsFromDb = timelineDao.getStatusesForAccount(
setOne.first.timelineUserId,
maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10
)
.blockingGet()
assertEquals(2, resultsFromDb.size)
@ -71,7 +77,6 @@ class TimelineDAOTest {
assertEquals(author, result.account)
assertEquals(status, result.status)
assertNull(result.reblogAccount)
}
@Test
@ -185,7 +190,6 @@ class TimelineDAOTest {
)
} else null
val even = accountId % 2 == 0L
val status = TimelineStatusEntity(
serverId = statusId.toString(),

View File

@ -2,13 +2,13 @@ package com.keylesspalace.tusky
import android.content.Intent
import android.os.Bundle
import androidx.annotation.StringRes
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.URLSpan
import android.text.util.Linkify
import android.widget.TextView
import androidx.annotation.StringRes
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NoUnderlineURLSpan

View File

@ -62,7 +62,16 @@ import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.AccountPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.DefaultTextWatcher
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewmodel.AccountViewModel
import dagger.android.DispatchingAndroidInjector
@ -233,7 +242,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabSelected(tab: TabLayout.Tab?) {}
})
}
@ -314,7 +322,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0
}
})
}
private fun makeNotificationBarTransparent() {
@ -347,12 +354,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
}
}
viewModel.accountFieldData.observe(this, {
viewModel.accountFieldData.observe(
this,
{
accountFieldAdapter.fields = it
accountFieldAdapter.notifyDataSetChanged()
})
}
)
viewModel.noteSaved.observe(this) {
binding.saveNoteInfo.visible(it, View.INVISIBLE)
}
@ -366,9 +375,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
viewModel.refresh()
adapter.refreshContent()
}
viewModel.isRefreshing.observe(this, { isRefreshing ->
viewModel.isRefreshing.observe(
this,
{ isRefreshing ->
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
})
}
)
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
}
@ -421,7 +433,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.centerCrop()
.into(binding.accountHeaderImageView)
binding.accountAvatarImageView.setOnClickListener { avatarView ->
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
@ -478,7 +489,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
}
}
/**
@ -554,8 +564,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
if(!viewModel.isSelf && followState == FollowState.FOLLOWING
&& (relation.subscribing != null || relation.notifying != null)) {
if (!viewModel.isSelf && followState == FollowState.FOLLOWING &&
(relation.subscribing != null || relation.notifying != null)
) {
binding.accountSubscribeButton.show()
binding.accountSubscribeButton.setOnClickListener {
viewModel.changeSubscribingState()
@ -648,7 +659,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.accountMuteButton.hide()
updateMuteButton()
}
} else {
binding.accountFloatingActionButton.hide()
binding.accountFollowButton.hide()
@ -698,11 +708,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} else {
getString(R.string.action_show_reblogs)
}
} else {
menu.removeItem(R.id.action_show_reblogs)
}
} else {
// It shouldn't be possible to block, mute or report yourself.
menu.removeItem(R.id.action_block)
@ -772,8 +780,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun mention() {
loadedAccount?.let {
val intent = ComposeActivity.startIntent(this,
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username)))
val intent = ComposeActivity.startIntent(
this,
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))
)
startActivity(intent)
}
}
@ -849,5 +859,4 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
return intent
}
}
}

View File

@ -36,7 +36,13 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@ -146,11 +152,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
viewModel.load(listId)
}
if (error is IOException) {
binding.messageView.setup(R.drawable.elephant_offline,
R.string.error_network, retryAction)
binding.messageView.setup(
R.drawable.elephant_offline,
R.string.error_network, retryAction
)
} else {
binding.messageView.setup(R.drawable.elephant_error,
R.string.error_generic, retryAction)
binding.messageView.setup(
R.drawable.elephant_error,
R.string.error_generic, retryAction
)
}
}
@ -203,8 +213,8 @@ class AccountsInListFragment : DialogFragment(), Injectable {
}
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
return oldItem.second == newItem.second
&& oldItem.first.deepEquals(newItem.first)
return oldItem.second == newItem.second &&
oldItem.first.deepEquals(newItem.first)
}
}

View File

@ -60,7 +60,6 @@ abstract class BottomSheetActivity : BaseActivity() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
})
}
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
@ -74,7 +73,8 @@ abstract class BottomSheetActivity : BaseActivity() {
resolve = true
).observeOn(AndroidSchedulers.mainThread())
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ (accounts, statuses) ->
.subscribe(
{ (accounts, statuses) ->
if (getCancelSearchRequested(url)) {
return@subscribe
}
@ -90,12 +90,14 @@ abstract class BottomSheetActivity : BaseActivity() {
}
performUrlFallbackAction(url, lookupFallbackBehavior)
}, {
},
{
if (!getCancelSearchRequested(url)) {
onEndSearch(url)
performUrlFallbackAction(url, lookupFallbackBehavior)
}
})
}
)
onBeginSearch(url)
}
@ -187,7 +189,8 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
if (uri.query != null ||
uri.fragment != null ||
uri.path == null) {
uri.path == null
) {
return false
}

View File

@ -36,18 +36,24 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.canhub.cropper.CropImage
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import com.canhub.cropper.CropImage
import javax.inject.Inject
class EditProfileActivity : BaseActivity(), Injectable {
@ -150,7 +156,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
.load(me.header)
.into(binding.headerPreview)
}
}
}
is Error -> {
@ -159,7 +164,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
viewModel.obtainProfile()
}
snackbar.show()
}
}
}
@ -179,7 +183,9 @@ class EditProfileActivity : BaseActivity(), Injectable {
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
viewModel.saveData.observe(this, {
viewModel.saveData.observe(
this,
{
when (it) {
is Success -> {
finish()
@ -191,8 +197,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
onSaveFailure(it.errorMessage)
}
}
})
}
)
}
override fun onSaveInstanceState(outState: Bundle) {
@ -203,18 +209,24 @@ class EditProfileActivity : BaseActivity(), Injectable {
override fun onStop() {
super.onStop()
if (!isFinishing) {
viewModel.updateProfile(binding.displayNameEditText.text.toString(),
viewModel.updateProfile(
binding.displayNameEditText.text.toString(),
binding.noteEditText.text.toString(),
binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData())
accountFieldEditAdapter.getFieldData()
)
}
}
private fun observeImage(liveData: LiveData<Resource<Bitmap>>,
private fun observeImage(
liveData: LiveData<Resource<Bitmap>>,
imageView: ImageView,
progressBar: View,
roundedCorners: Boolean) {
liveData.observe(this, {
roundedCorners: Boolean
) {
liveData.observe(
this,
{
when (it) {
is Success -> {
@ -242,10 +254,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
onResizeFailure()
it.consumed = true
}
}
}
})
}
)
}
private fun onMediaPick(pickType: PickType) {
@ -261,8 +273,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@ -310,11 +325,13 @@ class EditProfileActivity : BaseActivity(), Injectable {
return
}
viewModel.save(binding.displayNameEditText.text.toString(),
viewModel.save(
binding.displayNameEditText.text.toString(),
binding.noteEditText.text.toString(),
binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData(),
this)
this
)
}
private fun onSaveFailure(msg: String?) {
@ -409,5 +426,4 @@ class EditProfileActivity : BaseActivity(), Injectable {
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
endMediaPicking()
}
}

View File

@ -22,7 +22,6 @@ import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import java.lang.Exception
import javax.inject.Inject
class FiltersActivity : BaseActivity() {
@ -94,8 +93,10 @@ class FiltersActivity: BaseActivity() {
} else {
// Keep the filter, but remove it from this context
val oldFilter = filters[itemIndex]
val newFilter = Filter(oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord)
val newFilter = Filter(
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
)
updateFilter(newFilter, itemIndex)
}
}
@ -143,8 +144,10 @@ class FiltersActivity: BaseActivity() {
.setView(binding.root)
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
val oldFilter = filters[itemIndex]
val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked)
val newFilter = Filter(
oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked
)
updateFilter(newFilter, itemIndex)
}
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
@ -173,11 +176,15 @@ class FiltersActivity: BaseActivity() {
binding.filterProgressBar.hide()
binding.filterMessageView.show()
if (t is IOException) {
binding.filterMessageView.setup(R.drawable.elephant_offline,
R.string.error_network) { loadFilters() }
binding.filterMessageView.setup(
R.drawable.elephant_offline,
R.string.error_network
) { loadFilters() }
} else {
binding.filterMessageView.setup(R.drawable.elephant_error,
R.string.error_generic) { loadFilters() }
binding.filterMessageView.setup(
R.drawable.elephant_error,
R.string.error_generic
) { loadFilters() }
}
return@launch
}

View File

@ -16,9 +16,9 @@
package com.keylesspalace.tusky
import android.os.Bundle
import androidx.annotation.RawRes
import android.util.Log
import android.widget.TextView
import androidx.annotation.RawRes
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
import com.keylesspalace.tusky.util.IOUtils
import java.io.BufferedReader
@ -41,7 +41,6 @@ class LicenseActivity : BaseActivity() {
setTitle(R.string.title_licenses)
loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView)
}
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {

View File

@ -23,25 +23,41 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.PopupMenu
import android.widget.TextView
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
import autodispose2.autoDispose
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewmodel.ListsViewModel
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.*
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_NETWORK
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
@ -84,7 +100,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
binding.listsRecycler.adapter = adapter
binding.listsRecycler.layoutManager = LinearLayoutManager(this)
binding.listsRecycler.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
viewModel.state
.observeOn(AndroidSchedulers.mainThread())
@ -101,9 +118,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
.subscribe { event ->
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
when (event) {
CREATE_ERROR -> showMessage(R.string.error_create_list)
RENAME_ERROR -> showMessage(R.string.error_rename_list)
DELETE_ERROR -> showMessage(R.string.error_delete_list)
Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
Event.RENAME_ERROR -> showMessage(R.string.error_rename_list)
Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
}
}
}
@ -121,7 +138,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
.setView(layout)
.setPositiveButton(
if (list == null) R.string.action_create_list
else R.string.action_rename_list) { _, _ ->
else R.string.action_rename_list
) { _, _ ->
onPickedDialogName(editText.text, list?.id)
}
.setNegativeButton(android.R.string.cancel, null)
@ -145,7 +163,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
.show()
}
private fun update(state: ListsViewModel.State) {
adapter.submitList(state.lists)
binding.progressBar.visible(state.loadingState == LOADING)
@ -166,8 +183,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
LOADED ->
if (state.lists.isEmpty()) {
binding.messageView.show()
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
null)
binding.messageView.setup(
R.drawable.elephant_friend_empty, R.string.message_empty,
null
)
} else {
binding.messageView.hide()
}
@ -182,7 +201,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
private fun onListSelected(listId: String) {
startActivityWithSlideInAnimation(
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId))
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
)
}
private fun openListSettings(list: MastoList) {
@ -219,8 +239,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
}
}
private inner class ListsAdapter
: ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
private inner class ListsAdapter :
ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
@ -238,7 +258,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
holder.nameTextView.text = getItem(position).title
}
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view),
private inner class ListViewHolder(view: View) :
RecyclerView.ViewHolder(view),
View.OnClickListener {
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
val moreButton: ImageButton = view.findViewById(R.id.editListButton)

View File

@ -34,7 +34,11 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.rickRoll
import com.keylesspalace.tusky.util.shouldRickRoll
import com.keylesspalace.tusky.util.viewBinding
import okhttp3.HttpUrl
import retrofit2.Call
import retrofit2.Callback
@ -75,7 +79,8 @@ class LoginActivity : BaseActivity(), Injectable {
}
preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
)
binding.loginButton.setOnClickListener { onButtonClick() }
@ -95,7 +100,6 @@ class LoginActivity : BaseActivity(), Injectable {
} else {
binding.toolbar.visibility = View.GONE
}
}
override fun requiresLogin(): Boolean {
@ -134,8 +138,10 @@ class LoginActivity : BaseActivity(), Injectable {
}
val callback = object : Callback<AppCredentials> {
override fun onResponse(call: Call<AppCredentials>,
response: Response<AppCredentials>) {
override fun onResponse(
call: Call<AppCredentials>,
response: Response<AppCredentials>
) {
if (!response.isSuccessful) {
binding.loginButton.isEnabled = true
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
@ -165,11 +171,12 @@ class LoginActivity : BaseActivity(), Injectable {
}
mastodonApi
.authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri,
OAUTH_SCOPES, getString(R.string.tusky_website))
.authenticateApp(
domain, getString(R.string.app_name), oauthRedirectUri,
OAUTH_SCOPES, getString(R.string.tusky_website)
)
.enqueue(callback)
setLoading(true)
}
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
@ -224,31 +231,27 @@ class LoginActivity : BaseActivity(), Injectable {
} else {
setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s",
getString(R.string.error_retrieving_oauth_token),
response.message()))
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message()))
}
}
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s",
getString(R.string.error_retrieving_oauth_token),
t.message))
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message))
}
}
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
"authorization_code").enqueue(callback)
mastodonApi.fetchOAuthToken(
domain, clientId, clientSecret, redirectUri, code,
"authorization_code"
).enqueue(callback)
} else if (error != null) {
/* Authorization failed. Put the error response where the user can read it and they
* can try again. */
setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
Log.e(TAG, String.format("%s %s",
getString(R.string.error_authorization_denied),
error))
Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
} else {
// This case means a junk response was received somehow.
setLoading(false)

View File

@ -4,9 +4,9 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
@ -48,13 +48,15 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
private const val ARG_ARG = "arg"
@JvmStatic
fun newIntent(context: Context, kind: TimelineViewModel.Kind,
argument: String?): Intent {
fun newIntent(
context: Context,
kind: TimelineViewModel.Kind,
argument: String?
): Intent {
val intent = Intent(context, ModalTimelineActivity::class.java)
intent.putExtra(ARG_KIND, kind)
intent.putExtra(ARG_ARG, argument)
return intent
}
}
}

View File

@ -18,10 +18,9 @@ package com.keylesspalace.tusky
import android.content.Intent
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.components.notifications.NotificationHelper
import javax.inject.Inject
class SplashActivity : AppCompatActivity(), Injectable {
@ -46,5 +45,4 @@ class SplashActivity : AppCompatActivity(), Injectable {
startActivity(intent)
finish()
}
}

View File

@ -19,15 +19,12 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.commit
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.TimelineViewModel.Kind
import javax.inject.Inject
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -60,7 +57,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
val fragment = TimelineFragment.newInstance(kind)
replace(R.id.fragment_container, fragment)
}
}
override fun androidInjector() = dispatchingAndroidInjector
@ -81,5 +77,4 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
}
}
}

View File

@ -20,9 +20,9 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.fragment.NotificationsFragment
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.fragment.NotificationsFragment
/** this would be a good case for a sealed class, but that does not work nice with Room */
@ -34,7 +34,8 @@ const val DIRECT = "Direct"
const val HASHTAG = "Hashtag"
const val LIST = "List"
data class TabData(val id: String,
data class TabData(
val id: String,
@StringRes val text: Int,
@DrawableRes val icon: Int,
val fragment: (List<String>) -> Fragment,

View File

@ -333,7 +333,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
.subscribeOn(Schedulers.io())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe()
}
tabsChanged = true
}
@ -357,5 +356,4 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
private const val MIN_TAB_COUNT = 2
private const val MAX_TAB_COUNT = 5
}
}

View File

@ -61,7 +61,7 @@ import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.util.*
import java.util.Locale
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
@ -102,7 +102,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
// Setup the view pager.
ImagePagerAdapter(this, realAttachs, initialPosition)
} else {
imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
?: throw IllegalArgumentException("attachment list or image url has to be set")
@ -206,8 +205,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(Uri.parse(url))
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES,
getString(R.string.app_name) + "/" + filename)
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_PICTURES,
getString(R.string.app_name) + "/" + filename
)
downloadManager.enqueue(request)
}
@ -261,7 +262,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
}
private var isCreating: Boolean = false
private fun shareImage(directory: File, url: String) {
@ -284,7 +284,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
Log.e(TAG, "Error writing temporary media.")
}
return@fromCallable false
}
.subscribeOn(Schedulers.io())
@ -309,7 +308,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
Log.e(TAG, "Failed to download image", error)
}
)
}
private fun shareMediaFile(directory: File, url: String) {

View File

@ -121,5 +121,4 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
const val VIEW_TYPE_ACCOUNT = 0
const val VIEW_TYPE_FOOTER = 1
}
}

View File

@ -16,16 +16,19 @@
package com.keylesspalace.tusky.adapter
import android.text.method.LinkMovementMethod
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Field
import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.emojify
class AccountFieldAdapter(
private val linkListener: LinkListener,
@ -70,6 +73,5 @@ class AccountFieldAdapter(
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
}
}
}
}

View File

@ -82,9 +82,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
}
class MutableStringPair(var first: String, var second: String)
}

View File

@ -25,7 +25,8 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
@ -48,7 +49,6 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
}
return binding.root

View File

@ -22,7 +22,7 @@ import com.bumptech.glide.Glide
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.util.BindingHolder
import java.util.*
import java.util.Locale
class EmojiAdapter(
emojiList: List<Emoji>,

View File

@ -16,7 +16,6 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.interfaces.AccountActionListener

View File

@ -24,7 +24,10 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.util.visible
class FollowRequestViewHolder(
private val binding: ItemFollowRequestBinding,

View File

@ -16,8 +16,6 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.interfaces.AccountActionListener

View File

@ -34,7 +34,6 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val
}
override fun getItemCount() = if (accountLocked) 0 else 1
}
class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)

View File

@ -15,7 +15,7 @@
package com.keylesspalace.tusky.adapter
import androidx.recyclerview.widget.RecyclerView
import android.view.View
import androidx.recyclerview.widget.RecyclerView
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

View File

@ -13,7 +13,7 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
import java.util.*
import java.util.HashMap
/**
* Displays a list of muted accounts with mute/unmute account and mute/unmute notifications

View File

@ -20,9 +20,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
import com.keylesspalace.tusky.util.visible
class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding,
private val retryCallback: () -> Unit)
: RecyclerView.ViewHolder(binding.root) {
class NetworkStateViewHolder(
private val binding: ItemNetworkStateBinding,
private val retryCallback: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun setUpWithNetworkState(state: LoadState) {
binding.progressBar.visible(state == LoadState.Loading)
@ -38,5 +39,4 @@ class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding,
retryCallback()
}
}
}

View File

@ -46,7 +46,8 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
emojis: List<Emoji>,
mode: Int,
resultClickListener: View.OnClickListener?,
animateEmojis: Boolean) {
animateEmojis: Boolean
) {
this.pollOptions = options
this.voteCount = voteCount
this.votersCount = votersCount
@ -62,7 +63,6 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
.map { pollOptions.indexOf(it) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
@ -114,7 +114,6 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
}
}
}
}
companion object {

View File

@ -60,7 +60,6 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
textView.setOnClickListener(clickListener)
}
}
class PreviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

View File

@ -43,7 +43,8 @@ interface ItemInteractionListener {
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
}
class TabAdapter(private var data: List<TabData>,
class TabAdapter(
private var data: List<TabData>,
private val small: Boolean,
private val listener: ItemInteractionListener,
private var removeButtonEnabled: Boolean = false
@ -77,7 +78,6 @@ class TabAdapter(private var data: List<TabData>,
binding.textView.setOnClickListener {
listener.onTabAdded(tab)
}
} else {
val binding = holder.binding as ItemTabPreferenceBinding
@ -143,7 +143,6 @@ class TabAdapter(private var data: List<TabData>,
binding.actionChip.setOnClickListener {
listener.onActionChipClicked(tab, holder.bindingAdapterPosition)
}
} else {
binding.chipGroup.hide()
}

View File

@ -111,8 +111,8 @@ class ThreadAdapter(
fun getItem(position: Int): StatusViewData.Concrete? = statuses.getOrNull(position)
fun setDetailedStatusPosition(position: Int) {
if (position != detailedStatusPosition
&& detailedStatusPosition != RecyclerView.NO_POSITION
if (position != detailedStatusPosition &&
detailedStatusPosition != RecyclerView.NO_POSITION
) {
val prior = detailedStatusPosition
detailedStatusPosition = position

View File

@ -67,12 +67,12 @@ class AnnouncementAdapter(
}
item.reactions.forEachIndexed { i, reaction ->
(chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
isCheckable = true
checkedIcon = null
chips.addView(this, i)
})
}
.apply {
val emojiText = if (reaction.url == null) {
reaction.name
@ -81,12 +81,14 @@ class AnnouncementAdapter(
}
this.text = ("$emojiText ${reaction.count}")
.emojify(
listOf(Emoji(
listOf(
Emoji(
reaction.name,
reaction.url ?: "",
reaction.staticUrl ?: "",
null
)),
)
),
this,
animateEmojis
)

View File

@ -34,7 +34,12 @@ import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EmojiPicker
import javax.inject.Inject

View File

@ -27,7 +27,12 @@ import com.keylesspalace.tusky.entity.Announcement
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.Success
import io.reactivex.rxjava3.core.Single
import javax.inject.Inject
@ -45,7 +50,8 @@ class AnnouncementsViewModel @Inject constructor(
val emojis: LiveData<List<Emoji>> = emojisMutable
init {
Single.zip(mastodonApi.getCustomEmojis(),
Single.zip(
mastodonApi.getCustomEmojis(),
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
.onErrorResumeNext {
@ -62,22 +68,27 @@ class AnnouncementsViewModel @Inject constructor(
either.asRight().pollLimits?.maxOptionChars,
either.asRight().version
)
})
}
)
.doOnSuccess {
appDatabase.instanceDao().insertOrReplace(it)
}
.subscribe({
.subscribe(
{
emojisMutable.postValue(it.emojiList.orEmpty())
}, {
},
{
Log.w(TAG, "Failed to get custom emojis.", it)
})
}
)
.autoDispose()
}
fun load() {
announcementsMutable.postValue(Loading())
mastodonApi.listAnnouncements()
.subscribe({
.subscribe(
{
announcementsMutable.postValue(Success(it))
it.filter { announcement -> !announcement.read }
.forEach { announcement ->
@ -92,15 +103,18 @@ class AnnouncementsViewModel @Inject constructor(
)
.autoDispose()
}
}, {
},
{
announcementsMutable.postValue(Error(cause = it))
})
}
)
.autoDispose()
}
fun addReaction(announcementId: String, name: String) {
mastodonApi.addAnnouncementReaction(announcementId, name)
.subscribe({
.subscribe(
{
announcementsMutable.postValue(
Success(
announcements.value!!.data!!.map { announcement ->
@ -139,15 +153,18 @@ class AnnouncementsViewModel @Inject constructor(
}
)
)
}, {
},
{
Log.w(TAG, "Failed to add reaction to the announcement.", it)
})
}
)
.autoDispose()
}
fun removeReaction(announcementId: String, name: String) {
mastodonApi.removeAnnouncementReaction(announcementId, name)
.subscribe({
.subscribe(
{
announcementsMutable.postValue(
Success(
announcements.value!!.data!!.map { announcement ->
@ -174,9 +191,11 @@ class AnnouncementsViewModel @Inject constructor(
}
)
)
}, {
},
{
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
})
}
)
.autoDispose()
}

View File

@ -32,7 +32,10 @@ import android.view.KeyEvent
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.ImageButton
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.ColorInt
@ -70,7 +73,20 @@ import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.ComposeTokenizer
import com.keylesspalace.tusky.util.PickMediaFiles
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.afterTextChanged
import com.keylesspalace.tusky.util.combineLiveData
import com.keylesspalace.tusky.util.combineOptionalLiveData
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.highlightSpans
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.util.withLifecycleContext
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
@ -83,7 +99,8 @@ import javax.inject.Inject
import kotlin.math.max
import kotlin.math.min
class ComposeActivity : BaseActivity(),
class ComposeActivity :
BaseActivity(),
ComposeOptionsListener,
ComposeAutoCompleteAdapter.AutocompletionProvider,
OnEmojiSelectedListener,
@ -288,8 +305,9 @@ class ComposeActivity : BaseActivity(),
}
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O ||
Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1
) {
binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
}
}
@ -330,9 +348,9 @@ class ComposeActivity : BaseActivity(),
updateScheduleButton()
}
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
val active = poll == null
&& media!!.size != 4
&& (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
val active = poll == null &&
media!!.size != 4 &&
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
enableButton(binding.composeAddMediaButton, active, active)
enablePollButton(media.isNullOrEmpty())
}.subscribe()
@ -393,7 +411,6 @@ class ComposeActivity : BaseActivity(),
setDisplayShowHomeEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_close_24dp)
}
}
private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) {
@ -409,8 +426,10 @@ class ComposeActivity : BaseActivity(),
avatarSize / 8,
animateAvatars
)
binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description,
activeAccount.fullName)
binding.composeAvatar.contentDescription = getString(
R.string.compose_active_account_description,
activeAccount.fullName
)
}
private fun replaceTextAtCaret(text: CharSequence) {
@ -468,7 +487,6 @@ class ComposeActivity : BaseActivity(),
}
}
private fun atButtonClicked() {
prependSelectedWordsWith("@")
}
@ -502,7 +520,6 @@ class ComposeActivity : BaseActivity(),
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
binding.composeHideMediaButton.isClickable = false
ContextCompat.getColor(this, R.color.transparent_tusky_blue)
} else {
binding.composeHideMediaButton.isClickable = true
if (markMediaSensitive) {
@ -615,9 +632,11 @@ class ComposeActivity : BaseActivity(),
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
addMediaBehavior.removeBottomSheetCallback(this)
if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this@ComposeActivity,
ActivityCompat.requestPermissions(
this@ComposeActivity,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
)
} else {
pickMediaFile.launch(true)
}
@ -633,8 +652,10 @@ class ComposeActivity : BaseActivity(),
private fun openPollDialog() {
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
val instanceParams = viewModel.instanceParams.value!!
showAddPollDialog(this, viewModel.poll.value, instanceParams.pollMaxOptions,
instanceParams.pollMaxLength, viewModel::updatePoll)
showAddPollDialog(
this, viewModel.poll.value, instanceParams.pollMaxOptions,
instanceParams.pollMaxLength, viewModel::updatePoll
)
}
private fun setupPollView() {
@ -755,14 +776,17 @@ class ComposeActivity : BaseActivity(),
if (viewModel.media.value!!.isNotEmpty()) {
finishingUploadDialog = ProgressDialog.show(
this, getString(R.string.dialog_title_finishing_media_upload),
getString(R.string.dialog_message_uploading_media), true, true)
getString(R.string.dialog_message_uploading_media), true, true
)
}
viewModel.sendStatus(contentText, spoilerText).observe(this, {
viewModel.sendStatus(contentText, spoilerText).observe(
this,
{
finishingUploadDialog?.dismiss()
deleteDraftAndFinish()
})
}
)
} else {
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
enableButtons(true)
@ -776,8 +800,10 @@ class ComposeActivity : BaseActivity(),
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
pickMediaFile.launch(true)
} else {
Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission,
Snackbar.LENGTH_SHORT).apply {
Snackbar.make(
binding.activityCompose, R.string.error_media_upload_permission,
Snackbar.LENGTH_SHORT
).apply {
setAction(R.string.action_retry) { onMediaPick() }
// necessary so snackbar is shown over everything
view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
@ -798,24 +824,30 @@ class ComposeActivity : BaseActivity(),
}
// Continue only if the File was successfully created
photoUploadUri = FileProvider.getUriForFile(this,
photoUploadUri = FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID + ".fileprovider",
photoFile)
photoFile
)
takePicture.launch(photoUploadUri)
}
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
button.isEnabled = clickable
ThemeUtils.setDrawableTint(this, button.drawable,
ThemeUtils.setDrawableTint(
this, button.drawable,
if (colorActive) android.R.attr.textColorTertiary
else R.attr.textColorDisabled)
else R.attr.textColorDisabled
)
}
private fun enablePollButton(enable: Boolean) {
binding.addPollTextActionTextView.isEnabled = enable
val textColor = ThemeUtils.getColor(this,
val textColor = ThemeUtils.getColor(
this,
if (enable) android.R.attr.textColorTertiary
else R.attr.textColorDisabled)
else R.attr.textColorDisabled
)
binding.addPollTextActionTextView.setTextColor(textColor)
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
}
@ -847,7 +879,6 @@ class ComposeActivity : BaseActivity(),
}
displayTransientError(errorId)
}
}
}
}
@ -881,7 +912,8 @@ class ComposeActivity : BaseActivity(),
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
) {
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN

View File

@ -81,7 +81,9 @@ class MediaPreviewAdapter(
}
}
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
private val differ = AsyncListDiffer(
this,
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem.localId == newItem.localId
}
@ -89,10 +91,11 @@ class MediaPreviewAdapter(
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem == newItem
}
})
}
)
inner class PreviewViewHolder(val progressImageView: ProgressImageView)
: RecyclerView.ViewHolder(progressImageView) {
inner class PreviewViewHolder(val progressImageView: ProgressImageView) :
RecyclerView.ViewHolder(progressImageView) {
init {
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources

View File

@ -28,7 +28,10 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.ProgressRequestBody
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
import com.keylesspalace.tusky.util.getImageSquarePixels
import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.randomAlphanumericString
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
@ -37,7 +40,7 @@ import okhttp3.MultipartBody
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.*
import java.util.Date
sealed class UploadEvent {
data class ProgressEvent(val percentage: Int) : UploadEvent()
@ -101,12 +104,13 @@ class MediaUploaderImpl(
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
FileOutputStream(file.absoluteFile).use { out ->
input.copyTo(out)
uri = FileProvider.getUriForFile(context,
uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider",
file)
file
)
mediaSize = getMediaSize(contentResolver, uri)
}
}
} catch (e: IOException) {
Log.w(TAG, e)
@ -151,20 +155,22 @@ class MediaUploaderImpl(
var mimeType = contentResolver.getType(media.uri)
val map = MimeTypeMap.getSingleton()
val fileExtension = map.getExtensionFromMimeType(mimeType)
val filename = String.format("%s_%s_%s.%s",
val filename = "%s_%s_%s.%s".format(
context.getString(R.string.app_name),
Date().time.toString(),
randomAlphanumericString(10),
fileExtension)
fileExtension
)
val stream = contentResolver.openInputStream(media.uri)
if (mimeType == null) mimeType = "multipart/form-data"
var lastProgress = -1
val fileBody = ProgressRequestBody(stream, media.mediaSize,
mimeType.toMediaTypeOrNull()) { percentage ->
val fileBody = ProgressRequestBody(
stream, media.mediaSize,
mimeType.toMediaTypeOrNull()
) { percentage ->
if (percentage != lastProgress) {
emitter.onNext(UploadEvent.ProgressEvent(percentage))
}
@ -180,12 +186,15 @@ class MediaUploaderImpl(
}
val uploadDisposable = mastodonApi.uploadMedia(body, description)
.subscribe({ attachment ->
.subscribe(
{ attachment ->
emitter.onNext(UploadEvent.FinishedEvent(attachment))
emitter.onComplete()
}, { e ->
},
{ e ->
emitter.onError(e)
})
}
)
// Cancel the request when our observable is cancelled
emitter.setDisposable(uploadDisposable)
@ -194,15 +203,16 @@ class MediaUploaderImpl(
private fun downsize(media: QueuedMedia): QueuedMedia {
val file = createNewImageFile(context)
DownsizeImageTask.resize(arrayOf(media.uri),
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file)
DownsizeImageTask.resize(
arrayOf(media.uri),
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file
)
return media.copy(uri = file.toUri(), mediaSize = file.length())
}
private fun shouldResizeMedia(media: QueuedMedia): Boolean {
return media.type == QueuedMedia.Type.IMAGE
&& (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT
|| getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
return media.type == QueuedMedia.Type.IMAGE &&
(media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
}
private companion object {
@ -211,6 +221,5 @@ class MediaUploaderImpl(
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
}
}

View File

@ -82,11 +82,13 @@ fun showAddPollDialog(
val pollDuration = context.resources
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
onUpdatePoll(NewPoll(
onUpdatePoll(
NewPoll(
options = adapter.pollOptions,
expiresIn = pollDuration,
multiple = binding.multipleChoicesCheckBox.isChecked
))
)
)
dialog.dismiss()
}

View File

@ -40,7 +40,8 @@ import com.keylesspalace.tusky.util.withLifecycleContext
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
fun <T> T.makeCaptionDialog(existingDescription: String?,
fun <T> T.makeCaptionDialog(
existingDescription: String?,
previewUri: Uri,
onUpdateDescription: (String) -> LiveData<Boolean>
) where T : Activity, T : LifecycleOwner {
@ -60,14 +61,18 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
val input = EditText(this)
input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired,
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT)
input.hint = resources.getQuantityString(
R.plurals.hint_describe_for_visually_impaired,
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT
)
dialogLayout.addView(input)
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
input.setLines(2)
input.inputType = (InputType.TYPE_CLASS_TEXT
input.inputType = (
InputType.TYPE_CLASS_TEXT
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
)
input.setText(existingDescription)
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
@ -76,7 +81,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
withLifecycleContext {
onUpdateDescription(input.text.toString())
.observe { success -> if (!success) showFailedCaptionMessage() }
}
dialog.dismiss()
@ -90,7 +94,8 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
val window = dialog.window
window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
dialog.show()
@ -109,7 +114,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
})
}
private fun Activity.showFailedCaptionMessage() {
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
}

View File

@ -57,12 +57,10 @@ class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: Attr
R.id.directRadioButton
else ->
R.id.directRadioButton
}
check(selectedButton)
}
}
interface ComposeOptionsListener {

View File

@ -16,19 +16,21 @@
package com.keylesspalace.tusky.components.compose.view
import android.content.Context
import androidx.emoji.widget.EmojiEditTextHelper
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
import android.text.InputType
import android.text.method.KeyListener
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.emoji.widget.EmojiEditTextHelper
class EditTextTyped @JvmOverloads constructor(context: Context,
attributeSet: AttributeSet? = null)
: AppCompatMultiAutoCompleteTextView(context, attributeSet) {
class EditTextTyped @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null
) :
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
@ -52,8 +54,13 @@ class EditTextTyped @JvmOverloads constructor(context: Context,
val connection = super.onCreateInputConnection(editorInfo)
return if (onCommitContentListener != null) {
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
getEmojiEditTextHelper().onCreateInputConnection(InputConnectionCompat.createWrapper(connection, editorInfo,
onCommitContentListener!!), editorInfo)!!
getEmojiEditTextHelper().onCreateInputConnection(
InputConnectionCompat.createWrapper(
connection, editorInfo,
onCommitContentListener!!
),
editorInfo
)!!
} else {
connection
}

View File

@ -27,8 +27,9 @@ import com.keylesspalace.tusky.entity.NewPoll
class PollPreviewView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: LinearLayout(context, attrs, defStyleAttr) {
defStyleAttr: Int = 0
) :
LinearLayout(context, attrs, defStyleAttr) {
private val adapter = PreviewPollOptionsAdapter()

View File

@ -68,8 +68,5 @@ class TootButton
}
}
}
}
}

View File

@ -166,7 +166,8 @@ data class ConversationStatusEntity(
pinned = false,
muted = muted,
poll = poll,
card = null)
card = null
)
}
}

View File

@ -37,5 +37,4 @@ class ConversationLoadStateAdapter(
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return NetworkStateViewHolder(binding, retryCallback)
}
}

View File

@ -40,15 +40,16 @@ import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.IOException
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {

View File

@ -34,5 +34,4 @@ class ConversationsRepository @Inject constructor(
}.subscribeOn(Schedulers.io())
.subscribe()
}
}

View File

@ -38,7 +38,6 @@ class DraftMediaAdapter(
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
}
) {
@ -60,8 +59,8 @@ class DraftMediaAdapter(
}
}
inner class DraftMediaViewHolder(val imageView: ImageView)
: RecyclerView.ViewHolder(imageView) {
inner class DraftMediaViewHolder(val imageView: ImageView) :
RecyclerView.ViewHolder(imageView) {
init {
val thumbnailViewSize =
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)

View File

@ -93,7 +93,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
viewModel.getToot(draft.inReplyToId)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this))
.subscribe({ status ->
.subscribe(
{ status ->
val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id,
tootText = draft.content,
@ -110,8 +111,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
startActivity(ComposeActivity.startIntent(this, composeOptions))
}, { throwable ->
},
{ throwable ->
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
@ -126,7 +127,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
.show()
}
})
}
)
} else {
openDraftWithoutReply(draft)
}

View File

@ -33,5 +33,4 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
}
override fun androidInjector() = androidInjector
}

View File

@ -114,7 +114,8 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
api.domainBlocks(id, bottomId)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ response ->
.subscribe(
{ response ->
val instances = response.body()
if (response.isSuccessful && instances != null) {
@ -122,9 +123,11 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
} else {
onFetchInstancesFailure(Exception(response.message()))
}
}, {throwable ->
},
{ throwable ->
onFetchInstancesFailure(throwable)
})
}
)
}
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {

View File

@ -22,7 +22,11 @@ import android.util.Log
import androidx.annotation.DrawableRes
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.AccountListActivity
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.FiltersActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TabPreferenceActivity
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
@ -33,7 +37,12 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.listPreference
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -75,8 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setOnPreferenceClickListener {
val intent = Intent(context, TabPreferenceActivity::class.java)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
@ -88,8 +99,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.MUTES)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
@ -104,8 +117,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
@ -116,8 +131,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setOnPreferenceClickListener {
val intent = Intent(context, InstanceListActivity::class.java)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
@ -201,8 +218,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
preference {
setTitle(R.string.pref_title_public_filter_keywords)
setOnPreferenceClickListener {
launchFilterActivity(Filter.PUBLIC,
R.string.pref_title_public_filter_keywords)
launchFilterActivity(
Filter.PUBLIC,
R.string.pref_title_public_filter_keywords
)
true
}
}
@ -226,8 +245,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
preference {
setTitle(R.string.pref_title_thread_filter_keywords)
setOnPreferenceClickListener {
launchFilterActivity(Filter.THREAD,
R.string.pref_title_thread_filter_keywords)
launchFilterActivity(
Filter.THREAD,
R.string.pref_title_thread_filter_keywords
)
true
}
}
@ -255,7 +276,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
it.startActivity(intent)
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
}
}
@ -289,7 +309,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
Log.e("AccountPreferences", "failed updating settings on server", t)
showErrorSnackbar(visibility, sensitive)
}
})
}

View File

@ -124,8 +124,6 @@ class EmojiPreference(
finishDownload(font, binding)
}
).also { downloadDisposables[font.id] = it }
}
private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
@ -222,12 +220,14 @@ class EmojiPreference(
context,
0x1f973, // This is the codepoint of the party face emoji :D
launchIntent,
PendingIntent.FLAG_CANCEL_CURRENT)
PendingIntent.FLAG_CANCEL_CURRENT
)
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mgr.set(
AlarmManager.RTC,
System.currentTimeMillis() + 100,
mPendingIntent)
mPendingIntent
)
exitProcess(0)
}.show()
}

View File

@ -176,5 +176,4 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
return NotificationPreferencesFragment()
}
}
}

View File

@ -36,7 +36,9 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
class PreferencesActivity :
BaseActivity(),
SharedPreferences.OnSharedPreferenceChangeListener,
HasAndroidInjector {
@Inject
@ -91,7 +93,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
}
restartActivitiesOnExit = intent.getBooleanExtra("restart", false)
}
override fun onResume() {
@ -122,7 +123,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
restartActivitiesOnExit = true
this.restartCurrentActivity()
}
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash", "showCardsInTimelines", "confirmReblogs", "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
@ -179,5 +179,4 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
return intent
}
}
}

View File

@ -22,7 +22,14 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.settings.AppTheme
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.emojiPreference
import com.keylesspalace.tusky.settings.listPreference
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.getNonNullString
@ -122,7 +129,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.pref_title_bot_overlay)
isSingleLineTitle = false
setIcon(R.drawable.ic_bot_24dp)
}
switchPreference {
@ -259,7 +265,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
}
}
override fun onResume() {

View File

@ -50,7 +50,6 @@ class ProxyPreferencesFragment : PreferenceFragmentCompat() {
setSummaryProvider { text }
}
}
}
override fun onPause() {

View File

@ -126,7 +126,6 @@ class ReportViewModel @Inject constructor(
.subscribe(
{ data ->
updateRelationship(data.getOrNull(0))
},
{
updateRelationship(null)
@ -210,7 +209,6 @@ class ReportViewModel @Inject constructor(
}
)
.autoDispose()
}
fun checkClickedUrl(url: String?) {

View File

@ -25,11 +25,18 @@ import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.StatusViewHelper
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.shouldTrimStatus
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.viewdata.toViewData
import java.util.*
import java.util.Date
class StatusViewHolder(
private val binding: ItemReportStatusBinding,
@ -71,9 +78,11 @@ class StatusViewHolder(
val sensitive = status.sensitive
statusViewHelper.setMediasPreview(statusDisplayOptions, status.attachments,
statusViewHelper.setMediasPreview(
statusDisplayOptions, status.attachments,
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
mediaViewHeight)
mediaViewHeight
)
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
setCreatedAt(status.createdAt)
@ -81,8 +90,10 @@ class StatusViewHolder(
private fun updateTextView() {
status()?.let { status ->
setupCollapsedState(shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
viewState.isContentShow(status.id, status.sensitive), status.spoilerText)
setupCollapsedState(
shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
viewState.isContentShow(status.id, status.sensitive), status.spoilerText
)
if (status.spoilerText.isBlank()) {
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler)
@ -116,11 +127,13 @@ class StatusViewHolder(
}
}
private fun setTextVisible(expanded: Boolean,
private fun setTextVisible(
expanded: Boolean,
content: Spanned,
mentions: List<Status.Mention>?,
emojis: List<Emoji>,
listener: LinkListener) {
listener: LinkListener
) {
if (expanded) {
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener)

View File

@ -37,8 +37,10 @@ class StatusesAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return StatusViewHolder(binding, statusDisplayOptions, statusViewState, adapterHandler,
statusForPosition)
return StatusViewHolder(
binding, statusDisplayOptions, statusViewState, adapterHandler,
statusForPosition
)
}
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {

View File

@ -65,7 +65,6 @@ class StatusesPagingSource(
prevKey = result.firstOrNull()?.id,
nextKey = result.lastOrNull()?.id
)
} catch (e: Exception) {
Log.w("StatusesPagingSource", "failed to load statuses", e)
return LoadResult.Error(e)

View File

@ -56,27 +56,29 @@ class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable {
binding.progressMute.hide()
}
binding.buttonMute.setText(when (it.data) {
binding.buttonMute.setText(
when (it.data) {
true -> R.string.action_unmute
else -> R.string.action_mute
})
}
)
}
viewModel.blockState.observe(viewLifecycleOwner) {
if (it !is Loading) {
binding.buttonBlock.show()
binding.progressBlock.show()
}
else {
} else {
binding.buttonBlock.hide()
binding.progressBlock.hide()
}
binding.buttonBlock.setText(when (it.data) {
binding.buttonBlock.setText(
when (it.data) {
true -> R.string.action_unblock
else -> R.string.action_block
})
}
)
}
}
private fun handleClicks() {

View File

@ -67,8 +67,7 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
if (viewModel.isRemoteAccount) {
binding.checkIsNotifyRemote.show()
binding.reportDescriptionRemoteInstance.show()
}
else{
} else {
binding.checkIsNotifyRemote.hide()
binding.reportDescriptionRemoteInstance.hide()
}
@ -84,7 +83,6 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
is Success -> viewModel.navigateTo(Screen.Done)
is Loading -> showLoading()
is Error -> showError(it.cause)
}
}
}

View File

@ -132,9 +132,10 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
}
adapter.addLoadStateListener { loadState ->
if (loadState.refresh is LoadState.Error
|| loadState.append is LoadState.Error
|| loadState.prepend is LoadState.Error) {
if (loadState.refresh is LoadState.Error ||
loadState.append is LoadState.Error ||
loadState.prepend is LoadState.Error
) {
showError()
}

View File

@ -95,12 +95,16 @@ class SearchViewModel @Inject constructor(
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
timelineCases.delete(status.first.id)
.subscribe({
.subscribe(
{
if (loadedStatuses.remove(status))
statusesPagingSourceFactory.invalidate()
}, {
err -> Log.d(TAG, "Failed to delete status", err)
})
},
{
err ->
Log.d(TAG, "Failed to delete status", err)
}
)
.autoDispose()
}

View File

@ -24,8 +24,8 @@ import com.keylesspalace.tusky.adapter.AccountViewHolder
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.LinkListener
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean)
: PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
val view = LayoutInflater.from(parent.context)

View File

@ -24,8 +24,8 @@ import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.BindingHolder
class SearchHashtagsAdapter(private val linkListener: LinkListener)
: PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
class SearchHashtagsAdapter(private val linkListener: LinkListener) :
PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemHashtagBinding> {
val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)

View File

@ -34,5 +34,4 @@ class SearchPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(acti
}
override fun getItemCount() = 3
}

View File

@ -27,7 +27,8 @@ class SearchPagingSource<T: Any>(
private val searchType: SearchType,
private val searchRequest: String,
private val initialItems: List<T>?,
private val parser: (SearchResult) -> List<T>) : PagingSource<Int, T>() {
private val parser: (SearchResult) -> List<T>
) : PagingSource<Int, T>() {
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
return null

View File

@ -29,8 +29,11 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import javax.inject.Inject
abstract class SearchFragment<T: Any> : Fragment(R.layout.fragment_search),
LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener {
abstract class SearchFragment<T : Any> :
Fragment(R.layout.fragment_search),
LinkListener,
Injectable,
SwipeRefreshLayout.OnRefreshListener {
@Inject
lateinit var viewModelFactory: ViewModelFactory

View File

@ -125,13 +125,17 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
when (actionable.attachments[attachmentIndex].type) {
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
val attachments = AttachmentViewData.list(actionable)
val intent = ViewMediaActivity.newIntent(context, attachments,
attachmentIndex)
val intent = ViewMediaActivity.newIntent(
context, attachments,
attachmentIndex
)
if (view != null) {
val url = actionable.attachments[attachmentIndex].url
ViewCompat.setTransitionName(view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(),
view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
view, url
)
startActivity(intent, options.toBundle())
} else {
startActivity(intent)
@ -204,14 +208,17 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
remove(viewModel.activeAccount?.username)
}
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
val intent = ComposeActivity.startIntent(
requireContext(),
ComposeOptions(
inReplyToId = status.actionableId,
replyVisibility = actionableStatus.visibility,
contentWarning = actionableStatus.spoilerText,
mentionedUsernames = mentionedUsernames,
replyingStatusAuthor = actionableStatus.account.localUsername,
replyingStatusContent = actionableStatus.content.toString()
))
)
)
startActivity(intent)
}
@ -275,7 +282,8 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
R.string.action_unmute_conversation
} else {
R.string.action_mute_conversation
})
}
)
}
popup.setOnMenuItemClickListener { item ->
@ -383,11 +391,14 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
}
private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) {
bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener {
bottomSheetActivity?.showAccountChooserDialog(
dialogTitle, false,
object : AccountSelectionListener {
override fun onAccountSelected(account: AccountEntity) {
openAsAccount(statusUrl, account)
}
})
}
)
}
private fun openAsAccount(statusUrl: String, account: AccountEntity) {
@ -448,7 +459,8 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
viewModel.deleteStatus(id)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ deletedStatus ->
.subscribe(
{ deletedStatus ->
removeItem(position)
val redraftStatus = if (deletedStatus.isEmpty()) {
@ -457,7 +469,9 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
deletedStatus
}
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
val intent = ComposeActivity.startIntent(
requireContext(),
ComposeOptions(
tootText = redraftStatus.text ?: "",
inReplyToId = redraftStatus.inReplyToId,
visibility = redraftStatus.visibility,
@ -465,13 +479,15 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
mediaAttachments = redraftStatus.attachments,
sensitive = redraftStatus.sensitive,
poll = redraftStatus.poll?.toNewPoll(status.createdAt)
))
)
)
startActivity(intent)
}, { error ->
},
{ error ->
Log.w("SearchStatusesFragment", "error deleting status", error)
Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show()
})
}
)
}
.setNegativeButton(android.R.string.cancel, null)
.show()

View File

@ -25,10 +25,16 @@ import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.*
import autodispose2.androidx.lifecycle.autoDispose
import com.keylesspalace.tusky.AccountListActivity
import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent
@ -47,7 +53,13 @@ import com.keylesspalace.tusky.interfaces.RefreshableFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.EndlessOnScrollListener
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
@ -56,8 +68,13 @@ import io.reactivex.rxjava3.core.Observable
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable,
ReselectableFragment, RefreshableFragment {
class TimelineFragment :
SFragment(),
OnRefreshListener,
StatusActionListener,
Injectable,
ReselectableFragment,
RefreshableFragment {
@Inject
lateinit var viewModelFactory: ViewModelFactory
@ -161,8 +178,7 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
private fun setupRecyclerView() {
binding.recyclerView.setAccessibilityDelegateCompat(
ListStatusAccessibilityDelegate(binding.recyclerView, this)
{ pos -> viewModel.statuses.getOrNull(pos) }
ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos -> viewModel.statuses.getOrNull(pos) }
)
binding.recyclerView.setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
@ -330,8 +346,10 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
}
override fun onViewAccount(id: String) {
if ((viewModel.kind == TimelineViewModel.Kind.USER ||
viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES) &&
if ((
viewModel.kind == TimelineViewModel.Kind.USER ||
viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES
) &&
viewModel.id == id
) {
/* If already viewing an account page, then any requests to view that account page
@ -505,7 +523,6 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
private const val HASHTAGS_ARG = "hashtags"
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh"
fun newInstance(
kind: TimelineViewModel.Kind,
hashtagOrId: String? = null,
@ -531,7 +548,6 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
return fragment
}
private val diffCallback: DiffUtil.ItemCallback<StatusViewData> =
object : DiffUtil.ItemCallback<StatusViewData>() {
override fun areItemsTheSame(

View File

@ -5,11 +5,19 @@ import androidx.core.text.parseAsHtml
import androidx.core.text.toHtml
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.db.*
import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.components.timeline.TimelineRequestMode.DISK
import com.keylesspalace.tusky.components.timeline.TimelineRequestMode.NETWORK
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.TimelineAccountEntity
import com.keylesspalace.tusky.db.TimelineDao
import com.keylesspalace.tusky.db.TimelineStatusEntity
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.dec
import com.keylesspalace.tusky.util.inc
@ -17,9 +25,8 @@ import com.keylesspalace.tusky.util.trimTrailingWhitespace
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import java.io.IOException
import java.util.*
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
data class Placeholder(val id: String)
@ -31,7 +38,10 @@ enum class TimelineRequestMode {
interface TimelineRepository {
fun getStatuses(
maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
maxId: String?,
sinceId: String?,
sincedIdMinusOne: String?,
limit: Int,
requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>>
@ -52,8 +62,11 @@ class TimelineRepositoryImpl(
}
override fun getStatuses(
maxId: String?, sinceId: String?, sincedIdMinusOne: String?,
limit: Int, requestMode: TimelineRequestMode
maxId: String?,
sinceId: String?,
sincedIdMinusOne: String?,
limit: Int,
requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>> {
val acc = accountManager.activeAccount ?: throw IllegalStateException()
val accountId = acc.id
@ -66,9 +79,12 @@ class TimelineRepositoryImpl(
}
private fun getStatusesFromNetwork(
maxId: String?, sinceId: String?,
sinceIdMinusOne: String?, limit: Int,
accountId: Long, requestMode: TimelineRequestMode
maxId: String?,
sinceId: String?,
sinceIdMinusOne: String?,
limit: Int,
accountId: Long,
requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>> {
return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)
.map { response ->
@ -87,8 +103,11 @@ class TimelineRepositoryImpl(
}
private fun addFromDbIfNeeded(
accountId: Long, statuses: List<Either<Placeholder, Status>>,
maxId: String?, sinceId: String?, limit: Int,
accountId: Long,
statuses: List<Either<Placeholder, Status>>,
maxId: String?,
sinceId: String?,
limit: Int,
requestMode: TimelineRequestMode
): Single<List<TimelineStatus>> {
return if (requestMode != NETWORK && statuses.size < 2) {
@ -113,7 +132,9 @@ class TimelineRepositoryImpl(
}
private fun getStatusesFromDb(
accountId: Long, maxId: String?, sinceId: String?,
accountId: Long,
maxId: String?,
sinceId: String?,
limit: Int
): Single<out List<TimelineStatus>> {
return timelineDao.getStatusesForAccount(accountId, maxId, sinceId, limit)
@ -124,8 +145,10 @@ class TimelineRepositoryImpl(
}
private fun saveStatusesToDb(
accountId: Long, statuses: List<Status>,
maxId: String?, sinceId: String?
accountId: Long,
statuses: List<Status>,
maxId: String?,
sinceId: String?
): List<Either<Placeholder, Status>> {
var placeholderToInsert: Placeholder? = null
@ -347,7 +370,6 @@ fun TimelineAccountEntity.toAccount(gson: Gson): Account {
)
}
fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
return TimelineStatusEntity(
serverId = this.id,

View File

@ -3,7 +3,20 @@ package com.keylesspalace.tusky.components.timeline
import android.content.SharedPreferences
import android.util.Log
import androidx.lifecycle.viewModelScope
import com.keylesspalace.tusky.appstore.*
import com.keylesspalace.tusky.appstore.BlockEvent
import com.keylesspalace.tusky.appstore.BookmarkEvent
import com.keylesspalace.tusky.appstore.DomainMuteEvent
import com.keylesspalace.tusky.appstore.Event
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.FavoriteEvent
import com.keylesspalace.tusky.appstore.MuteConversationEvent
import com.keylesspalace.tusky.appstore.MuteEvent
import com.keylesspalace.tusky.appstore.PinEvent
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.appstore.ReblogEvent
import com.keylesspalace.tusky.appstore.StatusComposedEvent
import com.keylesspalace.tusky.appstore.StatusDeletedEvent
import com.keylesspalace.tusky.appstore.UnfollowEvent
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Poll
@ -12,7 +25,15 @@ import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.HttpHeaderLink
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.dec
import com.keylesspalace.tusky.util.firstIsInstanceOrNull
import com.keylesspalace.tusky.util.inc
import com.keylesspalace.tusky.util.isLessThan
import com.keylesspalace.tusky.util.toViewData
import com.keylesspalace.tusky.viewdata.StatusViewData
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
@ -238,8 +259,8 @@ class TimelineViewModel @Inject constructor(
private fun addStatusesBelow(statuses: MutableList<Either<Placeholder, Status>>) {
val fullFetch = isFullFetch(statuses)
// Remove placeholder in the bottom if it's there
if (this.statuses.isNotEmpty()
&& this.statuses.last() !is StatusViewData.Concrete
if (this.statuses.isNotEmpty() &&
this.statuses.last() !is StatusViewData.Concrete
) {
this.statuses.removeAt(this.statuses.lastIndex)
}
@ -318,7 +339,6 @@ class TimelineViewModel @Inject constructor(
} catch (t: Exception) {
ifExpected(t) {
Log.d(TAG, "Failed to reblog status " + status.id, t)
}
}
}
@ -485,9 +505,9 @@ class TimelineViewModel @Inject constructor(
}
private fun shouldFilterStatus(status: Status): Boolean {
return status.inReplyToId != null && filterRemoveReplies
|| status.reblog != null && filterRemoveReblogs
|| filterModel.shouldFilterStatus(status.actionableStatus)
return status.inReplyToId != null && filterRemoveReplies ||
status.reblog != null && filterRemoveReblogs ||
filterModel.shouldFilterStatus(status.actionableStatus)
}
private fun extractNextId(response: Response<*>): String? {
@ -644,7 +664,8 @@ class TimelineViewModel @Inject constructor(
private fun replacePlaceholderWithStatuses(
newStatuses: MutableList<Either<Placeholder, Status>>,
fullFetch: Boolean, pos: Int
fullFetch: Boolean,
pos: Int
) {
val placeholder = statuses[pos]
if (placeholder is StatusViewData.Placeholder) {
@ -873,9 +894,11 @@ class TimelineViewModel @Inject constructor(
Log.e(TAG, "Failed to fetch filters", t)
return@launch
}
filterModel.initWithFilters(filters.filter {
filterModel.initWithFilters(
filters.filter {
filterContextMatchesKind(kind, it.context)
})
}
)
filterViewData(this@TimelineViewModel.statuses)
}
}
@ -891,7 +914,6 @@ class TimelineViewModel @Inject constructor(
}
}
companion object {
private const val TAG = "TimelineVM"
internal const val LOAD_AT_ONCE = 30

View File

@ -15,7 +15,11 @@
package com.keylesspalace.tusky.db
import androidx.room.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface AccountDao {
@ -27,5 +31,4 @@ interface AccountDao {
@Query("SELECT * FROM AccountEntity ORDER BY id ASC")
fun loadAll(): List<AccountEntity>
}

View File

@ -21,14 +21,20 @@ import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.defaultTabs
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Status
@Entity(indices = [Index(value = ["domain", "accountId"],
unique = true)])
@Entity(
indices = [
Index(
value = ["domain", "accountId"],
unique = true
)
]
)
@TypeConverters(Converters::class)
data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
data class AccountEntity(
@field:PrimaryKey(autoGenerate = true) var id: Long,
val domain: String,
var accessToken: String,
var isActive: Boolean,
@ -56,7 +62,8 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var activeNotifications: String = "[]",
var emojis: List<Emoji> = emptyList(),
var tabPreferences: List<TabData> = defaultTabs(),
var notificationsFilter: String = "[\"follow_request\"]") {
var notificationsFilter: String = "[\"follow_request\"]"
) {
val identifier: String
get() = "$domain:$accountId"

View File

@ -18,7 +18,7 @@ package com.keylesspalace.tusky.db
import android.util.Log
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Status
import java.util.*
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@ -66,7 +66,6 @@ class AccountManager @Inject constructor(db: AppDatabase) {
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
val newAccountId = maxAccountId + 1
activeAccount = AccountEntity(id = newAccountId, domain = domain.lowercase(Locale.ROOT), accessToken = accessToken, isActive = true)
}
/**
@ -79,7 +78,6 @@ class AccountManager @Inject constructor(db: AppDatabase) {
Log.d(TAG, "saveAccount: saving account with id " + account.id)
accountDao.insertOrReplace(account)
}
}
/**
@ -103,9 +101,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
activeAccount = null
}
return activeAccount
}
}
/**
@ -135,7 +131,6 @@ class AccountManager @Inject constructor(db: AppDatabase) {
} else {
accounts.add(it)
}
}
}
@ -194,5 +189,4 @@ class AccountManager @Inject constructor(db: AppDatabase) {
id == accountId
}
}
}

View File

@ -39,5 +39,4 @@ interface ConversationsDao {
@Query("DELETE FROM ConversationEntity WHERE accountId = :accountId")
fun deleteForAccount(accountId: Long)
}

View File

@ -25,11 +25,16 @@ import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
import com.keylesspalace.tusky.createTabDataFromId
import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.trimTrailingWhitespace
import java.net.URLDecoder
import java.net.URLEncoder
import java.util.*
import java.util.ArrayList
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton

View File

@ -38,5 +38,4 @@ interface DraftDao {
@Query("SELECT * FROM DraftEntity WHERE id = :id")
suspend fun find(id: Int): DraftEntity?
}

View File

@ -17,11 +17,11 @@ abstract class TimelineDao {
@Insert(onConflict = REPLACE)
abstract fun insertStatus(timelineAccountEntity: TimelineStatusEntity): Long
@Insert(onConflict = IGNORE)
abstract fun insertStatusIfNotThere(timelineAccountEntity: TimelineStatusEntity): Long
@Query("""
@Query(
"""
SELECT s.serverId, s.url, s.timelineUserId,
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
@ -46,47 +46,62 @@ AND (CASE WHEN :sinceId IS NOT NULL THEN
(LENGTH(s.serverId) > LENGTH(:sinceId) OR LENGTH(s.serverId) == LENGTH(:sinceId) AND s.serverId > :sinceId)
ELSE 1 END)
ORDER BY LENGTH(s.serverId) DESC, s.serverId DESC
LIMIT :limit""")
LIMIT :limit"""
)
abstract fun getStatusesForAccount(account: Long, maxId: String?, sinceId: String?, limit: Int): Single<List<TimelineStatusWithAccount>>
@Transaction
open fun insertInTransaction(status: TimelineStatusEntity, account: TimelineAccountEntity,
reblogAccount: TimelineAccountEntity?) {
open fun insertInTransaction(
status: TimelineStatusEntity,
account: TimelineAccountEntity,
reblogAccount: TimelineAccountEntity?
) {
insertAccount(account)
reblogAccount?.let(this::insertAccount)
insertStatus(status)
}
@Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
@Query(
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
(LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId)
AND
(LENGTH(serverId) > LENGTH(:minId) OR LENGTH(serverId) == LENGTH(:minId) AND serverId > :minId)
""")
"""
)
abstract fun deleteRange(accountId: Long, minId: String, maxId: String)
@Query("""DELETE FROM TimelineStatusEntity WHERE authorServerId = null
@Query(
"""DELETE FROM TimelineStatusEntity WHERE authorServerId = null
AND timelineUserId = :account AND
(LENGTH(serverId) < LENGTH(:maxId) OR LENGTH(serverId) == LENGTH(:maxId) AND serverId < :maxId)
AND
(LENGTH(serverId) > LENGTH(:sinceId) OR LENGTH(serverId) == LENGTH(:sinceId) AND serverId > :sinceId)
""")
"""
)
abstract fun removeAllPlaceholdersBetween(account: Long, maxId: String, sinceId: String)
@Query("""UPDATE TimelineStatusEntity SET favourited = :favourited
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""")
@Query(
"""UPDATE TimelineStatusEntity SET favourited = :favourited
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract fun setFavourited(accountId: Long, statusId: String, favourited: Boolean)
@Query("""UPDATE TimelineStatusEntity SET bookmarked = :bookmarked
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""")
@Query(
"""UPDATE TimelineStatusEntity SET bookmarked = :bookmarked
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract fun setBookmarked(accountId: Long, statusId: String, bookmarked: Boolean)
@Query("""UPDATE TimelineStatusEntity SET reblogged = :reblogged
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""")
@Query(
"""UPDATE TimelineStatusEntity SET reblogged = :reblogged
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract fun setReblogged(accountId: Long, statusId: String, reblogged: Boolean)
@Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
(authorServerId = :userId OR reblogAccountId = :userId)""")
@Query(
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND
(authorServerId = :userId OR reblogAccountId = :userId)"""
)
abstract fun removeAllByUser(accountId: Long, userId: String)
@Query("DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId")
@ -95,14 +110,18 @@ WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId =
@Query("DELETE FROM TimelineAccountEntity WHERE timelineUserId = :accountId")
abstract fun removeAllUsersForAccount(accountId: Long)
@Query("""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId
AND serverId = :statusId""")
@Query(
"""DELETE FROM TimelineStatusEntity WHERE timelineUserId = :accountId
AND serverId = :statusId"""
)
abstract fun delete(accountId: Long, statusId: String)
@Query("""DELETE FROM TimelineStatusEntity WHERE createdAt < :olderThan""")
abstract fun cleanup(olderThan: Long)
@Query("""UPDATE TimelineStatusEntity SET poll = :poll
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)""")
@Query(
"""UPDATE TimelineStatusEntity SET poll = :poll
WHERE timelineUserId = :accountId AND (serverId = :statusId OR reblogServerId = :statusId)"""
)
abstract fun setVoted(accountId: Long, statusId: String, poll: String)
}

View File

@ -1,6 +1,10 @@
package com.keylesspalace.tusky.db
import androidx.room.*
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.TypeConverters
import com.keylesspalace.tusky.entity.Status
/**
@ -16,13 +20,15 @@ import com.keylesspalace.tusky.entity.Status
*/
@Entity(
primaryKeys = ["serverId", "timelineUserId"],
foreignKeys = ([
foreignKeys = (
[
ForeignKey(
entity = TimelineAccountEntity::class,
parentColumns = ["serverId", "timelineUserId"],
childColumns = ["authorServerId", "timelineUserId"]
)
]),
]
),
// Avoiding rescanning status table when accounts table changes. Recommended by Room(c).
indices = [Index("authorServerId", "timelineUserId")]
)
@ -70,7 +76,6 @@ data class TimelineAccountEntity(
val bot: Boolean
)
class TimelineStatusWithAccount {
@Embedded
lateinit var status: TimelineStatusEntity

View File

@ -15,7 +15,23 @@
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.AboutActivity
import com.keylesspalace.tusky.AccountActivity
import com.keylesspalace.tusky.AccountListActivity
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.EditProfileActivity
import com.keylesspalace.tusky.FiltersActivity
import com.keylesspalace.tusky.LicenseActivity
import com.keylesspalace.tusky.ListsActivity
import com.keylesspalace.tusky.LoginActivity
import com.keylesspalace.tusky.MainActivity
import com.keylesspalace.tusky.ModalTimelineActivity
import com.keylesspalace.tusky.SplashActivity
import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.TabPreferenceActivity
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.ViewThreadActivity
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.drafts.DraftsActivity

View File

@ -21,13 +21,13 @@ import dagger.Component
import dagger.android.support.AndroidSupportInjectionModule
import javax.inject.Singleton
/**
* Created by charlag on 3/21/18.
*/
@Singleton
@Component(modules = [
@Component(
modules = [
AppModule::class,
NetworkModule::class,
AndroidSupportInjectionModule::class,
@ -37,7 +37,8 @@ import javax.inject.Singleton
ViewModelModule::class,
RepositoryModule::class,
MediaUploaderModule::class
])
]
)
interface AppComponent {
@Component.Builder
interface Builder {

View File

@ -58,7 +58,6 @@ object AppInjector {
override fun onActivityStopped(activity: Activity) {
}
})
}
@ -74,7 +73,9 @@ object AppInjector {
AndroidSupportInjection.inject(f)
}
}
}, true)
},
true
)
}
}
}

View File

@ -13,7 +13,6 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di
import android.app.Application
@ -60,8 +59,10 @@ class AppModule {
}
@Provides
fun providesTimelineUseCases(api: MastodonApi,
eventHub: EventHub): TimelineCases {
fun providesTimelineUseCases(
api: MastodonApi,
eventHub: EventHub
): TimelineCases {
return TimelineCasesImpl(api, eventHub)
}
@ -75,7 +76,8 @@ class AppModule {
return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB")
.addTypeConverter(converters)
.allowMainThreadQueries()
.addMigrations(AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5,
.addMigrations(
AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5,
AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8,
AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11,
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
@ -92,5 +94,4 @@ class AppModule {
@Provides
@Singleton
fun notifier(context: Context): Notifier = SystemNotifier(context)
}

View File

@ -16,8 +16,8 @@
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
import dagger.Module
import dagger.android.ContributesAndroidInjector

View File

@ -13,23 +13,25 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.AccountsInListFragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
import com.keylesspalace.tusky.fragment.*
import com.keylesspalace.tusky.components.preference.AccountPreferencesFragment
import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment
import com.keylesspalace.tusky.components.preference.PreferencesFragment
import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment
import com.keylesspalace.tusky.components.preference.PreferencesFragment
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.fragment.AccountListFragment
import com.keylesspalace.tusky.fragment.AccountMediaFragment
import com.keylesspalace.tusky.fragment.NotificationsFragment
import com.keylesspalace.tusky.fragment.ViewThreadFragment
import dagger.Module
import dagger.android.ContributesAndroidInjector
@ -89,5 +91,4 @@ abstract class FragmentBuildersModule {
@ContributesAndroidInjector
abstract fun preferencesFragment(): PreferencesFragment
}

View File

@ -13,7 +13,6 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di
/**

View File

@ -112,7 +112,6 @@ class NetworkModule {
.addConverterFactory(GsonConverterFactory.create(gson))
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
.build()
}
@Provides

View File

@ -1,11 +1,11 @@
package com.keylesspalace.tusky.di
import com.google.gson.Gson
import com.keylesspalace.tusky.components.timeline.TimelineRepository
import com.keylesspalace.tusky.components.timeline.TimelineRepositoryImpl
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.components.timeline.TimelineRepository
import com.keylesspalace.tusky.components.timeline.TimelineRepositoryImpl
import dagger.Module
import dagger.Provides

View File

@ -11,7 +11,6 @@ import com.keylesspalace.tusky.components.drafts.DraftsViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
import com.keylesspalace.tusky.components.search.SearchViewModel
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.viewmodel.AccountViewModel
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
@ -63,7 +62,6 @@ abstract class ViewModelModule {
@ViewModelKey(ListsViewModel::class)
internal abstract fun listsViewModel(viewModel: ListsViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(AccountsInListViewModel::class)

View File

@ -57,22 +57,22 @@ data class Account(
}
fun deepEquals(other: Account): Boolean {
return id == other.id
&& localUsername == other.localUsername
&& displayName == other.displayName
&& note == other.note
&& url == other.url
&& avatar == other.avatar
&& header == other.header
&& locked == other.locked
&& followersCount == other.followersCount
&& followingCount == other.followingCount
&& statusesCount == other.statusesCount
&& source == other.source
&& bot == other.bot
&& emojis == other.emojis
&& fields == other.fields
&& moved == other.moved
return id == other.id &&
localUsername == other.localUsername &&
displayName == other.displayName &&
note == other.note &&
url == other.url &&
avatar == other.avatar &&
header == other.header &&
locked == other.locked &&
followersCount == other.followersCount &&
followingCount == other.followingCount &&
statusesCount == other.statusesCount &&
source == other.source &&
bot == other.bot &&
emojis == other.emojis &&
fields == other.fields &&
moved == other.moved
}
fun isRemote(): Boolean = this.username != this.localUsername

View File

@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity
import android.text.Spanned
import com.google.gson.annotations.SerializedName
import java.util.*
import java.util.Date
data class Announcement(
val id: String,

View File

@ -16,7 +16,8 @@
package com.keylesspalace.tusky.entity
import com.google.gson.annotations.SerializedName
import java.util.*
import java.util.ArrayList
import java.util.Date
data class DeletedStatus(
var text: String?,

View File

@ -45,4 +45,3 @@ data class Filter (
return filter?.id.equals(id)
}
}

View File

@ -1,7 +1,7 @@
package com.keylesspalace.tusky.entity
import com.google.gson.annotations.SerializedName
import java.util.*
import java.util.Date
/**
* API type for saving the scroll position of a timeline.

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