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

* add ktlint plugin to project and apply default code style

* some manual adjustments, fix wildcard imports

* update CONTRIBUTING.md

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,25 +23,41 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.PopupMenu
import android.widget.TextView
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
import autodispose2.autoDispose import autodispose2.autoDispose
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.components.timeline.TimelineViewModel
import com.keylesspalace.tusky.databinding.ActivityListsBinding import com.keylesspalace.tusky.databinding.ActivityListsBinding
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.entity.MastoList
import com.keylesspalace.tusky.components.timeline.TimelineViewModel import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewmodel.ListsViewModel import com.keylesspalace.tusky.viewmodel.ListsViewModel
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.* import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.* import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_NETWORK
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.colorInt
@ -84,12 +100,13 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
binding.listsRecycler.adapter = adapter binding.listsRecycler.adapter = adapter
binding.listsRecycler.layoutManager = LinearLayoutManager(this) binding.listsRecycler.layoutManager = LinearLayoutManager(this)
binding.listsRecycler.addItemDecoration( binding.listsRecycler.addItemDecoration(
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
)
viewModel.state viewModel.state
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this)) .autoDispose(from(this))
.subscribe(this::update) .subscribe(this::update)
viewModel.retryLoading() viewModel.retryLoading()
binding.addListButton.setOnClickListener { binding.addListButton.setOnClickListener {
@ -97,15 +114,15 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
} }
viewModel.events.observeOn(AndroidSchedulers.mainThread()) viewModel.events.observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this)) .autoDispose(from(this))
.subscribe { event -> .subscribe { event ->
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
when (event) { when (event) {
CREATE_ERROR -> showMessage(R.string.error_create_list) Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
RENAME_ERROR -> showMessage(R.string.error_rename_list) Event.RENAME_ERROR -> showMessage(R.string.error_rename_list)
DELETE_ERROR -> showMessage(R.string.error_delete_list) Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
}
} }
}
} }
private fun showlistNameDialog(list: MastoList?) { private fun showlistNameDialog(list: MastoList?) {
@ -115,17 +132,18 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
layout.addView(editText) layout.addView(editText)
val margin = Utils.dpToPx(this, 8) val margin = Utils.dpToPx(this, 8)
(editText.layoutParams as ViewGroup.MarginLayoutParams) (editText.layoutParams as ViewGroup.MarginLayoutParams)
.setMargins(margin, margin, margin, 0) .setMargins(margin, margin, margin, 0)
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setView(layout) .setView(layout)
.setPositiveButton( .setPositiveButton(
if (list == null) R.string.action_create_list if (list == null) R.string.action_create_list
else R.string.action_rename_list) { _, _ -> else R.string.action_rename_list
onPickedDialogName(editText.text, list?.id) ) { _, _ ->
} onPickedDialogName(editText.text, list?.id)
.setNegativeButton(android.R.string.cancel, null) }
.show() .setNegativeButton(android.R.string.cancel, null)
.show()
val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE) val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE)
editText.onTextChanged { s, _, _, _ -> editText.onTextChanged { s, _, _, _ ->
@ -137,15 +155,14 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
private fun showListDeleteDialog(list: MastoList) { private fun showListDeleteDialog(list: MastoList) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setMessage(getString(R.string.dialog_delete_list_warning, list.title)) .setMessage(getString(R.string.dialog_delete_list_warning, list.title))
.setPositiveButton(R.string.action_delete){ _, _ -> .setPositiveButton(R.string.action_delete) { _, _ ->
viewModel.deleteList(list.id) viewModel.deleteList(list.id)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
private fun update(state: ListsViewModel.State) { private fun update(state: ListsViewModel.State) {
adapter.submitList(state.lists) adapter.submitList(state.lists)
binding.progressBar.visible(state.loadingState == LOADING) binding.progressBar.visible(state.loadingState == LOADING)
@ -166,8 +183,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
LOADED -> LOADED ->
if (state.lists.isEmpty()) { if (state.lists.isEmpty()) {
binding.messageView.show() binding.messageView.show()
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, binding.messageView.setup(
null) R.drawable.elephant_friend_empty, R.string.message_empty,
null
)
} else { } else {
binding.messageView.hide() binding.messageView.hide()
} }
@ -176,13 +195,14 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
private fun showMessage(@StringRes messageId: Int) { private fun showMessage(@StringRes messageId: Int) {
Snackbar.make( Snackbar.make(
binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT
).show() ).show()
} }
private fun onListSelected(listId: String) { private fun onListSelected(listId: String) {
startActivityWithSlideInAnimation( startActivityWithSlideInAnimation(
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)) ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
)
} }
private fun openListSettings(list: MastoList) { private fun openListSettings(list: MastoList) {
@ -219,27 +239,28 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
} }
} }
private inner class ListsAdapter private inner class ListsAdapter :
: ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) { ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false) return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
.let(this::ListViewHolder) .let(this::ListViewHolder)
.apply { .apply {
val context = nameTextView.context val context = nameTextView.context
val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary) val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary)
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor } 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) { override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
holder.nameTextView.text = getItem(position).title holder.nameTextView.text = getItem(position).title
} }
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view), private inner class ListViewHolder(view: View) :
View.OnClickListener { RecyclerView.ViewHolder(view),
View.OnClickListener {
val nameTextView: TextView = view.findViewById(R.id.list_name_textview) val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
val moreButton: ImageButton = view.findViewById(R.id.editListButton) val moreButton: ImageButton = view.findViewById(R.id.editListButton)
@ -271,4 +292,4 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
companion object { companion object {
fun newIntent(context: Context) = Intent(context, ListsActivity::class.java) 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.AccessToken
import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.entity.AppCredentials
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.getNonNullString
import com.keylesspalace.tusky.util.rickRoll
import com.keylesspalace.tusky.util.shouldRickRoll
import com.keylesspalace.tusky.util.viewBinding
import okhttp3.HttpUrl import okhttp3.HttpUrl
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -62,28 +66,29 @@ class LoginActivity : BaseActivity(), Injectable {
setContentView(binding.root) 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.setText(BuildConfig.CUSTOM_INSTANCE)
binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
} }
if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
Glide.with(binding.loginLogo) Glide.with(binding.loginLogo)
.load(BuildConfig.CUSTOM_LOGO_URL) .load(BuildConfig.CUSTOM_LOGO_URL)
.placeholder(null) .placeholder(null)
.into(binding.loginLogo) .into(binding.loginLogo)
} }
preferences = getSharedPreferences( preferences = getSharedPreferences(
getString(R.string.preferences_file_key), Context.MODE_PRIVATE) getString(R.string.preferences_file_key), Context.MODE_PRIVATE
)
binding.loginButton.setOnClickListener { onButtonClick() } binding.loginButton.setOnClickListener { onButtonClick() }
binding.whatsAnInstanceTextView.setOnClickListener { binding.whatsAnInstanceTextView.setOnClickListener {
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setMessage(R.string.dialog_whats_an_instance) .setMessage(R.string.dialog_whats_an_instance)
.setPositiveButton(R.string.action_close, null) .setPositiveButton(R.string.action_close, null)
.show() .show()
val textView = dialog.findViewById<TextView>(android.R.id.message) val textView = dialog.findViewById<TextView>(android.R.id.message)
textView?.movementMethod = LinkMovementMethod.getInstance() textView?.movementMethod = LinkMovementMethod.getInstance()
} }
@ -95,7 +100,6 @@ class LoginActivity : BaseActivity(), Injectable {
} else { } else {
binding.toolbar.visibility = View.GONE binding.toolbar.visibility = View.GONE
} }
} }
override fun requiresLogin(): Boolean { override fun requiresLogin(): Boolean {
@ -104,7 +108,7 @@ class LoginActivity : BaseActivity(), Injectable {
override fun finish() { override fun finish() {
super.finish() super.finish()
if(isAdditionalLogin()) { if (isAdditionalLogin()) {
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right) overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right)
} }
} }
@ -134,8 +138,10 @@ class LoginActivity : BaseActivity(), Injectable {
} }
val callback = object : Callback<AppCredentials> { val callback = object : Callback<AppCredentials> {
override fun onResponse(call: Call<AppCredentials>, override fun onResponse(
response: Response<AppCredentials>) { call: Call<AppCredentials>,
response: Response<AppCredentials>
) {
if (!response.isSuccessful) { if (!response.isSuccessful) {
binding.loginButton.isEnabled = true binding.loginButton.isEnabled = true
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration) binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
@ -148,10 +154,10 @@ class LoginActivity : BaseActivity(), Injectable {
val clientSecret = credentials.clientSecret val clientSecret = credentials.clientSecret
preferences.edit() preferences.edit()
.putString("domain", domain) .putString("domain", domain)
.putString("clientId", clientId) .putString("clientId", clientId)
.putString("clientSecret", clientSecret) .putString("clientSecret", clientSecret)
.apply() .apply()
redirectUserToAuthorizeAndLogin(domain, clientId) redirectUserToAuthorizeAndLogin(domain, clientId)
} }
@ -165,11 +171,12 @@ class LoginActivity : BaseActivity(), Injectable {
} }
mastodonApi mastodonApi
.authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri, .authenticateApp(
OAUTH_SCOPES, getString(R.string.tusky_website)) domain, getString(R.string.app_name), oauthRedirectUri,
.enqueue(callback) OAUTH_SCOPES, getString(R.string.tusky_website)
)
.enqueue(callback)
setLoading(true) setLoading(true)
} }
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) { 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. */ * login there, and the server will redirect back to the app with its response. */
val endpoint = MastodonApi.ENDPOINT_AUTHORIZE val endpoint = MastodonApi.ENDPOINT_AUTHORIZE
val parameters = mapOf( val parameters = mapOf(
"client_id" to clientId, "client_id" to clientId,
"redirect_uri" to oauthRedirectUri, "redirect_uri" to oauthRedirectUri,
"response_type" to "code", "response_type" to "code",
"scope" to OAUTH_SCOPES "scope" to OAUTH_SCOPES
) )
val url = "https://" + domain + endpoint + "?" + toQueryString(parameters) val url = "https://" + domain + endpoint + "?" + toQueryString(parameters)
val uri = Uri.parse(url) val uri = Uri.parse(url)
@ -224,31 +231,27 @@ class LoginActivity : BaseActivity(), Injectable {
} else { } else {
setLoading(false) setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s", Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message()))
getString(R.string.error_retrieving_oauth_token),
response.message()))
} }
} }
override fun onFailure(call: Call<AccessToken>, t: Throwable) { override fun onFailure(call: Call<AccessToken>, t: Throwable) {
setLoading(false) setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token) binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
Log.e(TAG, String.format("%s %s", Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message))
getString(R.string.error_retrieving_oauth_token),
t.message))
} }
} }
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code, mastodonApi.fetchOAuthToken(
"authorization_code").enqueue(callback) domain, clientId, clientSecret, redirectUri, code,
"authorization_code"
).enqueue(callback)
} else if (error != null) { } else if (error != null) {
/* Authorization failed. Put the error response where the user can read it and they /* Authorization failed. Put the error response where the user can read it and they
* can try again. */ * can try again. */
setLoading(false) setLoading(false)
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied) binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
Log.e(TAG, String.format("%s %s", Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
getString(R.string.error_authorization_denied),
error))
} else { } else {
// This case means a junk response was received somehow. // This case means a junk response was received somehow.
setLoading(false) setLoading(false)
@ -340,14 +343,14 @@ class LoginActivity : BaseActivity(), Injectable {
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor) val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
val colorSchemeParams = CustomTabColorSchemeParams.Builder() val colorSchemeParams = CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor) .setToolbarColor(toolbarColor)
.setNavigationBarColor(navigationbarColor) .setNavigationBarColor(navigationbarColor)
.setNavigationBarDividerColor(navigationbarDividerColor) .setNavigationBarDividerColor(navigationbarDividerColor)
.build() .build()
val customTabsIntent = CustomTabsIntent.Builder() val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(colorSchemeParams) .setDefaultColorSchemeParams(colorSchemeParams)
.build() .build()
try { try {
customTabsIntent.launchUrl(context, uri) customTabsIntent.launchUrl(context, uri)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -24,11 +24,14 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.unicodeWrap
import com.keylesspalace.tusky.util.visible
class FollowRequestViewHolder( class FollowRequestViewHolder(
private val binding: ItemFollowRequestBinding, private val binding: ItemFollowRequestBinding,
private val showHeader: Boolean private val showHeader: Boolean
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.AccountActionListener
@ -38,4 +36,4 @@ class FollowRequestsAdapter(
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis) viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
viewHolder.setupActionListener(accountActionListener, accountList[position].id) 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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
val view = LayoutInflater.from(parent.context) 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) return HeaderViewHolder(view)
} }
@ -34,7 +34,6 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val
} }
override fun getItemCount() = if (accountLocked) 0 else 1 override fun getItemCount() = if (accountLocked) 0 else 1
} }
class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView) class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)

View File

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

View File

@ -13,7 +13,7 @@ import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar import com.keylesspalace.tusky.util.loadAvatar
import java.util.* import java.util.HashMap
/** /**
* Displays a list of muted accounts with mute/unmute account and mute/unmute notifications * Displays a list of muted accounts with mute/unmute account and mute/unmute notifications
@ -129,4 +129,4 @@ class MutesAdapter(
itemView.setOnClickListener { listener.onViewAccount(id) } 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.databinding.ItemNetworkStateBinding
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding, class NetworkStateViewHolder(
private val retryCallback: () -> Unit) private val binding: ItemNetworkStateBinding,
: RecyclerView.ViewHolder(binding.root) { private val retryCallback: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {
fun setUpWithNetworkState(state: LoadState) { fun setUpWithNetworkState(state: LoadState) {
binding.progressBar.visible(state == LoadState.Loading) binding.progressBar.visible(state == LoadState.Loading)
@ -38,5 +39,4 @@ class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding,
retryCallback() retryCallback()
} }
} }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,6 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
data class DomainMuteEvent(val instance: String): Dispatchable data class DomainMuteEvent(val instance: String) : Dispatchable
data class AnnouncementReadEvent(val announcementId: String): Dispatchable data class AnnouncementReadEvent(val announcementId: String) : Dispatchable
data class PinEvent(val statusId: String, val pinned: Boolean): 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) { override fun dispatch(event: Dispatchable) {
eventsSubject.onNext(event) 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.LinkHelper
import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.emojify
interface AnnouncementActionListener: LinkListener { interface AnnouncementActionListener : LinkListener {
fun openReactionPicker(announcementId: String, target: View) fun openReactionPicker(announcementId: String, target: View)
fun addReaction(announcementId: String, name: String) fun addReaction(announcementId: String, name: String)
fun removeReaction(announcementId: String, name: String) fun removeReaction(announcementId: String, name: String)
} }
class AnnouncementAdapter( class AnnouncementAdapter(
private var items: List<Announcement> = emptyList(), private var items: List<Announcement> = emptyList(),
private val listener: AnnouncementActionListener, private val listener: AnnouncementActionListener,
private val wellbeingEnabled: Boolean = false, private val wellbeingEnabled: Boolean = false,
private val animateEmojis: Boolean = false private val animateEmojis: Boolean = false
) : RecyclerView.Adapter<BindingHolder<ItemAnnouncementBinding>>() { ) : RecyclerView.Adapter<BindingHolder<ItemAnnouncementBinding>>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> {
@ -67,12 +67,12 @@ class AnnouncementAdapter(
} }
item.reactions.forEachIndexed { i, reaction -> item.reactions.forEachIndexed { i, reaction ->
(chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { ?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
isCheckable = true isCheckable = true
checkedIcon = null checkedIcon = null
chips.addView(this, i) chips.addView(this, i)
}) }
.apply { .apply {
val emojiText = if (reaction.url == null) { val emojiText = if (reaction.url == null) {
reaction.name reaction.name
@ -80,16 +80,18 @@ class AnnouncementAdapter(
context.getString(R.string.emoji_shortcode_format, reaction.name) context.getString(R.string.emoji_shortcode_format, reaction.name)
} }
this.text = ("$emojiText ${reaction.count}") this.text = ("$emojiText ${reaction.count}")
.emojify( .emojify(
listOf(Emoji( listOf(
reaction.name, Emoji(
reaction.url ?: "", reaction.name,
reaction.staticUrl ?: "", reaction.url ?: "",
null reaction.staticUrl ?: "",
)), null
this, )
animateEmojis ),
) this,
animateEmojis
)
isChecked = reaction.me 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.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Success
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.view.EmojiPicker import com.keylesspalace.tusky.view.EmojiPicker
import javax.inject.Inject import javax.inject.Inject
@ -52,13 +57,13 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
private val picker by lazy { EmojiPicker(this) } private val picker by lazy { EmojiPicker(this) }
private val pickerDialog by lazy { private val pickerDialog by lazy {
PopupWindow(this) PopupWindow(this)
.apply { .apply {
contentView = picker contentView = picker
isFocusable = true isFocusable = true
setOnDismissListener { setOnDismissListener {
currentAnnouncementId = null currentAnnouncementId = null
}
} }
}
} }
private var currentAnnouncementId: String? = 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.Emoji
import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.Instance
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.Either
import com.keylesspalace.tusky.util.Error
import com.keylesspalace.tusky.util.Loading
import com.keylesspalace.tusky.util.Resource
import com.keylesspalace.tusky.util.RxAwareViewModel
import com.keylesspalace.tusky.util.Success
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import javax.inject.Inject import javax.inject.Inject
class AnnouncementsViewModel @Inject constructor( class AnnouncementsViewModel @Inject constructor(
accountManager: AccountManager, accountManager: AccountManager,
private val appDatabase: AppDatabase, private val appDatabase: AppDatabase,
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val eventHub: EventHub private val eventHub: EventHub
) : RxAwareViewModel() { ) : RxAwareViewModel() {
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>() private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
@ -45,139 +50,153 @@ class AnnouncementsViewModel @Inject constructor(
val emojis: LiveData<List<Emoji>> = emojisMutable val emojis: LiveData<List<Emoji>> = emojisMutable
init { init {
Single.zip(mastodonApi.getCustomEmojis(), Single.zip(
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) mastodonApi.getCustomEmojis(),
.map<Either<InstanceEntity, Instance>> { Either.Left(it) } appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
.onErrorResumeNext { .map<Either<InstanceEntity, Instance>> { Either.Left(it) }
mastodonApi.getInstance() .onErrorResumeNext {
.map { Either.Right(it) } mastodonApi.getInstance()
}, .map { Either.Right(it) }
{ emojis, either -> },
either.asLeftOrNull()?.copy(emojiList = emojis) { emojis, either ->
?: InstanceEntity( either.asLeftOrNull()?.copy(emojiList = emojis)
accountManager.activeAccount?.domain!!, ?: InstanceEntity(
emojis, accountManager.activeAccount?.domain!!,
either.asRight().maxTootChars, emojis,
either.asRight().pollLimits?.maxOptions, either.asRight().maxTootChars,
either.asRight().pollLimits?.maxOptionChars, either.asRight().pollLimits?.maxOptions,
either.asRight().version either.asRight().pollLimits?.maxOptionChars,
) either.asRight().version
}) )
.doOnSuccess { }
appDatabase.instanceDao().insertOrReplace(it) )
} .doOnSuccess {
.subscribe({ appDatabase.instanceDao().insertOrReplace(it)
}
.subscribe(
{
emojisMutable.postValue(it.emojiList.orEmpty()) emojisMutable.postValue(it.emojiList.orEmpty())
}, { },
{
Log.w(TAG, "Failed to get custom emojis.", it) Log.w(TAG, "Failed to get custom emojis.", it)
}) }
.autoDispose() )
.autoDispose()
} }
fun load() { fun load() {
announcementsMutable.postValue(Loading()) announcementsMutable.postValue(Loading())
mastodonApi.listAnnouncements() mastodonApi.listAnnouncements()
.subscribe({ .subscribe(
{
announcementsMutable.postValue(Success(it)) announcementsMutable.postValue(Success(it))
it.filter { announcement -> !announcement.read } it.filter { announcement -> !announcement.read }
.forEach { announcement -> .forEach { announcement ->
mastodonApi.dismissAnnouncement(announcement.id) mastodonApi.dismissAnnouncement(announcement.id)
.subscribe( .subscribe(
{ {
eventHub.dispatch(AnnouncementReadEvent(announcement.id)) eventHub.dispatch(AnnouncementReadEvent(announcement.id))
}, },
{ throwable -> { throwable ->
Log.d(TAG, "Failed to mark announcement as read.", throwable) Log.d(TAG, "Failed to mark announcement as read.", throwable)
} }
) )
.autoDispose() .autoDispose()
} }
}, { },
{
announcementsMutable.postValue(Error(cause = it)) announcementsMutable.postValue(Error(cause = it))
}) }
.autoDispose() )
.autoDispose()
} }
fun addReaction(announcementId: String, name: String) { fun addReaction(announcementId: String, name: String) {
mastodonApi.addAnnouncementReaction(announcementId, name) mastodonApi.addAnnouncementReaction(announcementId, name)
.subscribe({ .subscribe(
{
announcementsMutable.postValue( announcementsMutable.postValue(
Success( Success(
announcements.value!!.data!!.map { announcement -> announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) { if (announcement.id == announcementId) {
announcement.copy( announcement.copy(
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
announcement.reactions.map { reaction -> announcement.reactions.map { reaction ->
if (reaction.name == name) { if (reaction.name == name) {
reaction.copy( reaction.copy(
count = reaction.count + 1, count = reaction.count + 1,
me = true me = true
) )
} else { } else {
reaction reaction
} }
} }
} else {
listOf(
*announcement.reactions.toTypedArray(),
emojis.value!!.find { emoji -> emoji.shortcode == name }
!!.run {
Announcement.Reaction(
name,
1,
true,
url,
staticUrl
)
}
)
}
)
} else { } 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) Log.w(TAG, "Failed to add reaction to the announcement.", it)
}) }
.autoDispose() )
.autoDispose()
} }
fun removeReaction(announcementId: String, name: String) { fun removeReaction(announcementId: String, name: String) {
mastodonApi.removeAnnouncementReaction(announcementId, name) mastodonApi.removeAnnouncementReaction(announcementId, name)
.subscribe({ .subscribe(
{
announcementsMutable.postValue( announcementsMutable.postValue(
Success( Success(
announcements.value!!.data!!.map { announcement -> announcements.value!!.data!!.map { announcement ->
if (announcement.id == announcementId) { if (announcement.id == announcementId) {
announcement.copy( announcement.copy(
reactions = announcement.reactions.mapNotNull { reaction -> reactions = announcement.reactions.mapNotNull { reaction ->
if (reaction.name == name) { if (reaction.name == name) {
if (reaction.count > 1) { if (reaction.count > 1) {
reaction.copy( reaction.copy(
count = reaction.count - 1, count = reaction.count - 1,
me = false me = false
) )
} else { } else {
null null
} }
} else { } else {
reaction reaction
} }
}
)
} else {
announcement
} }
} )
) } else {
announcement
}
}
)
) )
}, { },
{
Log.w(TAG, "Failed to remove reaction from the announcement.", it) Log.w(TAG, "Failed to remove reaction from the announcement.", it)
}) }
.autoDispose() )
.autoDispose()
} }
companion object { companion object {

View File

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

View File

@ -30,9 +30,9 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.view.ProgressImageView import com.keylesspalace.tusky.components.compose.view.ProgressImageView
class MediaPreviewAdapter( class MediaPreviewAdapter(
context: Context, context: Context,
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit, private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() { ) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
fun submitList(list: List<ComposeActivity.QueuedMedia>) { fun submitList(list: List<ComposeActivity.QueuedMedia>) {
@ -57,7 +57,7 @@ class MediaPreviewAdapter(
} }
private val thumbnailViewSize = 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 override fun getItemCount(): Int = differ.currentList.size
@ -74,31 +74,34 @@ class MediaPreviewAdapter(
holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp) holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
} else { } else {
Glide.with(holder.itemView.context) Glide.with(holder.itemView.context)
.load(item.uri) .load(item.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate() .dontAnimate()
.into(holder.progressImageView) .into(holder.progressImageView)
} }
} }
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() { private val differ = AsyncListDiffer(
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { this,
return oldItem.localId == newItem.localId 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 { override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
return oldItem == newItem return oldItem == newItem
}
} }
}) )
inner class PreviewViewHolder(val progressImageView: ProgressImageView) inner class PreviewViewHolder(val progressImageView: ProgressImageView) :
: RecyclerView.ViewHolder(progressImageView) { RecyclerView.ViewHolder(progressImageView) {
init { init {
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources val margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin) .getDimensionPixelSize(R.dimen.compose_media_preview_margin)
val marginBottom = itemView.context.resources 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) layoutParams.setMargins(margin, 0, margin, marginBottom)
progressImageView.layoutParams = layoutParams progressImageView.layoutParams = layoutParams
progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP 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.entity.Attachment
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.ProgressRequestBody import com.keylesspalace.tusky.network.ProgressRequestBody
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
import com.keylesspalace.tusky.util.getImageSquarePixels
import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.randomAlphanumericString
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
@ -37,7 +40,7 @@ import okhttp3.MultipartBody
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.util.* import java.util.Date
sealed class UploadEvent { sealed class UploadEvent {
data class ProgressEvent(val percentage: Int) : UploadEvent() data class ProgressEvent(val percentage: Int) : UploadEvent()
@ -50,9 +53,9 @@ fun createNewImageFile(context: Context): File {
val imageFileName = "Tusky_${randomId}_" val imageFileName = "Tusky_${randomId}_"
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile( return File.createTempFile(
imageFileName, /* prefix */ imageFileName, /* prefix */
".jpg", /* suffix */ ".jpg", /* suffix */
storageDir /* directory */ storageDir /* directory */
) )
} }
@ -69,18 +72,18 @@ class MediaTypeException : Exception()
class CouldNotOpenFileException : Exception() class CouldNotOpenFileException : Exception()
class MediaUploaderImpl( class MediaUploaderImpl(
private val context: Context, private val context: Context,
private val mastodonApi: MastodonApi private val mastodonApi: MastodonApi
) : MediaUploader { ) : MediaUploader {
override fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> { override fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
return Observable return Observable
.fromCallable { .fromCallable {
if (shouldResizeMedia(media)) { if (shouldResizeMedia(media)) {
downsize(media) downsize(media)
} else media } else media
} }
.switchMap { upload(it) } .switchMap { upload(it) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
} }
override fun prepareMedia(inUri: Uri): Single<PreparedMedia> { override fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
@ -101,12 +104,13 @@ class MediaUploaderImpl(
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
FileOutputStream(file.absoluteFile).use { out -> FileOutputStream(file.absoluteFile).use { out ->
input.copyTo(out) input.copyTo(out)
uri = FileProvider.getUriForFile(context, uri = FileProvider.getUriForFile(
BuildConfig.APPLICATION_ID + ".fileprovider", context,
file) BuildConfig.APPLICATION_ID + ".fileprovider",
file
)
mediaSize = getMediaSize(contentResolver, uri) mediaSize = getMediaSize(contentResolver, uri)
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
Log.w(TAG, e) Log.w(TAG, e)
@ -151,20 +155,22 @@ class MediaUploaderImpl(
var mimeType = contentResolver.getType(media.uri) var mimeType = contentResolver.getType(media.uri)
val map = MimeTypeMap.getSingleton() val map = MimeTypeMap.getSingleton()
val fileExtension = map.getExtensionFromMimeType(mimeType) val fileExtension = map.getExtensionFromMimeType(mimeType)
val filename = String.format("%s_%s_%s.%s", val filename = "%s_%s_%s.%s".format(
context.getString(R.string.app_name), context.getString(R.string.app_name),
Date().time.toString(), Date().time.toString(),
randomAlphanumericString(10), randomAlphanumericString(10),
fileExtension) fileExtension
)
val stream = contentResolver.openInputStream(media.uri) val stream = contentResolver.openInputStream(media.uri)
if (mimeType == null) mimeType = "multipart/form-data" if (mimeType == null) mimeType = "multipart/form-data"
var lastProgress = -1 var lastProgress = -1
val fileBody = ProgressRequestBody(stream, media.mediaSize, val fileBody = ProgressRequestBody(
mimeType.toMediaTypeOrNull()) { percentage -> stream, media.mediaSize,
mimeType.toMediaTypeOrNull()
) { percentage ->
if (percentage != lastProgress) { if (percentage != lastProgress) {
emitter.onNext(UploadEvent.ProgressEvent(percentage)) emitter.onNext(UploadEvent.ProgressEvent(percentage))
} }
@ -180,12 +186,15 @@ class MediaUploaderImpl(
} }
val uploadDisposable = mastodonApi.uploadMedia(body, description) val uploadDisposable = mastodonApi.uploadMedia(body, description)
.subscribe({ attachment -> .subscribe(
{ attachment ->
emitter.onNext(UploadEvent.FinishedEvent(attachment)) emitter.onNext(UploadEvent.FinishedEvent(attachment))
emitter.onComplete() emitter.onComplete()
}, { e -> },
{ e ->
emitter.onError(e) emitter.onError(e)
}) }
)
// Cancel the request when our observable is cancelled // Cancel the request when our observable is cancelled
emitter.setDisposable(uploadDisposable) emitter.setDisposable(uploadDisposable)
@ -194,15 +203,16 @@ class MediaUploaderImpl(
private fun downsize(media: QueuedMedia): QueuedMedia { private fun downsize(media: QueuedMedia): QueuedMedia {
val file = createNewImageFile(context) val file = createNewImageFile(context)
DownsizeImageTask.resize(arrayOf(media.uri), DownsizeImageTask.resize(
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file) arrayOf(media.uri),
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file
)
return media.copy(uri = file.toUri(), mediaSize = file.length()) return media.copy(uri = file.toUri(), mediaSize = file.length())
} }
private fun shouldResizeMedia(media: QueuedMedia): Boolean { private fun shouldResizeMedia(media: QueuedMedia): Boolean {
return media.type == QueuedMedia.Type.IMAGE return media.type == QueuedMedia.Type.IMAGE &&
&& (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|| getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
} }
private companion object { private companion object {
@ -211,6 +221,5 @@ class MediaUploaderImpl(
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
} }
} }

View File

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

View File

@ -27,11 +27,11 @@ import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.util.visible
class AddPollOptionsAdapter( class AddPollOptionsAdapter(
private var options: MutableList<String>, private var options: MutableList<String>,
private val maxOptionLength: Int, private val maxOptionLength: Int,
private val onOptionRemoved: (Boolean) -> Unit, private val onOptionRemoved: (Boolean) -> Unit,
private val onOptionChanged: (Boolean) -> Unit private val onOptionChanged: (Boolean) -> Unit
): RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() { ) : RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() {
val pollOptions: List<String> val pollOptions: List<String>
get() = options.toList() get() = options.toList()
@ -48,7 +48,7 @@ class AddPollOptionsAdapter(
binding.optionEditText.onTextChanged { s, _, _, _ -> binding.optionEditText.onTextChanged { s, _, _, _ ->
val pos = holder.bindingAdapterPosition val pos = holder.bindingAdapterPosition
if(pos != RecyclerView.NO_POSITION) { if (pos != RecyclerView.NO_POSITION) {
options[pos] = s.toString() options[pos] = s.toString()
onOptionChanged(validateInput()) 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 // https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
fun <T> T.makeCaptionDialog(existingDescription: String?, fun <T> T.makeCaptionDialog(
previewUri: Uri, existingDescription: String?,
onUpdateDescription: (String) -> LiveData<Boolean> previewUri: Uri,
onUpdateDescription: (String) -> LiveData<Boolean>
) where T : Activity, T : LifecycleOwner { ) where T : Activity, T : LifecycleOwner {
val dialogLayout = LinearLayout(this) val dialogLayout = LinearLayout(this)
val padding = Utils.dpToPx(this, 8) 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) (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
val input = EditText(this) val input = EditText(this)
input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired, input.hint = resources.getQuantityString(
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT) R.plurals.hint_describe_for_visually_impaired,
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT
)
dialogLayout.addView(input) dialogLayout.addView(input)
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
input.setLines(2) input.setLines(2)
input.inputType = (InputType.TYPE_CLASS_TEXT input.inputType = (
or InputType.TYPE_TEXT_FLAG_MULTI_LINE InputType.TYPE_CLASS_TEXT
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) or InputType.TYPE_TEXT_FLAG_MULTI_LINE
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
)
input.setText(existingDescription) input.setText(existingDescription)
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
@ -75,41 +80,40 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
onUpdateDescription(input.text.toString()) onUpdateDescription(input.text.toString())
withLifecycleContext { withLifecycleContext {
onUpdateDescription(input.text.toString()) onUpdateDescription(input.text.toString())
.observe { success -> if (!success) showFailedCaptionMessage() } .observe { success -> if (!success) showFailedCaptionMessage() }
} }
dialog.dismiss() dialog.dismiss()
} }
val dialog = AlertDialog.Builder(this) val dialog = AlertDialog.Builder(this)
.setView(dialogLayout) .setView(dialogLayout)
.setPositiveButton(android.R.string.ok, okListener) .setPositiveButton(android.R.string.ok, okListener)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.create() .create()
val window = dialog.window val window = dialog.window
window?.setSoftInputMode( window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
)
dialog.show() dialog.show()
// Load the image and manually set it into the ImageView because it doesn't have a fixed size. // Load the image and manually set it into the ImageView because it doesn't have a fixed size.
Glide.with(this) Glide.with(this)
.load(previewUri) .load(previewUri)
.downsample(DownsampleStrategy.CENTER_INSIDE) .downsample(DownsampleStrategy.CENTER_INSIDE)
.into(object : CustomTarget<Drawable>(4096, 4096) { .into(object : CustomTarget<Drawable>(4096, 4096) {
override fun onLoadCleared(placeholder: Drawable?) { override fun onLoadCleared(placeholder: Drawable?) {
imageView.setImageDrawable(placeholder) imageView.setImageDrawable(placeholder)
} }
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
imageView.setImageDrawable(resource) imageView.setImageDrawable(resource)
} }
}) })
} }
private fun Activity.showFailedCaptionMessage() { private fun Activity.showFailedCaptionMessage() {
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
} }

View File

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

View File

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

View File

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

View File

@ -28,15 +28,15 @@ import com.mikepenz.iconics.utils.sizeDp
class TootButton class TootButton
@JvmOverloads constructor( @JvmOverloads constructor(
context: Context, context: Context,
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : MaterialButton(context, attrs, defStyleAttr) { ) : MaterialButton(context, attrs, defStyleAttr) {
private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button) private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button)
init { init {
if(smallStyle) { if (smallStyle) {
setIconResource(R.drawable.ic_send_24dp) setIconResource(R.drawable.ic_send_24dp)
} else { } else {
setText(R.string.action_send) setText(R.string.action_send)
@ -47,7 +47,7 @@ class TootButton
} }
fun setStatusVisibility(visibility: Status.Visibility) { fun setStatusVisibility(visibility: Status.Visibility) {
if(!smallStyle) { if (!smallStyle) {
icon = when (visibility) { icon = when (visibility) {
Status.Visibility.PUBLIC -> { 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 com.keylesspalace.tusky.util.shouldTrimStatus
import java.util.Date import java.util.Date
@Entity(primaryKeys = ["id","accountId"]) @Entity(primaryKeys = ["id", "accountId"])
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
data class ConversationEntity( data class ConversationEntity(
val accountId: Long, val accountId: Long,
@ -98,7 +98,7 @@ data class ConversationStatusEntity(
if (inReplyToId != other.inReplyToId) return false if (inReplyToId != other.inReplyToId) return false
if (inReplyToAccountId != other.inReplyToAccountId) return false if (inReplyToAccountId != other.inReplyToAccountId) return false
if (account != other.account) 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 (createdAt != other.createdAt) return false
if (emojis != other.emojis) return false if (emojis != other.emojis) return false
if (favouritesCount != other.favouritesCount) return false if (favouritesCount != other.favouritesCount) return false
@ -157,7 +157,7 @@ data class ConversationStatusEntity(
reblogged = false, reblogged = false,
favourited = favourited, favourited = favourited,
bookmarked = bookmarked, bookmarked = bookmarked,
sensitive= sensitive, sensitive = sensitive,
spoilerText = spoilerText, spoilerText = spoilerText,
visibility = Status.Visibility.DIRECT, visibility = Status.Visibility.DIRECT,
attachments = attachments, attachments = attachments,
@ -166,7 +166,8 @@ data class ConversationStatusEntity(
pinned = false, pinned = false,
muted = muted, muted = muted,
poll = poll, poll = poll,
card = null) card = null
)
} }
} }

View File

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

View File

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

View File

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

View File

@ -28,18 +28,17 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.DraftAttachment import com.keylesspalace.tusky.db.DraftAttachment
class DraftMediaAdapter( class DraftMediaAdapter(
private val attachmentClick: () -> Unit private val attachmentClick: () -> Unit
) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>( ) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>(
object: DiffUtil.ItemCallback<DraftAttachment>() { object : DiffUtil.ItemCallback<DraftAttachment>() {
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem return oldItem == newItem
}
override fun areContentsTheSame(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 { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder {
@ -52,24 +51,24 @@ class DraftMediaAdapter(
holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp) holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
} else { } else {
Glide.with(holder.itemView.context) Glide.with(holder.itemView.context)
.load(attachment.uri) .load(attachment.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate() .dontAnimate()
.into(holder.imageView) .into(holder.imageView)
} }
} }
} }
inner class DraftMediaViewHolder(val imageView: ImageView) inner class DraftMediaViewHolder(val imageView: ImageView) :
: RecyclerView.ViewHolder(imageView) { RecyclerView.ViewHolder(imageView) {
init { init {
val thumbnailViewSize = val thumbnailViewSize =
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
val margin = itemView.context.resources val margin = itemView.context.resources
.getDimensionPixelSize(R.dimen.compose_media_preview_margin) .getDimensionPixelSize(R.dimen.compose_media_preview_margin)
val marginBottom = itemView.context.resources 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) layoutParams.setMargins(margin, 0, margin, marginBottom)
imageView.layoutParams = layoutParams imageView.layoutParams = layoutParams
imageView.scaleType = ImageView.ScaleType.CENTER_CROP 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) { if (draft.inReplyToId != null) {
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
viewModel.getToot(draft.inReplyToId) viewModel.getToot(draft.inReplyToId)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this)) .autoDispose(from(this))
.subscribe({ status -> .subscribe(
{ status ->
val composeOptions = ComposeActivity.ComposeOptions( val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id, draftId = draft.id,
tootText = draft.content, tootText = draft.content,
contentWarning = draft.contentWarning, contentWarning = draft.contentWarning,
inReplyToId = draft.inReplyToId, inReplyToId = draft.inReplyToId,
replyingStatusContent = status.content.toString(), replyingStatusContent = status.content.toString(),
replyingStatusAuthor = status.account.localUsername, replyingStatusAuthor = status.account.localUsername,
draftAttachments = draft.attachments, draftAttachments = draft.attachments,
poll = draft.poll, poll = draft.poll,
sensitive = draft.sensitive, sensitive = draft.sensitive,
visibility = draft.visibility visibility = draft.visibility
) )
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
startActivity(ComposeActivity.startIntent(this, composeOptions)) startActivity(ComposeActivity.startIntent(this, composeOptions))
},
}, { throwable -> { throwable ->
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
@ -124,9 +125,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
openDraftWithoutReply(draft) openDraftWithoutReply(draft)
} else { } else {
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
.show() .show()
} }
}) }
)
} else { } else {
openDraftWithoutReply(draft) openDraftWithoutReply(draft)
} }
@ -134,13 +136,13 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
private fun openDraftWithoutReply(draft: DraftEntity) { private fun openDraftWithoutReply(draft: DraftEntity) {
val composeOptions = ComposeActivity.ComposeOptions( val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id, draftId = draft.id,
tootText = draft.content, tootText = draft.content,
contentWarning = draft.contentWarning, contentWarning = draft.contentWarning,
draftAttachments = draft.attachments, draftAttachments = draft.attachments,
poll = draft.poll, poll = draft.poll,
sensitive = draft.sensitive, sensitive = draft.sensitive,
visibility = draft.visibility visibility = draft.visibility
) )
startActivity(ComposeActivity.startIntent(this, composeOptions)) startActivity(ComposeActivity.startIntent(this, composeOptions))
@ -149,10 +151,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
override fun onDeleteDraft(draft: DraftEntity) { override fun onDeleteDraft(draft: DraftEntity) {
viewModel.deleteDraft(draft) viewModel.deleteDraft(draft)
Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG) Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) { .setAction(R.string.action_undo) {
viewModel.restoreDraft(draft) viewModel.restoreDraft(draft)
} }
.show() .show()
} }
companion object { companion object {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,8 +36,10 @@ import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector import dagger.android.HasAndroidInjector
import javax.inject.Inject import javax.inject.Inject
class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener, class PreferencesActivity :
HasAndroidInjector { BaseActivity(),
SharedPreferences.OnSharedPreferenceChangeListener,
HasAndroidInjector {
@Inject @Inject
lateinit var eventHub: EventHub lateinit var eventHub: EventHub
@ -62,36 +64,35 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE" val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE"
val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag) val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { ?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
GENERAL_PREFERENCES -> { GENERAL_PREFERENCES -> {
setTitle(R.string.action_view_preferences) setTitle(R.string.action_view_preferences)
PreferencesFragment.newInstance() 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")
} }
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 { supportFragmentManager.commit {
replace(R.id.fragment_container, fragment, fragmentTag) replace(R.id.fragment_container, fragment, fragmentTag)
} }
restartActivitiesOnExit = intent.getBooleanExtra("restart", false) restartActivitiesOnExit = intent.getBooleanExtra("restart", false)
} }
override fun onResume() { override fun onResume() {
@ -122,7 +123,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
restartActivitiesOnExit = true restartActivitiesOnExit = true
this.restartCurrentActivity() this.restartCurrentActivity()
} }
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash", "showCardsInTimelines", "confirmReblogs", "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> { "useBlurhash", "showCardsInTimelines", "confirmReblogs", "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
@ -179,5 +179,4 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
return intent return intent
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,9 +26,9 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusDisplayOptions
class StatusesAdapter( class StatusesAdapter(
private val statusDisplayOptions: StatusDisplayOptions, private val statusDisplayOptions: StatusDisplayOptions,
private val statusViewState: StatusViewState, private val statusViewState: StatusViewState,
private val adapterHandler: AdapterHandler private val adapterHandler: AdapterHandler
) : PagingDataAdapter<Status, StatusViewHolder>(STATUS_COMPARATOR) { ) : PagingDataAdapter<Status, StatusViewHolder>(STATUS_COMPARATOR) {
private val statusForPosition: (Int) -> Status? = { position: Int -> private val statusForPosition: (Int) -> Status? = { position: Int ->
@ -37,8 +37,10 @@ class StatusesAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder {
val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemReportStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return StatusViewHolder(binding, statusDisplayOptions, statusViewState, adapterHandler, return StatusViewHolder(
statusForPosition) binding, statusDisplayOptions, statusViewState, adapterHandler,
statusForPosition
)
} }
override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { override fun onBindViewHolder(holder: StatusViewHolder, position: Int) {
@ -50,10 +52,10 @@ class StatusesAdapter(
companion object { companion object {
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() { val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() {
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
oldItem == newItem oldItem == newItem
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean = 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? { override fun getRefreshKey(state: PagingState<String, Status>): String? {
return state.anchorPosition?.let { anchorPosition -> return state.anchorPosition?.let { anchorPosition ->
state.closestItemToPosition(anchorPosition)?.id state.closestItemToPosition(anchorPosition)?.id
} }
} }
@ -65,7 +65,6 @@ class StatusesPagingSource(
prevKey = result.firstOrNull()?.id, prevKey = result.firstOrNull()?.id,
nextKey = result.lastOrNull()?.id nextKey = result.lastOrNull()?.id
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.w("StatusesPagingSource", "failed to load statuses", e) Log.w("StatusesPagingSource", "failed to load statuses", e)
return LoadResult.Error(e) return LoadResult.Error(e)
@ -86,4 +85,4 @@ class StatusesPagingSource(
excludeReblogs = true excludeReblogs = true
).await() ).await()
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -95,13 +95,17 @@ class SearchViewModel @Inject constructor(
fun removeItem(status: Pair<Status, StatusViewData.Concrete>) { fun removeItem(status: Pair<Status, StatusViewData.Concrete>) {
timelineCases.delete(status.first.id) timelineCases.delete(status.first.id)
.subscribe({ .subscribe(
{
if (loadedStatuses.remove(status)) if (loadedStatuses.remove(status))
statusesPagingSourceFactory.invalidate() statusesPagingSourceFactory.invalidate()
}, { },
err -> Log.d(TAG, "Failed to delete status", err) {
}) err ->
.autoDispose() Log.d(TAG, "Failed to delete status", err)
}
)
.autoDispose()
} }
fun expandedChange(status: Pair<Status, StatusViewData.Concrete>, expanded: Boolean) { 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 TAG = "SearchViewModel"
private const val DEFAULT_LOAD_SIZE = 20 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.entity.Account
import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.LinkListener
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
: PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) { PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_account, parent, false) .inflate(R.layout.item_account, parent, false)
return AccountViewHolder(view) return AccountViewHolder(view)
} }
@ -46,10 +46,10 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() { val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean = override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
oldItem.deepEquals(newItem) oldItem.deepEquals(newItem)
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean = 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.interfaces.LinkListener
import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.BindingHolder
class SearchHashtagsAdapter(private val linkListener: LinkListener) class SearchHashtagsAdapter(private val linkListener: LinkListener) :
: PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) { PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemHashtagBinding> { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemHashtagBinding> {
val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false) val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@ -43,10 +43,10 @@ class SearchHashtagsAdapter(private val linkListener: LinkListener)
val HASHTAG_COMPARATOR = object : DiffUtil.ItemCallback<HashTag>() { val HASHTAG_COMPARATOR = object : DiffUtil.ItemCallback<HashTag>() {
override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean =
oldItem.name == newItem.name oldItem.name == newItem.name
override fun areItemsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = 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 override fun getItemCount() = 3
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -74,15 +74,15 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
override fun createAdapter(): PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, *> { override fun createAdapter(): PagingDataAdapter<Pair<Status, StatusViewData.Concrete>, *> {
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context) val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
val statusDisplayOptions = StatusDisplayOptions( val statusDisplayOptions = StatusDisplayOptions(
animateAvatars = preferences.getBoolean("animateGifAvatars", false), animateAvatars = preferences.getBoolean("animateGifAvatars", false),
mediaPreviewEnabled = viewModel.mediaPreviewEnabled, mediaPreviewEnabled = viewModel.mediaPreviewEnabled,
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
showBotOverlay = preferences.getBoolean("showBotOverlay", true), showBotOverlay = preferences.getBoolean("showBotOverlay", true),
useBlurhash = preferences.getBoolean("useBlurhash", true), useBlurhash = preferences.getBoolean("useBlurhash", true),
cardViewMode = CardViewMode.NONE, cardViewMode = CardViewMode.NONE,
confirmReblogs = preferences.getBoolean("confirmReblogs", true), confirmReblogs = preferences.getBoolean("confirmReblogs", true),
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
) )
binding.searchRecyclerView.addItemDecoration(DividerItemDecoration(binding.searchRecyclerView.context, DividerItemDecoration.VERTICAL)) 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) { when (actionable.attachments[attachmentIndex].type) {
Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> {
val attachments = AttachmentViewData.list(actionable) val attachments = AttachmentViewData.list(actionable)
val intent = ViewMediaActivity.newIntent(context, attachments, val intent = ViewMediaActivity.newIntent(
attachmentIndex) context, attachments,
attachmentIndex
)
if (view != null) { if (view != null) {
val url = actionable.attachments[attachmentIndex].url val url = actionable.attachments[attachmentIndex].url
ViewCompat.setTransitionName(view, url) ViewCompat.setTransitionName(view, url)
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
view, url) requireActivity(),
view, url
)
startActivity(intent, options.toBundle()) startActivity(intent, options.toBundle())
} else { } else {
startActivity(intent) startActivity(intent)
@ -198,20 +202,23 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
private fun reply(status: Status) { private fun reply(status: Status) {
val actionableStatus = status.actionableStatus val actionableStatus = status.actionableStatus
val mentionedUsernames = actionableStatus.mentions.map { it.username } val mentionedUsernames = actionableStatus.mentions.map { it.username }
.toMutableSet() .toMutableSet()
.apply { .apply {
add(actionableStatus.account.username) add(actionableStatus.account.username)
remove(viewModel.activeAccount?.username) remove(viewModel.activeAccount?.username)
} }
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions( val intent = ComposeActivity.startIntent(
requireContext(),
ComposeOptions(
inReplyToId = status.actionableId, inReplyToId = status.actionableId,
replyVisibility = actionableStatus.visibility, replyVisibility = actionableStatus.visibility,
contentWarning = actionableStatus.spoilerText, contentWarning = actionableStatus.spoilerText,
mentionedUsernames = mentionedUsernames, mentionedUsernames = mentionedUsernames,
replyingStatusAuthor = actionableStatus.account.localUsername, replyingStatusAuthor = actionableStatus.account.localUsername,
replyingStatusContent = actionableStatus.content.toString() replyingStatusContent = actionableStatus.content.toString()
)) )
)
startActivity(intent) startActivity(intent)
} }
@ -244,7 +251,7 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
menu.findItem(R.id.status_unreblog_private).isVisible = reblogged menu.findItem(R.id.status_unreblog_private).isVisible = reblogged
} }
Status.Visibility.UNKNOWN, Status.Visibility.DIRECT -> { Status.Visibility.UNKNOWN, Status.Visibility.DIRECT -> {
} //Ignore } // Ignore
} }
} else { } else {
popup.inflate(R.menu.status_more) popup.inflate(R.menu.status_more)
@ -271,11 +278,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
if (mutable) { if (mutable) {
muteConversationItem.setTitle( muteConversationItem.setTitle(
if (status.muted == true) { if (status.muted == true) {
R.string.action_unmute_conversation R.string.action_unmute_conversation
} else { } else {
R.string.action_mute_conversation R.string.action_mute_conversation
}) }
)
} }
popup.setOnMenuItemClickListener { item -> popup.setOnMenuItemClickListener { item ->
@ -287,8 +295,8 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
sendIntent.action = Intent.ACTION_SEND sendIntent.action = Intent.ACTION_SEND
val stringToShare = statusToShare.account.username + val stringToShare = statusToShare.account.username +
" - " + " - " +
statusToShare.content.toString() statusToShare.content.toString()
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare) sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
sendIntent.type = "text/plain" sendIntent.type = "text/plain"
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_content_to))) 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) { private fun onBlock(accountId: String, accountUsername: String) {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.dialog_block_warning, accountUsername)) .setMessage(getString(R.string.dialog_block_warning, accountUsername))
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.blockAccount(accountId) } .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.blockAccount(accountId) }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
private fun onMute(accountId: String, accountUsername: String) { 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) { private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) {
bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener { bottomSheetActivity?.showAccountChooserDialog(
override fun onAccountSelected(account: AccountEntity) { dialogTitle, false,
openAsAccount(statusUrl, account) object : AccountSelectionListener {
override fun onAccountSelected(account: AccountEntity) {
openAsAccount(statusUrl, account)
}
} }
}) )
} }
private fun openAsAccount(statusUrl: String, account: AccountEntity) { 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) { private fun showConfirmDeleteDialog(id: String, position: Int) {
context?.let { context?.let {
AlertDialog.Builder(it) AlertDialog.Builder(it)
.setMessage(R.string.dialog_delete_toot_warning) .setMessage(R.string.dialog_delete_toot_warning)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.deleteStatus(id) viewModel.deleteStatus(id)
removeItem(position) removeItem(position)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
} }
private fun showConfirmEditDialog(id: String, position: Int, status: Status) { private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
activity?.let { activity?.let {
AlertDialog.Builder(it) AlertDialog.Builder(it)
.setMessage(R.string.dialog_redraft_toot_warning) .setMessage(R.string.dialog_redraft_toot_warning)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
viewModel.deleteStatus(id) viewModel.deleteStatus(id)
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) .autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
.subscribe({ deletedStatus -> .subscribe(
removeItem(position) { deletedStatus ->
removeItem(position)
val redraftStatus = if (deletedStatus.isEmpty()) { val redraftStatus = if (deletedStatus.isEmpty()) {
status.toDeletedStatus() status.toDeletedStatus()
} else { } else {
deletedStatus deletedStatus
} }
val intent = ComposeActivity.startIntent(requireContext(), ComposeOptions( val intent = ComposeActivity.startIntent(
tootText = redraftStatus.text ?: "", requireContext(),
inReplyToId = redraftStatus.inReplyToId, ComposeOptions(
visibility = redraftStatus.visibility, tootText = redraftStatus.text ?: "",
contentWarning = redraftStatus.spoilerText, inReplyToId = redraftStatus.inReplyToId,
mediaAttachments = redraftStatus.attachments, visibility = redraftStatus.visibility,
sensitive = redraftStatus.sensitive, contentWarning = redraftStatus.spoilerText,
poll = redraftStatus.poll?.toNewPoll(status.createdAt) mediaAttachments = redraftStatus.attachments,
)) sensitive = redraftStatus.sensitive,
startActivity(intent) poll = redraftStatus.poll?.toNewPoll(status.createdAt)
}, { error -> )
Log.w("SearchStatusesFragment", "error deleting status", error) )
Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show() startActivity(intent)
}) },
{ error ->
} Log.w("SearchStatusesFragment", "error deleting status", error)
.setNegativeButton(android.R.string.cancel, null) Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show()
.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.fragment.app.viewModels
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.* import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.SimpleItemAnimator
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import at.connyduck.sparkbutton.helpers.Utils import at.connyduck.sparkbutton.helpers.Utils
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.*
import autodispose2.androidx.lifecycle.autoDispose import autodispose2.androidx.lifecycle.autoDispose
import com.keylesspalace.tusky.AccountListActivity import com.keylesspalace.tusky.AccountListActivity
import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent
@ -47,7 +53,13 @@ import com.keylesspalace.tusky.interfaces.RefreshableFragment
import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.util.CardViewMode
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.EndlessOnScrollListener import com.keylesspalace.tusky.view.EndlessOnScrollListener
import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.AttachmentViewData
import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.StatusViewData
@ -56,8 +68,13 @@ import io.reactivex.rxjava3.core.Observable
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable, class TimelineFragment :
ReselectableFragment, RefreshableFragment { SFragment(),
OnRefreshListener,
StatusActionListener,
Injectable,
ReselectableFragment,
RefreshableFragment {
@Inject @Inject
lateinit var viewModelFactory: ViewModelFactory lateinit var viewModelFactory: ViewModelFactory
@ -161,8 +178,7 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
private fun setupRecyclerView() { private fun setupRecyclerView() {
binding.recyclerView.setAccessibilityDelegateCompat( binding.recyclerView.setAccessibilityDelegateCompat(
ListStatusAccessibilityDelegate(binding.recyclerView, this) ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos -> viewModel.statuses.getOrNull(pos) }
{ pos -> viewModel.statuses.getOrNull(pos) }
) )
binding.recyclerView.setHasFixedSize(true) binding.recyclerView.setHasFixedSize(true)
layoutManager = LinearLayoutManager(context) layoutManager = LinearLayoutManager(context)
@ -330,8 +346,10 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
} }
override fun onViewAccount(id: String) { override fun onViewAccount(id: String) {
if ((viewModel.kind == TimelineViewModel.Kind.USER || if ((
viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES) && viewModel.kind == TimelineViewModel.Kind.USER ||
viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES
) &&
viewModel.id == id viewModel.id == id
) { ) {
/* If already viewing an account page, then any requests to view that account page /* If already viewing an account page, then any requests to view that account page
@ -369,9 +387,9 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
private fun actionButtonPresent(): Boolean { private fun actionButtonPresent(): Boolean {
return viewModel.kind != TimelineViewModel.Kind.TAG && return viewModel.kind != TimelineViewModel.Kind.TAG &&
viewModel.kind != TimelineViewModel.Kind.FAVOURITES && viewModel.kind != TimelineViewModel.Kind.FAVOURITES &&
viewModel.kind != TimelineViewModel.Kind.BOOKMARKS && viewModel.kind != TimelineViewModel.Kind.BOOKMARKS &&
activity is ActionButtonActivity activity is ActionButtonActivity
} }
private fun updateViews() { private fun updateViews() {
@ -505,7 +523,6 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
private const val HASHTAGS_ARG = "hashtags" private const val HASHTAGS_ARG = "hashtags"
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh" private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh"
fun newInstance( fun newInstance(
kind: TimelineViewModel.Kind, kind: TimelineViewModel.Kind,
hashtagOrId: String? = null, hashtagOrId: String? = null,
@ -531,7 +548,6 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
return fragment return fragment
} }
private val diffCallback: DiffUtil.ItemCallback<StatusViewData> = private val diffCallback: DiffUtil.ItemCallback<StatusViewData> =
object : DiffUtil.ItemCallback<StatusViewData>() { object : DiffUtil.ItemCallback<StatusViewData>() {
override fun areItemsTheSame( override fun areItemsTheSame(
@ -555,9 +571,9 @@ class TimelineFragment : SFragment(), OnRefreshListener, StatusActionListener, I
return if (oldItem === newItem) { return if (oldItem === newItem) {
// If items are equal - update timestamp only // If items are equal - update timestamp only
listOf(StatusBaseViewHolder.Key.KEY_CREATED) listOf(StatusBaseViewHolder.Key.KEY_CREATED)
} else // If items are different - update the whole view holder } else // If items are different - update the whole view holder
null null
} }
} }
} }
} }

View File

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

View File

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