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:
parent
955267199e
commit
16ffcca748
227 changed files with 3933 additions and 3371 deletions
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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,105 +148,104 @@ 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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,8 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
|
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
|
||||||
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
|
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
|
||||||
|
|
||||||
|
@ -48,7 +49,6 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
|
||||||
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
|
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
|
||||||
|
|
||||||
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
|
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -12,7 +12,7 @@ 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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,25 +209,27 @@ 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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -176,5 +176,4 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
return NotificationPreferencesFragment()
|
return NotificationPreferencesFragment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -50,7 +50,6 @@ class ProxyPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
setSummaryProvider { text }
|
setSummaryProvider { text }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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?) {
|
||||||
|
|
|
@ -19,7 +19,7 @@ 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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,5 +34,4 @@ class SearchPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(acti
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getItemCount() = 3
|
override fun getItemCount() = 3
|
||||||
|
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,7 +571,7 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -15,7 +15,11 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.db
|
package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface AccountDao {
|
interface AccountDao {
|
||||||
|
@ -27,5 +31,4 @@ interface AccountDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM AccountEntity ORDER BY id ASC")
|
@Query("SELECT * FROM AccountEntity ORDER BY id ASC")
|
||||||
fun loadAll(): List<AccountEntity>
|
fun loadAll(): List<AccountEntity>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,42 +21,49 @@ import androidx.room.PrimaryKey
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.keylesspalace.tusky.TabData
|
import com.keylesspalace.tusky.TabData
|
||||||
import com.keylesspalace.tusky.defaultTabs
|
import com.keylesspalace.tusky.defaultTabs
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
|
||||||
@Entity(indices = [Index(value = ["domain", "accountId"],
|
@Entity(
|
||||||
unique = true)])
|
indices = [
|
||||||
|
Index(
|
||||||
|
value = ["domain", "accountId"],
|
||||||
|
unique = true
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
|
data class AccountEntity(
|
||||||
val domain: String,
|
@field:PrimaryKey(autoGenerate = true) var id: Long,
|
||||||
var accessToken: String,
|
val domain: String,
|
||||||
var isActive: Boolean,
|
var accessToken: String,
|
||||||
var accountId: String = "",
|
var isActive: Boolean,
|
||||||
var username: String = "",
|
var accountId: String = "",
|
||||||
var displayName: String = "",
|
var username: String = "",
|
||||||
var profilePictureUrl: String = "",
|
var displayName: String = "",
|
||||||
var notificationsEnabled: Boolean = true,
|
var profilePictureUrl: String = "",
|
||||||
var notificationsMentioned: Boolean = true,
|
var notificationsEnabled: Boolean = true,
|
||||||
var notificationsFollowed: Boolean = true,
|
var notificationsMentioned: Boolean = true,
|
||||||
var notificationsFollowRequested: Boolean = false,
|
var notificationsFollowed: Boolean = true,
|
||||||
var notificationsReblogged: Boolean = true,
|
var notificationsFollowRequested: Boolean = false,
|
||||||
var notificationsFavorited: Boolean = true,
|
var notificationsReblogged: Boolean = true,
|
||||||
var notificationsPolls: Boolean = true,
|
var notificationsFavorited: Boolean = true,
|
||||||
var notificationsSubscriptions: Boolean = true,
|
var notificationsPolls: Boolean = true,
|
||||||
var notificationSound: Boolean = true,
|
var notificationsSubscriptions: Boolean = true,
|
||||||
var notificationVibration: Boolean = true,
|
var notificationSound: Boolean = true,
|
||||||
var notificationLight: Boolean = true,
|
var notificationVibration: Boolean = true,
|
||||||
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
|
var notificationLight: Boolean = true,
|
||||||
var defaultMediaSensitivity: Boolean = false,
|
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
|
||||||
var alwaysShowSensitiveMedia: Boolean = false,
|
var defaultMediaSensitivity: Boolean = false,
|
||||||
var alwaysOpenSpoiler: Boolean = false,
|
var alwaysShowSensitiveMedia: Boolean = false,
|
||||||
var mediaPreviewEnabled: Boolean = true,
|
var alwaysOpenSpoiler: Boolean = false,
|
||||||
var lastNotificationId: String = "0",
|
var mediaPreviewEnabled: Boolean = true,
|
||||||
var activeNotifications: String = "[]",
|
var lastNotificationId: String = "0",
|
||||||
var emojis: List<Emoji> = emptyList(),
|
var activeNotifications: String = "[]",
|
||||||
var tabPreferences: List<TabData> = defaultTabs(),
|
var emojis: List<Emoji> = emptyList(),
|
||||||
var notificationsFilter: String = "[\"follow_request\"]") {
|
var tabPreferences: List<TabData> = defaultTabs(),
|
||||||
|
var notificationsFilter: String = "[\"follow_request\"]"
|
||||||
|
) {
|
||||||
|
|
||||||
val identifier: String
|
val identifier: String
|
||||||
get() = "$domain:$accountId"
|
get() = "$domain:$accountId"
|
||||||
|
|
|
@ -18,7 +18,7 @@ package com.keylesspalace.tusky.db
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
||||||
val newAccountId = maxAccountId + 1
|
val newAccountId = maxAccountId + 1
|
||||||
activeAccount = AccountEntity(id = newAccountId, domain = domain.lowercase(Locale.ROOT), accessToken = accessToken, isActive = true)
|
activeAccount = AccountEntity(id = newAccountId, domain = domain.lowercase(Locale.ROOT), accessToken = accessToken, isActive = true)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,7 +78,6 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
Log.d(TAG, "saveAccount: saving account with id " + account.id)
|
Log.d(TAG, "saveAccount: saving account with id " + account.id)
|
||||||
accountDao.insertOrReplace(account)
|
accountDao.insertOrReplace(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -103,9 +101,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
activeAccount = null
|
activeAccount = null
|
||||||
}
|
}
|
||||||
return activeAccount
|
return activeAccount
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -129,13 +125,12 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
val accountIndex = accounts.indexOf(it)
|
val accountIndex = accounts.indexOf(it)
|
||||||
|
|
||||||
if (accountIndex != -1) {
|
if (accountIndex != -1) {
|
||||||
//in case the user was already logged in with this account, remove the old information
|
// in case the user was already logged in with this account, remove the old information
|
||||||
accounts.removeAt(accountIndex)
|
accounts.removeAt(accountIndex)
|
||||||
accounts.add(accountIndex, it)
|
accounts.add(accountIndex, it)
|
||||||
} else {
|
} else {
|
||||||
accounts.add(it)
|
accounts.add(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,5 +189,4 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
id == accountId
|
id == accountId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -35,9 +35,8 @@ interface ConversationsDao {
|
||||||
suspend fun delete(conversation: ConversationEntity): Int
|
suspend fun delete(conversation: ConversationEntity): Int
|
||||||
|
|
||||||
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
|
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
|
||||||
fun conversationsForAccount(accountId: Long) : PagingSource<Int, ConversationEntity>
|
fun conversationsForAccount(accountId: Long): PagingSource<Int, ConversationEntity>
|
||||||
|
|
||||||
@Query("DELETE FROM ConversationEntity WHERE accountId = :accountId")
|
@Query("DELETE FROM ConversationEntity WHERE accountId = :accountId")
|
||||||
fun deleteForAccount(accountId: Long)
|
fun deleteForAccount(accountId: Long)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,18 +25,23 @@ import com.google.gson.reflect.TypeToken
|
||||||
import com.keylesspalace.tusky.TabData
|
import com.keylesspalace.tusky.TabData
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
||||||
import com.keylesspalace.tusky.createTabDataFromId
|
import com.keylesspalace.tusky.createTabDataFromId
|
||||||
import com.keylesspalace.tusky.entity.*
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.*
|
import java.util.ArrayList
|
||||||
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@ProvidedTypeConverter
|
@ProvidedTypeConverter
|
||||||
@Singleton
|
@Singleton
|
||||||
class Converters @Inject constructor (
|
class Converters @Inject constructor (
|
||||||
private val gson: Gson
|
private val gson: Gson
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
|
@ -62,10 +67,10 @@ class Converters @Inject constructor (
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToTabData(str: String?): List<TabData>? {
|
fun stringToTabData(str: String?): List<TabData>? {
|
||||||
return str?.split(";")
|
return str?.split(";")
|
||||||
?.map {
|
?.map {
|
||||||
val data = it.split(":")
|
val data = it.split(":")
|
||||||
createTabDataFromId(data[0], data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") })
|
createTabDataFromId(data[0], data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
|
@ -126,7 +131,7 @@ class Converters @Inject constructor (
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun spannedToString(spanned: Spanned?): String? {
|
fun spannedToString(spanned: Spanned?): String? {
|
||||||
if(spanned == null) {
|
if (spanned == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return spanned.toHtml()
|
return spanned.toHtml()
|
||||||
|
@ -134,7 +139,7 @@ class Converters @Inject constructor (
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun stringToSpanned(spannedString: String?): Spanned? {
|
fun stringToSpanned(spannedString: String?): Spanned? {
|
||||||
if(spannedString == null) {
|
if (spannedString == null) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return spannedString.parseAsHtml().trimTrailingWhitespace()
|
return spannedString.parseAsHtml().trimTrailingWhitespace()
|
||||||
|
|
|
@ -38,5 +38,4 @@ interface DraftDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM DraftEntity WHERE id = :id")
|
@Query("SELECT * FROM DraftEntity WHERE id = :id")
|
||||||
suspend fun find(id: Int): DraftEntity?
|
suspend fun find(id: Int): DraftEntity?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,24 +28,24 @@ import kotlinx.parcelize.Parcelize
|
||||||
@Entity
|
@Entity
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
data class DraftEntity(
|
data class DraftEntity(
|
||||||
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
val accountId: Long,
|
val accountId: Long,
|
||||||
val inReplyToId: String?,
|
val inReplyToId: String?,
|
||||||
val content: String?,
|
val content: String?,
|
||||||
val contentWarning: String?,
|
val contentWarning: String?,
|
||||||
val sensitive: Boolean,
|
val sensitive: Boolean,
|
||||||
val visibility: Status.Visibility,
|
val visibility: Status.Visibility,
|
||||||
val attachments: List<DraftAttachment>,
|
val attachments: List<DraftAttachment>,
|
||||||
val poll: NewPoll?,
|
val poll: NewPoll?,
|
||||||
val failedToSend: Boolean
|
val failedToSend: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class DraftAttachment(
|
data class DraftAttachment(
|
||||||
val uriString: String,
|
val uriString: String,
|
||||||
val description: String?,
|
val description: String?,
|
||||||
val type: Type
|
val type: Type
|
||||||
): Parcelable {
|
) : Parcelable {
|
||||||
val uri: Uri
|
val uri: Uri
|
||||||
get() = uriString.toUri()
|
get() = uriString.toUri()
|
||||||
|
|
||||||
|
|
|
@ -23,10 +23,10 @@ import com.keylesspalace.tusky.entity.Emoji
|
||||||
@Entity
|
@Entity
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
data class InstanceEntity(
|
data class InstanceEntity(
|
||||||
@field:PrimaryKey var instance: String,
|
@field:PrimaryKey var instance: String,
|
||||||
val emojiList: List<Emoji>?,
|
val emojiList: List<Emoji>?,
|
||||||
val maximumTootCharacters: Int?,
|
val maximumTootCharacters: Int?,
|
||||||
val maxPollOptions: Int?,
|
val maxPollOptions: Int?,
|
||||||
val maxPollOptionLength: Int?,
|
val maxPollOptionLength: Int?,
|
||||||
val version: String?
|
val version: String?
|
||||||
)
|
)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue