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```. 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 ### Translation
Translations are done through https://weblate.tusky.app/projects/tusky/tusky/ . Translations are done through our [Weblate](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. To add a new language, click on the 'Start a new translation' button on at the bottom of the page.
### Kotlin ### 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 ### 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 ### 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 ### 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: 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.room.testing.MigrationTestHelper
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
@ -33,12 +33,15 @@ class MigrationsTest {
val active = true val active = true
val accountId = "accountId" val accountId = "accountId"
val username = "username" 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, "https://picture.url", true, true, true, true, true, true, true,
true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false, 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`," + "`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," +
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," + "`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," + "`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
@ -46,7 +49,8 @@ class MigrationsTest {
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," + "`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
"`mediaPreviewEnabled`) " + "`mediaPreviewEnabled`) " +
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", "VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
values) values
)
db.close() db.close()

View file

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

View file

@ -2,13 +2,13 @@ package com.keylesspalace.tusky
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.annotation.StringRes
import android.text.SpannableString import android.text.SpannableString
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.text.style.URLSpan import android.text.style.URLSpan
import android.text.util.Linkify import android.text.util.Linkify
import android.widget.TextView import android.widget.TextView
import androidx.annotation.StringRes
import com.keylesspalace.tusky.databinding.ActivityAboutBinding import com.keylesspalace.tusky.databinding.ActivityAboutBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NoUnderlineURLSpan 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.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.AccountPagerAdapter import com.keylesspalace.tusky.pager.AccountPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys 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.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel
import dagger.android.DispatchingAndroidInjector import dagger.android.DispatchingAndroidInjector
@ -233,7 +242,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
override fun onTabUnselected(tab: TabLayout.Tab?) {} override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabSelected(tab: TabLayout.Tab?) {} override fun onTabSelected(tab: TabLayout.Tab?) {}
}) })
} }
@ -314,7 +322,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0 binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0
} }
}) })
} }
private fun makeNotificationBarTransparent() { private fun makeNotificationBarTransparent() {
@ -347,12 +354,14 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.setAction(R.string.action_retry) { viewModel.refresh() } .setAction(R.string.action_retry) { viewModel.refresh() }
.show() .show()
} }
} }
viewModel.accountFieldData.observe(this, { viewModel.accountFieldData.observe(
this,
{
accountFieldAdapter.fields = it accountFieldAdapter.fields = it
accountFieldAdapter.notifyDataSetChanged() accountFieldAdapter.notifyDataSetChanged()
}) }
)
viewModel.noteSaved.observe(this) { viewModel.noteSaved.observe(this) {
binding.saveNoteInfo.visible(it, View.INVISIBLE) binding.saveNoteInfo.visible(it, View.INVISIBLE)
} }
@ -366,9 +375,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
viewModel.refresh() viewModel.refresh()
adapter.refreshContent() adapter.refreshContent()
} }
viewModel.isRefreshing.observe(this, { isRefreshing -> viewModel.isRefreshing.observe(
this,
{ isRefreshing ->
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
}) }
)
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
} }
@ -421,7 +433,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
.centerCrop() .centerCrop()
.into(binding.accountHeaderImageView) .into(binding.accountHeaderImageView)
binding.accountAvatarImageView.setOnClickListener { avatarView -> binding.accountAvatarImageView.setOnClickListener { avatarView ->
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar) val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
@ -478,7 +489,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null) 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 // 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 // it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
if(!viewModel.isSelf && followState == FollowState.FOLLOWING if (!viewModel.isSelf && followState == FollowState.FOLLOWING &&
&& (relation.subscribing != null || relation.notifying != null)) { (relation.subscribing != null || relation.notifying != null)
) {
binding.accountSubscribeButton.show() binding.accountSubscribeButton.show()
binding.accountSubscribeButton.setOnClickListener { binding.accountSubscribeButton.setOnClickListener {
viewModel.changeSubscribingState() viewModel.changeSubscribingState()
@ -648,7 +659,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.accountMuteButton.hide() binding.accountMuteButton.hide()
updateMuteButton() updateMuteButton()
} }
} else { } else {
binding.accountFloatingActionButton.hide() binding.accountFloatingActionButton.hide()
binding.accountFollowButton.hide() binding.accountFollowButton.hide()
@ -698,11 +708,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} else { } else {
getString(R.string.action_show_reblogs) getString(R.string.action_show_reblogs)
} }
} else { } else {
menu.removeItem(R.id.action_show_reblogs) menu.removeItem(R.id.action_show_reblogs)
} }
} else { } else {
// It shouldn't be possible to block, mute or report yourself. // It shouldn't be possible to block, mute or report yourself.
menu.removeItem(R.id.action_block) menu.removeItem(R.id.action_block)
@ -772,8 +780,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun mention() { private fun mention() {
loadedAccount?.let { loadedAccount?.let {
val intent = ComposeActivity.startIntent(this, val intent = ComposeActivity.startIntent(
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))) this,
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))
)
startActivity(intent) startActivity(intent)
} }
} }
@ -849,5 +859,4 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
return intent return intent
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -16,9 +16,9 @@
package com.keylesspalace.tusky package com.keylesspalace.tusky
import android.os.Bundle import android.os.Bundle
import androidx.annotation.RawRes
import android.util.Log import android.util.Log
import android.widget.TextView import android.widget.TextView
import androidx.annotation.RawRes
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
import com.keylesspalace.tusky.util.IOUtils import com.keylesspalace.tusky.util.IOUtils
import java.io.BufferedReader import java.io.BufferedReader
@ -41,7 +41,6 @@ class LicenseActivity : BaseActivity() {
setTitle(R.string.title_licenses) setTitle(R.string.title_licenses)
loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView) loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView)
} }
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) { 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.activity.viewModels
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog 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.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
import autodispose2.autoDispose import autodispose2.autoDispose
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.components.timeline.TimelineViewModel import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.* 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
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.* import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.* 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.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
@ -84,7 +100,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
binding.listsRecycler.adapter = adapter binding.listsRecycler.adapter = adapter
binding.listsRecycler.layoutManager = LinearLayoutManager(this) binding.listsRecycler.layoutManager = LinearLayoutManager(this)
binding.listsRecycler.addItemDecoration( binding.listsRecycler.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
viewModel.state viewModel.state
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
@ -101,9 +118,9 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
.subscribe { event -> .subscribe { event ->
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
when (event) { when (event) {
CREATE_ERROR -> showMessage(R.string.error_create_list) Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
RENAME_ERROR -> showMessage(R.string.error_rename_list) Event.RENAME_ERROR -> showMessage(R.string.error_rename_list)
DELETE_ERROR -> showMessage(R.string.error_delete_list) Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
} }
} }
} }
@ -121,7 +138,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
.setView(layout) .setView(layout)
.setPositiveButton( .setPositiveButton(
if (list == null) R.string.action_create_list 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) onPickedDialogName(editText.text, list?.id)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
@ -145,7 +163,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
.show() .show()
} }
private fun update(state: ListsViewModel.State) { private fun update(state: ListsViewModel.State) {
adapter.submitList(state.lists) adapter.submitList(state.lists)
binding.progressBar.visible(state.loadingState == LOADING) binding.progressBar.visible(state.loadingState == LOADING)
@ -166,8 +183,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
LOADED -> LOADED ->
if (state.lists.isEmpty()) { if (state.lists.isEmpty()) {
binding.messageView.show() binding.messageView.show()
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, binding.messageView.setup(
null) R.drawable.elephant_friend_empty, R.string.message_empty,
null
)
} else { } else {
binding.messageView.hide() binding.messageView.hide()
} }
@ -182,7 +201,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
private fun onListSelected(listId: String) { private fun onListSelected(listId: String) {
startActivityWithSlideInAnimation( startActivityWithSlideInAnimation(
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)) ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
)
} }
private fun openListSettings(list: MastoList) { private fun openListSettings(list: MastoList) {
@ -219,8 +239,8 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
} }
} }
private inner class ListsAdapter private inner class ListsAdapter :
: ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) { ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false) 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 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 { View.OnClickListener {
val nameTextView: TextView = view.findViewById(R.id.list_name_textview) val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
val moreButton: ImageButton = view.findViewById(R.id.editListButton) 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.AccessToken
import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.network.MastodonApi 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 okhttp3.HttpUrl
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -75,7 +79,8 @@ class LoginActivity : BaseActivity(), Injectable {
} }
preferences = getSharedPreferences( preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE) getString(R.string.preferences_file_key), Context.MODE_PRIVATE
)
binding.loginButton.setOnClickListener { onButtonClick() } binding.loginButton.setOnClickListener { onButtonClick() }
@ -95,7 +100,6 @@ class LoginActivity : BaseActivity(), Injectable {
} else { } else {
binding.toolbar.visibility = View.GONE binding.toolbar.visibility = View.GONE
} }
} }
override fun requiresLogin(): Boolean { override fun requiresLogin(): Boolean {
@ -134,8 +138,10 @@ class LoginActivity : BaseActivity(), Injectable {
} }
val callback = object : Callback<AppCredentials> { val callback = object : Callback<AppCredentials> {
override fun onResponse(call: Call<AppCredentials>, override fun onResponse(
response: Response<AppCredentials>) { call: Call<AppCredentials>,
response: Response<AppCredentials>
) {
if (!response.isSuccessful) { if (!response.isSuccessful) {
binding.loginButton.isEnabled = true binding.loginButton.isEnabled = true
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
@ -165,11 +171,12 @@ class LoginActivity : BaseActivity(), Injectable {
} }
mastodonApi mastodonApi
.authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri, .authenticateApp(
OAUTH_SCOPES, getString(R.string.tusky_website)) domain, getString(R.string.app_name), oauthRedirectUri,
OAUTH_SCOPES, getString(R.string.tusky_website)
)
.enqueue(callback) .enqueue(callback)
setLoading(true) setLoading(true)
} }
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) { private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
@ -224,31 +231,27 @@ class LoginActivity : BaseActivity(), Injectable {
} else { } else {
setLoading(false) setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s", Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message()))
getString(R.string.error_retrieving_oauth_token),
response.message()))
} }
} }
override fun onFailure(call: Call<AccessToken>, t: Throwable) { override fun onFailure(call: Call<AccessToken>, t: Throwable) {
setLoading(false) setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s", Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message))
getString(R.string.error_retrieving_oauth_token),
t.message))
} }
} }
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code, mastodonApi.fetchOAuthToken(
"authorization_code").enqueue(callback) domain, clientId, clientSecret, redirectUri, code,
"authorization_code"
).enqueue(callback)
} else if (error != null) { } else if (error != null) {
/* Authorization failed. Put the error response where the user can read it and they /* Authorization failed. Put the error response where the user can read it and they
* can try again. */ * can try again. */
setLoading(false) setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied) binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
Log.e(TAG, String.format("%s %s", Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
getString(R.string.error_authorization_denied),
error))
} else { } else {
// This case means a junk response was received somehow. // This case means a junk response was received somehow.
setLoading(false) setLoading(false)

View file

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

View file

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

View file

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

View file

@ -20,9 +20,9 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment 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.TimelineFragment
import com.keylesspalace.tusky.components.timeline.TimelineViewModel 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 */ /** 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 HASHTAG = "Hashtag"
const val LIST = "List" const val LIST = "List"
data class TabData(val id: String, data class TabData(
val id: String,
@StringRes val text: Int, @StringRes val text: Int,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
val fragment: (List<String>) -> Fragment, val fragment: (List<String>) -> Fragment,

View file

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

View file

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

View file

@ -16,16 +16,19 @@
package com.keylesspalace.tusky.adapter package com.keylesspalace.tusky.adapter
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Field import com.keylesspalace.tusky.entity.Field
import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.interfaces.LinkListener 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( class AccountFieldAdapter(
private val linkListener: LinkListener, private val linkListener: LinkListener,
@ -70,6 +73,5 @@ class AccountFieldAdapter(
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) 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) {} override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
}) })
} }
class MutableStringPair(var first: String, var second: String) 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.databinding.ItemAutocompleteAccountBinding
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.settings.PrefKeys 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) { 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) val animateAvatar = pm.getBoolean("animateGifAvatars", false)
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar) loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
} }
return binding.root return binding.root

View file

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

View file

@ -16,7 +16,6 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.interfaces.AccountActionListener 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.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener 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( class FollowRequestViewHolder(
private val binding: ItemFollowRequestBinding, private val binding: ItemFollowRequestBinding,

View file

@ -16,8 +16,6 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.interfaces.AccountActionListener 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 override fun getItemCount() = if (accountLocked) 0 else 1
} }
class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView) class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)

View file

@ -15,7 +15,7 @@
package com.keylesspalace.tusky.adapter package com.keylesspalace.tusky.adapter
import androidx.recyclerview.widget.RecyclerView
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) 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.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar 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 * 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.databinding.ItemNetworkStateBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding, class NetworkStateViewHolder(
private val retryCallback: () -> Unit) private val binding: ItemNetworkStateBinding,
: RecyclerView.ViewHolder(binding.root) { private val retryCallback: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun setUpWithNetworkState(state: LoadState) { fun setUpWithNetworkState(state: LoadState) {
binding.progressBar.visible(state == LoadState.Loading) binding.progressBar.visible(state == LoadState.Loading)
@ -38,5 +39,4 @@ class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding,
retryCallback() retryCallback()
} }
} }
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,10 @@ import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup 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.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.ColorInt 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.NewPoll
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.settings.PrefKeys 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.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
@ -83,7 +99,8 @@ import javax.inject.Inject
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
class ComposeActivity : BaseActivity(), class ComposeActivity :
BaseActivity(),
ComposeOptionsListener, ComposeOptionsListener,
ComposeAutoCompleteAdapter.AutocompletionProvider, ComposeAutoCompleteAdapter.AutocompletionProvider,
OnEmojiSelectedListener, OnEmojiSelectedListener,
@ -288,8 +305,9 @@ class ComposeActivity : BaseActivity(),
} }
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093 // work around Android platform bug -> https://issuetracker.google.com/issues/67102093
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O ||
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1
) {
binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null) binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
} }
} }
@ -330,9 +348,9 @@ class ComposeActivity : BaseActivity(),
updateScheduleButton() updateScheduleButton()
} }
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll -> combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
val active = poll == null val active = poll == null &&
&& media!!.size != 4 media!!.size != 4 &&
&& (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
enableButton(binding.composeAddMediaButton, active, active) enableButton(binding.composeAddMediaButton, active, active)
enablePollButton(media.isNullOrEmpty()) enablePollButton(media.isNullOrEmpty())
}.subscribe() }.subscribe()
@ -393,7 +411,6 @@ class ComposeActivity : BaseActivity(),
setDisplayShowHomeEnabled(true) setDisplayShowHomeEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_close_24dp) setHomeAsUpIndicator(R.drawable.ic_close_24dp)
} }
} }
private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) { private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) {
@ -409,8 +426,10 @@ class ComposeActivity : BaseActivity(),
avatarSize / 8, avatarSize / 8,
animateAvatars animateAvatars
) )
binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description, binding.composeAvatar.contentDescription = getString(
activeAccount.fullName) R.string.compose_active_account_description,
activeAccount.fullName
)
} }
private fun replaceTextAtCaret(text: CharSequence) { private fun replaceTextAtCaret(text: CharSequence) {
@ -468,7 +487,6 @@ class ComposeActivity : BaseActivity(),
} }
} }
private fun atButtonClicked() { private fun atButtonClicked() {
prependSelectedWordsWith("@") prependSelectedWordsWith("@")
} }
@ -502,7 +520,6 @@ class ComposeActivity : BaseActivity(),
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
binding.composeHideMediaButton.isClickable = false binding.composeHideMediaButton.isClickable = false
ContextCompat.getColor(this, R.color.transparent_tusky_blue) ContextCompat.getColor(this, R.color.transparent_tusky_blue)
} else { } else {
binding.composeHideMediaButton.isClickable = true binding.composeHideMediaButton.isClickable = true
if (markMediaSensitive) { if (markMediaSensitive) {
@ -615,9 +632,11 @@ class ComposeActivity : BaseActivity(),
if (newState == BottomSheetBehavior.STATE_COLLAPSED) { if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
addMediaBehavior.removeBottomSheetCallback(this) addMediaBehavior.removeBottomSheetCallback(this)
if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { 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), arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
)
} else { } else {
pickMediaFile.launch(true) pickMediaFile.launch(true)
} }
@ -633,8 +652,10 @@ class ComposeActivity : BaseActivity(),
private fun openPollDialog() { private fun openPollDialog() {
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
val instanceParams = viewModel.instanceParams.value!! val instanceParams = viewModel.instanceParams.value!!
showAddPollDialog(this, viewModel.poll.value, instanceParams.pollMaxOptions, showAddPollDialog(
instanceParams.pollMaxLength, viewModel::updatePoll) this, viewModel.poll.value, instanceParams.pollMaxOptions,
instanceParams.pollMaxLength, viewModel::updatePoll
)
} }
private fun setupPollView() { private fun setupPollView() {
@ -755,14 +776,17 @@ class ComposeActivity : BaseActivity(),
if (viewModel.media.value!!.isNotEmpty()) { if (viewModel.media.value!!.isNotEmpty()) {
finishingUploadDialog = ProgressDialog.show( finishingUploadDialog = ProgressDialog.show(
this, getString(R.string.dialog_title_finishing_media_upload), 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() finishingUploadDialog?.dismiss()
deleteDraftAndFinish() deleteDraftAndFinish()
}) }
)
} else { } else {
binding.composeEditField.error = getString(R.string.error_compose_character_limit) binding.composeEditField.error = getString(R.string.error_compose_character_limit)
enableButtons(true) enableButtons(true)
@ -776,8 +800,10 @@ class ComposeActivity : BaseActivity(),
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
pickMediaFile.launch(true) pickMediaFile.launch(true)
} else { } else {
Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission, Snackbar.make(
Snackbar.LENGTH_SHORT).apply { binding.activityCompose, R.string.error_media_upload_permission,
Snackbar.LENGTH_SHORT
).apply {
setAction(R.string.action_retry) { onMediaPick() } setAction(R.string.action_retry) { onMediaPick() }
// necessary so snackbar is shown over everything // necessary so snackbar is shown over everything
view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) 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 // Continue only if the File was successfully created
photoUploadUri = FileProvider.getUriForFile(this, photoUploadUri = FileProvider.getUriForFile(
this,
BuildConfig.APPLICATION_ID + ".fileprovider", BuildConfig.APPLICATION_ID + ".fileprovider",
photoFile) photoFile
)
takePicture.launch(photoUploadUri) takePicture.launch(photoUploadUri)
} }
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
button.isEnabled = clickable button.isEnabled = clickable
ThemeUtils.setDrawableTint(this, button.drawable, ThemeUtils.setDrawableTint(
this, button.drawable,
if (colorActive) android.R.attr.textColorTertiary if (colorActive) android.R.attr.textColorTertiary
else R.attr.textColorDisabled) else R.attr.textColorDisabled
)
} }
private fun enablePollButton(enable: Boolean) { private fun enablePollButton(enable: Boolean) {
binding.addPollTextActionTextView.isEnabled = enable binding.addPollTextActionTextView.isEnabled = enable
val textColor = ThemeUtils.getColor(this, val textColor = ThemeUtils.getColor(
this,
if (enable) android.R.attr.textColorTertiary if (enable) android.R.attr.textColorTertiary
else R.attr.textColorDisabled) else R.attr.textColorDisabled
)
binding.addPollTextActionTextView.setTextColor(textColor) binding.addPollTextActionTextView.setTextColor(textColor)
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
} }
@ -847,7 +879,6 @@ class ComposeActivity : BaseActivity(),
} }
displayTransientError(errorId) displayTransientError(errorId)
} }
} }
} }
} }
@ -881,7 +912,8 @@ class ComposeActivity : BaseActivity(),
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED || if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED || addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED || emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) { scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
) {
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
emojiBehavior.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 { override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem.localId == newItem.localId return oldItem.localId == newItem.localId
} }
@ -89,10 +91,11 @@ class MediaPreviewAdapter(
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem == newItem return oldItem == newItem
} }
}) }
)
inner class PreviewViewHolder(val progressImageView: ProgressImageView) inner class PreviewViewHolder(val progressImageView: ProgressImageView) :
: RecyclerView.ViewHolder(progressImageView) { RecyclerView.ViewHolder(progressImageView) {
init { init {
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources 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.entity.Attachment
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.ProgressRequestBody 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.Observable
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
@ -37,7 +40,7 @@ import okhttp3.MultipartBody
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.* import java.util.Date
sealed class UploadEvent { sealed class UploadEvent {
data class ProgressEvent(val percentage: Int) : UploadEvent() data class ProgressEvent(val percentage: Int) : UploadEvent()
@ -101,12 +104,13 @@ class MediaUploaderImpl(
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
FileOutputStream(file.absoluteFile).use { out -> FileOutputStream(file.absoluteFile).use { out ->
input.copyTo(out) input.copyTo(out)
uri = FileProvider.getUriForFile(context, uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".fileprovider", BuildConfig.APPLICATION_ID + ".fileprovider",
file) file
)
mediaSize = getMediaSize(contentResolver, uri) mediaSize = getMediaSize(contentResolver, uri)
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.w(TAG, e) Log.w(TAG, e)
@ -151,20 +155,22 @@ class MediaUploaderImpl(
var mimeType = contentResolver.getType(media.uri) var mimeType = contentResolver.getType(media.uri)
val map = MimeTypeMap.getSingleton() val map = MimeTypeMap.getSingleton()
val fileExtension = map.getExtensionFromMimeType(mimeType) 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), context.getString(R.string.app_name),
Date().time.toString(), Date().time.toString(),
randomAlphanumericString(10), randomAlphanumericString(10),
fileExtension) fileExtension
)
val stream = contentResolver.openInputStream(media.uri) val stream = contentResolver.openInputStream(media.uri)
if (mimeType == null) mimeType = "multipart/form-data" if (mimeType == null) mimeType = "multipart/form-data"
var lastProgress = -1 var lastProgress = -1
val fileBody = ProgressRequestBody(stream, media.mediaSize, val fileBody = ProgressRequestBody(
mimeType.toMediaTypeOrNull()) { percentage -> stream, media.mediaSize,
mimeType.toMediaTypeOrNull()
) { percentage ->
if (percentage != lastProgress) { if (percentage != lastProgress) {
emitter.onNext(UploadEvent.ProgressEvent(percentage)) emitter.onNext(UploadEvent.ProgressEvent(percentage))
} }
@ -180,12 +186,15 @@ class MediaUploaderImpl(
} }
val uploadDisposable = mastodonApi.uploadMedia(body, description) val uploadDisposable = mastodonApi.uploadMedia(body, description)
.subscribe({ attachment -> .subscribe(
{ attachment ->
emitter.onNext(UploadEvent.FinishedEvent(attachment)) emitter.onNext(UploadEvent.FinishedEvent(attachment))
emitter.onComplete() emitter.onComplete()
}, { e -> },
{ e ->
emitter.onError(e) emitter.onError(e)
}) }
)
// Cancel the request when our observable is cancelled // Cancel the request when our observable is cancelled
emitter.setDisposable(uploadDisposable) emitter.setDisposable(uploadDisposable)
@ -194,15 +203,16 @@ class MediaUploaderImpl(
private fun downsize(media: QueuedMedia): QueuedMedia { private fun downsize(media: QueuedMedia): QueuedMedia {
val file = createNewImageFile(context) val file = createNewImageFile(context)
DownsizeImageTask.resize(arrayOf(media.uri), DownsizeImageTask.resize(
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file) arrayOf(media.uri),
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file
)
return media.copy(uri = file.toUri(), mediaSize = file.length()) return media.copy(uri = file.toUri(), mediaSize = file.length())
} }
private fun shouldResizeMedia(media: QueuedMedia): Boolean { private fun shouldResizeMedia(media: QueuedMedia): Boolean {
return media.type == QueuedMedia.Type.IMAGE return media.type == QueuedMedia.Type.IMAGE &&
&& (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|| getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
} }
private companion object { private companion object {
@ -211,6 +221,5 @@ class MediaUploaderImpl(
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
} }
} }

View file

@ -82,11 +82,13 @@ fun showAddPollDialog(
val pollDuration = context.resources val pollDuration = context.resources
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId] .getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
onUpdatePoll(NewPoll( onUpdatePoll(
NewPoll(
options = adapter.pollOptions, options = adapter.pollOptions,
expiresIn = pollDuration, expiresIn = pollDuration,
multiple = binding.multipleChoicesCheckBox.isChecked multiple = binding.multipleChoicesCheckBox.isChecked
)) )
)
dialog.dismiss() 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 // https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
fun <T> T.makeCaptionDialog(existingDescription: String?, fun <T> T.makeCaptionDialog(
existingDescription: String?,
previewUri: Uri, previewUri: Uri,
onUpdateDescription: (String) -> LiveData<Boolean> onUpdateDescription: (String) -> LiveData<Boolean>
) where T : Activity, T : LifecycleOwner { ) 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) (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
val input = EditText(this) val input = EditText(this)
input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired, input.hint = resources.getQuantityString(
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT) R.plurals.hint_describe_for_visually_impaired,
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT
)
dialogLayout.addView(input) dialogLayout.addView(input)
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
input.setLines(2) 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_MULTI_LINE
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
)
input.setText(existingDescription) input.setText(existingDescription)
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
@ -76,7 +81,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
withLifecycleContext { withLifecycleContext {
onUpdateDescription(input.text.toString()) onUpdateDescription(input.text.toString())
.observe { success -> if (!success) showFailedCaptionMessage() } .observe { success -> if (!success) showFailedCaptionMessage() }
} }
dialog.dismiss() dialog.dismiss()
@ -90,7 +94,8 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
val window = dialog.window val window = dialog.window
window?.setSoftInputMode( window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
dialog.show() dialog.show()
@ -109,7 +114,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
}) })
} }
private fun Activity.showFailedCaptionMessage() { private fun Activity.showFailedCaptionMessage() {
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() 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 R.id.directRadioButton
else -> else ->
R.id.directRadioButton R.id.directRadioButton
} }
check(selectedButton) check(selectedButton)
} }
} }
interface ComposeOptionsListener { interface ComposeOptionsListener {

View file

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

View file

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

View file

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

View file

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

View file

@ -37,5 +37,4 @@ class ConversationLoadStateAdapter(
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return NetworkStateViewHolder(binding, retryCallback) 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.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys 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.CardViewMode
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -22,7 +22,14 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Notification 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.ThemeUtils
import com.keylesspalace.tusky.util.deserialize import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.getNonNullString
@ -122,7 +129,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.pref_title_bot_overlay) setTitle(R.string.pref_title_bot_overlay)
isSingleLineTitle = false isSingleLineTitle = false
setIcon(R.drawable.ic_bot_24dp) setIcon(R.drawable.ic_bot_24dp)
} }
switchPreference { switchPreference {
@ -259,7 +265,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
sizePx = iconSize sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor) colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
} }
} }
override fun onResume() { override fun onResume() {

View file

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

View file

@ -126,7 +126,6 @@ class ReportViewModel @Inject constructor(
.subscribe( .subscribe(
{ data -> { data ->
updateRelationship(data.getOrNull(0)) updateRelationship(data.getOrNull(0))
}, },
{ {
updateRelationship(null) updateRelationship(null)
@ -210,7 +209,6 @@ class ReportViewModel @Inject constructor(
} }
) )
.autoDispose() .autoDispose()
} }
fun checkClickedUrl(url: String?) { 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.Emoji
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener 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.COLLAPSE_INPUT_FILTER
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_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 com.keylesspalace.tusky.viewdata.toViewData
import java.util.* import java.util.Date
class StatusViewHolder( class StatusViewHolder(
private val binding: ItemReportStatusBinding, private val binding: ItemReportStatusBinding,
@ -71,9 +78,11 @@ class StatusViewHolder(
val sensitive = status.sensitive val sensitive = status.sensitive
statusViewHelper.setMediasPreview(statusDisplayOptions, status.attachments, statusViewHelper.setMediasPreview(
statusDisplayOptions, status.attachments,
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
mediaViewHeight) mediaViewHeight
)
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions) statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
setCreatedAt(status.createdAt) setCreatedAt(status.createdAt)
@ -81,8 +90,10 @@ class StatusViewHolder(
private fun updateTextView() { private fun updateTextView() {
status()?.let { status -> status()?.let { status ->
setupCollapsedState(shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true), setupCollapsedState(
viewState.isContentShow(status.id, status.sensitive), status.spoilerText) shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
viewState.isContentShow(status.id, status.sensitive), status.spoilerText
)
if (status.spoilerText.isBlank()) { if (status.spoilerText.isBlank()) {
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler) 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, content: Spanned,
mentions: List<Status.Mention>?, mentions: List<Status.Mention>?,
emojis: List<Emoji>, emojis: List<Emoji>,
listener: LinkListener) { listener: LinkListener
) {
if (expanded) { if (expanded) {
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis) val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener) LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,8 +24,8 @@ import com.keylesspalace.tusky.adapter.AccountViewHolder
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
: PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) { PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
val view = LayoutInflater.from(parent.context) 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.interfaces.LinkListener
import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.BindingHolder
class SearchHashtagsAdapter(private val linkListener: LinkListener) class SearchHashtagsAdapter(private val linkListener: LinkListener) :
: PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) { PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemHashtagBinding> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemHashtagBinding> {
val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false) 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 override fun getItemCount() = 3
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,5 +39,4 @@ interface ConversationsDao {
@Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId")
fun deleteForAccount(accountId: Long) 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.TabData
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
import com.keylesspalace.tusky.createTabDataFromId 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 com.keylesspalace.tusky.util.trimTrailingWhitespace
import java.net.URLDecoder import java.net.URLDecoder
import java.net.URLEncoder import java.net.URLEncoder
import java.util.* import java.util.ArrayList
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton

View file

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

View file

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

View file

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

View file

@ -15,7 +15,23 @@
package com.keylesspalace.tusky.di 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.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.drafts.DraftsActivity

View file

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

View file

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

View file

@ -16,8 +16,8 @@
package com.keylesspalace.tusky.di package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver
import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver
import dagger.Module import dagger.Module
import dagger.android.ContributesAndroidInjector 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, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.AccountsInListFragment import com.keylesspalace.tusky.AccountsInListFragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment 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.AccountPreferencesFragment
import com.keylesspalace.tusky.components.preference.NotificationPreferencesFragment 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.ReportDoneFragment
import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment
import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment
import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment
import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment 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.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.Module
import dagger.android.ContributesAndroidInjector import dagger.android.ContributesAndroidInjector
@ -89,5 +91,4 @@ abstract class FragmentBuildersModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun preferencesFragment(): PreferencesFragment 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, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.di package com.keylesspalace.tusky.di
/** /**

View file

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

View file

@ -1,11 +1,11 @@
package com.keylesspalace.tusky.di package com.keylesspalace.tusky.di
import com.google.gson.Gson 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.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi 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.Module
import dagger.Provides 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.report.ReportViewModel
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
import com.keylesspalace.tusky.components.search.SearchViewModel 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.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.viewmodel.AccountViewModel import com.keylesspalace.tusky.viewmodel.AccountViewModel
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
@ -63,7 +62,6 @@ abstract class ViewModelModule {
@ViewModelKey(ListsViewModel::class) @ViewModelKey(ListsViewModel::class)
internal abstract fun listsViewModel(viewModel: ListsViewModel): ViewModel internal abstract fun listsViewModel(viewModel: ListsViewModel): ViewModel
@Binds @Binds
@IntoMap @IntoMap
@ViewModelKey(AccountsInListViewModel::class) @ViewModelKey(AccountsInListViewModel::class)

View file

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

View file

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

View file

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

View file

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

View file

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