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

* add ktlint plugin to project and apply default code style

* some manual adjustments, fix wildcard imports

* update CONTRIBUTING.md

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

View File

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

View File

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

View File

@ -3,9 +3,13 @@ package com.keylesspalace.tusky
import androidx.room.Room
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.keylesspalace.tusky.db.*
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.components.timeline.TimelineRepository
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.TimelineAccountEntity
import com.keylesspalace.tusky.db.TimelineDao
import com.keylesspalace.tusky.db.TimelineStatusEntity
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
import com.keylesspalace.tusky.entity.Status
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
@ -41,9 +45,11 @@ class TimelineDAOTest {
timelineDao.insertInTransaction(status, author, reblogger)
}
val resultsFromDb = timelineDao.getStatusesForAccount(setOne.first.timelineUserId,
maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10)
.blockingGet()
val resultsFromDb = timelineDao.getStatusesForAccount(
setOne.first.timelineUserId,
maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10
)
.blockingGet()
assertEquals(2, resultsFromDb.size)
for ((set, fromDb) in listOf(setTwo, setOne).zip(resultsFromDb)) {
@ -64,14 +70,13 @@ class TimelineDAOTest {
timelineDao.insertStatusIfNotThere(placeholder)
val fromDb = timelineDao.getStatusesForAccount(status.timelineUserId, null, null, 10)
.blockingGet()
.blockingGet()
val result = fromDb.first()
assertEquals(1, fromDb.size)
assertEquals(author, result.account)
assertEquals(status, result.status)
assertNull(result.reblogAccount)
}
@Test
@ -79,22 +84,22 @@ class TimelineDAOTest {
val now = System.currentTimeMillis()
val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000
val oldThisAccount = makeStatus(
statusId = 5,
createdAt = oldDate
statusId = 5,
createdAt = oldDate
)
val oldAnotherAccount = makeStatus(
statusId = 10,
createdAt = oldDate,
accountId = 2
statusId = 10,
createdAt = oldDate,
accountId = 2
)
val recentThisAccount = makeStatus(
statusId = 30,
createdAt = System.currentTimeMillis()
statusId = 30,
createdAt = System.currentTimeMillis()
)
val recentAnotherAccount = makeStatus(
statusId = 60,
createdAt = System.currentTimeMillis(),
accountId = 2
statusId = 60,
createdAt = System.currentTimeMillis(),
accountId = 2
)
for ((status, author, reblogAuthor) in listOf(oldThisAccount, oldAnotherAccount, recentThisAccount, recentAnotherAccount)) {
@ -104,15 +109,15 @@ class TimelineDAOTest {
timelineDao.cleanup(now - TimelineRepository.CLEANUP_INTERVAL)
assertEquals(
listOf(recentThisAccount),
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
.map { it.toTriple() }
listOf(recentThisAccount),
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
.map { it.toTriple() }
)
assertEquals(
listOf(recentAnotherAccount),
timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet()
.map { it.toTriple() }
listOf(recentAnotherAccount),
timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet()
.map { it.toTriple() }
)
}
@ -120,9 +125,9 @@ class TimelineDAOTest {
fun overwriteDeletedStatus() {
val oldStatuses = listOf(
makeStatus(statusId = 3),
makeStatus(statusId = 2),
makeStatus(statusId = 1)
makeStatus(statusId = 3),
makeStatus(statusId = 2),
makeStatus(statusId = 1)
)
timelineDao.deleteRange(1, oldStatuses.last().first.serverId, oldStatuses.first().first.serverId)
@ -133,8 +138,8 @@ class TimelineDAOTest {
// status 2 gets deleted, newly loaded status contain only 1 + 3
val newStatuses = listOf(
makeStatus(statusId = 3),
makeStatus(statusId = 1)
makeStatus(statusId = 3),
makeStatus(statusId = 1)
)
timelineDao.deleteRange(1, newStatuses.last().first.serverId, newStatuses.first().first.serverId)
@ -143,107 +148,106 @@ class TimelineDAOTest {
timelineDao.insertInTransaction(status, author, reblogAuthor)
}
//make sure status 2 is no longer in db
// make sure status 2 is no longer in db
assertEquals(
newStatuses,
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
.map { it.toTriple() }
newStatuses,
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
.map { it.toTriple() }
)
}
private fun makeStatus(
accountId: Long = 1,
statusId: Long = 10,
reblog: Boolean = false,
createdAt: Long = statusId,
authorServerId: String = "20"
accountId: Long = 1,
statusId: Long = 10,
reblog: Boolean = false,
createdAt: Long = statusId,
authorServerId: String = "20"
): Triple<TimelineStatusEntity, TimelineAccountEntity, TimelineAccountEntity?> {
val author = TimelineAccountEntity(
authorServerId,
accountId,
"localUsername",
"username",
"displayName",
"blah",
"avatar",
"[\"tusky\": \"http://tusky.cool/emoji.jpg\"]",
false
authorServerId,
accountId,
"localUsername",
"username",
"displayName",
"blah",
"avatar",
"[\"tusky\": \"http://tusky.cool/emoji.jpg\"]",
false
)
val reblogAuthor = if (reblog) {
TimelineAccountEntity(
"R$authorServerId",
accountId,
"RlocalUsername",
"Rusername",
"RdisplayName",
"Rblah",
"Ravatar",
"[]",
false
"R$authorServerId",
accountId,
"RlocalUsername",
"Rusername",
"RdisplayName",
"Rblah",
"Ravatar",
"[]",
false
)
} else null
val even = accountId % 2 == 0L
val status = TimelineStatusEntity(
serverId = statusId.toString(),
url = "url$statusId",
timelineUserId = accountId,
authorServerId = authorServerId,
inReplyToId = "inReplyToId$statusId",
inReplyToAccountId = "inReplyToAccountId$statusId",
content = "Content!$statusId",
createdAt = createdAt,
emojis = "emojis$statusId",
reblogsCount = 1 * statusId.toInt(),
favouritesCount = 2 * statusId.toInt(),
reblogged = even,
favourited = !even,
bookmarked = false,
sensitive = even,
spoilerText = "spoier$statusId",
visibility = Status.Visibility.PRIVATE,
attachments = "attachments$accountId",
mentions = "mentions$accountId",
application = "application$accountId",
reblogServerId = if (reblog) (statusId * 100).toString() else null,
reblogAccountId = reblogAuthor?.serverId,
poll = null,
muted = false
serverId = statusId.toString(),
url = "url$statusId",
timelineUserId = accountId,
authorServerId = authorServerId,
inReplyToId = "inReplyToId$statusId",
inReplyToAccountId = "inReplyToAccountId$statusId",
content = "Content!$statusId",
createdAt = createdAt,
emojis = "emojis$statusId",
reblogsCount = 1 * statusId.toInt(),
favouritesCount = 2 * statusId.toInt(),
reblogged = even,
favourited = !even,
bookmarked = false,
sensitive = even,
spoilerText = "spoier$statusId",
visibility = Status.Visibility.PRIVATE,
attachments = "attachments$accountId",
mentions = "mentions$accountId",
application = "application$accountId",
reblogServerId = if (reblog) (statusId * 100).toString() else null,
reblogAccountId = reblogAuthor?.serverId,
poll = null,
muted = false
)
return Triple(status, author, reblogAuthor)
}
private fun createPlaceholder(serverId: String, timelineUserId: Long): TimelineStatusEntity {
return TimelineStatusEntity(
serverId = serverId,
url = null,
timelineUserId = timelineUserId,
authorServerId = null,
inReplyToId = null,
inReplyToAccountId = null,
content = null,
createdAt = 0L,
emojis = null,
reblogsCount = 0,
favouritesCount = 0,
reblogged = false,
favourited = false,
bookmarked = false,
sensitive = false,
spoilerText = null,
visibility = null,
attachments = null,
mentions = null,
application = null,
reblogServerId = null,
reblogAccountId = null,
poll = null,
muted = false
serverId = serverId,
url = null,
timelineUserId = timelineUserId,
authorServerId = null,
inReplyToId = null,
inReplyToAccountId = null,
content = null,
createdAt = 0L,
emojis = null,
reblogsCount = 0,
favouritesCount = 0,
reblogged = false,
favourited = false,
bookmarked = false,
sensitive = false,
spoilerText = null,
visibility = null,
attachments = null,
mentions = null,
application = null,
reblogServerId = null,
reblogAccountId = null,
poll = null,
muted = false
)
}
private fun TimelineStatusWithAccount.toTriple() = Triple(status, account, reblogAccount)
}
}

View File

@ -2,13 +2,13 @@ package com.keylesspalace.tusky
import android.content.Intent
import android.os.Bundle
import androidx.annotation.StringRes
import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.method.LinkMovementMethod
import android.text.style.URLSpan
import android.text.util.Linkify
import android.widget.TextView
import androidx.annotation.StringRes
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
@ -32,7 +32,7 @@ class AboutActivity : BottomSheetActivity(), Injectable {
binding.versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME)
if(BuildConfig.CUSTOM_INSTANCE.isBlank()) {
if (BuildConfig.CUSTOM_INSTANCE.isBlank()) {
binding.aboutPoweredByTusky.hide()
}

View File

@ -62,7 +62,16 @@ import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.pager.AccountPagerAdapter
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.DefaultTextWatcher
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.showMuteAccountDialog
import com.keylesspalace.tusky.viewmodel.AccountViewModel
import dagger.android.DispatchingAndroidInjector
@ -82,7 +91,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate)
private lateinit var accountFieldAdapter : AccountFieldAdapter
private lateinit var accountFieldAdapter: AccountFieldAdapter
private var followState: FollowState = FollowState.NOT_FOLLOWING
private var blocking: Boolean = false
@ -233,7 +242,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
override fun onTabUnselected(tab: TabLayout.Tab?) {}
override fun onTabSelected(tab: TabLayout.Tab?) {}
})
}
@ -266,8 +274,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
fillColor = ColorStateList.valueOf(toolbarColor)
elevation = appBarElevation
shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius))
.build()
.setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius))
.build()
}
binding.accountAvatarImageView.background = avatarBackground
@ -314,7 +322,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0
}
})
}
private fun makeNotificationBarTransparent() {
@ -331,8 +338,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
is Success -> onAccountChanged(it.data)
is Error -> {
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
}
}
}
@ -344,15 +351,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (it is Error) {
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
.setAction(R.string.action_retry) { viewModel.refresh() }
.show()
}
}
viewModel.accountFieldData.observe(this, {
accountFieldAdapter.fields = it
accountFieldAdapter.notifyDataSetChanged()
})
viewModel.accountFieldData.observe(
this,
{
accountFieldAdapter.fields = it
accountFieldAdapter.notifyDataSetChanged()
}
)
viewModel.noteSaved.observe(this) {
binding.saveNoteInfo.visible(it, View.INVISIBLE)
}
@ -366,9 +375,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
viewModel.refresh()
adapter.refreshContent()
}
viewModel.isRefreshing.observe(this, { isRefreshing ->
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
})
viewModel.isRefreshing.observe(
this,
{ isRefreshing ->
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
}
)
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
}
@ -382,7 +394,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this)
// accountFieldAdapter.fields = account.fields ?: emptyList()
// accountFieldAdapter.fields = account.fields ?: emptyList()
accountFieldAdapter.emojis = account.emojis ?: emptyList()
accountFieldAdapter.notifyDataSetChanged()
@ -409,18 +421,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
loadedAccount?.let { account ->
loadAvatar(
account.avatar,
binding.accountAvatarImageView,
resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp),
animateAvatar
account.avatar,
binding.accountAvatarImageView,
resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp),
animateAvatar
)
Glide.with(this)
.asBitmap()
.load(account.header)
.centerCrop()
.into(binding.accountHeaderImageView)
.asBitmap()
.load(account.header)
.centerCrop()
.into(binding.accountHeaderImageView)
binding.accountAvatarImageView.setOnClickListener { avatarView ->
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
@ -478,7 +489,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
}
}
/**
@ -554,15 +564,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
if(!viewModel.isSelf && followState == FollowState.FOLLOWING
&& (relation.subscribing != null || relation.notifying != null)) {
if (!viewModel.isSelf && followState == FollowState.FOLLOWING &&
(relation.subscribing != null || relation.notifying != null)
) {
binding.accountSubscribeButton.show()
binding.accountSubscribeButton.setOnClickListener {
viewModel.changeSubscribingState()
}
if(relation.notifying != null)
if (relation.notifying != null)
subscribing = relation.notifying
else if(relation.subscribing != null)
else if (relation.subscribing != null)
subscribing = relation.subscribing
}
@ -577,7 +588,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
updateButtons()
}
private val noteWatcher = object: DefaultTextWatcher() {
private val noteWatcher = object : DefaultTextWatcher() {
override fun afterTextChanged(s: Editable) {
viewModel.noteChanged(s.toString())
}
@ -615,11 +626,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
}
private fun updateSubscribeButton() {
if(followState != FollowState.FOLLOWING) {
if (followState != FollowState.FOLLOWING) {
binding.accountSubscribeButton.hide()
}
if(subscribing) {
if (subscribing) {
binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp)
binding.accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account)
} else {
@ -648,7 +659,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
binding.accountMuteButton.hide()
updateMuteButton()
}
} else {
binding.accountFloatingActionButton.hide()
binding.accountFollowButton.hide()
@ -698,11 +708,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
} else {
getString(R.string.action_show_reblogs)
}
} else {
menu.removeItem(R.id.action_show_reblogs)
}
} else {
// It shouldn't be possible to block, mute or report yourself.
menu.removeItem(R.id.action_block)
@ -717,39 +725,39 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun showFollowRequestPendingDialog() {
AlertDialog.Builder(this)
.setMessage(R.string.dialog_message_cancel_follow_request)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
.setMessage(R.string.dialog_message_cancel_follow_request)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun showUnfollowWarningDialog() {
AlertDialog.Builder(this)
.setMessage(R.string.dialog_unfollow_warning)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
.setMessage(R.string.dialog_unfollow_warning)
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun toggleBlockDomain(instance: String) {
if(blockingDomain) {
if (blockingDomain) {
viewModel.unblockDomain(instance)
} else {
AlertDialog.Builder(this)
.setMessage(getString(R.string.mute_domain_warning, instance))
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) }
.setNegativeButton(android.R.string.cancel, null)
.show()
.setMessage(getString(R.string.mute_domain_warning, instance))
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
private fun toggleBlock() {
if (viewModel.relationshipData.value?.data?.blocking != true) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
.setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() }
.setNegativeButton(android.R.string.cancel, null)
.show()
} else {
viewModel.changeBlockState()
}
@ -759,8 +767,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
if (viewModel.relationshipData.value?.data?.muting != true) {
loadedAccount?.let {
showMuteAccountDialog(
this,
it.username
this,
it.username
) { notifications, duration ->
viewModel.muteAccount(notifications, duration)
}
@ -772,8 +780,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private fun mention() {
loadedAccount?.let {
val intent = ComposeActivity.startIntent(this,
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username)))
val intent = ComposeActivity.startIntent(
this,
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))
)
startActivity(intent)
}
}
@ -849,5 +859,4 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
return intent
}
}
}

View File

@ -64,9 +64,9 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
}
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked))
.commit()
.beginTransaction()
.replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked))
.commit()
}
override fun androidInjector() = dispatchingAndroidInjector

View File

@ -36,7 +36,13 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
import com.keylesspalace.tusky.viewmodel.State
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
@ -93,19 +99,19 @@ class AccountsInListFragment : DialogFragment(), Injectable {
binding.accountsSearchRecycler.adapter = searchAdapter
viewModel.state
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this))
.subscribe { state ->
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this))
.subscribe { state ->
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
when (state.accounts) {
is Either.Right -> binding.messageView.hide()
is Either.Left -> handleError(state.accounts.value)
}
setupSearchView(state)
when (state.accounts) {
is Either.Right -> binding.messageView.hide()
is Either.Left -> handleError(state.accounts.value)
}
setupSearchView(state)
}
binding.searchView.isSubmitButtonEnabled = true
binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
@ -146,11 +152,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
viewModel.load(listId)
}
if (error is IOException) {
binding.messageView.setup(R.drawable.elephant_offline,
R.string.error_network, retryAction)
binding.messageView.setup(
R.drawable.elephant_offline,
R.string.error_network, retryAction
)
} else {
binding.messageView.setup(R.drawable.elephant_error,
R.string.error_generic, retryAction)
binding.messageView.setup(
R.drawable.elephant_error,
R.string.error_generic, retryAction
)
}
}
@ -184,7 +194,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
onRemoveFromList(getItem(holder.bindingAdapterPosition).id)
}
binding.rejectButton.contentDescription =
binding.root.context.getString(R.string.action_remove_from_list)
binding.root.context.getString(R.string.action_remove_from_list)
return holder
}
@ -203,8 +213,8 @@ class AccountsInListFragment : DialogFragment(), Injectable {
}
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
return oldItem.second == newItem.second
&& oldItem.first.deepEquals(newItem.first)
return oldItem.second == newItem.second &&
oldItem.first.deepEquals(newItem.first)
}
}
@ -260,4 +270,4 @@ class AccountsInListFragment : DialogFragment(), Injectable {
return AccountsInListFragment().apply { arguments = args }
}
}
}
}

View File

@ -60,7 +60,6 @@ abstract class BottomSheetActivity : BaseActivity() {
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
})
}
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
@ -70,11 +69,12 @@ abstract class BottomSheetActivity : BaseActivity() {
}
mastodonApi.searchObservable(
query = url,
resolve = true
query = url,
resolve = true
).observeOn(AndroidSchedulers.mainThread())
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ (accounts, statuses) ->
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ (accounts, statuses) ->
if (getCancelSearchRequested(url)) {
return@subscribe
}
@ -90,12 +90,14 @@ abstract class BottomSheetActivity : BaseActivity() {
}
performUrlFallbackAction(url, lookupFallbackBehavior)
}, {
},
{
if (!getCancelSearchRequested(url)) {
onEndSearch(url)
performUrlFallbackAction(url, lookupFallbackBehavior)
}
})
}
)
onBeginSearch(url)
}
@ -186,20 +188,21 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
}
if (uri.query != null ||
uri.fragment != null ||
uri.path == null) {
uri.fragment != null ||
uri.path == null
) {
return false
}
val path = uri.path
return path.matches("^/@[^/]+$".toRegex()) ||
path.matches("^/@[^/]+/\\d+$".toRegex()) ||
path.matches("^/users/\\w+$".toRegex()) ||
path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
path.matches("^/profile/\\w+$".toRegex())
path.matches("^/@[^/]+/\\d+$".toRegex()) ||
path.matches("^/users/\\w+$".toRegex()) ||
path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
path.matches("^/profile/\\w+$".toRegex())
}
enum class PostLookupFallbackBehavior {

View File

@ -36,18 +36,24 @@ import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.bitmap.FitCenter
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.canhub.cropper.CropImage
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import com.canhub.cropper.CropImage
import javax.inject.Inject
class EditProfileActivity : BaseActivity(), Injectable {
@ -110,11 +116,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
binding.addFieldButton.setOnClickListener {
accountFieldEditAdapter.addField()
if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
if (accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
it.isVisible = false
}
binding.scrollView.post{
binding.scrollView.post {
binding.scrollView.smoothScrollTo(0, it.bottom)
}
}
@ -134,23 +140,22 @@ class EditProfileActivity : BaseActivity(), Injectable {
accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList())
binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS
if(viewModel.avatarData.value == null) {
if (viewModel.avatarData.value == null) {
Glide.with(this)
.load(me.avatar)
.placeholder(R.drawable.avatar_default)
.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
)
.into(binding.avatarPreview)
.load(me.avatar)
.placeholder(R.drawable.avatar_default)
.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
)
.into(binding.avatarPreview)
}
if(viewModel.headerData.value == null) {
if (viewModel.headerData.value == null) {
Glide.with(this)
.load(me.header)
.into(binding.headerPreview)
.load(me.header)
.into(binding.headerPreview)
}
}
}
is Error -> {
@ -159,7 +164,6 @@ class EditProfileActivity : BaseActivity(), Injectable {
viewModel.obtainProfile()
}
snackbar.show()
}
}
}
@ -179,20 +183,22 @@ class EditProfileActivity : BaseActivity(), Injectable {
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
viewModel.saveData.observe(this, {
when(it) {
is Success -> {
finish()
}
is Loading -> {
binding.saveProgressBar.visibility = View.VISIBLE
}
is Error -> {
onSaveFailure(it.errorMessage)
viewModel.saveData.observe(
this,
{
when (it) {
is Success -> {
finish()
}
is Loading -> {
binding.saveProgressBar.visibility = View.VISIBLE
}
is Error -> {
onSaveFailure(it.errorMessage)
}
}
}
})
)
}
override fun onSaveInstanceState(outState: Bundle) {
@ -202,50 +208,56 @@ class EditProfileActivity : BaseActivity(), Injectable {
override fun onStop() {
super.onStop()
if(!isFinishing) {
viewModel.updateProfile(binding.displayNameEditText.text.toString(),
binding.noteEditText.text.toString(),
binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData())
if (!isFinishing) {
viewModel.updateProfile(
binding.displayNameEditText.text.toString(),
binding.noteEditText.text.toString(),
binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData()
)
}
}
private fun observeImage(liveData: LiveData<Resource<Bitmap>>,
imageView: ImageView,
progressBar: View,
roundedCorners: Boolean) {
liveData.observe(this, {
private fun observeImage(
liveData: LiveData<Resource<Bitmap>>,
imageView: ImageView,
progressBar: View,
roundedCorners: Boolean
) {
liveData.observe(
this,
{
when (it) {
is Success -> {
val glide = Glide.with(imageView)
when (it) {
is Success -> {
val glide = Glide.with(imageView)
.load(it.data)
if (roundedCorners) {
glide.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
)
}
if (roundedCorners) {
glide.transform(
FitCenter(),
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
)
}
glide.into(imageView)
glide.into(imageView)
imageView.show()
progressBar.hide()
}
is Loading -> {
progressBar.show()
}
is Error -> {
progressBar.hide()
if(!it.consumed) {
onResizeFailure()
it.consumed = true
imageView.show()
progressBar.hide()
}
is Loading -> {
progressBar.show()
}
is Error -> {
progressBar.hide()
if (!it.consumed) {
onResizeFailure()
it.consumed = true
}
}
}
}
})
)
}
private fun onMediaPick(pickType: PickType) {
@ -261,8 +273,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
grantResults: IntArray) {
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
@ -307,14 +322,16 @@ class EditProfileActivity : BaseActivity(), Injectable {
private fun save() {
if (currentlyPicking != PickType.NOTHING) {
return
return
}
viewModel.save(binding.displayNameEditText.text.toString(),
binding.noteEditText.text.toString(),
binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData(),
this)
viewModel.save(
binding.displayNameEditText.text.toString(),
binding.noteEditText.text.toString(),
binding.lockedCheckBox.isChecked,
accountFieldEditAdapter.getFieldData(),
this
)
}
private fun onSaveFailure(msg: String?) {
@ -352,10 +369,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
AVATAR_PICK_RESULT -> {
if (resultCode == Activity.RESULT_OK && data != null) {
CropImage.activity(data.data)
.setInitialCropWindowPaddingRatio(0f)
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
.start(this)
.setInitialCropWindowPaddingRatio(0f)
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
.start(this)
} else {
endMediaPicking()
}
@ -363,10 +380,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
HEADER_PICK_RESULT -> {
if (resultCode == Activity.RESULT_OK && data != null) {
CropImage.activity(data.data)
.setInitialCropWindowPaddingRatio(0f)
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
.start(this)
.setInitialCropWindowPaddingRatio(0f)
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
.start(this)
} else {
endMediaPicking()
}
@ -383,7 +400,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
}
private fun beginResize(uri: Uri?) {
if(uri == null) {
if (uri == null) {
currentlyPicking = PickType.NOTHING
return
}
@ -409,5 +426,4 @@ class EditProfileActivity : BaseActivity(), Injectable {
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
endMediaPicking()
}
}

View File

@ -22,10 +22,9 @@ import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.io.IOException
import java.lang.Exception
import javax.inject.Inject
class FiltersActivity: BaseActivity() {
class FiltersActivity : BaseActivity() {
@Inject
lateinit var api: MastodonApi
@ -34,7 +33,7 @@ class FiltersActivity: BaseActivity() {
private val binding by viewBinding(ActivityFiltersBinding::inflate)
private lateinit var context : String
private lateinit var context: String
private lateinit var filters: MutableList<Filter>
override fun onCreate(savedInstanceState: Bundle?) {
@ -58,7 +57,7 @@ class FiltersActivity: BaseActivity() {
private fun updateFilter(filter: Filter, itemIndex: Int) {
api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt)
.enqueue(object: Callback<Filter>{
.enqueue(object : Callback<Filter> {
override fun onFailure(call: Call<Filter>, t: Throwable) {
Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show()
}
@ -80,7 +79,7 @@ class FiltersActivity: BaseActivity() {
val filter = filters[itemIndex]
if (filter.context.size == 1) {
// This is the only context for this filter; delete it
api.deleteFilter(filters[itemIndex].id).enqueue(object: Callback<ResponseBody> {
api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback<ResponseBody> {
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show()
}
@ -94,17 +93,19 @@ class FiltersActivity: BaseActivity() {
} else {
// Keep the filter, but remove it from this context
val oldFilter = filters[itemIndex]
val newFilter = Filter(oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord)
val newFilter = Filter(
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
)
updateFilter(newFilter, itemIndex)
}
}
private fun createFilter(phrase: String, wholeWord: Boolean) {
api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object: Callback<Filter> {
api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object : Callback<Filter> {
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
val filterResponse = response.body()
if(response.isSuccessful && filterResponse != null) {
if (response.isSuccessful && filterResponse != null) {
filters.add(filterResponse)
refreshFilterDisplay()
eventHub.dispatch(PreferenceChangedEvent(context))
@ -123,13 +124,13 @@ class FiltersActivity: BaseActivity() {
val binding = DialogFilterBinding.inflate(layoutInflater)
binding.phraseWholeWord.isChecked = true
AlertDialog.Builder(this@FiltersActivity)
.setTitle(R.string.filter_addition_dialog_title)
.setView(binding.root)
.setPositiveButton(android.R.string.ok){ _, _ ->
createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked)
}
.setNeutralButton(android.R.string.cancel, null)
.show()
.setTitle(R.string.filter_addition_dialog_title)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ ->
createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked)
}
.setNeutralButton(android.R.string.cancel, null)
.show()
}
private fun setupEditDialogForItem(itemIndex: Int) {
@ -139,19 +140,21 @@ class FiltersActivity: BaseActivity() {
binding.phraseWholeWord.isChecked = filter.wholeWord
AlertDialog.Builder(this@FiltersActivity)
.setTitle(R.string.filter_edit_dialog_title)
.setView(binding.root)
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
val oldFilter = filters[itemIndex]
val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked)
updateFilter(newFilter, itemIndex)
}
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
deleteFilter(itemIndex)
}
.setNeutralButton(android.R.string.cancel, null)
.show()
.setTitle(R.string.filter_edit_dialog_title)
.setView(binding.root)
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
val oldFilter = filters[itemIndex]
val newFilter = Filter(
oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked
)
updateFilter(newFilter, itemIndex)
}
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
deleteFilter(itemIndex)
}
.setNeutralButton(android.R.string.cancel, null)
.show()
}
private fun refreshFilterDisplay() {
@ -173,11 +176,15 @@ class FiltersActivity: BaseActivity() {
binding.filterProgressBar.hide()
binding.filterMessageView.show()
if (t is IOException) {
binding.filterMessageView.setup(R.drawable.elephant_offline,
R.string.error_network) { loadFilters() }
binding.filterMessageView.setup(
R.drawable.elephant_offline,
R.string.error_network
) { loadFilters() }
} else {
binding.filterMessageView.setup(R.drawable.elephant_error,
R.string.error_generic) { loadFilters() }
binding.filterMessageView.setup(
R.drawable.elephant_error,
R.string.error_generic
) { loadFilters() }
}
return@launch
}
@ -195,4 +202,4 @@ class FiltersActivity: BaseActivity() {
const val FILTERS_CONTEXT = "filters_context"
const val FILTERS_TITLE = "filters_title"
}
}
}

View File

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

View File

@ -23,25 +23,41 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.PopupMenu
import android.widget.TextView
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.*
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
import autodispose2.autoDispose
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewmodel.ListsViewModel
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.*
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_NETWORK
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
@ -84,12 +100,13 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
binding.listsRecycler.adapter = adapter
binding.listsRecycler.layoutManager = LinearLayoutManager(this)
binding.listsRecycler.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
viewModel.state
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this))
.subscribe(this::update)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this))
.subscribe(this::update)
viewModel.retryLoading()
binding.addListButton.setOnClickListener {
@ -97,15 +114,15 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
}
viewModel.events.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this))
.subscribe { event ->
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
when (event) {
CREATE_ERROR -> showMessage(R.string.error_create_list)
RENAME_ERROR -> showMessage(R.string.error_rename_list)
DELETE_ERROR -> showMessage(R.string.error_delete_list)
}
.autoDispose(from(this))
.subscribe { event ->
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
when (event) {
Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
Event.RENAME_ERROR -> showMessage(R.string.error_rename_list)
Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
}
}
}
private fun showlistNameDialog(list: MastoList?) {
@ -115,17 +132,18 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
layout.addView(editText)
val margin = Utils.dpToPx(this, 8)
(editText.layoutParams as ViewGroup.MarginLayoutParams)
.setMargins(margin, margin, margin, 0)
.setMargins(margin, margin, margin, 0)
val dialog = AlertDialog.Builder(this)
.setView(layout)
.setPositiveButton(
if (list == null) R.string.action_create_list
else R.string.action_rename_list) { _, _ ->
onPickedDialogName(editText.text, list?.id)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
.setView(layout)
.setPositiveButton(
if (list == null) R.string.action_create_list
else R.string.action_rename_list
) { _, _ ->
onPickedDialogName(editText.text, list?.id)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE)
editText.onTextChanged { s, _, _, _ ->
@ -137,15 +155,14 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
private fun showListDeleteDialog(list: MastoList) {
AlertDialog.Builder(this)
.setMessage(getString(R.string.dialog_delete_list_warning, list.title))
.setPositiveButton(R.string.action_delete){ _, _ ->
viewModel.deleteList(list.id)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
.setMessage(getString(R.string.dialog_delete_list_warning, list.title))
.setPositiveButton(R.string.action_delete) { _, _ ->
viewModel.deleteList(list.id)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun update(state: ListsViewModel.State) {
adapter.submitList(state.lists)
binding.progressBar.visible(state.loadingState == LOADING)
@ -166,8 +183,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
LOADED ->
if (state.lists.isEmpty()) {
binding.messageView.show()
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
null)
binding.messageView.setup(
R.drawable.elephant_friend_empty, R.string.message_empty,
null
)
} else {
binding.messageView.hide()
}
@ -176,13 +195,14 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
private fun showMessage(@StringRes messageId: Int) {
Snackbar.make(
binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT
binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT
).show()
}
private fun onListSelected(listId: String) {
startActivityWithSlideInAnimation(
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId))
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
)
}
private fun openListSettings(list: MastoList) {
@ -219,27 +239,28 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
}
}
private inner class ListsAdapter
: ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
private inner class ListsAdapter :
ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
.let(this::ListViewHolder)
.apply {
val context = nameTextView.context
val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary)
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor }
.let(this::ListViewHolder)
.apply {
val context = nameTextView.context
val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary)
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor }
nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
}
nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
}
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
holder.nameTextView.text = getItem(position).title
}
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view),
View.OnClickListener {
private inner class ListViewHolder(view: View) :
RecyclerView.ViewHolder(view),
View.OnClickListener {
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
val moreButton: ImageButton = view.findViewById(R.id.editListButton)
@ -271,4 +292,4 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
companion object {
fun newIntent(context: Context) = Intent(context, ListsActivity::class.java)
}
}
}

View File

@ -34,7 +34,11 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.rickRoll
import com.keylesspalace.tusky.util.shouldRickRoll
import com.keylesspalace.tusky.util.viewBinding
import okhttp3.HttpUrl
import retrofit2.Call
import retrofit2.Callback
@ -62,28 +66,29 @@ class LoginActivity : BaseActivity(), Injectable {
setContentView(binding.root)
if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
if (savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
}
if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
Glide.with(binding.loginLogo)
.load(BuildConfig.CUSTOM_LOGO_URL)
.placeholder(null)
.into(binding.loginLogo)
.load(BuildConfig.CUSTOM_LOGO_URL)
.placeholder(null)
.into(binding.loginLogo)
}
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.whatsAnInstanceTextView.setOnClickListener {
val dialog = AlertDialog.Builder(this)
.setMessage(R.string.dialog_whats_an_instance)
.setPositiveButton(R.string.action_close, null)
.show()
.setMessage(R.string.dialog_whats_an_instance)
.setPositiveButton(R.string.action_close, null)
.show()
val textView = dialog.findViewById<TextView>(android.R.id.message)
textView?.movementMethod = LinkMovementMethod.getInstance()
}
@ -95,7 +100,6 @@ class LoginActivity : BaseActivity(), Injectable {
} else {
binding.toolbar.visibility = View.GONE
}
}
override fun requiresLogin(): Boolean {
@ -104,7 +108,7 @@ class LoginActivity : BaseActivity(), Injectable {
override fun finish() {
super.finish()
if(isAdditionalLogin()) {
if (isAdditionalLogin()) {
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right)
}
}
@ -134,8 +138,10 @@ class LoginActivity : BaseActivity(), Injectable {
}
val callback = object : Callback<AppCredentials> {
override fun onResponse(call: Call<AppCredentials>,
response: Response<AppCredentials>) {
override fun onResponse(
call: Call<AppCredentials>,
response: Response<AppCredentials>
) {
if (!response.isSuccessful) {
binding.loginButton.isEnabled = true
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
@ -148,10 +154,10 @@ class LoginActivity : BaseActivity(), Injectable {
val clientSecret = credentials.clientSecret
preferences.edit()
.putString("domain", domain)
.putString("clientId", clientId)
.putString("clientSecret", clientSecret)
.apply()
.putString("domain", domain)
.putString("clientId", clientId)
.putString("clientSecret", clientSecret)
.apply()
redirectUserToAuthorizeAndLogin(domain, clientId)
}
@ -165,11 +171,12 @@ class LoginActivity : BaseActivity(), Injectable {
}
mastodonApi
.authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri,
OAUTH_SCOPES, getString(R.string.tusky_website))
.enqueue(callback)
.authenticateApp(
domain, getString(R.string.app_name), oauthRedirectUri,
OAUTH_SCOPES, getString(R.string.tusky_website)
)
.enqueue(callback)
setLoading(true)
}
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
@ -177,10 +184,10 @@ class LoginActivity : BaseActivity(), Injectable {
* login there, and the server will redirect back to the app with its response. */
val endpoint = MastodonApi.ENDPOINT_AUTHORIZE
val parameters = mapOf(
"client_id" to clientId,
"redirect_uri" to oauthRedirectUri,
"response_type" to "code",
"scope" to OAUTH_SCOPES
"client_id" to clientId,
"redirect_uri" to oauthRedirectUri,
"response_type" to "code",
"scope" to OAUTH_SCOPES
)
val url = "https://" + domain + endpoint + "?" + toQueryString(parameters)
val uri = Uri.parse(url)
@ -224,31 +231,27 @@ class LoginActivity : BaseActivity(), Injectable {
} else {
setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s",
getString(R.string.error_retrieving_oauth_token),
response.message()))
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message()))
}
}
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s",
getString(R.string.error_retrieving_oauth_token),
t.message))
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message))
}
}
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
"authorization_code").enqueue(callback)
mastodonApi.fetchOAuthToken(
domain, clientId, clientSecret, redirectUri, code,
"authorization_code"
).enqueue(callback)
} else if (error != null) {
/* Authorization failed. Put the error response where the user can read it and they
* can try again. */
setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
Log.e(TAG, String.format("%s %s",
getString(R.string.error_authorization_denied),
error))
Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
} else {
// This case means a junk response was received somehow.
setLoading(false)
@ -340,14 +343,14 @@ class LoginActivity : BaseActivity(), Injectable {
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor)
.setNavigationBarColor(navigationbarColor)
.setNavigationBarDividerColor(navigationbarDividerColor)
.build()
.setToolbarColor(toolbarColor)
.setNavigationBarColor(navigationbarColor)
.setNavigationBarDividerColor(navigationbarDividerColor)
.build()
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(colorSchemeParams)
.build()
.setDefaultColorSchemeParams(colorSchemeParams)
.build()
try {
customTabsIntent.launchUrl(context, uri)

View File

@ -4,9 +4,9 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
@ -31,11 +31,11 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind
?: TimelineViewModel.Kind.HOME
?: TimelineViewModel.Kind.HOME
val argument = intent?.getStringExtra(ARG_ARG)
supportFragmentManager.beginTransaction()
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
.commit()
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
.commit()
}
}
@ -48,13 +48,15 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
private const val ARG_ARG = "arg"
@JvmStatic
fun newIntent(context: Context, kind: TimelineViewModel.Kind,
argument: String?): Intent {
fun newIntent(
context: Context,
kind: TimelineViewModel.Kind,
argument: String?
): Intent {
val intent = Intent(context, ModalTimelineActivity::class.java)
intent.putExtra(ARG_KIND, kind)
intent.putExtra(ARG_ARG, argument)
return intent
}
}
}

View File

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

View File

@ -19,15 +19,12 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.commit
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.TimelineViewModel.Kind
import javax.inject.Inject
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@ -44,7 +41,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
setSupportActionBar(binding.includedToolbar.toolbar)
val title = if(kind == Kind.FAVOURITES) {
val title = if (kind == Kind.FAVOURITES) {
R.string.title_favourites
} else {
R.string.title_bookmarks
@ -60,7 +57,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
val fragment = TimelineFragment.newInstance(kind)
replace(R.id.fragment_container, fragment)
}
}
override fun androidInjector() = dispatchingAndroidInjector
@ -71,15 +67,14 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
@JvmStatic
fun newFavouritesIntent(context: Context) =
Intent(context, StatusListActivity::class.java).apply {
putExtra(EXTRA_KIND, Kind.FAVOURITES.name)
}
Intent(context, StatusListActivity::class.java).apply {
putExtra(EXTRA_KIND, Kind.FAVOURITES.name)
}
@JvmStatic
fun newBookmarksIntent(context: Context) =
Intent(context, StatusListActivity::class.java).apply {
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
}
Intent(context, StatusListActivity::class.java).apply {
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
}
}
}

View File

@ -20,9 +20,9 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.fragment.NotificationsFragment
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.fragment.NotificationsFragment
/** this would be a good case for a sealed class, but that does not work nice with Room */
@ -34,71 +34,72 @@ const val DIRECT = "Direct"
const val HASHTAG = "Hashtag"
const val LIST = "List"
data class TabData(val id: String,
@StringRes val text: Int,
@DrawableRes val icon: Int,
val fragment: (List<String>) -> Fragment,
val arguments: List<String> = emptyList(),
val title: (Context) -> String = { context -> context.getString(text)}
)
data class TabData(
val id: String,
@StringRes val text: Int,
@DrawableRes val icon: Int,
val fragment: (List<String>) -> Fragment,
val arguments: List<String> = emptyList(),
val title: (Context) -> String = { context -> context.getString(text) }
)
fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabData {
return when (id) {
HOME -> TabData(
HOME,
R.string.title_home,
R.drawable.ic_home_24dp,
{ TimelineFragment.newInstance(TimelineViewModel.Kind.HOME) }
HOME,
R.string.title_home,
R.drawable.ic_home_24dp,
{ TimelineFragment.newInstance(TimelineViewModel.Kind.HOME) }
)
NOTIFICATIONS -> TabData(
NOTIFICATIONS,
R.string.title_notifications,
R.drawable.ic_notifications_24dp,
{ NotificationsFragment.newInstance() }
NOTIFICATIONS,
R.string.title_notifications,
R.drawable.ic_notifications_24dp,
{ NotificationsFragment.newInstance() }
)
LOCAL -> TabData(
LOCAL,
R.string.title_public_local,
R.drawable.ic_local_24dp,
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL) }
LOCAL,
R.string.title_public_local,
R.drawable.ic_local_24dp,
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL) }
)
FEDERATED -> TabData(
FEDERATED,
R.string.title_public_federated,
R.drawable.ic_public_24dp,
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED) }
FEDERATED,
R.string.title_public_federated,
R.drawable.ic_public_24dp,
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED) }
)
DIRECT -> TabData(
DIRECT,
R.string.title_direct_messages,
R.drawable.ic_reblog_direct_24dp,
{ ConversationsFragment.newInstance() }
DIRECT,
R.string.title_direct_messages,
R.drawable.ic_reblog_direct_24dp,
{ ConversationsFragment.newInstance() }
)
HASHTAG -> TabData(
HASHTAG,
R.string.hashtags,
R.drawable.ic_hashtag,
{ args -> TimelineFragment.newHashtagInstance(args) },
arguments,
{ context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) }}
HASHTAG,
R.string.hashtags,
R.drawable.ic_hashtag,
{ args -> TimelineFragment.newHashtagInstance(args) },
arguments,
{ context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) } }
)
LIST -> TabData(
LIST,
R.string.list,
R.drawable.ic_list,
{ args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) },
arguments,
{ arguments.getOrNull(1).orEmpty() }
)
LIST,
R.string.list,
R.drawable.ic_list,
{ args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) },
arguments,
{ arguments.getOrNull(1).orEmpty() }
)
else -> throw IllegalArgumentException("unknown tab type")
}
}
fun defaultTabs(): List<TabData> {
return listOf(
createTabDataFromId(HOME),
createTabDataFromId(NOTIFICATIONS),
createTabDataFromId(LOCAL),
createTabDataFromId(FEDERATED)
createTabDataFromId(HOME),
createTabDataFromId(NOTIFICATIONS),
createTabDataFromId(LOCAL),
createTabDataFromId(FEDERATED)
)
}
}

View File

@ -221,26 +221,26 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
frameLayout.addView(editText)
val dialog = AlertDialog.Builder(this)
.setTitle(R.string.add_hashtag_title)
.setView(frameLayout)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_save) { _, _ ->
val input = editText.text.toString().trim()
if (tab == null) {
val newTab = createTabDataFromId(HASHTAG, listOf(input))
currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
} else {
val newTab = tab.copy(arguments = tab.arguments + input)
currentTabs[tabPosition] = newTab
.setTitle(R.string.add_hashtag_title)
.setView(frameLayout)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.action_save) { _, _ ->
val input = editText.text.toString().trim()
if (tab == null) {
val newTab = createTabDataFromId(HASHTAG, listOf(input))
currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
} else {
val newTab = tab.copy(arguments = tab.arguments + input)
currentTabs[tabPosition] = newTab
currentTabsAdapter.notifyItemChanged(tabPosition)
}
updateAvailableTabs()
saveTabs()
currentTabsAdapter.notifyItemChanged(tabPosition)
}
.create()
updateAvailableTabs()
saveTabs()
}
.create()
editText.onTextChanged { s, _, _, _ ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(s)
@ -254,28 +254,28 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
private fun showSelectListDialog() {
val adapter = ListSelectionAdapter(this)
mastodonApi.getLists()
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe (
{ lists ->
adapter.addAll(lists)
},
{ throwable ->
Log.e("TabPreferenceActivity", "failed to load lists", throwable)
}
)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ lists ->
adapter.addAll(lists)
},
{ throwable ->
Log.e("TabPreferenceActivity", "failed to load lists", throwable)
}
)
AlertDialog.Builder(this)
.setTitle(R.string.select_list_title)
.setAdapter(adapter) { _, position ->
val list = adapter.getItem(position)
val newTab = createTabDataFromId(LIST, listOf(list!!.id, list.title))
currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
updateAvailableTabs()
saveTabs()
}
.show()
.setTitle(R.string.select_list_title)
.setAdapter(adapter) { _, position ->
val list = adapter.getItem(position)
val newTab = createTabDataFromId(LIST, listOf(list!!.id, list.title))
currentTabs.add(newTab)
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
updateAvailableTabs()
saveTabs()
}
.show()
}
private fun validateHashtag(input: CharSequence?): Boolean {
@ -330,10 +330,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
it.tabPreferences = currentTabs
accountManager.saveAccount(it)
}
.subscribeOn(Schedulers.io())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe()
.subscribeOn(Schedulers.io())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe()
}
tabsChanged = true
}
@ -357,5 +356,4 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
private const val MIN_TAB_COUNT = 2
private const val MAX_TAB_COUNT = 5
}
}

View File

@ -68,8 +68,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
// init the custom emoji fonts
val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0)
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
.getConfig(this)
.setReplaceAll(true)
.getConfig(this)
.setReplaceAll(true)
EmojiCompat.init(emojiConfig)
// init night mode
@ -81,10 +81,10 @@ class TuskyApplication : Application(), HasAndroidInjector {
}
WorkManager.initialize(
this,
androidx.work.Configuration.Builder()
.setWorkerFactory(notificationWorkerFactory)
.build()
this,
androidx.work.Configuration.Builder()
.setWorkerFactory(notificationWorkerFactory)
.build()
)
}
@ -104,4 +104,4 @@ class TuskyApplication : Application(), HasAndroidInjector {
@JvmStatic
lateinit var localeManager: LocaleManager
}
}
}

View File

@ -61,7 +61,7 @@ import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.util.*
import java.util.Locale
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
@ -102,17 +102,16 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
// Setup the view pager.
ImagePagerAdapter(this, realAttachs, initialPosition)
} else {
imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
?: throw IllegalArgumentException("attachment list or image url has to be set")
?: throw IllegalArgumentException("attachment list or image url has to be set")
SingleImagePagerAdapter(this, imageUrl!!)
}
binding.viewPager.adapter = adapter
binding.viewPager.setCurrentItem(initialPosition, false)
binding.viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
binding.toolbar.title = getPageTitle(position)
}
@ -183,17 +182,17 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
binding.toolbar.animate().alpha(alpha)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.toolbar.visibility = visibility
animation.removeListener(this)
}
})
.start()
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
binding.toolbar.visibility = visibility
animation.removeListener(this)
}
})
.start()
}
private fun getPageTitle(position: Int): CharSequence {
if(attachments == null) {
if (attachments == null) {
return ""
}
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size)
@ -206,8 +205,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(Uri.parse(url))
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES,
getString(R.string.app_name) + "/" + filename)
request.setDestinationInExternalPublicDir(
Environment.DIRECTORY_PICTURES,
getString(R.string.app_name) + "/" + filename
)
downloadManager.enqueue(request)
}
@ -261,7 +262,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
}
private var isCreating: Boolean = false
private fun shareImage(directory: File, url: String) {
@ -270,7 +270,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
invalidateOptionsMenu()
val file = File(directory, getTemporaryMediaFilename("png"))
val futureTask: FutureTarget<Bitmap> =
Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit()
Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit()
Single.fromCallable {
val bitmap = futureTask.get()
try {
@ -284,32 +284,30 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
Log.e(TAG, "Error writing temporary media.")
}
return@fromCallable false
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnDispose {
futureTask.cancel(true)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnDispose {
futureTask.cancel(true)
}
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ result ->
Log.d(TAG, "Download image result: $result")
isCreating = false
invalidateOptionsMenu()
binding.progressBarShare.visibility = View.GONE
if (result)
shareFile(file, "image/png")
},
{ error ->
isCreating = false
invalidateOptionsMenu()
binding.progressBarShare.visibility = View.GONE
Log.e(TAG, "Failed to download image", error)
}
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ result ->
Log.d(TAG, "Download image result: $result")
isCreating = false
invalidateOptionsMenu()
binding.progressBarShare.visibility = View.GONE
if (result)
shareFile(file, "image/png")
},
{ error ->
isCreating = false
invalidateOptionsMenu()
binding.progressBarShare.visibility = View.GONE
Log.e(TAG, "Failed to download image", error)
}
)
)
}
private fun shareMediaFile(directory: File, url: String) {
@ -352,7 +350,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
}
abstract class ViewMediaAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) {
abstract class ViewMediaAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
abstract fun onTransitionEnd(position: Int)
}

View File

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

View File

@ -16,20 +16,23 @@
package com.keylesspalace.tusky.adapter
import android.text.method.LinkMovementMethod
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Field
import com.keylesspalace.tusky.entity.IdentityProof
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.emojify
class AccountFieldAdapter(
private val linkListener: LinkListener,
private val animateEmojis: Boolean
private val linkListener: LinkListener,
private val animateEmojis: Boolean
) : RecyclerView.Adapter<BindingHolder<ItemAccountFieldBinding>>() {
var emojis: List<Emoji> = emptyList()
@ -47,7 +50,7 @@ class AccountFieldAdapter(
val nameTextView = holder.binding.accountFieldName
val valueTextView = holder.binding.accountFieldValue
if(proofOrField.isLeft()) {
if (proofOrField.isLeft()) {
val identityProof = proofOrField.asLeft()
nameTextView.text = identityProof.provider
@ -55,7 +58,7 @@ class AccountFieldAdapter(
valueTextView.movementMethod = LinkMovementMethod.getInstance()
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
} else {
val field = proofOrField.asRight()
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
@ -64,12 +67,11 @@ class AccountFieldAdapter(
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener)
if(field.verifiedAt != null) {
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
if (field.verifiedAt != null) {
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
} else {
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 )
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
}
}
}
}

View File

@ -34,7 +34,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
fields.forEach { field ->
fieldData.add(MutableStringPair(field.name, field.value))
}
if(fieldData.isEmpty()) {
if (fieldData.isEmpty()) {
fieldData.add(MutableStringPair("", ""))
}
@ -63,7 +63,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
holder.binding.accountFieldName.setText(fieldData[position].first)
holder.binding.accountFieldValue.setText(fieldData[position].second)
holder.binding.accountFieldName.addTextChangedListener(object: TextWatcher {
holder.binding.accountFieldName.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(newText: Editable) {
fieldData[holder.bindingAdapterPosition].first = newText.toString()
}
@ -73,7 +73,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
holder.binding.accountFieldValue.addTextChangedListener(object: TextWatcher {
holder.binding.accountFieldValue.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(newText: Editable) {
fieldData[holder.bindingAdapterPosition].second = newText.toString()
}
@ -82,9 +82,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
}
class MutableStringPair (var first: String, var second: String)
class MutableStringPair(var first: String, var second: String)
}

View File

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

View File

@ -77,4 +77,4 @@ class BlocksAdapter(
itemView.setOnClickListener { listener.onViewAccount(id) }
}
}
}
}

View File

@ -22,15 +22,15 @@ import com.bumptech.glide.Glide
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.util.BindingHolder
import java.util.*
import java.util.Locale
class EmojiAdapter(
emojiList: List<Emoji>,
private val onEmojiSelectedListener: OnEmojiSelectedListener
emojiList: List<Emoji>,
private val onEmojiSelectedListener: OnEmojiSelectedListener
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
private val emojiList : List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
override fun getItemCount() = emojiList.size
@ -44,8 +44,8 @@ class EmojiAdapter(
val emojiImageView = holder.binding.root
Glide.with(emojiImageView)
.load(emoji.url)
.into(emojiImageView)
.load(emoji.url)
.into(emojiImageView)
emojiImageView.setOnClickListener {
onEmojiSelectedListener.onEmojiSelected(emoji.shortcode)

View File

@ -16,7 +16,6 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.interfaces.AccountActionListener
@ -36,4 +35,4 @@ class FollowAdapter(
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
viewHolder.setupActionListener(accountActionListener)
}
}
}

View File

@ -24,11 +24,14 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.util.visible
class FollowRequestViewHolder(
private val binding: ItemFollowRequestBinding,
private val showHeader: Boolean
private val binding: ItemFollowRequestBinding,
private val showHeader: Boolean
) : RecyclerView.ViewHolder(binding.root) {
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {

View File

@ -16,8 +16,6 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.interfaces.AccountActionListener
@ -38,4 +36,4 @@ class FollowRequestsAdapter(
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
viewHolder.setupActionListener(accountActionListener, accountList[position].id)
}
}
}

View File

@ -25,7 +25,7 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_follow_requests_header, parent, false) as TextView
.inflate(R.layout.item_follow_requests_header, parent, false) as TextView
return HeaderViewHolder(view)
}
@ -34,7 +34,6 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val
}
override fun getItemCount() = if (accountLocked) 0 else 1
}
class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)

View File

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

View File

@ -13,7 +13,7 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
import java.util.*
import java.util.HashMap
/**
* Displays a list of muted accounts with mute/unmute account and mute/unmute notifications
@ -129,4 +129,4 @@ class MutesAdapter(
itemView.setOnClickListener { listener.onViewAccount(id) }
}
}
}
}

View File

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

View File

@ -38,4 +38,4 @@ class PlaceholderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
listener.onLoadMore(bindingAdapterPosition)
}
}
}
}

View File

@ -29,7 +29,7 @@ import com.keylesspalace.tusky.viewdata.PollOptionViewData
import com.keylesspalace.tusky.viewdata.buildDescription
import com.keylesspalace.tusky.viewdata.calculatePercent
class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
private var pollOptions: List<PollOptionViewData> = emptyList()
private var voteCount: Int = 0
@ -40,13 +40,14 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
private var animateEmojis = false
fun setup(
options: List<PollOptionViewData>,
voteCount: Int,
votersCount: Int?,
emojis: List<Emoji>,
mode: Int,
resultClickListener: View.OnClickListener?,
animateEmojis: Boolean) {
options: List<PollOptionViewData>,
voteCount: Int,
votersCount: Int?,
emojis: List<Emoji>,
mode: Int,
resultClickListener: View.OnClickListener?,
animateEmojis: Boolean
) {
this.pollOptions = options
this.voteCount = voteCount
this.votersCount = votersCount
@ -57,12 +58,11 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
notifyDataSetChanged()
}
fun getSelected() : List<Int> {
fun getSelected(): List<Int> {
return pollOptions.filter { it.selected }
.map { pollOptions.indexOf(it) }
.map { pollOptions.indexOf(it) }
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BindingHolder(binding)
@ -82,12 +82,12 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
radioButton.visible(mode == SINGLE)
checkBox.visible(mode == MULTIPLE)
when(mode) {
when (mode) {
RESULT -> {
val percent = calculatePercent(option.votesCount, votersCount, voteCount)
val emojifiedPollOptionText = buildDescription(option.title, percent, resultTextView.context)
.emojify(emojis, resultTextView, animateEmojis)
resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
.emojify(emojis, resultTextView, animateEmojis)
resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
val level = percent * 100
@ -114,7 +114,6 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
}
}
}
}
companion object {

View File

@ -23,7 +23,7 @@ import androidx.core.widget.TextViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
class PreviewPollOptionsAdapter : RecyclerView.Adapter<PreviewViewHolder>() {
private var options: List<String> = emptyList()
private var multiple: Boolean = false
@ -60,7 +60,6 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
textView.setOnClickListener(clickListener)
}
}
class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
class PreviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

View File

@ -43,10 +43,11 @@ interface ItemInteractionListener {
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
}
class TabAdapter(private var data: List<TabData>,
private val small: Boolean,
private val listener: ItemInteractionListener,
private var removeButtonEnabled: Boolean = false
class TabAdapter(
private var data: List<TabData>,
private val small: Boolean,
private val listener: ItemInteractionListener,
private var removeButtonEnabled: Boolean = false
) : RecyclerView.Adapter<BindingHolder<ViewBinding>>() {
fun updateData(newData: List<TabData>) {
@ -77,7 +78,6 @@ class TabAdapter(private var data: List<TabData>,
binding.textView.setOnClickListener {
listener.onTabAdded(tab)
}
} else {
val binding = holder.binding as ItemTabPreferenceBinding
@ -102,9 +102,9 @@ class TabAdapter(private var data: List<TabData>,
}
binding.removeButton.isEnabled = removeButtonEnabled
ThemeUtils.setDrawableTint(
holder.itemView.context,
binding.removeButton.drawable,
(if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled)
holder.itemView.context,
binding.removeButton.drawable,
(if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled)
)
if (tab.id == HASHTAG) {
@ -118,14 +118,14 @@ class TabAdapter(private var data: List<TabData>,
tab.arguments.forEachIndexed { i, arg ->
val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
?: Chip(context).apply {
binding.chipGroup.addView(this, binding.chipGroup.size - 1)
chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary))
}
?: Chip(context).apply {
binding.chipGroup.addView(this, binding.chipGroup.size - 1)
chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary))
}
chip.text = arg
if(tab.arguments.size <= 1) {
if (tab.arguments.size <= 1) {
chip.chipIcon = null
chip.setOnClickListener(null)
} else {
@ -136,14 +136,13 @@ class TabAdapter(private var data: List<TabData>,
}
}
while(binding.chipGroup.size - 1 > tab.arguments.size) {
while (binding.chipGroup.size - 1 > tab.arguments.size) {
binding.chipGroup.removeViewAt(tab.arguments.size)
}
binding.actionChip.setOnClickListener {
listener.onActionChipClicked(tab, holder.bindingAdapterPosition)
}
} else {
binding.chipGroup.hide()
}

View File

@ -111,8 +111,8 @@ class ThreadAdapter(
fun getItem(position: Int): StatusViewData.Concrete? = statuses.getOrNull(position)
fun setDetailedStatusPosition(position: Int) {
if (position != detailedStatusPosition
&& detailedStatusPosition != RecyclerView.NO_POSITION
if (position != detailedStatusPosition &&
detailedStatusPosition != RecyclerView.NO_POSITION
) {
val prior = detailedStatusPosition
detailedStatusPosition = position
@ -126,4 +126,4 @@ class ThreadAdapter(
private const val VIEW_TYPE_STATUS = 0
private const val VIEW_TYPE_STATUS_DETAILED = 1
}
}
}

View File

@ -9,10 +9,10 @@ import io.reactivex.rxjava3.schedulers.Schedulers
import javax.inject.Inject
class CacheUpdater @Inject constructor(
eventHub: EventHub,
accountManager: AccountManager,
private val appDatabase: AppDatabase,
gson: Gson
eventHub: EventHub,
accountManager: AccountManager,
private val appDatabase: AppDatabase,
gson: Gson
) {
private val disposable: Disposable
@ -27,7 +27,7 @@ class CacheUpdater @Inject constructor(
is ReblogEvent ->
timelineDao.setReblogged(accountId, event.statusId, event.reblog)
is BookmarkEvent ->
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark )
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark)
is UnfollowEvent ->
timelineDao.removeAllByUser(accountId, event.accountId)
is StatusDeletedEvent ->
@ -49,7 +49,7 @@ class CacheUpdater @Inject constructor(
appDatabase.timelineDao().removeAllForAccount(accountId)
appDatabase.timelineDao().removeAllUsersForAccount(accountId)
}
.subscribeOn(Schedulers.io())
.subscribe()
.subscribeOn(Schedulers.io())
.subscribe()
}
}
}

View File

@ -19,6 +19,6 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
data class DomainMuteEvent(val instance: String): Dispatchable
data class AnnouncementReadEvent(val announcementId: String): Dispatchable
data class PinEvent(val statusId: String, val pinned: Boolean): Dispatchable
data class DomainMuteEvent(val instance: String) : Dispatchable
data class AnnouncementReadEvent(val announcementId: String) : Dispatchable
data class PinEvent(val statusId: String, val pinned: Boolean) : Dispatchable

View File

@ -19,4 +19,4 @@ object EventHubImpl : EventHub {
override fun dispatch(event: Dispatchable) {
eventsSubject.onNext(event)
}
}
}

View File

@ -31,17 +31,17 @@ import com.keylesspalace.tusky.util.BindingHolder
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.emojify
interface AnnouncementActionListener: LinkListener {
interface AnnouncementActionListener : LinkListener {
fun openReactionPicker(announcementId: String, target: View)
fun addReaction(announcementId: String, name: String)
fun removeReaction(announcementId: String, name: String)
}
class AnnouncementAdapter(
private var items: List<Announcement> = emptyList(),
private val listener: AnnouncementActionListener,
private val wellbeingEnabled: Boolean = false,
private val animateEmojis: Boolean = false
private var items: List<Announcement> = emptyList(),
private val listener: AnnouncementActionListener,
private val wellbeingEnabled: Boolean = false,
private val animateEmojis: Boolean = false
) : RecyclerView.Adapter<BindingHolder<ItemAnnouncementBinding>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> {
@ -67,12 +67,12 @@ class AnnouncementAdapter(
}
item.reactions.forEachIndexed { i, reaction ->
(chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
isCheckable = true
checkedIcon = null
chips.addView(this, i)
})
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
isCheckable = true
checkedIcon = null
chips.addView(this, i)
}
.apply {
val emojiText = if (reaction.url == null) {
reaction.name
@ -80,16 +80,18 @@ class AnnouncementAdapter(
context.getString(R.string.emoji_shortcode_format, reaction.name)
}
this.text = ("$emojiText ${reaction.count}")
.emojify(
listOf(Emoji(
reaction.name,
reaction.url ?: "",
reaction.staticUrl ?: "",
null
)),
this,
animateEmojis
)
.emojify(
listOf(
Emoji(
reaction.name,
reaction.url ?: "",
reaction.staticUrl ?: "",
null
)
),
this,
animateEmojis
)
isChecked = reaction.me

View File

@ -34,7 +34,12 @@ import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EmojiPicker
import javax.inject.Inject
@ -52,13 +57,13 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
private val picker by lazy { EmojiPicker(this) }
private val pickerDialog by lazy {
PopupWindow(this)
.apply {
contentView = picker
isFocusable = true
setOnDismissListener {
currentAnnouncementId = null
}
.apply {
contentView = picker
isFocusable = true
setOnDismissListener {
currentAnnouncementId = null
}
}
}
private var currentAnnouncementId: String? = null

View File

@ -27,15 +27,20 @@ import com.keylesspalace.tusky.entity.Announcement
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.Success
import io.reactivex.rxjava3.core.Single
import javax.inject.Inject
class AnnouncementsViewModel @Inject constructor(
accountManager: AccountManager,
private val appDatabase: AppDatabase,
private val mastodonApi: MastodonApi,
private val eventHub: EventHub
accountManager: AccountManager,
private val appDatabase: AppDatabase,
private val mastodonApi: MastodonApi,
private val eventHub: EventHub
) : RxAwareViewModel() {
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
@ -45,139 +50,153 @@ class AnnouncementsViewModel @Inject constructor(
val emojis: LiveData<List<Emoji>> = emojisMutable
init {
Single.zip(mastodonApi.getCustomEmojis(),
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
.onErrorResumeNext {
mastodonApi.getInstance()
.map { Either.Right(it) }
},
{ emojis, either ->
either.asLeftOrNull()?.copy(emojiList = emojis)
?: InstanceEntity(
accountManager.activeAccount?.domain!!,
emojis,
either.asRight().maxTootChars,
either.asRight().pollLimits?.maxOptions,
either.asRight().pollLimits?.maxOptionChars,
either.asRight().version
)
})
.doOnSuccess {
appDatabase.instanceDao().insertOrReplace(it)
}
.subscribe({
Single.zip(
mastodonApi.getCustomEmojis(),
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
.onErrorResumeNext {
mastodonApi.getInstance()
.map { Either.Right(it) }
},
{ emojis, either ->
either.asLeftOrNull()?.copy(emojiList = emojis)
?: InstanceEntity(
accountManager.activeAccount?.domain!!,
emojis,
either.asRight().maxTootChars,
either.asRight().pollLimits?.maxOptions,
either.asRight().pollLimits?.maxOptionChars,
either.asRight().version
)
}
)
.doOnSuccess {
appDatabase.instanceDao().insertOrReplace(it)
}
.subscribe(
{
emojisMutable.postValue(it.emojiList.orEmpty())
}, {
},
{
Log.w(TAG, "Failed to get custom emojis.", it)
})
.autoDispose()
}
)
.autoDispose()
}
fun load() {
announcementsMutable.postValue(Loading())
mastodonApi.listAnnouncements()
.subscribe({
.subscribe(
{
announcementsMutable.postValue(Success(it))
it.filter { announcement -> !announcement.read }
.forEach { announcement ->
mastodonApi.dismissAnnouncement(announcement.id)
.subscribe(
{
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
},
{ throwable ->
Log.d(TAG, "Failed to mark announcement as read.", throwable)
}
)
.autoDispose()
}
}, {
.forEach { announcement ->
mastodonApi.dismissAnnouncement(announcement.id)
.subscribe(
{
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
},
{ throwable ->
Log.d(TAG, "Failed to mark announcement as read.", throwable)
}
)
.autoDispose()
}
},
{
announcementsMutable.postValue(Error(cause = it))
})
.autoDispose()
}
)
.autoDispose()
}
fun addReaction(announcementId: String, name: String) {
mastodonApi.addAnnouncementReaction(announcementId, name)
.subscribe({
.subscribe(
{
announcementsMutable.postValue(
Success(
announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) {
announcement.copy(
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
announcement.reactions.map { reaction ->
if (reaction.name == name) {
reaction.copy(
count = reaction.count + 1,
me = true
)
} else {
reaction
}
}
} else {
listOf(
*announcement.reactions.toTypedArray(),
emojis.value!!.find { emoji -> emoji.shortcode == name }
!!.run {
Announcement.Reaction(
name,
1,
true,
url,
staticUrl
)
}
)
}
)
Success(
announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) {
announcement.copy(
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
announcement.reactions.map { reaction ->
if (reaction.name == name) {
reaction.copy(
count = reaction.count + 1,
me = true
)
} else {
reaction
}
}
} else {
announcement
listOf(
*announcement.reactions.toTypedArray(),
emojis.value!!.find { emoji -> emoji.shortcode == name }
!!.run {
Announcement.Reaction(
name,
1,
true,
url,
staticUrl
)
}
)
}
}
)
)
} else {
announcement
}
}
)
)
}, {
},
{
Log.w(TAG, "Failed to add reaction to the announcement.", it)
})
.autoDispose()
}
)
.autoDispose()
}
fun removeReaction(announcementId: String, name: String) {
mastodonApi.removeAnnouncementReaction(announcementId, name)
.subscribe({
.subscribe(
{
announcementsMutable.postValue(
Success(
announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) {
announcement.copy(
reactions = announcement.reactions.mapNotNull { reaction ->
if (reaction.name == name) {
if (reaction.count > 1) {
reaction.copy(
count = reaction.count - 1,
me = false
)
} else {
null
}
} else {
reaction
}
}
)
} else {
announcement
Success(
announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) {
announcement.copy(
reactions = announcement.reactions.mapNotNull { reaction ->
if (reaction.name == name) {
if (reaction.count > 1) {
reaction.copy(
count = reaction.count - 1,
me = false
)
} else {
null
}
} else {
reaction
}
}
}
)
)
} else {
announcement
}
}
)
)
}, {
},
{
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
})
.autoDispose()
}
)
.autoDispose()
}
companion object {

View File

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

View File

@ -30,9 +30,9 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.view.ProgressImageView
class MediaPreviewAdapter(
context: Context,
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
context: Context,
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
fun submitList(list: List<ComposeActivity.QueuedMedia>) {
@ -57,7 +57,7 @@ class MediaPreviewAdapter(
}
private val thumbnailViewSize =
context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
override fun getItemCount(): Int = differ.currentList.size
@ -74,31 +74,34 @@ class MediaPreviewAdapter(
holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
} else {
Glide.with(holder.itemView.context)
.load(item.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.into(holder.progressImageView)
.load(item.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.into(holder.progressImageView)
}
}
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem.localId == newItem.localId
}
private val differ = AsyncListDiffer(
this,
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem.localId == newItem.localId
}
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem == newItem
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem == newItem
}
}
})
)
inner class PreviewViewHolder(val progressImageView: ProgressImageView)
: RecyclerView.ViewHolder(progressImageView) {
inner class PreviewViewHolder(val progressImageView: ProgressImageView) :
RecyclerView.ViewHolder(progressImageView) {
init {
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
val marginBottom = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
layoutParams.setMargins(margin, 0, margin, marginBottom)
progressImageView.layoutParams = layoutParams
progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP
@ -107,4 +110,4 @@ class MediaPreviewAdapter(
}
}
}
}
}

View File

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

View File

@ -26,33 +26,33 @@ import com.keylesspalace.tusky.databinding.DialogAddPollBinding
import com.keylesspalace.tusky.entity.NewPoll
fun showAddPollDialog(
context: Context,
poll: NewPoll?,
maxOptionCount: Int,
maxOptionLength: Int,
onUpdatePoll: (NewPoll) -> Unit
context: Context,
poll: NewPoll?,
maxOptionCount: Int,
maxOptionLength: Int,
onUpdatePoll: (NewPoll) -> Unit
) {
val binding = DialogAddPollBinding.inflate(LayoutInflater.from(context))
val dialog = AlertDialog.Builder(context)
.setIcon(R.drawable.ic_poll_24dp)
.setTitle(R.string.create_poll_title)
.setView(binding.root)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, null)
.create()
.setIcon(R.drawable.ic_poll_24dp)
.setTitle(R.string.create_poll_title)
.setView(binding.root)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, null)
.create()
val adapter = AddPollOptionsAdapter(
options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
maxOptionLength = maxOptionLength,
onOptionRemoved = { valid ->
binding.addChoiceButton.isEnabled = true
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
},
onOptionChanged = { valid ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
}
options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
maxOptionLength = maxOptionLength,
onOptionRemoved = { valid ->
binding.addChoiceButton.isEnabled = true
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
},
onOptionChanged = { valid ->
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
}
)
binding.pollChoices.adapter = adapter
@ -80,13 +80,15 @@ fun showAddPollDialog(
val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition
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,
expiresIn = pollDuration,
multiple = binding.multipleChoicesCheckBox.isChecked
))
)
)
dialog.dismiss()
}
@ -96,4 +98,4 @@ fun showAddPollDialog(
// make the dialog focusable so the keyboard does not stay behind it
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
}
}

View File

@ -27,11 +27,11 @@ import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.visible
class AddPollOptionsAdapter(
private var options: MutableList<String>,
private val maxOptionLength: Int,
private val onOptionRemoved: (Boolean) -> Unit,
private val onOptionChanged: (Boolean) -> Unit
): RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() {
private var options: MutableList<String>,
private val maxOptionLength: Int,
private val onOptionRemoved: (Boolean) -> Unit,
private val onOptionChanged: (Boolean) -> Unit
) : RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() {
val pollOptions: List<String>
get() = options.toList()
@ -48,7 +48,7 @@ class AddPollOptionsAdapter(
binding.optionEditText.onTextChanged { s, _, _, _ ->
val pos = holder.bindingAdapterPosition
if(pos != RecyclerView.NO_POSITION) {
if (pos != RecyclerView.NO_POSITION) {
options[pos] = s.toString()
onOptionChanged(validateInput())
}

View File

@ -40,9 +40,10 @@ import com.keylesspalace.tusky.util.withLifecycleContext
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
fun <T> T.makeCaptionDialog(existingDescription: String?,
previewUri: Uri,
onUpdateDescription: (String) -> LiveData<Boolean>
fun <T> T.makeCaptionDialog(
existingDescription: String?,
previewUri: Uri,
onUpdateDescription: (String) -> LiveData<Boolean>
) where T : Activity, T : LifecycleOwner {
val dialogLayout = LinearLayout(this)
val padding = Utils.dpToPx(this, 8)
@ -60,14 +61,18 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
val input = EditText(this)
input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired,
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT)
input.hint = resources.getQuantityString(
R.plurals.hint_describe_for_visually_impaired,
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT
)
dialogLayout.addView(input)
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
input.setLines(2)
input.inputType = (InputType.TYPE_CLASS_TEXT
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
input.inputType = (
InputType.TYPE_CLASS_TEXT
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
)
input.setText(existingDescription)
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
@ -75,41 +80,40 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
onUpdateDescription(input.text.toString())
withLifecycleContext {
onUpdateDescription(input.text.toString())
.observe { success -> if (!success) showFailedCaptionMessage() }
.observe { success -> if (!success) showFailedCaptionMessage() }
}
dialog.dismiss()
}
val dialog = AlertDialog.Builder(this)
.setView(dialogLayout)
.setPositiveButton(android.R.string.ok, okListener)
.setNegativeButton(android.R.string.cancel, null)
.create()
.setView(dialogLayout)
.setPositiveButton(android.R.string.ok, okListener)
.setNegativeButton(android.R.string.cancel, null)
.create()
val window = dialog.window
window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
dialog.show()
// Load the image and manually set it into the ImageView because it doesn't have a fixed size.
Glide.with(this)
.load(previewUri)
.downsample(DownsampleStrategy.CENTER_INSIDE)
.into(object : CustomTarget<Drawable>(4096, 4096) {
override fun onLoadCleared(placeholder: Drawable?) {
imageView.setImageDrawable(placeholder)
}
.load(previewUri)
.downsample(DownsampleStrategy.CENTER_INSIDE)
.into(object : CustomTarget<Drawable>(4096, 4096) {
override fun onLoadCleared(placeholder: Drawable?) {
imageView.setImageDrawable(placeholder)
}
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
imageView.setImageDrawable(resource)
}
})
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
imageView.setImageDrawable(resource)
}
})
}
private fun Activity.showFailedCaptionMessage() {
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
}

View File

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

View File

@ -16,25 +16,27 @@
package com.keylesspalace.tusky.components.compose.view
import android.content.Context
import androidx.emoji.widget.EmojiEditTextHelper
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
import android.text.InputType
import android.text.method.KeyListener
import android.util.AttributeSet
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
import androidx.core.view.inputmethod.EditorInfoCompat
import androidx.core.view.inputmethod.InputConnectionCompat
import androidx.emoji.widget.EmojiEditTextHelper
class EditTextTyped @JvmOverloads constructor(context: Context,
attributeSet: AttributeSet? = null)
: AppCompatMultiAutoCompleteTextView(context, attributeSet) {
class EditTextTyped @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null
) :
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
init {
//fix a bug with autocomplete and some keyboards
// fix a bug with autocomplete and some keyboards
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
inputType = newInputType
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener))
@ -52,8 +54,13 @@ class EditTextTyped @JvmOverloads constructor(context: Context,
val connection = super.onCreateInputConnection(editorInfo)
return if (onCommitContentListener != null) {
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
getEmojiEditTextHelper().onCreateInputConnection(InputConnectionCompat.createWrapper(connection, editorInfo,
onCommitContentListener!!), editorInfo)!!
getEmojiEditTextHelper().onCreateInputConnection(
InputConnectionCompat.createWrapper(
connection, editorInfo,
onCommitContentListener!!
),
editorInfo
)!!
} else {
connection
}

View File

@ -25,10 +25,11 @@ import com.keylesspalace.tusky.databinding.ViewPollPreviewBinding
import com.keylesspalace.tusky.entity.NewPoll
class PollPreviewView @JvmOverloads constructor(
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0)
: LinearLayout(context, attrs, defStyleAttr) {
context: Context?,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) :
LinearLayout(context, attrs, defStyleAttr) {
private val adapter = PreviewPollOptionsAdapter()
@ -46,7 +47,7 @@ class PollPreviewView @JvmOverloads constructor(
binding.pollPreviewOptions.adapter = adapter
}
fun setPoll(poll: NewPoll){
fun setPoll(poll: NewPoll) {
adapter.update(poll.options, poll.multiple)
val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast {
@ -59,4 +60,4 @@ class PollPreviewView @JvmOverloads constructor(
super.setOnClickListener(l)
adapter.setOnClickListener(l)
}
}
}

View File

@ -28,15 +28,15 @@ import com.mikepenz.iconics.utils.sizeDp
class TootButton
@JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : MaterialButton(context, attrs, defStyleAttr) {
private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button)
init {
if(smallStyle) {
if (smallStyle) {
setIconResource(R.drawable.ic_send_24dp)
} else {
setText(R.string.action_send)
@ -47,7 +47,7 @@ class TootButton
}
fun setStatusVisibility(visibility: Status.Visibility) {
if(!smallStyle) {
if (!smallStyle) {
icon = when (visibility) {
Status.Visibility.PUBLIC -> {
@ -68,8 +68,5 @@ class TootButton
}
}
}
}
}

View File

@ -30,7 +30,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.shouldTrimStatus
import java.util.Date
@Entity(primaryKeys = ["id","accountId"])
@Entity(primaryKeys = ["id", "accountId"])
@TypeConverters(Converters::class)
data class ConversationEntity(
val accountId: Long,
@ -98,7 +98,7 @@ data class ConversationStatusEntity(
if (inReplyToId != other.inReplyToId) return false
if (inReplyToAccountId != other.inReplyToAccountId) return false
if (account != other.account) return false
if (content.toString() != other.content.toString()) return false //TODO find a better method to compare two spanned strings
if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings
if (createdAt != other.createdAt) return false
if (emojis != other.emojis) return false
if (favouritesCount != other.favouritesCount) return false
@ -157,7 +157,7 @@ data class ConversationStatusEntity(
reblogged = false,
favourited = favourited,
bookmarked = bookmarked,
sensitive= sensitive,
sensitive = sensitive,
spoilerText = spoilerText,
visibility = Status.Visibility.DIRECT,
attachments = attachments,
@ -166,7 +166,8 @@ data class ConversationStatusEntity(
pinned = false,
muted = muted,
poll = poll,
card = null)
card = null
)
}
}

View File

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

View File

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

View File

@ -32,7 +32,6 @@ class ConversationsRepository @Inject constructor(
Single.fromCallable {
db.conversationDao().deleteForAccount(accountId)
}.subscribeOn(Schedulers.io())
.subscribe()
.subscribe()
}
}
}

View File

@ -28,18 +28,17 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.DraftAttachment
class DraftMediaAdapter(
private val attachmentClick: () -> Unit
private val attachmentClick: () -> Unit
) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>(
object: DiffUtil.ItemCallback<DraftAttachment>() {
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
object : DiffUtil.ItemCallback<DraftAttachment>() {
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder {
@ -52,24 +51,24 @@ class DraftMediaAdapter(
holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
} else {
Glide.with(holder.itemView.context)
.load(attachment.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.into(holder.imageView)
.load(attachment.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.into(holder.imageView)
}
}
}
inner class DraftMediaViewHolder(val imageView: ImageView)
: RecyclerView.ViewHolder(imageView) {
inner class DraftMediaViewHolder(val imageView: ImageView) :
RecyclerView.ViewHolder(imageView) {
init {
val thumbnailViewSize =
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
val marginBottom = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
layoutParams.setMargins(margin, 0, margin, marginBottom)
imageView.layoutParams = layoutParams
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
@ -78,4 +77,4 @@ class DraftMediaAdapter(
}
}
}
}
}

View File

@ -91,27 +91,28 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
if (draft.inReplyToId != null) {
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
viewModel.getToot(draft.inReplyToId)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this))
.subscribe({ status ->
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this))
.subscribe(
{ status ->
val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id,
tootText = draft.content,
contentWarning = draft.contentWarning,
inReplyToId = draft.inReplyToId,
replyingStatusContent = status.content.toString(),
replyingStatusAuthor = status.account.localUsername,
draftAttachments = draft.attachments,
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility
draftId = draft.id,
tootText = draft.content,
contentWarning = draft.contentWarning,
inReplyToId = draft.inReplyToId,
replyingStatusContent = status.content.toString(),
replyingStatusAuthor = status.account.localUsername,
draftAttachments = draft.attachments,
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility
)
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
startActivity(ComposeActivity.startIntent(this, composeOptions))
}, { throwable ->
},
{ throwable ->
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
@ -124,9 +125,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
openDraftWithoutReply(draft)
} else {
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
.show()
.show()
}
})
}
)
} else {
openDraftWithoutReply(draft)
}
@ -134,13 +136,13 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
private fun openDraftWithoutReply(draft: DraftEntity) {
val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id,
tootText = draft.content,
contentWarning = draft.contentWarning,
draftAttachments = draft.attachments,
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility
draftId = draft.id,
tootText = draft.content,
contentWarning = draft.contentWarning,
draftAttachments = draft.attachments,
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility
)
startActivity(ComposeActivity.startIntent(this, composeOptions))
@ -149,10 +151,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
override fun onDeleteDraft(draft: DraftEntity) {
viewModel.deleteDraft(draft)
Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) {
viewModel.restoreDraft(draft)
}
.show()
.setAction(R.string.action_undo) {
viewModel.restoreDraft(draft)
}
.show()
}
companion object {

View File

@ -9,7 +9,7 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
class InstanceListActivity: BaseActivity(), HasAndroidInjector {
class InstanceListActivity : BaseActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@ -27,11 +27,10 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
}
supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, InstanceListFragment())
.commit()
.beginTransaction()
.replace(R.id.fragment_container, InstanceListFragment())
.commit()
}
override fun androidInjector() = androidInjector
}
}

View File

@ -8,8 +8,8 @@ import com.keylesspalace.tusky.databinding.ItemMutedDomainBinding
import com.keylesspalace.tusky.util.BindingHolder
class DomainMutesAdapter(
private val actionListener: InstanceActionListener
): RecyclerView.Adapter<BindingHolder<ItemMutedDomainBinding>>() {
private val actionListener: InstanceActionListener
) : RecyclerView.Adapter<BindingHolder<ItemMutedDomainBinding>>() {
var instances: MutableList<String> = mutableListOf()
var bottomLoading: Boolean = false

View File

@ -29,7 +29,7 @@ import retrofit2.Response
import java.io.IOException
import javax.inject.Inject
class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
class InstanceListFragment : Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
@Inject
lateinit var api: MastodonApi
@ -65,7 +65,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
override fun mute(mute: Boolean, instance: String, position: Int) {
if (mute) {
api.blockDomain(instance).enqueue(object: Callback<Any> {
api.blockDomain(instance).enqueue(object : Callback<Any> {
override fun onFailure(call: Call<Any>, t: Throwable) {
Log.e(TAG, "Error muting domain $instance")
}
@ -79,7 +79,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
}
})
} else {
api.unblockDomain(instance).enqueue(object: Callback<Any> {
api.unblockDomain(instance).enqueue(object : Callback<Any> {
override fun onFailure(call: Call<Any>, t: Throwable) {
Log.e(TAG, "Error unmuting domain $instance")
}
@ -88,10 +88,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
if (response.isSuccessful) {
adapter.removeItem(position)
Snackbar.make(binding.recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) {
mute(true, instance, position)
}
.show()
.setAction(R.string.action_undo) {
mute(true, instance, position)
}
.show()
} else {
Log.e(TAG, "Error unmuting domain $instance")
}
@ -112,9 +112,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
}
api.domainBlocks(id, bottomId)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ response ->
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ response ->
val instances = response.body()
if (response.isSuccessful && instances != null) {
@ -122,9 +123,11 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
} else {
onFetchInstancesFailure(Exception(response.message()))
}
}, {throwable ->
},
{ throwable ->
onFetchInstancesFailure(throwable)
})
}
)
}
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
@ -141,9 +144,9 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
if (adapter.itemCount == 0) {
binding.messageView.show()
binding.messageView.setup(
R.drawable.elephant_friend_empty,
R.string.message_empty,
null
R.drawable.elephant_friend_empty,
R.string.message_empty,
null
)
} else {
binding.messageView.hide()
@ -174,4 +177,4 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
companion object {
private const val TAG = "InstanceList" // logging tag
}
}
}

View File

@ -2,4 +2,4 @@ package com.keylesspalace.tusky.components.instancemute.interfaces
interface InstanceActionListener {
fun mute(mute: Boolean, instance: String, position: Int)
}
}

View File

@ -10,9 +10,9 @@ import com.keylesspalace.tusky.util.isLessThan
import javax.inject.Inject
class NotificationFetcher @Inject constructor(
private val mastodonApi: MastodonApi,
private val accountManager: AccountManager,
private val notifier: Notifier
private val mastodonApi: MastodonApi,
private val accountManager: AccountManager,
private val notifier: Notifier
) {
fun fetchAndShow() {
for (account in accountManager.getAllAccountsOrderedByActive()) {
@ -39,9 +39,9 @@ class NotificationFetcher @Inject constructor(
}
Log.d(TAG, "getting Notifications for " + account.fullName)
val notifications = mastodonApi.notificationsWithAuth(
authHeader,
account.domain,
account.lastNotificationId
authHeader,
account.domain,
account.lastNotificationId
).blockingGet()
val newId = account.lastNotificationId
@ -63,9 +63,9 @@ class NotificationFetcher @Inject constructor(
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
return try {
val allMarkers = mastodonApi.markersWithAuth(
authHeader,
account.domain,
listOf("notifications")
authHeader,
account.domain,
listOf("notifications")
).blockingGet()
val notificationMarker = allMarkers["notifications"]
Log.d(TAG, "Fetched marker: $notificationMarker")
@ -79,4 +79,4 @@ class NotificationFetcher @Inject constructor(
companion object {
const val TAG = "NotificationFetcher"
}
}
}

View File

@ -23,9 +23,9 @@ import androidx.work.WorkerParameters
import javax.inject.Inject
class NotificationWorker(
context: Context,
params: WorkerParameters,
private val notificationsFetcher: NotificationFetcher
context: Context,
params: WorkerParameters,
private val notificationsFetcher: NotificationFetcher
) : Worker(context, params) {
override fun doWork(): Result {
@ -35,13 +35,13 @@ class NotificationWorker(
}
class NotificationWorkerFactory @Inject constructor(
private val notificationsFetcher: NotificationFetcher
private val notificationsFetcher: NotificationFetcher
) : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
if (workerClassName == NotificationWorker::class.java.name) {
return NotificationWorker(appContext, workerParameters, notificationsFetcher)

View File

@ -12,9 +12,9 @@ interface Notifier {
}
class SystemNotifier(
private val context: Context
private val context: Context
) : Notifier {
override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) {
NotificationHelper.make(context, notification, account, isFirstInBatch)
}
}
}

View File

@ -22,7 +22,11 @@ import android.util.Log
import androidx.annotation.DrawableRes
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.AccountListActivity
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.FiltersActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TabPreferenceActivity
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
@ -33,7 +37,12 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.listPreference
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
@ -75,8 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setOnPreferenceClickListener {
val intent = Intent(context, TabPreferenceActivity::class.java)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
@ -88,8 +99,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.MUTES)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
@ -104,8 +117,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
val intent = Intent(context, AccountListActivity::class.java)
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
@ -116,8 +131,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
setOnPreferenceClickListener {
val intent = Intent(context, InstanceListActivity::class.java)
activity?.startActivity(intent)
activity?.overridePendingTransition(R.anim.slide_from_right,
R.anim.slide_to_left)
activity?.overridePendingTransition(
R.anim.slide_from_right,
R.anim.slide_to_left
)
true
}
}
@ -130,7 +147,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
key = PrefKeys.DEFAULT_POST_PRIVACY
setSummaryProvider { entry }
val visibility = accountManager.activeAccount?.defaultPostPrivacy
?: Status.Visibility.PUBLIC
?: Status.Visibility.PUBLIC
value = visibility.serverString()
setIcon(getIconForVisibility(visibility))
setOnPreferenceChangeListener { _, newValue ->
@ -147,7 +164,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
key = PrefKeys.DEFAULT_MEDIA_SENSITIVITY
isSingleLineTitle = false
val sensitivity = accountManager.activeAccount?.defaultMediaSensitivity
?: false
?: false
setDefaultValue(sensitivity)
setIcon(getIconForSensitivity(sensitivity))
setOnPreferenceChangeListener { _, newValue ->
@ -201,8 +218,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
preference {
setTitle(R.string.pref_title_public_filter_keywords)
setOnPreferenceClickListener {
launchFilterActivity(Filter.PUBLIC,
R.string.pref_title_public_filter_keywords)
launchFilterActivity(
Filter.PUBLIC,
R.string.pref_title_public_filter_keywords
)
true
}
}
@ -226,8 +245,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
preference {
setTitle(R.string.pref_title_thread_filter_keywords)
setOnPreferenceClickListener {
launchFilterActivity(Filter.THREAD,
R.string.pref_title_thread_filter_keywords)
launchFilterActivity(
Filter.THREAD,
R.string.pref_title_thread_filter_keywords
)
true
}
}
@ -255,7 +276,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
it.startActivity(intent)
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
}
}
}
@ -268,36 +288,35 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null) {
mastodonApi.accountUpdateSource(visibility, sensitive)
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
val account = response.body()
if (response.isSuccessful && account != null) {
.enqueue(object : Callback<Account> {
override fun onResponse(call: Call<Account>, response: Response<Account>) {
val account = response.body()
if (response.isSuccessful && account != null) {
accountManager.activeAccount?.let {
it.defaultPostPrivacy = account.source?.privacy
?: Status.Visibility.PUBLIC
it.defaultMediaSensitivity = account.source?.sensitive ?: false
accountManager.saveAccount(it)
}
} else {
Log.e("AccountPreferences", "failed updating settings on server")
showErrorSnackbar(visibility, sensitive)
accountManager.activeAccount?.let {
it.defaultPostPrivacy = account.source?.privacy
?: Status.Visibility.PUBLIC
it.defaultMediaSensitivity = account.source?.sensitive ?: false
accountManager.saveAccount(it)
}
}
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("AccountPreferences", "failed updating settings on server", t)
} else {
Log.e("AccountPreferences", "failed updating settings on server")
showErrorSnackbar(visibility, sensitive)
}
}
})
override fun onFailure(call: Call<Account>, t: Throwable) {
Log.e("AccountPreferences", "failed updating settings on server", t)
showErrorSnackbar(visibility, sensitive)
}
})
}
private fun showErrorSnackbar(visibility: String?, sensitive: Boolean?) {
view?.let { view ->
Snackbar.make(view, R.string.pref_failed_to_sync, Snackbar.LENGTH_LONG)
.setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) }
.show()
.setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) }
.show()
}
}

View File

@ -34,8 +34,8 @@ import kotlin.system.exitProcess
* This Preference lets the user select their preferred emoji font
*/
class EmojiPreference(
context: Context,
private val okHttpClient: OkHttpClient
context: Context,
private val okHttpClient: OkHttpClient
) : Preference(context) {
private lateinit var selected: EmojiCompatFont
@ -51,7 +51,7 @@ class EmojiPreference(
// Find out which font is currently active
selected = EmojiCompatFont.byId(
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
)
// We'll use this later to determine if anything has changed
original = selected
@ -67,10 +67,10 @@ class EmojiPreference(
setupItem(SYSTEM_DEFAULT, binding.itemNomoji)
AlertDialog.Builder(context)
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
.setNegativeButton(android.R.string.cancel, null)
.show()
.setView(binding.root)
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
@ -100,32 +100,30 @@ class EmojiPreference(
binding.emojiProgress.progress = 0
binding.emojiDownloadCancel.show()
font.downloadFontFile(context, okHttpClient)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
// The progress is returned as a float between 0 and 1, or -1 if it could not determined
if (progress >= 0) {
binding.emojiProgress.isIndeterminate = false
val max = binding.emojiProgress.max.toFloat()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
binding.emojiProgress.setProgress((max * progress).toInt(), true)
} else {
binding.emojiProgress.progress = (max * progress).toInt()
}
} else {
binding.emojiProgress.isIndeterminate = true
}
},
{
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
updateItem(font, binding)
},
{
finishDownload(font, binding)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ progress ->
// The progress is returned as a float between 0 and 1, or -1 if it could not determined
if (progress >= 0) {
binding.emojiProgress.isIndeterminate = false
val max = binding.emojiProgress.max.toFloat()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
binding.emojiProgress.setProgress((max * progress).toInt(), true)
} else {
binding.emojiProgress.progress = (max * progress).toInt()
}
).also { downloadDisposables[font.id] = it }
} else {
binding.emojiProgress.isIndeterminate = true
}
},
{
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
updateItem(font, binding)
},
{
finishDownload(font, binding)
}
).also { downloadDisposables[font.id] = it }
}
private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
@ -197,10 +195,10 @@ class EmojiPreference(
val index = selected.id
Log.i(TAG, "saveSelectedFont: Font ID: $index")
PreferenceManager
.getDefaultSharedPreferences(context)
.edit()
.putInt(key, index)
.apply()
.getDefaultSharedPreferences(context)
.edit()
.putInt(key, index)
.apply()
summary = selected.getDisplay(context)
}
@ -211,29 +209,31 @@ class EmojiPreference(
saveSelectedFont()
if (selected !== original || updated) {
AlertDialog.Builder(context)
.setTitle(R.string.restart_required)
.setMessage(R.string.restart_emoji)
.setNegativeButton(R.string.later, null)
.setPositiveButton(R.string.restart) { _, _ ->
// Restart the app
// From https://stackoverflow.com/a/17166729/5070653
val launchIntent = Intent(context, SplashActivity::class.java)
val mPendingIntent = PendingIntent.getActivity(
context,
0x1f973, // This is the codepoint of the party face emoji :D
launchIntent,
PendingIntent.FLAG_CANCEL_CURRENT)
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mgr.set(
AlarmManager.RTC,
System.currentTimeMillis() + 100,
mPendingIntent)
exitProcess(0)
}.show()
.setTitle(R.string.restart_required)
.setMessage(R.string.restart_emoji)
.setNegativeButton(R.string.later, null)
.setPositiveButton(R.string.restart) { _, _ ->
// Restart the app
// From https://stackoverflow.com/a/17166729/5070653
val launchIntent = Intent(context, SplashActivity::class.java)
val mPendingIntent = PendingIntent.getActivity(
context,
0x1f973, // This is the codepoint of the party face emoji :D
launchIntent,
PendingIntent.FLAG_CANCEL_CURRENT
)
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
mgr.set(
AlarmManager.RTC,
System.currentTimeMillis() + 100,
mPendingIntent
)
exitProcess(0)
}.show()
}
}
companion object {
private const val TAG = "EmojiPreference"
}
}
}

View File

@ -111,7 +111,7 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
true
}
}
switchPreference {
setTitle(R.string.pref_title_notification_filter_subscriptions)
key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS
@ -176,5 +176,4 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
return NotificationPreferencesFragment()
}
}
}

View File

@ -36,8 +36,10 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
import javax.inject.Inject
class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
HasAndroidInjector {
class PreferencesActivity :
BaseActivity(),
SharedPreferences.OnSharedPreferenceChangeListener,
HasAndroidInjector {
@Inject
lateinit var eventHub: EventHub
@ -62,36 +64,35 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE"
val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance()
}
ACCOUNT_PREFERENCES -> {
setTitle(R.string.action_view_account_preferences)
AccountPreferencesFragment.newInstance()
}
NOTIFICATION_PREFERENCES -> {
setTitle(R.string.pref_title_edit_notification_settings)
NotificationPreferencesFragment.newInstance()
}
TAB_FILTER_PREFERENCES -> {
setTitle(R.string.pref_title_status_tabs)
TabFilterPreferencesFragment.newInstance()
}
PROXY_PREFERENCES -> {
setTitle(R.string.pref_title_http_proxy_settings)
ProxyPreferencesFragment.newInstance()
}
else -> throw IllegalArgumentException("preferenceType not known")
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance()
}
ACCOUNT_PREFERENCES -> {
setTitle(R.string.action_view_account_preferences)
AccountPreferencesFragment.newInstance()
}
NOTIFICATION_PREFERENCES -> {
setTitle(R.string.pref_title_edit_notification_settings)
NotificationPreferencesFragment.newInstance()
}
TAB_FILTER_PREFERENCES -> {
setTitle(R.string.pref_title_status_tabs)
TabFilterPreferencesFragment.newInstance()
}
PROXY_PREFERENCES -> {
setTitle(R.string.pref_title_http_proxy_settings)
ProxyPreferencesFragment.newInstance()
}
else -> throw IllegalArgumentException("preferenceType not known")
}
supportFragmentManager.commit {
replace(R.id.fragment_container, fragment, fragmentTag)
}
restartActivitiesOnExit = intent.getBooleanExtra("restart", false)
}
override fun onResume() {
@ -122,7 +123,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
restartActivitiesOnExit = true
this.restartCurrentActivity()
}
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash", "showCardsInTimelines", "confirmReblogs", "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
@ -179,5 +179,4 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
return intent
}
}
}

View File

@ -22,7 +22,14 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.settings.*
import com.keylesspalace.tusky.settings.AppTheme
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.settings.emojiPreference
import com.keylesspalace.tusky.settings.listPreference
import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.getNonNullString
@ -122,7 +129,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setTitle(R.string.pref_title_bot_overlay)
isSingleLineTitle = false
setIcon(R.drawable.ic_bot_24dp)
}
switchPreference {
@ -259,7 +265,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
sizePx = iconSize
colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
}
}
override fun onResume() {
@ -274,7 +279,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
try {
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
.toInt()
.toInt()
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
httpProxyPref?.summary = "$httpServer:$httpPort"

View File

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

View File

@ -126,12 +126,12 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector {
@JvmStatic
fun getIntent(context: Context, accountId: String, userName: String, statusId: String? = null) =
Intent(context, ReportActivity::class.java)
.apply {
putExtra(ACCOUNT_ID, accountId)
putExtra(ACCOUNT_USERNAME, userName)
putExtra(STATUS_ID, statusId)
}
Intent(context, ReportActivity::class.java)
.apply {
putExtra(ACCOUNT_ID, accountId)
putExtra(ACCOUNT_USERNAME, userName)
putExtra(STATUS_ID, statusId)
}
}
override fun androidInjector() = androidInjector

View File

@ -43,8 +43,8 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
class ReportViewModel @Inject constructor(
private val mastodonApi: MastodonApi,
private val eventHub: EventHub
private val mastodonApi: MastodonApi,
private val eventHub: EventHub
) : RxAwareViewModel() {
private val navigationMutable = MutableLiveData<Screen?>()
@ -121,18 +121,17 @@ class ReportViewModel @Inject constructor(
muteStateMutable.value = Loading()
blockStateMutable.value = Loading()
mastodonApi.relationships(ids)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ data ->
updateRelationship(data.getOrNull(0))
},
{
updateRelationship(null)
}
)
.autoDispose()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ data ->
updateRelationship(data.getOrNull(0))
},
{
updateRelationship(null)
}
)
.autoDispose()
}
private fun updateRelationship(relationship: Relationship?) {
@ -152,20 +151,20 @@ class ReportViewModel @Inject constructor(
} else {
mastodonApi.muteAccount(accountId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val muting = relationship?.muting == true
muteStateMutable.value = Success(muting)
if (muting) {
eventHub.dispatch(MuteEvent(accountId))
}
},
{ error ->
muteStateMutable.value = Error(false, error.message)
}
).autoDispose()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val muting = relationship?.muting == true
muteStateMutable.value = Success(muting)
if (muting) {
eventHub.dispatch(MuteEvent(accountId))
}
},
{ error ->
muteStateMutable.value = Error(false, error.message)
}
).autoDispose()
muteStateMutable.value = Loading()
}
@ -177,21 +176,21 @@ class ReportViewModel @Inject constructor(
} else {
mastodonApi.blockAccount(accountId)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val blocking = relationship?.blocking == true
blockStateMutable.value = Success(blocking)
if (blocking) {
eventHub.dispatch(BlockEvent(accountId))
}
},
{ error ->
blockStateMutable.value = Error(false, error.message)
}
)
.autoDispose()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ relationship ->
val blocking = relationship?.blocking == true
blockStateMutable.value = Success(blocking)
if (blocking) {
eventHub.dispatch(BlockEvent(accountId))
}
},
{ error ->
blockStateMutable.value = Error(false, error.message)
}
)
.autoDispose()
blockStateMutable.value = Loading()
}
@ -199,18 +198,17 @@ class ReportViewModel @Inject constructor(
fun doReport() {
reportingStateMutable.value = Loading()
mastodonApi.reportObservable(accountId, selectedIds.toList(), reportNote, if (isRemoteAccount) isRemoteNotify else null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
reportingStateMutable.value = Success(true)
},
{ error ->
reportingStateMutable.value = Error(cause = error)
}
)
.autoDispose()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
reportingStateMutable.value = Success(true)
},
{ error ->
reportingStateMutable.value = Error(cause = error)
}
)
.autoDispose()
}
fun checkClickedUrl(url: String?) {

View File

@ -21,4 +21,4 @@ enum class Screen {
Done,
Back,
Finish
}
}

View File

@ -19,8 +19,8 @@ import android.view.View
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener
interface AdapterHandler: LinkListener {
interface AdapterHandler : LinkListener {
fun showMedia(v: View?, status: Status?, idx: Int)
fun setStatusChecked(status: Status, isChecked: Boolean)
fun isStatusChecked(id: String): Boolean
}
}

View File

@ -33,4 +33,4 @@ class ReportPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(acti
}
override fun getItemCount() = 3
}
}

View File

@ -25,18 +25,25 @@ import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.util.LinkHelper
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.StatusViewHelper
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER
import com.keylesspalace.tusky.util.TimestampUtils
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.shouldTrimStatus
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.viewdata.toViewData
import java.util.*
import java.util.Date
class StatusViewHolder(
private val binding: ItemReportStatusBinding,
private val statusDisplayOptions: StatusDisplayOptions,
private val viewState: StatusViewState,
private val adapterHandler: AdapterHandler,
private val getStatusForPosition: (Int) -> Status?
private val binding: ItemReportStatusBinding,
private val statusDisplayOptions: StatusDisplayOptions,
private val viewState: StatusViewState,
private val adapterHandler: AdapterHandler,
private val getStatusForPosition: (Int) -> Status?
) : RecyclerView.ViewHolder(binding.root) {
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
private val statusViewHelper = StatusViewHelper(itemView)
@ -71,9 +78,11 @@ class StatusViewHolder(
val sensitive = status.sensitive
statusViewHelper.setMediasPreview(statusDisplayOptions, status.attachments,
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
mediaViewHeight)
statusViewHelper.setMediasPreview(
statusDisplayOptions, status.attachments,
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
mediaViewHeight
)
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
setCreatedAt(status.createdAt)
@ -81,8 +90,10 @@ class StatusViewHolder(
private fun updateTextView() {
status()?.let { status ->
setupCollapsedState(shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
viewState.isContentShow(status.id, status.sensitive), status.spoilerText)
setupCollapsedState(
shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
viewState.isContentShow(status.id, status.sensitive), status.spoilerText
)
if (status.spoilerText.isBlank()) {
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler)
@ -109,18 +120,20 @@ class StatusViewHolder(
}
private fun setContentWarningButtonText(contentShown: Boolean) {
if(contentShown) {
if (contentShown) {
binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less)
} else {
binding.statusContentWarningButton.setText(R.string.status_content_warning_show_more)
}
}
private fun setTextVisible(expanded: Boolean,
content: Spanned,
mentions: List<Status.Mention>?,
emojis: List<Emoji>,
listener: LinkListener) {
private fun setTextVisible(
expanded: Boolean,
content: Spanned,
mentions: List<Status.Mention>?,
emojis: List<Emoji>,
listener: LinkListener
) {
if (expanded) {
val emojifiedText = content.emojify(emojis, binding.statusContent, statusDisplayOptions.animateEmojis)
LinkHelper.setClickableText(binding.statusContent, emojifiedText, mentions, listener)
@ -152,7 +165,7 @@ class StatusViewHolder(
private fun setupCollapsedState(collapsible: Boolean, collapsed: Boolean, expanded: Boolean, spoilerText: String) {
/* input filter for TextViews have to be set before text */
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
binding.buttonToggleContent.setOnClickListener{
binding.buttonToggleContent.setOnClickListener {
status()?.let { status ->
viewState.setCollapsed(status.id, !collapsed)
updateTextView()
@ -174,4 +187,4 @@ class StatusViewHolder(
}
private fun status() = getStatusForPosition(bindingAdapterPosition)
}
}

View File

@ -26,9 +26,9 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions
class StatusesAdapter(
private val statusDisplayOptions: StatusDisplayOptions,
private val statusViewState: StatusViewState,
private val adapterHandler: AdapterHandler
private val statusDisplayOptions: StatusDisplayOptions,
private val statusViewState: StatusViewState,
private val adapterHandler: AdapterHandler
) : PagingDataAdapter<Status, StatusViewHolder>(STATUS_COMPARATOR) {
private val statusForPosition: (Int) -> Status? = { position: Int ->
@ -37,8 +37,10 @@ class StatusesAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return StatusViewHolder(binding, statusDisplayOptions, statusViewState, adapterHandler,
statusForPosition)
return StatusViewHolder(
binding, statusDisplayOptions, statusViewState, adapterHandler,
statusForPosition
)
}
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
@ -50,10 +52,10 @@ class StatusesAdapter(
companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() {
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem == newItem
oldItem == newItem
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem.id == newItem.id
oldItem.id == newItem.id
}
}
}

View File

@ -32,7 +32,7 @@ class StatusesPagingSource(
override fun getRefreshKey(state: PagingState<String, Status>): String? {
return state.anchorPosition?.let { anchorPosition ->
state.closestItemToPosition(anchorPosition)?.id
state.closestItemToPosition(anchorPosition)?.id
}
}
@ -65,7 +65,6 @@ class StatusesPagingSource(
prevKey = result.firstOrNull()?.id,
nextKey = result.lastOrNull()?.id
)
} catch (e: Exception) {
Log.w("StatusesPagingSource", "failed to load statuses", e)
return LoadResult.Error(e)
@ -86,4 +85,4 @@ class StatusesPagingSource(
excludeReblogs = true
).await()
}
}
}

View File

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

View File

@ -64,11 +64,10 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
private fun fillViews() {
binding.editNote.setText(viewModel.reportNote)
if (viewModel.isRemoteAccount){
if (viewModel.isRemoteAccount) {
binding.checkIsNotifyRemote.show()
binding.reportDescriptionRemoteInstance.show()
}
else{
} else {
binding.checkIsNotifyRemote.hide()
binding.reportDescriptionRemoteInstance.hide()
}
@ -84,7 +83,6 @@ class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable {
is Success -> viewModel.navigateTo(Screen.Done)
is Loading -> showLoading()
is Error -> showError(it.cause)
}
}
}

View File

@ -107,15 +107,15 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
private fun initStatusesView() {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = false,
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = false,
useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
animateAvatars = false,
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = false,
useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
)
adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this)
@ -132,9 +132,10 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
}
adapter.addLoadStateListener { loadState ->
if (loadState.refresh is LoadState.Error
|| loadState.append is LoadState.Error
|| loadState.prepend is LoadState.Error) {
if (loadState.refresh is LoadState.Error ||
loadState.append is LoadState.Error ||
loadState.prepend is LoadState.Error
) {
showError()
}

View File

@ -30,7 +30,7 @@ class StatusViewState {
fun setCollapsed(id: String, isCollapsed: Boolean) = setStateEnabled(longContentCollapsedState, id, isCollapsed)
private fun isStateEnabled(map: Map<String, Boolean>, id: String, def: Boolean): Boolean = map[id]
?: def
?: def
private fun setStateEnabled(map: MutableMap<String, Boolean>, id: String, state: Boolean) = map.put(id, state)
}
}

View File

@ -29,9 +29,9 @@ import kotlinx.coroutines.rx3.await
import javax.inject.Inject
class ScheduledTootViewModel @Inject constructor(
val mastodonApi: MastodonApi,
val eventHub: EventHub
): ViewModel() {
val mastodonApi: MastodonApi,
val eventHub: EventHub
) : ViewModel() {
private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi)

View File

@ -76,7 +76,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
menuInflater.inflate(R.menu.search_toolbar, menu)
val searchView = menu.findItem(R.id.action_search)
.actionView as SearchView
.actionView as SearchView
setupSearchView(searchView)
searchView.setQuery(viewModel.currentQuery, false)

View File

@ -19,4 +19,4 @@ enum class SearchType(val apiParameter: String) {
Status("statuses"),
Account("accounts"),
Hashtag("hashtags")
}
}

View File

@ -95,13 +95,17 @@ class SearchViewModel @Inject constructor(
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
timelineCases.delete(status.first.id)
.subscribe({
.subscribe(
{
if (loadedStatuses.remove(status))
statusesPagingSourceFactory.invalidate()
}, {
err -> Log.d(TAG, "Failed to delete status", err)
})
.autoDispose()
},
{
err ->
Log.d(TAG, "Failed to delete status", err)
}
)
.autoDispose()
}
fun expandedChange(status: Pair<Status, StatusViewData.Concrete>, expanded: Boolean) {
@ -225,4 +229,4 @@ class SearchViewModel @Inject constructor(
private const val TAG = "SearchViewModel"
private const val DEFAULT_LOAD_SIZE = 20
}
}
}

View File

@ -24,12 +24,12 @@ import com.keylesspalace.tusky.adapter.AccountViewHolder
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.LinkListener
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean)
: PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_account, parent, false)
.inflate(R.layout.item_account, parent, false)
return AccountViewHolder(view)
}
@ -46,10 +46,10 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
oldItem.deepEquals(newItem)
oldItem.deepEquals(newItem)
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean =
oldItem.id == newItem.id
oldItem.id == newItem.id
}
}
}

View File

@ -24,8 +24,8 @@ import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.interfaces.LinkListener
import com.keylesspalace.tusky.util.BindingHolder
class SearchHashtagsAdapter(private val linkListener: LinkListener)
: PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
class SearchHashtagsAdapter(private val linkListener: LinkListener) :
PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemHashtagBinding> {
val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@ -43,10 +43,10 @@ class SearchHashtagsAdapter(private val linkListener: LinkListener)
val HASHTAG_COMPARATOR = object : DiffUtil.ItemCallback<HashTag>() {
override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean =
oldItem.name == newItem.name
oldItem.name == newItem.name
override fun areItemsTheSame(oldItem: HashTag, newItem: HashTag): Boolean =
oldItem.name == newItem.name
oldItem.name == newItem.name
}
}
}

View File

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

View File

@ -22,12 +22,13 @@ import com.keylesspalace.tusky.entity.SearchResult
import com.keylesspalace.tusky.network.MastodonApi
import kotlinx.coroutines.rx3.await
class SearchPagingSource<T: Any>(
class SearchPagingSource<T : Any>(
private val mastodonApi: MastodonApi,
private val searchType: SearchType,
private val searchRequest: String,
private val initialItems: List<T>?,
private val parser: (SearchResult) -> List<T>) : PagingSource<Int, T>() {
private val parser: (SearchResult) -> List<T>
) : PagingSource<Int, T>() {
override fun getRefreshKey(state: PagingState<Int, T>): Int? {
return null
@ -80,4 +81,4 @@ class SearchPagingSource<T: Any>(
return LoadResult.Error(e)
}
}
}
}

View File

@ -50,4 +50,4 @@ class SearchPagingSourceFactory<T : Any>(
fun invalidate() {
currentSource?.invalidate()
}
}
}

View File

@ -28,9 +28,9 @@ class SearchAccountsFragment : SearchFragment<Account>() {
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
return SearchAccountsAdapter(
this,
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
this,
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
)
}

View File

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

View File

@ -74,15 +74,15 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
override fun createAdapter(): PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
)
binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL))
@ -125,13 +125,17 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
when (actionable.attachments[attachmentIndex].type) {
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
val attachments = AttachmentViewData.list(actionable)
val intent = ViewMediaActivity.newIntent(context, attachments,
attachmentIndex)
val intent = ViewMediaActivity.newIntent(
context, attachments,
attachmentIndex
)
if (view != null) {
val url = actionable.attachments[attachmentIndex].url
ViewCompat.setTransitionName(view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(),
view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(),
view, url
)
startActivity(intent, options.toBundle())
} else {
startActivity(intent)
@ -198,20 +202,23 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
private fun reply(status: Status) {
val actionableStatus = status.actionableStatus
val mentionedUsernames = actionableStatus.mentions.map { it.username }
.toMutableSet()
.apply {
add(actionableStatus.account.username)
remove(viewModel.activeAccount?.username)
}
.toMutableSet()
.apply {
add(actionableStatus.account.username)
remove(viewModel.activeAccount?.username)
}
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
val intent = ComposeActivity.startIntent(
requireContext(),
ComposeOptions(
inReplyToId = status.actionableId,
replyVisibility = actionableStatus.visibility,
contentWarning = actionableStatus.spoilerText,
mentionedUsernames = mentionedUsernames,
replyingStatusAuthor = actionableStatus.account.localUsername,
replyingStatusContent = actionableStatus.content.toString()
))
)
)
startActivity(intent)
}
@ -244,7 +251,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
menu.findItem(R.id.status_unreblog_private).isVisible = reblogged
}
Status.Visibility.UNKNOWN, Status.Visibility.DIRECT -> {
} //Ignore
} // Ignore
}
} else {
popup.inflate(R.menu.status_more)
@ -271,11 +278,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
}
if (mutable) {
muteConversationItem.setTitle(
if (status.muted == true) {
R.string.action_unmute_conversation
} else {
R.string.action_mute_conversation
})
if (status.muted == true) {
R.string.action_unmute_conversation
} else {
R.string.action_mute_conversation
}
)
}
popup.setOnMenuItemClickListener { item ->
@ -287,8 +295,8 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
sendIntent.action = Intent.ACTION_SEND
val stringToShare = statusToShare.account.username +
" - " +
statusToShare.content.toString()
" - " +
statusToShare.content.toString()
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
sendIntent.type = "text/plain"
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_content_to)))
@ -361,10 +369,10 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
private fun onBlock(accountId: String, accountUsername: String) {
AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.dialog_block_warning, accountUsername))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.blockAccount(accountId) }
.setNegativeButton(android.R.string.cancel, null)
.show()
.setMessage(getString(R.string.dialog_block_warning, accountUsername))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.blockAccount(accountId) }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun onMute(accountId: String, accountUsername: String) {
@ -383,11 +391,14 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
}
private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) {
bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener {
override fun onAccountSelected(account: AccountEntity) {
openAsAccount(statusUrl, account)
bottomSheetActivity?.showAccountChooserDialog(
dialogTitle, false,
object : AccountSelectionListener {
override fun onAccountSelected(account: AccountEntity) {
openAsAccount(statusUrl, account)
}
}
})
)
}
private fun openAsAccount(statusUrl: String, account: AccountEntity) {
@ -430,51 +441,56 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
private fun showConfirmDeleteDialog(id: String, position: Int) {
context?.let {
AlertDialog.Builder(it)
.setMessage(R.string.dialog_delete_toot_warning)
.setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.deleteStatus(id)
removeItem(position)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
.setMessage(R.string.dialog_delete_toot_warning)
.setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.deleteStatus(id)
removeItem(position)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
activity?.let {
AlertDialog.Builder(it)
.setMessage(R.string.dialog_redraft_toot_warning)
.setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.deleteStatus(id)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ deletedStatus ->
removeItem(position)
.setMessage(R.string.dialog_redraft_toot_warning)
.setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.deleteStatus(id)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(
{ deletedStatus ->
removeItem(position)
val redraftStatus = if (deletedStatus.isEmpty()) {
status.toDeletedStatus()
} else {
deletedStatus
}
val redraftStatus = if (deletedStatus.isEmpty()) {
status.toDeletedStatus()
} else {
deletedStatus
}
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions(
tootText = redraftStatus.text ?: "",
inReplyToId = redraftStatus.inReplyToId,
visibility = redraftStatus.visibility,
contentWarning = redraftStatus.spoilerText,
mediaAttachments = redraftStatus.attachments,
sensitive = redraftStatus.sensitive,
poll = redraftStatus.poll?.toNewPoll(status.createdAt)
))
startActivity(intent)
}, { error ->
Log.w("SearchStatusesFragment", "error deleting status", error)
Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show()
})
}
.setNegativeButton(android.R.string.cancel, null)
.show()
val intent = ComposeActivity.startIntent(
requireContext(),
ComposeOptions(
tootText = redraftStatus.text ?: "",
inReplyToId = redraftStatus.inReplyToId,
visibility = redraftStatus.visibility,
contentWarning = redraftStatus.spoilerText,
mediaAttachments = redraftStatus.attachments,
sensitive = redraftStatus.sensitive,
poll = redraftStatus.poll?.toNewPoll(status.createdAt)
)
)
startActivity(intent)
},
{ error ->
Log.w("SearchStatusesFragment", "error deleting status", error)
Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show()
}
)
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
}

View File

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

View File

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

View File

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

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