Merge tag 'v16.0'
This commit is contained in:
commit
450f9f310e
435 changed files with 16070 additions and 12463 deletions
|
|
@ -2,8 +2,8 @@ package com.keylesspalace.tusky
|
|||
|
||||
import androidx.room.testing.MigrationTestHelper
|
||||
import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Rule
|
||||
|
|
@ -18,9 +18,9 @@ class MigrationsTest {
|
|||
@JvmField
|
||||
@Rule
|
||||
var helper: MigrationTestHelper = MigrationTestHelper(
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
InstrumentationRegistry.getInstrumentation(),
|
||||
AppDatabase::class.java.canonicalName,
|
||||
FrameworkSQLiteOpenHelperFactory()
|
||||
)
|
||||
|
||||
@Test
|
||||
|
|
@ -33,12 +33,15 @@ class MigrationsTest {
|
|||
val active = true
|
||||
val accountId = "accountId"
|
||||
val username = "username"
|
||||
val values = arrayOf(id, domain, token, active, accountId, username, "Display Name",
|
||||
"https://picture.url", true, true, true, true, true, true, true,
|
||||
true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false,
|
||||
false, true)
|
||||
val values = arrayOf(
|
||||
id, domain, token, active, accountId, username, "Display Name",
|
||||
"https://picture.url", true, true, true, true, true, true, true,
|
||||
true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false,
|
||||
false, true
|
||||
)
|
||||
|
||||
db.execSQL("INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
|
||||
db.execSQL(
|
||||
"INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," +
|
||||
"`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," +
|
||||
"`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," +
|
||||
"`notificationsFavorited`,`notificationSound`,`notificationVibration`," +
|
||||
|
|
@ -46,7 +49,8 @@ class MigrationsTest {
|
|||
"`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," +
|
||||
"`mediaPreviewEnabled`) " +
|
||||
"VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
values)
|
||||
values
|
||||
)
|
||||
|
||||
db.close()
|
||||
|
||||
|
|
@ -61,4 +65,4 @@ class MigrationsTest {
|
|||
assertEquals(accountId, cursor.getString(4))
|
||||
assertEquals(username, cursor.getString(5))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,249 +0,0 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import androidx.room.Room
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.keylesspalace.tusky.db.*
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.repository.TimelineRepository
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TimelineDAOTest {
|
||||
private lateinit var timelineDao: TimelineDao
|
||||
private lateinit var db: AppDatabase
|
||||
|
||||
@Before
|
||||
fun createDb() {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
|
||||
timelineDao = db.timelineDao()
|
||||
}
|
||||
|
||||
@After
|
||||
fun closeDb() {
|
||||
db.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertGetStatus() {
|
||||
val setOne = makeStatus(statusId = 3)
|
||||
val setTwo = makeStatus(statusId = 20, reblog = true)
|
||||
val ignoredOne = makeStatus(statusId = 1)
|
||||
val ignoredTwo = makeStatus(accountId = 2)
|
||||
|
||||
for ((status, author, reblogger) in listOf(setOne, setTwo, ignoredOne, ignoredTwo)) {
|
||||
timelineDao.insertInTransaction(status, author, reblogger)
|
||||
}
|
||||
|
||||
val resultsFromDb = timelineDao.getStatusesForAccount(setOne.first.timelineUserId,
|
||||
maxId = "21", sinceId = ignoredOne.first.serverId, limit = 10)
|
||||
.blockingGet()
|
||||
|
||||
assertEquals(2, resultsFromDb.size)
|
||||
for ((set, fromDb) in listOf(setTwo, setOne).zip(resultsFromDb)) {
|
||||
val (status, author, reblogger) = set
|
||||
assertEquals(status, fromDb.status)
|
||||
assertEquals(author, fromDb.account)
|
||||
assertEquals(reblogger, fromDb.reblogAccount)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun doNotOverwrite() {
|
||||
val (status, author) = makeStatus()
|
||||
timelineDao.insertInTransaction(status, author, null)
|
||||
|
||||
val placeholder = createPlaceholder(status.serverId, status.timelineUserId)
|
||||
|
||||
timelineDao.insertStatusIfNotThere(placeholder)
|
||||
|
||||
val fromDb = timelineDao.getStatusesForAccount(status.timelineUserId, null, null, 10)
|
||||
.blockingGet()
|
||||
val result = fromDb.first()
|
||||
|
||||
assertEquals(1, fromDb.size)
|
||||
assertEquals(author, result.account)
|
||||
assertEquals(status, result.status)
|
||||
assertNull(result.reblogAccount)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cleanup() {
|
||||
val now = System.currentTimeMillis()
|
||||
val oldDate = now - TimelineRepository.CLEANUP_INTERVAL - 20_000
|
||||
val oldThisAccount = makeStatus(
|
||||
statusId = 5,
|
||||
createdAt = oldDate
|
||||
)
|
||||
val oldAnotherAccount = makeStatus(
|
||||
statusId = 10,
|
||||
createdAt = oldDate,
|
||||
accountId = 2
|
||||
)
|
||||
val recentThisAccount = makeStatus(
|
||||
statusId = 30,
|
||||
createdAt = System.currentTimeMillis()
|
||||
)
|
||||
val recentAnotherAccount = makeStatus(
|
||||
statusId = 60,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
accountId = 2
|
||||
)
|
||||
|
||||
for ((status, author, reblogAuthor) in listOf(oldThisAccount, oldAnotherAccount, recentThisAccount, recentAnotherAccount)) {
|
||||
timelineDao.insertInTransaction(status, author, reblogAuthor)
|
||||
}
|
||||
|
||||
timelineDao.cleanup(now - TimelineRepository.CLEANUP_INTERVAL)
|
||||
|
||||
assertEquals(
|
||||
listOf(recentThisAccount),
|
||||
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
|
||||
.map { it.toTriple() }
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(recentAnotherAccount),
|
||||
timelineDao.getStatusesForAccount(2, null, null, 100).blockingGet()
|
||||
.map { it.toTriple() }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun overwriteDeletedStatus() {
|
||||
|
||||
val oldStatuses = listOf(
|
||||
makeStatus(statusId = 3),
|
||||
makeStatus(statusId = 2),
|
||||
makeStatus(statusId = 1)
|
||||
)
|
||||
|
||||
timelineDao.deleteRange(1, oldStatuses.last().first.serverId, oldStatuses.first().first.serverId)
|
||||
|
||||
for ((status, author, reblogAuthor) in oldStatuses) {
|
||||
timelineDao.insertInTransaction(status, author, reblogAuthor)
|
||||
}
|
||||
|
||||
// status 2 gets deleted, newly loaded status contain only 1 + 3
|
||||
val newStatuses = listOf(
|
||||
makeStatus(statusId = 3),
|
||||
makeStatus(statusId = 1)
|
||||
)
|
||||
|
||||
timelineDao.deleteRange(1, newStatuses.last().first.serverId, newStatuses.first().first.serverId)
|
||||
|
||||
for ((status, author, reblogAuthor) in newStatuses) {
|
||||
timelineDao.insertInTransaction(status, author, reblogAuthor)
|
||||
}
|
||||
|
||||
//make sure status 2 is no longer in db
|
||||
|
||||
assertEquals(
|
||||
newStatuses,
|
||||
timelineDao.getStatusesForAccount(1, null, null, 100).blockingGet()
|
||||
.map { it.toTriple() }
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeStatus(
|
||||
accountId: Long = 1,
|
||||
statusId: Long = 10,
|
||||
reblog: Boolean = false,
|
||||
createdAt: Long = statusId,
|
||||
authorServerId: String = "20"
|
||||
): Triple<TimelineStatusEntity, TimelineAccountEntity, TimelineAccountEntity?> {
|
||||
val author = TimelineAccountEntity(
|
||||
authorServerId,
|
||||
accountId,
|
||||
"localUsername",
|
||||
"username",
|
||||
"displayName",
|
||||
"blah",
|
||||
"avatar",
|
||||
"[\"tusky\": \"http://tusky.cool/emoji.jpg\"]",
|
||||
false
|
||||
)
|
||||
|
||||
val reblogAuthor = if (reblog) {
|
||||
TimelineAccountEntity(
|
||||
"R$authorServerId",
|
||||
accountId,
|
||||
"RlocalUsername",
|
||||
"Rusername",
|
||||
"RdisplayName",
|
||||
"Rblah",
|
||||
"Ravatar",
|
||||
"[]",
|
||||
false
|
||||
)
|
||||
} else null
|
||||
|
||||
|
||||
val even = accountId % 2 == 0L
|
||||
val status = TimelineStatusEntity(
|
||||
serverId = statusId.toString(),
|
||||
url = "url$statusId",
|
||||
timelineUserId = accountId,
|
||||
authorServerId = authorServerId,
|
||||
inReplyToId = "inReplyToId$statusId",
|
||||
inReplyToAccountId = "inReplyToAccountId$statusId",
|
||||
content = "Content!$statusId",
|
||||
createdAt = createdAt,
|
||||
emojis = "emojis$statusId",
|
||||
reblogsCount = 1 * statusId.toInt(),
|
||||
favouritesCount = 2 * statusId.toInt(),
|
||||
reblogged = even,
|
||||
favourited = !even,
|
||||
bookmarked = false,
|
||||
sensitive = even,
|
||||
spoilerText = "spoier$statusId",
|
||||
visibility = Status.Visibility.PRIVATE,
|
||||
attachments = "attachments$accountId",
|
||||
mentions = "mentions$accountId",
|
||||
application = "application$accountId",
|
||||
reblogServerId = if (reblog) (statusId * 100).toString() else null,
|
||||
reblogAccountId = reblogAuthor?.serverId,
|
||||
poll = null,
|
||||
muted = false
|
||||
)
|
||||
return Triple(status, author, reblogAuthor)
|
||||
}
|
||||
|
||||
private fun createPlaceholder(serverId: String, timelineUserId: Long): TimelineStatusEntity {
|
||||
return TimelineStatusEntity(
|
||||
serverId = serverId,
|
||||
url = null,
|
||||
timelineUserId = timelineUserId,
|
||||
authorServerId = null,
|
||||
inReplyToId = null,
|
||||
inReplyToAccountId = null,
|
||||
content = null,
|
||||
createdAt = 0L,
|
||||
emojis = null,
|
||||
reblogsCount = 0,
|
||||
favouritesCount = 0,
|
||||
reblogged = false,
|
||||
favourited = false,
|
||||
bookmarked = false,
|
||||
sensitive = false,
|
||||
spoilerText = null,
|
||||
visibility = null,
|
||||
attachments = null,
|
||||
mentions = null,
|
||||
application = null,
|
||||
reblogServerId = null,
|
||||
reblogAccountId = null,
|
||||
poll = null,
|
||||
muted = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun TimelineStatusWithAccount.toTriple() = Triple(status, account, reblogAccount)
|
||||
}
|
||||
|
|
@ -34,9 +34,6 @@
|
|||
android:resource="@xml/share_shortcuts" />
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SavedTootActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden" />
|
||||
<activity
|
||||
android:name=".LoginActivity"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
|
@ -114,7 +111,7 @@
|
|||
android:name=".ViewMediaActivity"
|
||||
android:theme="@style/TuskyBaseTheme" />
|
||||
<activity
|
||||
android:name=".AccountActivity"
|
||||
android:name=".components.account.AccountActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" />
|
||||
<activity android:name=".EditProfileActivity" />
|
||||
<activity android:name=".components.preference.PreferencesActivity" />
|
||||
|
|
@ -123,7 +120,7 @@
|
|||
<activity android:name=".AboutActivity" />
|
||||
<activity android:name=".TabPreferenceActivity" />
|
||||
<activity
|
||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||
android:name="com.canhub.cropper.CropImageActivity"
|
||||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
<activity
|
||||
android:name=".components.search.SearchActivity"
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@ package com.keylesspalace.tusky
|
|||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.style.URLSpan
|
||||
import android.text.util.Linkify
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.StringRes
|
||||
import com.keylesspalace.tusky.databinding.ActivityAboutBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.util.CustomURLSpan
|
||||
import com.keylesspalace.tusky.util.NoUnderlineURLSpan
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
|
||||
class AboutActivity : BottomSheetActivity(), Injectable {
|
||||
|
|
@ -32,7 +32,7 @@ class AboutActivity : BottomSheetActivity(), Injectable {
|
|||
|
||||
binding.versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME)
|
||||
|
||||
if(BuildConfig.CUSTOM_INSTANCE.isBlank()) {
|
||||
if (BuildConfig.CUSTOM_INSTANCE.isBlank()) {
|
||||
binding.aboutPoweredByTusky.hide()
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) {
|
|||
val end = builder.getSpanEnd(span)
|
||||
val flags = builder.getSpanFlags(span)
|
||||
|
||||
val customSpan = object : CustomURLSpan(span.url) {}
|
||||
val customSpan = NoUnderlineURLSpan(span.url)
|
||||
|
||||
builder.removeSpan(span)
|
||||
builder.setSpan(customSpan, start, end, flags)
|
||||
|
|
|
|||
|
|
@ -64,9 +64,9 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector {
|
|||
}
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked))
|
||||
.commit()
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, AccountListFragment.newInstance(type, id, accountLocked))
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun androidInjector() = dispatchingAndroidInjector
|
||||
|
|
|
|||
|
|
@ -28,18 +28,24 @@ import androidx.preference.PreferenceManager
|
|||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.State
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -93,19 +99,19 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
binding.accountsSearchRecycler.adapter = searchAdapter
|
||||
|
||||
viewModel.state
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe { state ->
|
||||
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe { state ->
|
||||
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
|
||||
|
||||
when (state.accounts) {
|
||||
is Either.Right -> binding.messageView.hide()
|
||||
is Either.Left -> handleError(state.accounts.value)
|
||||
}
|
||||
|
||||
setupSearchView(state)
|
||||
when (state.accounts) {
|
||||
is Either.Right -> binding.messageView.hide()
|
||||
is Either.Left -> handleError(state.accounts.value)
|
||||
}
|
||||
|
||||
setupSearchView(state)
|
||||
}
|
||||
|
||||
binding.searchView.isSubmitButtonEnabled = true
|
||||
binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
|
|
@ -146,11 +152,15 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
viewModel.load(listId)
|
||||
}
|
||||
if (error is IOException) {
|
||||
binding.messageView.setup(R.drawable.elephant_offline,
|
||||
R.string.error_network, retryAction)
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_offline,
|
||||
R.string.error_network, retryAction
|
||||
)
|
||||
} else {
|
||||
binding.messageView.setup(R.drawable.elephant_error,
|
||||
R.string.error_generic, retryAction)
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_error,
|
||||
R.string.error_generic, retryAction
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,7 +194,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
onRemoveFromList(getItem(holder.bindingAdapterPosition).id)
|
||||
}
|
||||
binding.rejectButton.contentDescription =
|
||||
binding.root.context.getString(R.string.action_remove_from_list)
|
||||
binding.root.context.getString(R.string.action_remove_from_list)
|
||||
|
||||
return holder
|
||||
}
|
||||
|
|
@ -203,8 +213,8 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||
return oldItem.second == newItem.second
|
||||
&& oldItem.first.deepEquals(newItem.first)
|
||||
return oldItem.second == newItem.second &&
|
||||
oldItem.first.deepEquals(newItem.first)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,4 +270,4 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
return AccountsInListFragment().apply { arguments = args }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -199,6 +199,7 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
|
|||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requesters.containsKey(requestCode)) {
|
||||
PermissionRequester requester = requesters.remove(requestCode);
|
||||
requester.onRequestPermissionsResult(permissions, grantResults);
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@ import android.widget.LinearLayout
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import javax.inject.Inject
|
||||
|
|
@ -60,7 +61,6 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
open fun viewUrl(url: String, lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER) {
|
||||
|
|
@ -70,11 +70,12 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
mastodonApi.searchObservable(
|
||||
query = url,
|
||||
resolve = true
|
||||
query = url,
|
||||
resolve = true
|
||||
).observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({ (accounts, statuses) ->
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
{ (accounts, statuses) ->
|
||||
if (getCancelSearchRequested(url)) {
|
||||
return@subscribe
|
||||
}
|
||||
|
|
@ -90,12 +91,14 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
if (!getCancelSearchRequested(url)) {
|
||||
onEndSearch(url)
|
||||
performUrlFallbackAction(url, lookupFallbackBehavior)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
onBeginSearch(url)
|
||||
}
|
||||
|
|
@ -177,6 +180,8 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
// https://friendica.foo.bar/profile/user
|
||||
// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
||||
// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
|
||||
// https://pixelfed.social/p/connyduck/391263492998670833
|
||||
// https://pixelfed.social/connyduck
|
||||
fun looksLikeMastodonUrl(urlString: String): Boolean {
|
||||
val uri: URI
|
||||
try {
|
||||
|
|
@ -186,20 +191,23 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
|
|||
}
|
||||
|
||||
if (uri.query != null ||
|
||||
uri.fragment != null ||
|
||||
uri.path == null) {
|
||||
uri.fragment != null ||
|
||||
uri.path == null
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
val path = uri.path
|
||||
return path.matches("^/@[^/]+$".toRegex()) ||
|
||||
path.matches("^/@[^/]+/\\d+$".toRegex()) ||
|
||||
path.matches("^/users/\\w+$".toRegex()) ||
|
||||
path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
|
||||
path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
|
||||
path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
|
||||
path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
|
||||
path.matches("^/profile/\\w+$".toRegex())
|
||||
path.matches("^/@[^/]+/\\d+$".toRegex()) ||
|
||||
path.matches("^/users/\\w+$".toRegex()) ||
|
||||
path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
|
||||
path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
|
||||
path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
|
||||
path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
|
||||
path.matches("^/profile/\\w+$".toRegex()) ||
|
||||
path.matches("^/p/\\w+/\\d+$".toRegex()) ||
|
||||
path.matches("^/\\w+$".toRegex())
|
||||
}
|
||||
|
||||
enum class PostLookupFallbackBehavior {
|
||||
|
|
|
|||
|
|
@ -36,18 +36,24 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.canhub.cropper.CropImage
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import com.theartofdev.edmodo.cropper.CropImage
|
||||
import javax.inject.Inject
|
||||
|
||||
class EditProfileActivity : BaseActivity(), Injectable {
|
||||
|
|
@ -110,11 +116,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
binding.addFieldButton.setOnClickListener {
|
||||
accountFieldEditAdapter.addField()
|
||||
if(accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
|
||||
if (accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) {
|
||||
it.isVisible = false
|
||||
}
|
||||
|
||||
binding.scrollView.post{
|
||||
binding.scrollView.post {
|
||||
binding.scrollView.smoothScrollTo(0, it.bottom)
|
||||
}
|
||||
}
|
||||
|
|
@ -134,23 +140,22 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList())
|
||||
binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS
|
||||
|
||||
if(viewModel.avatarData.value == null) {
|
||||
if (viewModel.avatarData.value == null) {
|
||||
Glide.with(this)
|
||||
.load(me.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
)
|
||||
.into(binding.avatarPreview)
|
||||
.load(me.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
)
|
||||
.into(binding.avatarPreview)
|
||||
}
|
||||
|
||||
if(viewModel.headerData.value == null) {
|
||||
if (viewModel.headerData.value == null) {
|
||||
Glide.with(this)
|
||||
.load(me.header)
|
||||
.into(binding.headerPreview)
|
||||
.load(me.header)
|
||||
.into(binding.headerPreview)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
is Error -> {
|
||||
|
|
@ -159,19 +164,17 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
viewModel.obtainProfile()
|
||||
}
|
||||
snackbar.show()
|
||||
|
||||
}
|
||||
is Loading -> { }
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.obtainInstance()
|
||||
viewModel.instanceData.observe(this) { result ->
|
||||
when (result) {
|
||||
is Success -> {
|
||||
val instance = result.data
|
||||
if (instance?.maxBioChars != null && instance.maxBioChars > 0) {
|
||||
binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars
|
||||
}
|
||||
if (result is Success) {
|
||||
val instance = result.data
|
||||
if (instance?.maxBioChars != null && instance.maxBioChars > 0) {
|
||||
binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -179,20 +182,22 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
|
||||
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
|
||||
|
||||
viewModel.saveData.observe(this, {
|
||||
when(it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
}
|
||||
is Loading -> {
|
||||
binding.saveProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is Error -> {
|
||||
onSaveFailure(it.errorMessage)
|
||||
viewModel.saveData.observe(
|
||||
this,
|
||||
{
|
||||
when (it) {
|
||||
is Success -> {
|
||||
finish()
|
||||
}
|
||||
is Loading -> {
|
||||
binding.saveProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
is Error -> {
|
||||
onSaveFailure(it.errorMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
|
@ -202,50 +207,56 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
if(!isFinishing) {
|
||||
viewModel.updateProfile(binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData())
|
||||
if (!isFinishing) {
|
||||
viewModel.updateProfile(
|
||||
binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeImage(liveData: LiveData<Resource<Bitmap>>,
|
||||
imageView: ImageView,
|
||||
progressBar: View,
|
||||
roundedCorners: Boolean) {
|
||||
liveData.observe(this, {
|
||||
private fun observeImage(
|
||||
liveData: LiveData<Resource<Bitmap>>,
|
||||
imageView: ImageView,
|
||||
progressBar: View,
|
||||
roundedCorners: Boolean
|
||||
) {
|
||||
liveData.observe(
|
||||
this,
|
||||
{
|
||||
|
||||
when (it) {
|
||||
is Success -> {
|
||||
val glide = Glide.with(imageView)
|
||||
when (it) {
|
||||
is Success -> {
|
||||
val glide = Glide.with(imageView)
|
||||
.load(it.data)
|
||||
|
||||
if (roundedCorners) {
|
||||
glide.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
)
|
||||
}
|
||||
if (roundedCorners) {
|
||||
glide.transform(
|
||||
FitCenter(),
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||
)
|
||||
}
|
||||
|
||||
glide.into(imageView)
|
||||
glide.into(imageView)
|
||||
|
||||
imageView.show()
|
||||
progressBar.hide()
|
||||
}
|
||||
is Loading -> {
|
||||
progressBar.show()
|
||||
}
|
||||
is Error -> {
|
||||
progressBar.hide()
|
||||
if(!it.consumed) {
|
||||
onResizeFailure()
|
||||
it.consumed = true
|
||||
imageView.show()
|
||||
progressBar.hide()
|
||||
}
|
||||
is Loading -> {
|
||||
progressBar.show()
|
||||
}
|
||||
is Error -> {
|
||||
progressBar.hide()
|
||||
if (!it.consumed) {
|
||||
onResizeFailure()
|
||||
it.consumed = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private fun onMediaPick(pickType: PickType) {
|
||||
|
|
@ -261,8 +272,12 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
when (requestCode) {
|
||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
|
|
@ -307,14 +322,16 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
|
||||
private fun save() {
|
||||
if (currentlyPicking != PickType.NOTHING) {
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.save(binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData(),
|
||||
this)
|
||||
viewModel.save(
|
||||
binding.displayNameEditText.text.toString(),
|
||||
binding.noteEditText.text.toString(),
|
||||
binding.lockedCheckBox.isChecked,
|
||||
accountFieldEditAdapter.getFieldData(),
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
private fun onSaveFailure(msg: String?) {
|
||||
|
|
@ -352,10 +369,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
AVATAR_PICK_RESULT -> {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
CropImage.activity(data.data)
|
||||
.setInitialCropWindowPaddingRatio(0f)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
||||
.start(this)
|
||||
.setInitialCropWindowPaddingRatio(0f)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
||||
.start(this)
|
||||
} else {
|
||||
endMediaPicking()
|
||||
}
|
||||
|
|
@ -363,10 +380,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
HEADER_PICK_RESULT -> {
|
||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
||||
CropImage.activity(data.data)
|
||||
.setInitialCropWindowPaddingRatio(0f)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||
.start(this)
|
||||
.setInitialCropWindowPaddingRatio(0f)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||
.start(this)
|
||||
} else {
|
||||
endMediaPicking()
|
||||
}
|
||||
|
|
@ -374,7 +391,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> {
|
||||
val result = CropImage.getActivityResult(data)
|
||||
when (resultCode) {
|
||||
Activity.RESULT_OK -> beginResize(result.uri)
|
||||
Activity.RESULT_OK -> beginResize(result?.uriContent)
|
||||
CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure()
|
||||
else -> endMediaPicking()
|
||||
}
|
||||
|
|
@ -382,7 +399,12 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
private fun beginResize(uri: Uri) {
|
||||
private fun beginResize(uri: Uri?) {
|
||||
if (uri == null) {
|
||||
currentlyPicking = PickType.NOTHING
|
||||
return
|
||||
}
|
||||
|
||||
beginMediaPicking()
|
||||
|
||||
when (currentlyPicking) {
|
||||
|
|
@ -398,12 +420,10 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
currentlyPicking = PickType.NOTHING
|
||||
|
||||
}
|
||||
|
||||
private fun onResizeFailure() {
|
||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||
endMediaPicking()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.widget.AdapterView
|
|||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.databinding.ActivityFiltersBinding
|
||||
|
|
@ -14,6 +15,8 @@ import com.keylesspalace.tusky.network.MastodonApi
|
|||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
|
|
@ -21,7 +24,7 @@ import retrofit2.Response
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class FiltersActivity: BaseActivity() {
|
||||
class FiltersActivity : BaseActivity() {
|
||||
@Inject
|
||||
lateinit var api: MastodonApi
|
||||
|
||||
|
|
@ -30,7 +33,7 @@ class FiltersActivity: BaseActivity() {
|
|||
|
||||
private val binding by viewBinding(ActivityFiltersBinding::inflate)
|
||||
|
||||
private lateinit var context : String
|
||||
private lateinit var context: String
|
||||
private lateinit var filters: MutableList<Filter>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
|
@ -54,7 +57,7 @@ class FiltersActivity: BaseActivity() {
|
|||
|
||||
private fun updateFilter(filter: Filter, itemIndex: Int) {
|
||||
api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt)
|
||||
.enqueue(object: Callback<Filter>{
|
||||
.enqueue(object : Callback<Filter> {
|
||||
override fun onFailure(call: Call<Filter>, t: Throwable) {
|
||||
Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
@ -76,7 +79,7 @@ class FiltersActivity: BaseActivity() {
|
|||
val filter = filters[itemIndex]
|
||||
if (filter.context.size == 1) {
|
||||
// This is the only context for this filter; delete it
|
||||
api.deleteFilter(filters[itemIndex].id).enqueue(object: Callback<ResponseBody> {
|
||||
api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback<ResponseBody> {
|
||||
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
|
||||
Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
@ -90,17 +93,19 @@ class FiltersActivity: BaseActivity() {
|
|||
} else {
|
||||
// Keep the filter, but remove it from this context
|
||||
val oldFilter = filters[itemIndex]
|
||||
val newFilter = Filter(oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
||||
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord)
|
||||
val newFilter = Filter(
|
||||
oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context },
|
||||
oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord
|
||||
)
|
||||
updateFilter(newFilter, itemIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFilter(phrase: String, wholeWord: Boolean) {
|
||||
api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object: Callback<Filter> {
|
||||
api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object : Callback<Filter> {
|
||||
override fun onResponse(call: Call<Filter>, response: Response<Filter>) {
|
||||
val filterResponse = response.body()
|
||||
if(response.isSuccessful && filterResponse != null) {
|
||||
if (response.isSuccessful && filterResponse != null) {
|
||||
filters.add(filterResponse)
|
||||
refreshFilterDisplay()
|
||||
eventHub.dispatch(PreferenceChangedEvent(context))
|
||||
|
|
@ -119,13 +124,13 @@ class FiltersActivity: BaseActivity() {
|
|||
val binding = DialogFilterBinding.inflate(layoutInflater)
|
||||
binding.phraseWholeWord.isChecked = true
|
||||
AlertDialog.Builder(this@FiltersActivity)
|
||||
.setTitle(R.string.filter_addition_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok){ _, _ ->
|
||||
createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked)
|
||||
}
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setTitle(R.string.filter_addition_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked)
|
||||
}
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun setupEditDialogForItem(itemIndex: Int) {
|
||||
|
|
@ -135,19 +140,21 @@ class FiltersActivity: BaseActivity() {
|
|||
binding.phraseWholeWord.isChecked = filter.wholeWord
|
||||
|
||||
AlertDialog.Builder(this@FiltersActivity)
|
||||
.setTitle(R.string.filter_edit_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
|
||||
val oldFilter = filters[itemIndex]
|
||||
val newFilter = Filter(oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
|
||||
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked)
|
||||
updateFilter(newFilter, itemIndex)
|
||||
}
|
||||
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
||||
deleteFilter(itemIndex)
|
||||
}
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setTitle(R.string.filter_edit_dialog_title)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.filter_dialog_update_button) { _, _ ->
|
||||
val oldFilter = filters[itemIndex]
|
||||
val newFilter = Filter(
|
||||
oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context,
|
||||
oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked
|
||||
)
|
||||
updateFilter(newFilter, itemIndex)
|
||||
}
|
||||
.setNegativeButton(R.string.filter_dialog_remove_button) { _, _ ->
|
||||
deleteFilter(itemIndex)
|
||||
}
|
||||
.setNeutralButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun refreshFilterDisplay() {
|
||||
|
|
@ -162,41 +169,37 @@ class FiltersActivity: BaseActivity() {
|
|||
binding.addFilterButton.hide()
|
||||
binding.filterProgressBar.show()
|
||||
|
||||
api.getFilters().enqueue(object : Callback<List<Filter>> {
|
||||
override fun onResponse(call: Call<List<Filter>>, response: Response<List<Filter>>) {
|
||||
val filterResponse = response.body()
|
||||
if(response.isSuccessful && filterResponse != null) {
|
||||
|
||||
filters = filterResponse.filter { filter -> filter.context.contains(context) }.toMutableList()
|
||||
refreshFilterDisplay()
|
||||
|
||||
binding.filtersView.show()
|
||||
binding.addFilterButton.show()
|
||||
binding.filterProgressBar.hide()
|
||||
} else {
|
||||
binding.filterProgressBar.hide()
|
||||
binding.filterMessageView.show()
|
||||
binding.filterMessageView.setup(R.drawable.elephant_error,
|
||||
R.string.error_generic) { loadFilters() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Filter>>, t: Throwable) {
|
||||
lifecycleScope.launch {
|
||||
val newFilters = try {
|
||||
api.getFilters().await()
|
||||
} catch (t: Exception) {
|
||||
binding.filterProgressBar.hide()
|
||||
binding.filterMessageView.show()
|
||||
if (t is IOException) {
|
||||
binding.filterMessageView.setup(R.drawable.elephant_offline,
|
||||
R.string.error_network) { loadFilters() }
|
||||
binding.filterMessageView.setup(
|
||||
R.drawable.elephant_offline,
|
||||
R.string.error_network
|
||||
) { loadFilters() }
|
||||
} else {
|
||||
binding.filterMessageView.setup(R.drawable.elephant_error,
|
||||
R.string.error_generic) { loadFilters() }
|
||||
binding.filterMessageView.setup(
|
||||
R.drawable.elephant_error,
|
||||
R.string.error_generic
|
||||
) { loadFilters() }
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
})
|
||||
|
||||
filters = newFilters.filter { it.context.contains(context) }.toMutableList()
|
||||
refreshFilterDisplay()
|
||||
|
||||
binding.filtersView.show()
|
||||
binding.addFilterButton.show()
|
||||
binding.filterProgressBar.hide()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FILTERS_CONTEXT = "filters_context"
|
||||
const val FILTERS_TITLE = "filters_title"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@
|
|||
package com.keylesspalace.tusky
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.RawRes
|
||||
import android.util.Log
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.RawRes
|
||||
import com.keylesspalace.tusky.databinding.ActivityLicenseBinding
|
||||
import com.keylesspalace.tusky.util.IOUtils
|
||||
import java.io.BufferedReader
|
||||
|
|
@ -41,7 +41,6 @@ class LicenseActivity : BaseActivity() {
|
|||
setTitle(R.string.title_licenses)
|
||||
|
||||
loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView)
|
||||
|
||||
}
|
||||
|
||||
private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) {
|
||||
|
|
|
|||
|
|
@ -23,32 +23,48 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.TextView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.recyclerview.widget.*
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.databinding.ActivityListsBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.MastoList
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.onTextChanged
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event.*
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_NETWORK
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED
|
||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDispose
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
|
@ -84,12 +100,13 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
binding.listsRecycler.adapter = adapter
|
||||
binding.listsRecycler.layoutManager = LinearLayoutManager(this)
|
||||
binding.listsRecycler.addItemDecoration(
|
||||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||
DividerItemDecoration(this, DividerItemDecoration.VERTICAL)
|
||||
)
|
||||
|
||||
viewModel.state
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe(this::update)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe(this::update)
|
||||
viewModel.retryLoading()
|
||||
|
||||
binding.addListButton.setOnClickListener {
|
||||
|
|
@ -97,15 +114,15 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
}
|
||||
|
||||
viewModel.events.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe { event ->
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
when (event) {
|
||||
CREATE_ERROR -> showMessage(R.string.error_create_list)
|
||||
RENAME_ERROR -> showMessage(R.string.error_rename_list)
|
||||
DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
||||
}
|
||||
.autoDispose(from(this))
|
||||
.subscribe { event ->
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
when (event) {
|
||||
Event.CREATE_ERROR -> showMessage(R.string.error_create_list)
|
||||
Event.RENAME_ERROR -> showMessage(R.string.error_rename_list)
|
||||
Event.DELETE_ERROR -> showMessage(R.string.error_delete_list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showlistNameDialog(list: MastoList?) {
|
||||
|
|
@ -115,17 +132,18 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
layout.addView(editText)
|
||||
val margin = Utils.dpToPx(this, 8)
|
||||
(editText.layoutParams as ViewGroup.MarginLayoutParams)
|
||||
.setMargins(margin, margin, margin, 0)
|
||||
.setMargins(margin, margin, margin, 0)
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(layout)
|
||||
.setPositiveButton(
|
||||
if (list == null) R.string.action_create_list
|
||||
else R.string.action_rename_list) { _, _ ->
|
||||
onPickedDialogName(editText.text, list?.id)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setView(layout)
|
||||
.setPositiveButton(
|
||||
if (list == null) R.string.action_create_list
|
||||
else R.string.action_rename_list
|
||||
) { _, _ ->
|
||||
onPickedDialogName(editText.text, list?.id)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
|
||||
val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE)
|
||||
editText.onTextChanged { s, _, _, _ ->
|
||||
|
|
@ -137,15 +155,14 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
|
||||
private fun showListDeleteDialog(list: MastoList) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.dialog_delete_list_warning, list.title))
|
||||
.setPositiveButton(R.string.action_delete){ _, _ ->
|
||||
viewModel.deleteList(list.id)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setMessage(getString(R.string.dialog_delete_list_warning, list.title))
|
||||
.setPositiveButton(R.string.action_delete) { _, _ ->
|
||||
viewModel.deleteList(list.id)
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
private fun update(state: ListsViewModel.State) {
|
||||
adapter.submitList(state.lists)
|
||||
binding.progressBar.visible(state.loadingState == LOADING)
|
||||
|
|
@ -166,8 +183,10 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
LOADED ->
|
||||
if (state.lists.isEmpty()) {
|
||||
binding.messageView.show()
|
||||
binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
|
||||
null)
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_friend_empty, R.string.message_empty,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
binding.messageView.hide()
|
||||
}
|
||||
|
|
@ -176,13 +195,14 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
|
||||
private fun showMessage(@StringRes messageId: Int) {
|
||||
Snackbar.make(
|
||||
binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT
|
||||
binding.listsRecycler, messageId, Snackbar.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun onListSelected(listId: String) {
|
||||
startActivityWithSlideInAnimation(
|
||||
ModalTimelineActivity.newIntent(this, TimelineFragment.Kind.LIST, listId))
|
||||
ModalTimelineActivity.newIntent(this, TimelineViewModel.Kind.LIST, listId)
|
||||
)
|
||||
}
|
||||
|
||||
private fun openListSettings(list: MastoList) {
|
||||
|
|
@ -219,27 +239,28 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
}
|
||||
}
|
||||
|
||||
private inner class ListsAdapter
|
||||
: ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
|
||||
private inner class ListsAdapter :
|
||||
ListAdapter<MastoList, ListsAdapter.ListViewHolder>(ListsDiffer) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder {
|
||||
return LayoutInflater.from(parent.context).inflate(R.layout.item_list, parent, false)
|
||||
.let(this::ListViewHolder)
|
||||
.apply {
|
||||
val context = nameTextView.context
|
||||
val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary)
|
||||
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor }
|
||||
.let(this::ListViewHolder)
|
||||
.apply {
|
||||
val context = nameTextView.context
|
||||
val iconColor = ThemeUtils.getColor(context, android.R.attr.textColorTertiary)
|
||||
val icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_list).apply { sizeDp = 20; colorInt = iconColor }
|
||||
|
||||
nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
|
||||
}
|
||||
nameTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(icon, null, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
|
||||
holder.nameTextView.text = getItem(position).title
|
||||
}
|
||||
|
||||
private inner class ListViewHolder(view: View) : RecyclerView.ViewHolder(view),
|
||||
View.OnClickListener {
|
||||
private inner class ListViewHolder(view: View) :
|
||||
RecyclerView.ViewHolder(view),
|
||||
View.OnClickListener {
|
||||
val nameTextView: TextView = view.findViewById(R.id.list_name_textview)
|
||||
val moreButton: ImageButton = view.findViewById(R.id.editListButton)
|
||||
|
||||
|
|
@ -271,4 +292,4 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
companion object {
|
||||
fun newIntent(context: Context) = Intent(context, ListsActivity::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,11 @@ import com.keylesspalace.tusky.di.Injectable
|
|||
import com.keylesspalace.tusky.entity.AccessToken
|
||||
import com.keylesspalace.tusky.entity.AppCredentials
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.getNonNullString
|
||||
import com.keylesspalace.tusky.util.rickRoll
|
||||
import com.keylesspalace.tusky.util.shouldRickRoll
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import okhttp3.HttpUrl
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
|
|
@ -62,29 +66,30 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
setContentView(binding.root)
|
||||
|
||||
if(savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
|
||||
if (savedInstanceState == null && BuildConfig.CUSTOM_INSTANCE.isNotBlank() && !isAdditionalLogin()) {
|
||||
binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE)
|
||||
binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length)
|
||||
}
|
||||
|
||||
if(BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
|
||||
if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) {
|
||||
Glide.with(binding.loginLogo)
|
||||
.load(BuildConfig.CUSTOM_LOGO_URL)
|
||||
.placeholder(null)
|
||||
.into(binding.loginLogo)
|
||||
.load(BuildConfig.CUSTOM_LOGO_URL)
|
||||
.placeholder(null)
|
||||
.into(binding.loginLogo)
|
||||
}
|
||||
|
||||
preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE)
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
binding.loginButton.setOnClickListener { onButtonClick() }
|
||||
binding.registerButton.setOnClickListener { onRegisterClick() }
|
||||
|
||||
binding.whatsAnInstanceTextView.setOnClickListener {
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setMessage(R.string.dialog_whats_an_instance)
|
||||
.setPositiveButton(R.string.action_close, null)
|
||||
.show()
|
||||
.setMessage(R.string.dialog_whats_an_instance)
|
||||
.setPositiveButton(R.string.action_close, null)
|
||||
.show()
|
||||
val textView = dialog.findViewById<TextView>(android.R.id.message)
|
||||
textView?.movementMethod = LinkMovementMethod.getInstance()
|
||||
}
|
||||
|
|
@ -96,7 +101,6 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
} else {
|
||||
binding.toolbar.visibility = View.GONE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun requiresLogin(): Boolean {
|
||||
|
|
@ -105,7 +109,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
if(isAdditionalLogin()) {
|
||||
if (isAdditionalLogin()) {
|
||||
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
}
|
||||
}
|
||||
|
|
@ -150,8 +154,10 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
val callback = object : Callback<AppCredentials> {
|
||||
override fun onResponse(call: Call<AppCredentials>,
|
||||
response: Response<AppCredentials>) {
|
||||
override fun onResponse(
|
||||
call: Call<AppCredentials>,
|
||||
response: Response<AppCredentials>
|
||||
) {
|
||||
if (!response.isSuccessful) {
|
||||
binding.loginButton.isEnabled = true
|
||||
binding.domainTextInputLayout.error = getString(R.string.error_failed_app_registration)
|
||||
|
|
@ -164,10 +170,10 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
val clientSecret = credentials.clientSecret
|
||||
|
||||
preferences.edit()
|
||||
.putString("domain", domain)
|
||||
.putString("clientId", clientId)
|
||||
.putString("clientSecret", clientSecret)
|
||||
.apply()
|
||||
.putString("domain", domain)
|
||||
.putString("clientId", clientId)
|
||||
.putString("clientSecret", clientSecret)
|
||||
.apply()
|
||||
|
||||
redirectUserToAuthorizeAndLogin(domain, clientId)
|
||||
}
|
||||
|
|
@ -181,11 +187,12 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
|
||||
mastodonApi
|
||||
.authenticateApp(domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website))
|
||||
.enqueue(callback)
|
||||
.authenticateApp(
|
||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||
)
|
||||
.enqueue(callback)
|
||||
setLoading(true)
|
||||
|
||||
}
|
||||
|
||||
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
|
||||
|
|
@ -193,10 +200,10 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
* login there, and the server will redirect back to the app with its response. */
|
||||
val endpoint = MastodonApi.ENDPOINT_AUTHORIZE
|
||||
val parameters = mapOf(
|
||||
"client_id" to clientId,
|
||||
"redirect_uri" to oauthRedirectUri,
|
||||
"response_type" to "code",
|
||||
"scope" to OAUTH_SCOPES
|
||||
"client_id" to clientId,
|
||||
"redirect_uri" to oauthRedirectUri,
|
||||
"response_type" to "code",
|
||||
"scope" to OAUTH_SCOPES
|
||||
)
|
||||
val url = "https://" + domain + endpoint + "?" + toQueryString(parameters)
|
||||
val uri = Uri.parse(url)
|
||||
|
|
@ -240,31 +247,27 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
} else {
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
||||
Log.e(TAG, String.format("%s %s",
|
||||
getString(R.string.error_retrieving_oauth_token),
|
||||
response.message()))
|
||||
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), response.message()))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<AccessToken>, t: Throwable) {
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error = getString(R.string.error_retrieving_oauth_token)
|
||||
Log.e(TAG, String.format("%s %s",
|
||||
getString(R.string.error_retrieving_oauth_token),
|
||||
t.message))
|
||||
Log.e(TAG, "%s %s".format(getString(R.string.error_retrieving_oauth_token), t.message))
|
||||
}
|
||||
}
|
||||
|
||||
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
|
||||
"authorization_code").enqueue(callback)
|
||||
mastodonApi.fetchOAuthToken(
|
||||
domain, clientId, clientSecret, redirectUri, code,
|
||||
"authorization_code"
|
||||
).enqueue(callback)
|
||||
} else if (error != null) {
|
||||
/* Authorization failed. Put the error response where the user can read it and they
|
||||
* can try again. */
|
||||
setLoading(false)
|
||||
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
|
||||
Log.e(TAG, String.format("%s %s",
|
||||
getString(R.string.error_authorization_denied),
|
||||
error))
|
||||
Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
|
||||
} else {
|
||||
// This case means a junk response was received somehow.
|
||||
setLoading(false)
|
||||
|
|
@ -356,14 +359,14 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor)
|
||||
|
||||
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
|
||||
.setToolbarColor(toolbarColor)
|
||||
.setNavigationBarColor(navigationbarColor)
|
||||
.setNavigationBarDividerColor(navigationbarDividerColor)
|
||||
.build()
|
||||
.setToolbarColor(toolbarColor)
|
||||
.setNavigationBarColor(navigationbarColor)
|
||||
.setNavigationBarDividerColor(navigationbarDividerColor)
|
||||
.build()
|
||||
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
.setDefaultColorSchemeParams(colorSchemeParams)
|
||||
.build()
|
||||
.setDefaultColorSchemeParams(colorSchemeParams)
|
||||
.build()
|
||||
|
||||
try {
|
||||
customTabsIntent.launchUrl(context, uri)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ import android.content.Context
|
|||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
|
|
@ -31,24 +34,30 @@ import android.widget.ImageView
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import autodispose2.androidx.lifecycle.autoDispose
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestManager
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.target.FixedSizeDrawable
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||
import com.keylesspalace.tusky.appstore.CacheUpdater
|
||||
import com.keylesspalace.tusky.appstore.Event
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
||||
|
|
@ -61,15 +70,20 @@ import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
|||
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.fragment.SFragment
|
||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.removeShortcut
|
||||
import com.keylesspalace.tusky.util.updateShortcut
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
|
|
@ -78,15 +92,30 @@ import com.mikepenz.materialdrawer.holder.BadgeStyle
|
|||
import com.mikepenz.materialdrawer.holder.ColorHolder
|
||||
import com.mikepenz.materialdrawer.holder.StringHolder
|
||||
import com.mikepenz.materialdrawer.iconics.iconicsIcon
|
||||
import com.mikepenz.materialdrawer.model.*
|
||||
import com.mikepenz.materialdrawer.model.interfaces.*
|
||||
import com.mikepenz.materialdrawer.util.*
|
||||
import com.mikepenz.materialdrawer.model.AbstractDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.DividerDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.SecondaryDrawerItem
|
||||
import com.mikepenz.materialdrawer.model.interfaces.IProfile
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.descriptionText
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.iconUrl
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameRes
|
||||
import com.mikepenz.materialdrawer.model.interfaces.nameText
|
||||
import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader
|
||||
import com.mikepenz.materialdrawer.util.addItems
|
||||
import com.mikepenz.materialdrawer.util.addItemsAtPosition
|
||||
import com.mikepenz.materialdrawer.util.updateBadge
|
||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector {
|
||||
|
|
@ -102,9 +131,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
@Inject
|
||||
lateinit var conversationRepository: ConversationsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var appDb: AppDatabase
|
||||
|
||||
@Inject
|
||||
lateinit var draftHelper: DraftHelper
|
||||
|
||||
|
|
@ -135,10 +161,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
super.onCreate(savedInstanceState)
|
||||
|
||||
val activeAccount = accountManager.activeAccount
|
||||
if (activeAccount == null) {
|
||||
// will be redirected to LoginActivity by BaseActivity
|
||||
return
|
||||
}
|
||||
?: return // will be redirected to LoginActivity by BaseActivity
|
||||
|
||||
var showNotificationTab = false
|
||||
if (intent != null) {
|
||||
/** there are two possibilities the accountId can be passed to MainActivity:
|
||||
|
|
@ -163,19 +187,22 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
forwardShare(intent)
|
||||
} else {
|
||||
// No account was provided, show the chooser
|
||||
showAccountChooserDialog(getString(R.string.action_share_as), true, object : AccountSelectionListener {
|
||||
override fun onAccountSelected(account: AccountEntity) {
|
||||
val requestedId = account.id
|
||||
if (requestedId == activeAccount.id) {
|
||||
// The correct account is already active
|
||||
forwardShare(intent)
|
||||
} else {
|
||||
// A different account was requested, restart the activity
|
||||
intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId)
|
||||
changeAccount(requestedId, intent)
|
||||
showAccountChooserDialog(
|
||||
getString(R.string.action_share_as), true,
|
||||
object : AccountSelectionListener {
|
||||
override fun onAccountSelected(account: AccountEntity) {
|
||||
val requestedId = account.id
|
||||
if (requestedId == activeAccount.id) {
|
||||
// The correct account is already active
|
||||
forwardShare(intent)
|
||||
} else {
|
||||
// A different account was requested, restart the activity
|
||||
intent.putExtra(NotificationHelper.ACCOUNT_ID, requestedId)
|
||||
changeAccount(requestedId, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
} else if (accountRequested && savedInstanceState == null) {
|
||||
// user clicked a notification, show notification tab
|
||||
|
|
@ -226,24 +253,23 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
NotificationHelper.disablePullNotifications(this)
|
||||
}
|
||||
eventHub.events
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event: Event? ->
|
||||
when (event) {
|
||||
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
|
||||
is MainTabsChangedEvent -> setupTabs(false)
|
||||
is AnnouncementReadEvent -> {
|
||||
unreadAnnouncementsCount--
|
||||
updateAnnouncementsBadge()
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { event: Event? ->
|
||||
when (event) {
|
||||
is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData)
|
||||
is MainTabsChangedEvent -> setupTabs(false)
|
||||
is AnnouncementReadEvent -> {
|
||||
unreadAnnouncementsCount--
|
||||
updateAnnouncementsBadge()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Schedulers.io().scheduleDirect {
|
||||
// Flush old media that was cached for sharing
|
||||
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
||||
}
|
||||
draftWarning()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
|
@ -331,12 +357,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
|
||||
currentHiddenInList = true
|
||||
onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) }
|
||||
addProfile(ProfileSettingDrawerItem().apply {
|
||||
identifier = DRAWER_ITEM_ADD_ACCOUNT
|
||||
nameRes = R.string.add_account_name
|
||||
descriptionRes = R.string.add_account_description
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_add
|
||||
}, 0)
|
||||
addProfile(
|
||||
ProfileSettingDrawerItem().apply {
|
||||
identifier = DRAWER_ITEM_ADD_ACCOUNT
|
||||
nameRes = R.string.add_account_name
|
||||
descriptionRes = R.string.add_account_description
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_add
|
||||
},
|
||||
0
|
||||
)
|
||||
attachToSliderView(binding.mainDrawer)
|
||||
dividerBelowHeader = false
|
||||
closeDrawerOnProfileListClick = true
|
||||
|
|
@ -350,13 +379,13 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) {
|
||||
if (animateAvatars) {
|
||||
glide.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
} else {
|
||||
glide.asBitmap()
|
||||
.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
.load(uri)
|
||||
.placeholder(placeholder)
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -376,114 +405,116 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
binding.mainDrawer.apply {
|
||||
tintStatusBar = true
|
||||
addItems(
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_edit_profile
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
||||
onClick = {
|
||||
val intent = Intent(context, EditProfileActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_favourites
|
||||
isSelectable = false
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_star
|
||||
onClick = {
|
||||
val intent = StatusListActivity.newFavouritesIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_bookmarks
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_bookmark
|
||||
onClick = {
|
||||
val intent = StatusListActivity.newBookmarksIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_follow_requests
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person_add
|
||||
onClick = {
|
||||
val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_lists
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ListsActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_saved_toot
|
||||
iconRes = R.drawable.ic_notebook
|
||||
onClick = {
|
||||
val intent = DraftsActivity.newIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_scheduled_toot
|
||||
iconRes = R.drawable.ic_access_time
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
identifier = DRAWER_ITEM_ANNOUNCEMENTS
|
||||
nameRes = R.string.title_announcements
|
||||
iconRes = R.drawable.ic_bullhorn_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
|
||||
}
|
||||
badgeStyle = BadgeStyle().apply {
|
||||
textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
|
||||
color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
|
||||
}
|
||||
},
|
||||
DividerDrawerItem(),
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_account_preferences
|
||||
iconRes = R.drawable.ic_account_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_preferences
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.about_title_activity
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_info
|
||||
onClick = {
|
||||
val intent = Intent(context, AboutActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_logout
|
||||
iconRes = R.drawable.ic_logout
|
||||
onClick = ::logout
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_edit_profile
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person
|
||||
onClick = {
|
||||
val intent = Intent(context, EditProfileActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_favourites
|
||||
isSelectable = false
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_star
|
||||
onClick = {
|
||||
val intent = StatusListActivity.newFavouritesIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_bookmarks
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_bookmark
|
||||
onClick = {
|
||||
val intent = StatusListActivity.newBookmarksIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_view_follow_requests
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_person_add
|
||||
onClick = {
|
||||
val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS, accountLocked = accountLocked)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_lists
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_list
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ListsActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_drafts
|
||||
iconRes = R.drawable.ic_notebook
|
||||
onClick = {
|
||||
val intent = DraftsActivity.newIntent(context)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_access_scheduled_toot
|
||||
iconRes = R.drawable.ic_access_time
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
||||
}
|
||||
},
|
||||
primaryDrawerItem {
|
||||
identifier = DRAWER_ITEM_ANNOUNCEMENTS
|
||||
nameRes = R.string.title_announcements
|
||||
iconRes = R.drawable.ic_bullhorn_24dp
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context))
|
||||
}
|
||||
badgeStyle = BadgeStyle().apply {
|
||||
textColor = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorOnPrimary))
|
||||
color = ColorHolder.fromColor(ThemeUtils.getColor(this@MainActivity, R.attr.colorPrimary))
|
||||
}
|
||||
},
|
||||
DividerDrawerItem(),
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_account_preferences
|
||||
iconRes = R.drawable.ic_account_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_view_preferences
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_settings
|
||||
onClick = {
|
||||
val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.about_title_activity
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_info
|
||||
onClick = {
|
||||
val intent = Intent(context, AboutActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
},
|
||||
secondaryDrawerItem {
|
||||
nameRes = R.string.action_logout
|
||||
iconRes = R.drawable.ic_logout
|
||||
onClick = ::logout
|
||||
}
|
||||
)
|
||||
|
||||
if (addSearchButton) {
|
||||
binding.mainDrawer.addItemsAtPosition(4,
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_search
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
|
||||
}
|
||||
})
|
||||
binding.mainDrawer.addItemsAtPosition(
|
||||
4,
|
||||
primaryDrawerItem {
|
||||
nameRes = R.string.action_search
|
||||
iconicsIcon = GoogleMaterial.Icon.gmd_search
|
||||
onClick = {
|
||||
startActivityWithSlideInAnimation(SearchActivity.getIntent(context))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
setSavedInstance(savedInstanceState)
|
||||
|
|
@ -491,11 +522,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
|
||||
if (BuildConfig.DEBUG) {
|
||||
binding.mainDrawer.addItems(
|
||||
secondaryDrawerItem {
|
||||
nameText = "debug"
|
||||
isEnabled = false
|
||||
textColor = ColorStateList.valueOf(Color.GREEN)
|
||||
}
|
||||
secondaryDrawerItem {
|
||||
nameText = "debug"
|
||||
isEnabled = false
|
||||
textColor = ColorStateList.valueOf(Color.GREEN)
|
||||
}
|
||||
)
|
||||
}
|
||||
EmojiCompat.get().registerInitCallback(emojiInitCallback)
|
||||
|
|
@ -528,7 +559,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
activeTabLayout.removeAllTabs()
|
||||
for (i in tabs.indices) {
|
||||
val tab = activeTabLayout.newTab()
|
||||
.setIcon(tabs[i].icon)
|
||||
.setIcon(tabs[i].icon)
|
||||
if (tabs[i].id == LIST) {
|
||||
tab.contentDescription = tabs[i].arguments[1]
|
||||
} else {
|
||||
|
|
@ -580,31 +611,29 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
binding.mainToolbar.setOnClickListener {
|
||||
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
|
||||
val activeAccount = accountManager.activeAccount
|
||||
|
||||
//open profile when active image was clicked
|
||||
// open profile when active image was clicked
|
||||
if (current && activeAccount != null) {
|
||||
val intent = AccountActivity.getIntent(this, activeAccount.accountId)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
return false
|
||||
}
|
||||
//open LoginActivity to add new account
|
||||
// open LoginActivity to add new account
|
||||
if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||
startActivityWithSlideInAnimation(LoginActivity.getIntent(this, true))
|
||||
return false
|
||||
}
|
||||
//change Account
|
||||
// change Account
|
||||
changeAccount(profile.identifier, null)
|
||||
return false
|
||||
}
|
||||
|
||||
private fun changeAccount(newSelectedId: Long, forward: Intent?) {
|
||||
cacheUpdater.stop()
|
||||
SFragment.flushFilters()
|
||||
accountManager.setActiveAccount(newSelectedId)
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
|
@ -621,49 +650,55 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private fun logout() {
|
||||
accountManager.activeAccount?.let { activeAccount ->
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.action_logout)
|
||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
|
||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this)
|
||||
.setTitle(R.string.action_logout)
|
||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
lifecycleScope.launch {
|
||||
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity)
|
||||
cacheUpdater.clearForUser(activeAccount.id)
|
||||
conversationRepository.deleteCacheForAccount(activeAccount.id)
|
||||
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
|
||||
removeShortcut(this, activeAccount)
|
||||
removeShortcut(this@MainActivity, activeAccount)
|
||||
val newAccount = accountManager.logActiveAccountOut()
|
||||
if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||
NotificationHelper.disablePullNotifications(this)
|
||||
if (!NotificationHelper.areNotificationsEnabled(
|
||||
this@MainActivity,
|
||||
accountManager
|
||||
)
|
||||
) {
|
||||
NotificationHelper.disablePullNotifications(this@MainActivity)
|
||||
}
|
||||
val intent = if (newAccount == null) {
|
||||
LoginActivity.getIntent(this, false)
|
||||
LoginActivity.getIntent(this@MainActivity, false)
|
||||
} else {
|
||||
Intent(this, MainActivity::class.java)
|
||||
Intent(this@MainActivity, MainActivity::class.java)
|
||||
}
|
||||
startActivity(intent)
|
||||
finishWithoutSlideOutAnimation()
|
||||
}
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.show()
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchUserInfo() {
|
||||
mastodonApi.accountVerifyCredentials()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ userInfo ->
|
||||
onFetchUserInfoSuccess(userInfo)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||
}
|
||||
)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ userInfo ->
|
||||
onFetchUserInfoSuccess(userInfo)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onFetchUserInfoSuccess(me: Account) {
|
||||
glide.asBitmap()
|
||||
.load(me.header)
|
||||
.into(header.accountHeaderBackground)
|
||||
.load(me.header)
|
||||
.into(header.accountHeaderBackground)
|
||||
|
||||
loadDrawerAvatar(me.avatar, false)
|
||||
|
||||
|
|
@ -679,49 +714,85 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
|
||||
val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
|
||||
|
||||
glide.asDrawable()
|
||||
.load(avatarUrl)
|
||||
.transform(
|
||||
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
|
||||
|
||||
if (animateAvatars) {
|
||||
glide.asDrawable()
|
||||
.load(avatarUrl)
|
||||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) {
|
||||
placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
}
|
||||
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) {
|
||||
placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
}
|
||||
.into(object : CustomTarget<Drawable>(navIconSize, navIconSize) {
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
|
||||
}
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
if (resource is Animatable) {
|
||||
resource.start()
|
||||
}
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
glide.asBitmap()
|
||||
.load(avatarUrl)
|
||||
.transform(
|
||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
|
||||
)
|
||||
.apply {
|
||||
if (showPlaceholder) {
|
||||
placeholder(R.drawable.avatar_default)
|
||||
}
|
||||
}
|
||||
})
|
||||
.into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) {
|
||||
|
||||
override fun onLoadStarted(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(BitmapDrawable(resources, resource), navIconSize, navIconSize)
|
||||
}
|
||||
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
if (placeholder != null) {
|
||||
binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchAnnouncements() {
|
||||
mastodonApi.listAnnouncements(false)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ announcements ->
|
||||
unreadAnnouncementsCount = announcements.count { !it.read }
|
||||
updateAnnouncementsBadge()
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to fetch announcements.", it)
|
||||
}
|
||||
)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(
|
||||
{ announcements ->
|
||||
unreadAnnouncementsCount = announcements.count { !it.read }
|
||||
updateAnnouncementsBadge()
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to fetch announcements.", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun updateAnnouncementsBadge() {
|
||||
|
|
@ -755,30 +826,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
header.setActiveProfile(accountManager.activeAccount!!.id)
|
||||
}
|
||||
|
||||
private fun draftWarning() {
|
||||
val sharedPrefsKey = "show_draft_warning"
|
||||
appDb.tootDao().savedTootCount()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { draftCount ->
|
||||
val showDraftWarning = preferences.getBoolean(sharedPrefsKey, true)
|
||||
if (draftCount > 0 && showDraftWarning) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.new_drafts_warning)
|
||||
.setNegativeButton("Don't show again") { _, _ ->
|
||||
preferences.edit(commit = true) {
|
||||
putBoolean(sharedPrefsKey, false)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getActionButton(): FloatingActionButton? = binding.composeButton
|
||||
override fun getActionButton() = binding.composeButton
|
||||
|
||||
override fun androidInjector() = androidInjector
|
||||
|
||||
|
|
@ -792,20 +840,20 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
|||
|
||||
private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem {
|
||||
return PrimaryDrawerItem()
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
}
|
||||
|
||||
private inline fun secondaryDrawerItem(block: SecondaryDrawerItem.() -> Unit): SecondaryDrawerItem {
|
||||
return SecondaryDrawerItem()
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
.apply {
|
||||
isSelectable = false
|
||||
isIconTinted = true
|
||||
}
|
||||
.apply(block)
|
||||
}
|
||||
|
||||
private var AbstractDrawerItem<*, *>.onClick: () -> Unit
|
||||
|
|
|
|||
|
|
@ -4,8 +4,9 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.databinding.ActivityModalTimelineBinding
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
|
|
@ -29,12 +30,12 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
|
|||
}
|
||||
|
||||
if (supportFragmentManager.findFragmentById(R.id.contentFrame) == null) {
|
||||
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineFragment.Kind
|
||||
?: TimelineFragment.Kind.HOME
|
||||
val kind = intent?.getSerializableExtra(ARG_KIND) as? TimelineViewModel.Kind
|
||||
?: TimelineViewModel.Kind.HOME
|
||||
val argument = intent?.getStringExtra(ARG_ARG)
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
|
||||
.commit()
|
||||
.replace(R.id.contentFrame, TimelineFragment.newInstance(kind, argument))
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -47,13 +48,15 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn
|
|||
private const val ARG_ARG = "arg"
|
||||
|
||||
@JvmStatic
|
||||
fun newIntent(context: Context, kind: TimelineFragment.Kind,
|
||||
argument: String?): Intent {
|
||||
fun newIntent(
|
||||
context: Context,
|
||||
kind: TimelineViewModel.Kind,
|
||||
argument: String?
|
||||
): Intent {
|
||||
val intent = Intent(context, ModalTimelineActivity::class.java)
|
||||
intent.putExtra(ARG_KIND, kind)
|
||||
intent.putExtra(ARG_ARG, argument)
|
||||
return intent
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,209 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.keylesspalace.tusky.adapter.SavedTootAdapter;
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||
import com.keylesspalace.tusky.db.AppDatabase;
|
||||
import com.keylesspalace.tusky.db.TootDao;
|
||||
import com.keylesspalace.tusky.db.TootEntity;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
import com.keylesspalace.tusky.util.SaveTootHelper;
|
||||
import com.keylesspalace.tusky.view.BackgroundMessageView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
||||
import static com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||
|
||||
public final class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction,
|
||||
Injectable {
|
||||
|
||||
// ui
|
||||
private SavedTootAdapter adapter;
|
||||
private BackgroundMessageView errorMessageView;
|
||||
|
||||
private List<TootEntity> toots = new ArrayList<>();
|
||||
@Nullable
|
||||
private AsyncTask<?, ?, ?> asyncTask;
|
||||
|
||||
@Inject
|
||||
EventHub eventHub;
|
||||
@Inject
|
||||
AppDatabase database;
|
||||
@Inject
|
||||
SaveTootHelper saveTootHelper;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
eventHub.getEvents()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.ofType(StatusComposedEvent.class)
|
||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.subscribe((__) -> this.fetchToots());
|
||||
|
||||
setContentView(R.layout.activity_saved_toot);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar bar = getSupportActionBar();
|
||||
if (bar != null) {
|
||||
bar.setTitle(getString(R.string.title_drafts));
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
bar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.recyclerView);
|
||||
errorMessageView = findViewById(R.id.errorMessageView);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
DividerItemDecoration divider = new DividerItemDecoration(
|
||||
this, layoutManager.getOrientation());
|
||||
recyclerView.addItemDecoration(divider);
|
||||
adapter = new SavedTootAdapter(this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
fetchToots();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (asyncTask != null) asyncTask.cancel(true);
|
||||
}
|
||||
|
||||
private void fetchToots() {
|
||||
asyncTask = new FetchPojosTask(this, database.tootDao())
|
||||
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void setNoContent(int size) {
|
||||
if (size == 0) {
|
||||
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status, null);
|
||||
errorMessageView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
errorMessageView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(int position, TootEntity item) {
|
||||
|
||||
saveTootHelper.deleteDraft(item);
|
||||
|
||||
toots.remove(position);
|
||||
// update adapter
|
||||
if (adapter != null) {
|
||||
adapter.removeItem(position);
|
||||
setNoContent(toots.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void click(int position, TootEntity item) {
|
||||
Gson gson = new Gson();
|
||||
Type stringListType = new TypeToken<List<String>>() {}.getType();
|
||||
List<String> jsonUrls = gson.fromJson(item.getUrls(), stringListType);
|
||||
List<String> descriptions = gson.fromJson(item.getDescriptions(), stringListType);
|
||||
|
||||
ComposeOptions composeOptions = new ComposeOptions(
|
||||
/*scheduledTootUid*/null,
|
||||
item.getUid(),
|
||||
/*drafId*/null,
|
||||
item.getText(),
|
||||
jsonUrls,
|
||||
descriptions,
|
||||
/*mentionedUsernames*/null,
|
||||
item.getInReplyToId(),
|
||||
/*replyVisibility*/null,
|
||||
item.getVisibility(),
|
||||
item.getContentWarning(),
|
||||
item.getInReplyToUsername(),
|
||||
item.getInReplyToText(),
|
||||
/*mediaAttachments*/null,
|
||||
/*draftAttachments*/null,
|
||||
/*scheduledAt*/null,
|
||||
/*sensitive*/null,
|
||||
/*poll*/null,
|
||||
/* modifiedInitialState */ true
|
||||
);
|
||||
Intent intent = ComposeActivity.startIntent(this, composeOptions);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
static final class FetchPojosTask extends AsyncTask<Void, Void, List<TootEntity>> {
|
||||
|
||||
private final WeakReference<SavedTootActivity> activityRef;
|
||||
private final TootDao tootDao;
|
||||
|
||||
FetchPojosTask(SavedTootActivity activity, TootDao tootDao) {
|
||||
this.activityRef = new WeakReference<>(activity);
|
||||
this.tootDao = tootDao;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<TootEntity> doInBackground(Void... voids) {
|
||||
return tootDao.loadAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<TootEntity> pojos) {
|
||||
super.onPostExecute(pojos);
|
||||
SavedTootActivity activity = activityRef.get();
|
||||
if (activity == null) return;
|
||||
|
||||
activity.toots.clear();
|
||||
activity.toots.addAll(pojos);
|
||||
|
||||
// set ui
|
||||
activity.setNoContent(pojos.size());
|
||||
activity.adapter.setItems(activity.toots);
|
||||
activity.adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,10 +18,9 @@ package com.keylesspalace.tusky
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||
import javax.inject.Inject
|
||||
|
||||
class SplashActivity : AppCompatActivity(), Injectable {
|
||||
|
|
@ -46,5 +45,4 @@ class SplashActivity : AppCompatActivity(), Injectable {
|
|||
startActivity(intent)
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,15 +19,12 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.commit
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
|
||||
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
|
||||
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment.Kind
|
||||
|
||||
import javax.inject.Inject
|
||||
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||
|
||||
|
|
@ -44,7 +41,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
|
||||
val title = if(kind == Kind.FAVOURITES) {
|
||||
val title = if (kind == Kind.FAVOURITES) {
|
||||
R.string.title_favourites
|
||||
} else {
|
||||
R.string.title_bookmarks
|
||||
|
|
@ -60,7 +57,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
val fragment = TimelineFragment.newInstance(kind)
|
||||
replace(R.id.fragment_container, fragment)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun androidInjector() = dispatchingAndroidInjector
|
||||
|
|
@ -71,15 +67,14 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
|
|||
|
||||
@JvmStatic
|
||||
fun newFavouritesIntent(context: Context) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.FAVOURITES.name)
|
||||
}
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.FAVOURITES.name)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun newBookmarksIntent(context: Context) =
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
||||
}
|
||||
Intent(context, StatusListActivity::class.java).apply {
|
||||
putExtra(EXTRA_KIND, Kind.BOOKMARKS.name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.fragment.NotificationsFragment
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
|
||||
/** this would be a good case for a sealed class, but that does not work nice with Room */
|
||||
|
||||
|
|
@ -33,71 +34,72 @@ const val DIRECT = "Direct"
|
|||
const val HASHTAG = "Hashtag"
|
||||
const val LIST = "List"
|
||||
|
||||
data class TabData(val id: String,
|
||||
@StringRes val text: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
val fragment: (List<String>) -> Fragment,
|
||||
val arguments: List<String> = emptyList(),
|
||||
val title: (Context) -> String = { context -> context.getString(text)}
|
||||
)
|
||||
data class TabData(
|
||||
val id: String,
|
||||
@StringRes val text: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
val fragment: (List<String>) -> Fragment,
|
||||
val arguments: List<String> = emptyList(),
|
||||
val title: (Context) -> String = { context -> context.getString(text) }
|
||||
)
|
||||
|
||||
fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabData {
|
||||
return when (id) {
|
||||
HOME -> TabData(
|
||||
HOME,
|
||||
R.string.title_home,
|
||||
R.drawable.ic_home_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.HOME) }
|
||||
HOME,
|
||||
R.string.title_home,
|
||||
R.drawable.ic_home_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineViewModel.Kind.HOME) }
|
||||
)
|
||||
NOTIFICATIONS -> TabData(
|
||||
NOTIFICATIONS,
|
||||
R.string.title_notifications,
|
||||
R.drawable.ic_notifications_24dp,
|
||||
{ NotificationsFragment.newInstance() }
|
||||
NOTIFICATIONS,
|
||||
R.string.title_notifications,
|
||||
R.drawable.ic_notifications_24dp,
|
||||
{ NotificationsFragment.newInstance() }
|
||||
)
|
||||
LOCAL -> TabData(
|
||||
LOCAL,
|
||||
R.string.title_public_local,
|
||||
R.drawable.ic_local_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_LOCAL) }
|
||||
LOCAL,
|
||||
R.string.title_public_local,
|
||||
R.drawable.ic_local_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL) }
|
||||
)
|
||||
FEDERATED -> TabData(
|
||||
FEDERATED,
|
||||
R.string.title_public_federated,
|
||||
R.drawable.ic_public_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineFragment.Kind.PUBLIC_FEDERATED) }
|
||||
FEDERATED,
|
||||
R.string.title_public_federated,
|
||||
R.drawable.ic_public_24dp,
|
||||
{ TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED) }
|
||||
)
|
||||
DIRECT -> TabData(
|
||||
DIRECT,
|
||||
R.string.title_direct_messages,
|
||||
R.drawable.ic_reblog_direct_24dp,
|
||||
{ ConversationsFragment.newInstance() }
|
||||
DIRECT,
|
||||
R.string.title_direct_messages,
|
||||
R.drawable.ic_reblog_direct_24dp,
|
||||
{ ConversationsFragment.newInstance() }
|
||||
)
|
||||
HASHTAG -> TabData(
|
||||
HASHTAG,
|
||||
R.string.hashtags,
|
||||
R.drawable.ic_hashtag,
|
||||
{ args -> TimelineFragment.newHashtagInstance(args) },
|
||||
arguments,
|
||||
{ context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) }}
|
||||
HASHTAG,
|
||||
R.string.hashtags,
|
||||
R.drawable.ic_hashtag,
|
||||
{ args -> TimelineFragment.newHashtagInstance(args) },
|
||||
arguments,
|
||||
{ context -> arguments.joinToString(separator = " ") { context.getString(R.string.title_tag, it) } }
|
||||
)
|
||||
LIST -> TabData(
|
||||
LIST,
|
||||
R.string.list,
|
||||
R.drawable.ic_list,
|
||||
{ args -> TimelineFragment.newInstance(TimelineFragment.Kind.LIST, args.getOrNull(0).orEmpty()) },
|
||||
arguments,
|
||||
{ arguments.getOrNull(1).orEmpty() }
|
||||
)
|
||||
LIST,
|
||||
R.string.list,
|
||||
R.drawable.ic_list,
|
||||
{ args -> TimelineFragment.newInstance(TimelineViewModel.Kind.LIST, args.getOrNull(0).orEmpty()) },
|
||||
arguments,
|
||||
{ arguments.getOrNull(1).orEmpty() }
|
||||
)
|
||||
else -> throw IllegalArgumentException("unknown tab type")
|
||||
}
|
||||
}
|
||||
|
||||
fun defaultTabs(): List<TabData> {
|
||||
return listOf(
|
||||
createTabDataFromId(HOME),
|
||||
createTabDataFromId(NOTIFICATIONS),
|
||||
createTabDataFromId(LOCAL),
|
||||
createTabDataFromId(FEDERATED)
|
||||
createTabDataFromId(HOME),
|
||||
createTabDataFromId(NOTIFICATIONS),
|
||||
createTabDataFromId(LOCAL),
|
||||
createTabDataFromId(FEDERATED)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.transition.TransitionManager
|
||||
import at.connyduck.sparkbutton.helpers.Utils
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.transition.MaterialArcMotion
|
||||
import com.google.android.material.transition.MaterialContainerTransform
|
||||
import com.keylesspalace.tusky.adapter.ItemInteractionListener
|
||||
|
|
@ -44,11 +46,9 @@ import com.keylesspalace.tusky.network.MastodonApi
|
|||
import com.keylesspalace.tusky.util.onTextChanged
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -221,26 +221,26 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
frameLayout.addView(editText)
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(R.string.add_hashtag_title)
|
||||
.setView(frameLayout)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.action_save) { _, _ ->
|
||||
val input = editText.text.toString().trim()
|
||||
if (tab == null) {
|
||||
val newTab = createTabDataFromId(HASHTAG, listOf(input))
|
||||
currentTabs.add(newTab)
|
||||
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
|
||||
} else {
|
||||
val newTab = tab.copy(arguments = tab.arguments + input)
|
||||
currentTabs[tabPosition] = newTab
|
||||
.setTitle(R.string.add_hashtag_title)
|
||||
.setView(frameLayout)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.action_save) { _, _ ->
|
||||
val input = editText.text.toString().trim()
|
||||
if (tab == null) {
|
||||
val newTab = createTabDataFromId(HASHTAG, listOf(input))
|
||||
currentTabs.add(newTab)
|
||||
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
|
||||
} else {
|
||||
val newTab = tab.copy(arguments = tab.arguments + input)
|
||||
currentTabs[tabPosition] = newTab
|
||||
|
||||
currentTabsAdapter.notifyItemChanged(tabPosition)
|
||||
}
|
||||
|
||||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
currentTabsAdapter.notifyItemChanged(tabPosition)
|
||||
}
|
||||
.create()
|
||||
|
||||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
}
|
||||
.create()
|
||||
|
||||
editText.onTextChanged { s, _, _, _ ->
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(s)
|
||||
|
|
@ -254,28 +254,28 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
private fun showSelectListDialog() {
|
||||
val adapter = ListSelectionAdapter(this)
|
||||
mastodonApi.getLists()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe (
|
||||
{ lists ->
|
||||
adapter.addAll(lists)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e("TabPreferenceActivity", "failed to load lists", throwable)
|
||||
}
|
||||
)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
{ lists ->
|
||||
adapter.addAll(lists)
|
||||
},
|
||||
{ throwable ->
|
||||
Log.e("TabPreferenceActivity", "failed to load lists", throwable)
|
||||
}
|
||||
)
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.select_list_title)
|
||||
.setAdapter(adapter) { _, position ->
|
||||
val list = adapter.getItem(position)
|
||||
val newTab = createTabDataFromId(LIST, listOf(list!!.id, list.title))
|
||||
currentTabs.add(newTab)
|
||||
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
|
||||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
}
|
||||
.show()
|
||||
.setTitle(R.string.select_list_title)
|
||||
.setAdapter(adapter) { _, position ->
|
||||
val list = adapter.getItem(position)
|
||||
val newTab = createTabDataFromId(LIST, listOf(list!!.id, list.title))
|
||||
currentTabs.add(newTab)
|
||||
currentTabsAdapter.notifyItemInserted(currentTabs.size - 1)
|
||||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun validateHashtag(input: CharSequence?): Boolean {
|
||||
|
|
@ -330,10 +330,9 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
it.tabPreferences = currentTabs
|
||||
accountManager.saveAccount(it)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe()
|
||||
|
||||
.subscribeOn(Schedulers.io())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe()
|
||||
}
|
||||
tabsChanged = true
|
||||
}
|
||||
|
|
@ -357,5 +356,4 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
private const val MIN_TAB_COUNT = 2
|
||||
private const val MAX_TAB_COUNT = 5
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,16 +22,16 @@ import android.util.Log
|
|||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.WorkManager
|
||||
import autodispose2.AutoDisposePlugins
|
||||
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
||||
import com.keylesspalace.tusky.di.AppInjector
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
||||
import com.keylesspalace.tusky.util.LocaleManager
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.uber.autodispose.AutoDisposePlugins
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||
import org.conscrypt.Conscrypt
|
||||
import java.security.Security
|
||||
import javax.inject.Inject
|
||||
|
|
@ -68,8 +68,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
// init the custom emoji fonts
|
||||
val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0)
|
||||
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
|
||||
.getConfig(this)
|
||||
.setReplaceAll(true)
|
||||
.getConfig(this)
|
||||
.setReplaceAll(true)
|
||||
EmojiCompat.init(emojiConfig)
|
||||
|
||||
// init night mode
|
||||
|
|
@ -81,10 +81,10 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
}
|
||||
|
||||
WorkManager.initialize(
|
||||
this,
|
||||
androidx.work.Configuration.Builder()
|
||||
.setWorkerFactory(notificationWorkerFactory)
|
||||
.build()
|
||||
this,
|
||||
androidx.work.Configuration.Builder()
|
||||
.setWorkerFactory(notificationWorkerFactory)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -104,4 +104,4 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
|||
@JvmStatic
|
||||
lateinit var localeManager: LocaleManager
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,27 +41,27 @@ import androidx.fragment.app.FragmentActivity
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import androidx.viewpager2.widget.ViewPager2
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
|
||||
import autodispose2.autoDispose
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.FutureTarget
|
||||
import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID
|
||||
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.fragment.ViewImageFragment
|
||||
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
||||
import com.keylesspalace.tusky.pager.ImagePagerAdapter
|
||||
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
|
||||
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
|
||||
|
||||
|
|
@ -102,17 +102,16 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
val realAttachs = attachments!!.map(AttachmentViewData::attachment)
|
||||
// Setup the view pager.
|
||||
ImagePagerAdapter(this, realAttachs, initialPosition)
|
||||
|
||||
} else {
|
||||
imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL)
|
||||
?: throw IllegalArgumentException("attachment list or image url has to be set")
|
||||
?: throw IllegalArgumentException("attachment list or image url has to be set")
|
||||
|
||||
SingleImagePagerAdapter(this, imageUrl!!)
|
||||
}
|
||||
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.viewPager.setCurrentItem(initialPosition, false)
|
||||
binding.viewPager.registerOnPageChangeCallback(object: ViewPager2.OnPageChangeCallback() {
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
binding.toolbar.title = getPageTitle(position)
|
||||
}
|
||||
|
|
@ -138,6 +137,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
|
||||
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE
|
||||
|
||||
window.statusBarColor = Color.BLACK
|
||||
window.sharedElementEnterTransition.addListener(object : NoopTransitionListener {
|
||||
override fun onTransitionEnd(transition: Transition) {
|
||||
|
|
@ -182,17 +182,17 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
}
|
||||
|
||||
binding.toolbar.animate().alpha(alpha)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
binding.toolbar.visibility = visibility
|
||||
animation.removeListener(this)
|
||||
}
|
||||
})
|
||||
.start()
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
binding.toolbar.visibility = visibility
|
||||
animation.removeListener(this)
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
|
||||
private fun getPageTitle(position: Int): CharSequence {
|
||||
if(attachments == null) {
|
||||
if (attachments == null) {
|
||||
return ""
|
||||
}
|
||||
return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size)
|
||||
|
|
@ -205,8 +205,10 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
|
||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES,
|
||||
getString(R.string.app_name) + "/" + filename)
|
||||
request.setDestinationInExternalPublicDir(
|
||||
Environment.DIRECTORY_PICTURES,
|
||||
getString(R.string.app_name) + "/" + filename
|
||||
)
|
||||
downloadManager.enqueue(request)
|
||||
}
|
||||
|
||||
|
|
@ -260,7 +262,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
|
||||
}
|
||||
|
||||
|
||||
private var isCreating: Boolean = false
|
||||
|
||||
private fun shareImage(directory: File, url: String) {
|
||||
|
|
@ -269,7 +270,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
invalidateOptionsMenu()
|
||||
val file = File(directory, getTemporaryMediaFilename("png"))
|
||||
val futureTask: FutureTarget<Bitmap> =
|
||||
Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit()
|
||||
Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submit()
|
||||
Single.fromCallable {
|
||||
val bitmap = futureTask.get()
|
||||
try {
|
||||
|
|
@ -283,32 +284,30 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
Log.e(TAG, "Error writing temporary media.")
|
||||
}
|
||||
return@fromCallable false
|
||||
|
||||
}
|
||||
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnDispose {
|
||||
futureTask.cancel(true)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnDispose {
|
||||
futureTask.cancel(true)
|
||||
}
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
{ result ->
|
||||
Log.d(TAG, "Download image result: $result")
|
||||
isCreating = false
|
||||
invalidateOptionsMenu()
|
||||
binding.progressBarShare.visibility = View.GONE
|
||||
if (result)
|
||||
shareFile(file, "image/png")
|
||||
},
|
||||
{ error ->
|
||||
isCreating = false
|
||||
invalidateOptionsMenu()
|
||||
binding.progressBarShare.visibility = View.GONE
|
||||
Log.e(TAG, "Failed to download image", error)
|
||||
}
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
{ result ->
|
||||
Log.d(TAG, "Download image result: $result")
|
||||
isCreating = false
|
||||
invalidateOptionsMenu()
|
||||
binding.progressBarShare.visibility = View.GONE
|
||||
if (result)
|
||||
shareFile(file, "image/png")
|
||||
},
|
||||
{ error ->
|
||||
isCreating = false
|
||||
invalidateOptionsMenu()
|
||||
binding.progressBarShare.visibility = View.GONE
|
||||
Log.e(TAG, "Failed to download image", error)
|
||||
}
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
private fun shareMediaFile(directory: File, url: String) {
|
||||
|
|
@ -351,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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import androidx.appcompat.widget.Toolbar;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.util.ListUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
static final int VIEW_TYPE_ACCOUNT = 0;
|
||||
static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
List<Account> accountList;
|
||||
AccountActionListener accountActionListener;
|
||||
private boolean bottomLoading;
|
||||
protected final boolean animateEmojis;
|
||||
protected final boolean animateAvatar;
|
||||
|
||||
AccountAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
||||
this.accountList = new ArrayList<>();
|
||||
this.accountActionListener = accountActionListener;
|
||||
this.animateAvatar = animateAvatar;
|
||||
this.animateEmojis = animateEmojis;
|
||||
bottomLoading = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return accountList.size() + (bottomLoading ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == accountList.size() && bottomLoading) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
return VIEW_TYPE_ACCOUNT;
|
||||
}
|
||||
}
|
||||
|
||||
public void update(@NonNull List<Account> newAccounts) {
|
||||
accountList = ListUtils.removeDuplicates(newAccounts);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(@NonNull List<Account> newAccounts) {
|
||||
int end = accountList.size();
|
||||
Account last = accountList.get(end - 1);
|
||||
if (last != null && !findAccount(newAccounts, last.getId())) {
|
||||
accountList.addAll(newAccounts);
|
||||
notifyItemRangeInserted(end, newAccounts.size());
|
||||
}
|
||||
}
|
||||
|
||||
public void setBottomLoading(boolean loading) {
|
||||
boolean wasLoading = bottomLoading;
|
||||
if(wasLoading == loading) {
|
||||
return;
|
||||
}
|
||||
bottomLoading = loading;
|
||||
if(loading) {
|
||||
notifyItemInserted(accountList.size());
|
||||
} else {
|
||||
notifyItemRemoved(accountList.size());
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findAccount(@NonNull List<Account> accounts, String id) {
|
||||
for (Account account : accounts) {
|
||||
if (account.getId().equals(id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Account removeItem(int position) {
|
||||
if (position < 0 || position >= accountList.size()) {
|
||||
return null;
|
||||
}
|
||||
Account account = accountList.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
return account;
|
||||
}
|
||||
|
||||
public void addItem(@NonNull Account account, int position) {
|
||||
if (position < 0 || position > accountList.size()) {
|
||||
return;
|
||||
}
|
||||
accountList.add(position, account);
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
/* Copyright 2021 Tusky Contributors.
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.removeDuplicates
|
||||
|
||||
/** Generic adapter with bottom loading indicator. */
|
||||
abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructor(
|
||||
var accountActionListener: AccountActionListener,
|
||||
protected val animateAvatar: Boolean,
|
||||
protected val animateEmojis: Boolean
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
|
||||
var accountList = mutableListOf<Account>()
|
||||
private var bottomLoading: Boolean = false
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return accountList.size + if (bottomLoading) 1 else 0
|
||||
}
|
||||
|
||||
abstract fun createAccountViewHolder(parent: ViewGroup): AVH
|
||||
|
||||
abstract fun onBindAccountViewHolder(viewHolder: AVH, position: Int)
|
||||
|
||||
final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
this.onBindAccountViewHolder(holder as AVH, position)
|
||||
}
|
||||
}
|
||||
|
||||
final override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_ACCOUNT -> this.createAccountViewHolder(parent)
|
||||
VIEW_TYPE_FOOTER -> this.createFooterViewHolder(parent)
|
||||
else -> error("Unknown item type: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createFooterViewHolder(
|
||||
parent: ViewGroup,
|
||||
): RecyclerView.ViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_footer, parent, false)
|
||||
return LoadingFooterViewHolder(view)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == accountList.size && bottomLoading) {
|
||||
VIEW_TYPE_FOOTER
|
||||
} else {
|
||||
VIEW_TYPE_ACCOUNT
|
||||
}
|
||||
}
|
||||
|
||||
fun update(newAccounts: List<Account>) {
|
||||
accountList = removeDuplicates(newAccounts)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun addItems(newAccounts: List<Account>) {
|
||||
val end = accountList.size
|
||||
val last = accountList[end - 1]
|
||||
if (newAccounts.none { it.id == last.id }) {
|
||||
accountList.addAll(newAccounts)
|
||||
notifyItemRangeInserted(end, newAccounts.size)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBottomLoading(loading: Boolean) {
|
||||
val wasLoading = bottomLoading
|
||||
if (wasLoading == loading) {
|
||||
return
|
||||
}
|
||||
bottomLoading = loading
|
||||
if (loading) {
|
||||
notifyItemInserted(accountList.size)
|
||||
} else {
|
||||
notifyItemRemoved(accountList.size)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeItem(position: Int): Account? {
|
||||
if (position < 0 || position >= accountList.size) {
|
||||
return null
|
||||
}
|
||||
val account = accountList.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
return account
|
||||
}
|
||||
|
||||
fun addItem(account: Account, position: Int) {
|
||||
if (position < 0 || position > accountList.size) {
|
||||
return
|
||||
}
|
||||
accountList.add(position, account)
|
||||
notifyItemInserted(position)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE_ACCOUNT = 0
|
||||
const val VIEW_TYPE_FOOTER = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
fields.forEach { field ->
|
||||
fieldData.add(MutableStringPair(field.name, field.value))
|
||||
}
|
||||
if(fieldData.isEmpty()) {
|
||||
if (fieldData.isEmpty()) {
|
||||
fieldData.add(MutableStringPair("", ""))
|
||||
}
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
holder.binding.accountFieldName.setText(fieldData[position].first)
|
||||
holder.binding.accountFieldValue.setText(fieldData[position].second)
|
||||
|
||||
holder.binding.accountFieldName.addTextChangedListener(object: TextWatcher {
|
||||
holder.binding.accountFieldName.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(newText: Editable) {
|
||||
fieldData[holder.bindingAdapterPosition].first = newText.toString()
|
||||
}
|
||||
|
|
@ -73,7 +73,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
holder.binding.accountFieldValue.addTextChangedListener(object: TextWatcher {
|
||||
holder.binding.accountFieldValue.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(newText: Editable) {
|
||||
fieldData[holder.bindingAdapterPosition].second = newText.toString()
|
||||
}
|
||||
|
|
@ -82,9 +82,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter<BindingHolder<ItemEditField
|
|||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
class MutableStringPair (var first: String, var second: String)
|
||||
|
||||
class MutableStringPair(var first: String, var second: String)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
||||
class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(context, R.layout.item_autocomplete_account) {
|
||||
|
||||
|
|
@ -48,9 +49,8 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter<AccountEntity>(co
|
|||
val animateAvatar = pm.getBoolean("animateGifAvatars", false)
|
||||
|
||||
loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatar)
|
||||
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
|
||||
public class BlocksAdapter extends AccountAdapter {
|
||||
|
||||
public BlocksAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
||||
super(accountActionListener, animateAvatar, animateEmojis);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_ACCOUNT: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_blocked_user, parent, false);
|
||||
return new BlockedUserViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new LoadingFooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
|
||||
holder.setupActionListener(accountActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
static class BlockedUserViewHolder extends RecyclerView.ViewHolder {
|
||||
private ImageView avatar;
|
||||
private TextView username;
|
||||
private TextView displayName;
|
||||
private ImageButton unblock;
|
||||
private String id;
|
||||
|
||||
BlockedUserViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
avatar = itemView.findViewById(R.id.blocked_user_avatar);
|
||||
username = itemView.findViewById(R.id.blocked_user_username);
|
||||
displayName = itemView.findViewById(R.id.blocked_user_display_name);
|
||||
unblock = itemView.findViewById(R.id.blocked_user_unblock);
|
||||
|
||||
}
|
||||
|
||||
void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
|
||||
id = account.getId();
|
||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
||||
displayName.setText(emojifiedName);
|
||||
String format = username.getContext().getString(R.string.status_username_format);
|
||||
String formattedUsername = String.format(format, account.getUsername());
|
||||
username.setText(formattedUsername);
|
||||
int avatarRadius = avatar.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
||||
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
|
||||
}
|
||||
|
||||
void setupActionListener(final AccountActionListener listener) {
|
||||
unblock.setOnClickListener(v -> {
|
||||
int position = getBindingAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onBlock(false, id, position);
|
||||
}
|
||||
});
|
||||
itemView.setOnClickListener(v -> listener.onViewAccount(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
|
||||
/** Displays a list of blocked accounts. */
|
||||
class BlocksAdapter(
|
||||
accountActionListener: AccountActionListener,
|
||||
animateAvatar: Boolean,
|
||||
animateEmojis: Boolean
|
||||
) : AccountAdapter<BlocksAdapter.BlockedUserViewHolder>(
|
||||
accountActionListener,
|
||||
animateAvatar,
|
||||
animateEmojis
|
||||
) {
|
||||
override fun createAccountViewHolder(parent: ViewGroup): BlockedUserViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_blocked_user, parent, false)
|
||||
return BlockedUserViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindAccountViewHolder(viewHolder: BlockedUserViewHolder, position: Int) {
|
||||
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
|
||||
viewHolder.setupActionListener(accountActionListener)
|
||||
}
|
||||
|
||||
class BlockedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val avatar: ImageView = itemView.findViewById(R.id.blocked_user_avatar)
|
||||
private val username: TextView = itemView.findViewById(R.id.blocked_user_username)
|
||||
private val displayName: TextView = itemView.findViewById(R.id.blocked_user_display_name)
|
||||
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
|
||||
private var id: String? = null
|
||||
|
||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
id = account.id
|
||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||
displayName.text = emojifiedName
|
||||
val format = username.context.getString(R.string.status_username_format)
|
||||
val formattedUsername = String.format(format, account.username)
|
||||
username.text = formattedUsername
|
||||
val avatarRadius = avatar.context.resources
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||
loadAvatar(account.avatar, avatar, avatarRadius, animateAvatar)
|
||||
}
|
||||
|
||||
fun setupActionListener(listener: AccountActionListener) {
|
||||
unblock.setOnClickListener {
|
||||
val position = bindingAdapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onBlock(false, id, position)
|
||||
}
|
||||
}
|
||||
itemView.setOnClickListener { listener.onViewAccount(id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,15 +22,15 @@ import com.bumptech.glide.Glide
|
|||
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
|
||||
class EmojiAdapter(
|
||||
emojiList: List<Emoji>,
|
||||
private val onEmojiSelectedListener: OnEmojiSelectedListener
|
||||
emojiList: List<Emoji>,
|
||||
private val onEmojiSelectedListener: OnEmojiSelectedListener
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() {
|
||||
|
||||
private val emojiList : List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
|
||||
.sortedBy { it.shortcode.toLowerCase(Locale.ROOT) }
|
||||
private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker }
|
||||
.sortedBy { it.shortcode.lowercase(Locale.ROOT) }
|
||||
|
||||
override fun getItemCount() = emojiList.size
|
||||
|
||||
|
|
@ -44,8 +44,8 @@ class EmojiAdapter(
|
|||
val emojiImageView = holder.binding.root
|
||||
|
||||
Glide.with(emojiImageView)
|
||||
.load(emoji.url)
|
||||
.into(emojiImageView)
|
||||
.load(emoji.url)
|
||||
.into(emojiImageView)
|
||||
|
||||
emojiImageView.setOnClickListener {
|
||||
onEmojiSelectedListener.onEmojiSelected(emoji.shortcode)
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
|
||||
/** Both for follows and following lists. */
|
||||
public class FollowAdapter extends AccountAdapter {
|
||||
|
||||
public FollowAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
||||
super(accountActionListener, animateAvatar, animateEmojis);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_ACCOUNT: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_account, parent, false);
|
||||
return new AccountViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new LoadingFooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
AccountViewHolder holder = (AccountViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
|
||||
holder.setupActionListener(accountActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
|
||||
/** Displays either a follows or following list. */
|
||||
class FollowAdapter(
|
||||
accountActionListener: AccountActionListener,
|
||||
animateAvatar: Boolean,
|
||||
animateEmojis: Boolean
|
||||
) : AccountAdapter<AccountViewHolder>(accountActionListener, animateAvatar, animateEmojis) {
|
||||
override fun createAccountViewHolder(parent: ViewGroup): AccountViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_account, parent, false)
|
||||
return AccountViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindAccountViewHolder(viewHolder: AccountViewHolder, position: Int) {
|
||||
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
|
||||
viewHolder.setupActionListener(accountActionListener)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,11 +24,14 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
|
||||
class FollowRequestViewHolder(
|
||||
private val binding: ItemFollowRequestBinding,
|
||||
private val showHeader: Boolean
|
||||
private val binding: ItemFollowRequestBinding,
|
||||
private val showHeader: Boolean
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
|
||||
public class FollowRequestsAdapter extends AccountAdapter {
|
||||
|
||||
public FollowRequestsAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
||||
super(accountActionListener, animateAvatar, animateEmojis);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_ACCOUNT: {
|
||||
ItemFollowRequestBinding binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
|
||||
return new FollowRequestViewHolder(binding, false);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new LoadingFooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis);
|
||||
holder.setupActionListener(accountActionListener, accountList.get(position).getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
|
||||
/** Displays a list of follow requests with accept/reject buttons. */
|
||||
class FollowRequestsAdapter(
|
||||
accountActionListener: AccountActionListener,
|
||||
animateAvatar: Boolean,
|
||||
animateEmojis: Boolean
|
||||
) : AccountAdapter<FollowRequestViewHolder>(accountActionListener, animateAvatar, animateEmojis) {
|
||||
override fun createAccountViewHolder(parent: ViewGroup): FollowRequestViewHolder {
|
||||
val binding = ItemFollowRequestBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
return FollowRequestViewHolder(binding, false)
|
||||
}
|
||||
|
||||
override fun onBindAccountViewHolder(viewHolder: FollowRequestViewHolder, position: Int) {
|
||||
viewHolder.setupWithAccount(accountList[position], animateAvatar, animateEmojis)
|
||||
viewHolder.setupActionListener(accountActionListener, accountList[position].id)
|
||||
}
|
||||
}
|
||||
|
|
@ -25,7 +25,7 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val
|
|||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_follow_requests_header, parent, false) as TextView
|
||||
.inflate(R.layout.item_follow_requests_header, parent, false) as TextView
|
||||
return HeaderViewHolder(view)
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +34,6 @@ class FollowRequestsHeaderAdapter(private val instanceName: String, private val
|
|||
}
|
||||
|
||||
override fun getItemCount() = if (accountLocked) 0 else 1
|
||||
|
||||
}
|
||||
|
||||
class HeaderViewHolder(var textView: TextView) : RecyclerView.ViewHolder(textView)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
|
|
|||
|
|
@ -1,131 +0,0 @@
|
|||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class MutesAdapter extends AccountAdapter {
|
||||
private HashMap<String, Boolean> mutingNotificationsMap;
|
||||
|
||||
public MutesAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) {
|
||||
super(accountActionListener, animateAvatar, animateEmojis);
|
||||
mutingNotificationsMap = new HashMap<String, Boolean>();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_ACCOUNT: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_muted_user, parent, false);
|
||||
return new MutesAdapter.MutedUserViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new LoadingFooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
|
||||
Account account = accountList.get(position);
|
||||
holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId()), animateAvatar, animateEmojis);
|
||||
holder.setupActionListener(accountActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMutingNotifications(String id, boolean mutingNotifications, int position) {
|
||||
mutingNotificationsMap.put(id, mutingNotifications);
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
public void updateMutingNotificationsMap(HashMap<String, Boolean> newMutingNotificationsMap) {
|
||||
mutingNotificationsMap.putAll(newMutingNotificationsMap);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class MutedUserViewHolder extends RecyclerView.ViewHolder {
|
||||
private ImageView avatar;
|
||||
private TextView username;
|
||||
private TextView displayName;
|
||||
private ImageButton unmute;
|
||||
private ImageButton muteNotifications;
|
||||
private String id;
|
||||
private boolean notifications;
|
||||
|
||||
MutedUserViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
avatar = itemView.findViewById(R.id.muted_user_avatar);
|
||||
username = itemView.findViewById(R.id.muted_user_username);
|
||||
displayName = itemView.findViewById(R.id.muted_user_display_name);
|
||||
unmute = itemView.findViewById(R.id.muted_user_unmute);
|
||||
muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications);
|
||||
}
|
||||
|
||||
void setupWithAccount(Account account, Boolean mutingNotifications, boolean animateAvatar, boolean animateEmojis) {
|
||||
id = account.getId();
|
||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
||||
displayName.setText(emojifiedName);
|
||||
String format = username.getContext().getString(R.string.status_username_format);
|
||||
String formattedUsername = String.format(format, account.getUsername());
|
||||
username.setText(formattedUsername);
|
||||
int avatarRadius = avatar.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
||||
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
|
||||
|
||||
String unmuteString = unmute.getContext().getString(R.string.action_unmute_desc, formattedUsername);
|
||||
unmute.setContentDescription(unmuteString);
|
||||
ViewCompat.setTooltipText(unmute, unmuteString);
|
||||
|
||||
if (mutingNotifications == null) {
|
||||
muteNotifications.setEnabled(false);
|
||||
notifications = true;
|
||||
} else {
|
||||
muteNotifications.setEnabled(true);
|
||||
notifications = mutingNotifications;
|
||||
}
|
||||
|
||||
if (notifications) {
|
||||
muteNotifications.setImageResource(R.drawable.ic_notifications_24dp);
|
||||
String unmuteNotificationsString = muteNotifications.getContext()
|
||||
.getString(R.string.action_unmute_notifications_desc, formattedUsername);
|
||||
muteNotifications.setContentDescription(unmuteNotificationsString);
|
||||
ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString);
|
||||
} else {
|
||||
muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp);
|
||||
String muteNotificationsString = muteNotifications.getContext()
|
||||
.getString(R.string.action_mute_notifications_desc, formattedUsername);
|
||||
muteNotifications.setContentDescription(muteNotificationsString);
|
||||
ViewCompat.setTooltipText(muteNotifications, muteNotificationsString);
|
||||
}
|
||||
}
|
||||
|
||||
void setupActionListener(final AccountActionListener listener) {
|
||||
unmute.setOnClickListener(v -> listener.onMute(false, id, getBindingAdapterPosition(), false));
|
||||
muteNotifications.setOnClickListener(
|
||||
v -> listener.onMute(true, id, getBindingAdapterPosition(), !notifications));
|
||||
itemView.setOnClickListener(v -> listener.onViewAccount(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import java.util.HashMap
|
||||
|
||||
/**
|
||||
* Displays a list of muted accounts with mute/unmute account and mute/unmute notifications
|
||||
* buttons.
|
||||
* */
|
||||
class MutesAdapter(
|
||||
accountActionListener: AccountActionListener,
|
||||
animateAvatar: Boolean,
|
||||
animateEmojis: Boolean
|
||||
) : AccountAdapter<MutesAdapter.MutedUserViewHolder>(
|
||||
accountActionListener,
|
||||
animateAvatar,
|
||||
animateEmojis
|
||||
) {
|
||||
private val mutingNotificationsMap = HashMap<String, Boolean>()
|
||||
|
||||
override fun createAccountViewHolder(parent: ViewGroup): MutedUserViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_muted_user, parent, false)
|
||||
return MutedUserViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindAccountViewHolder(viewHolder: MutedUserViewHolder, position: Int) {
|
||||
val account = accountList[position]
|
||||
viewHolder.setupWithAccount(
|
||||
account,
|
||||
mutingNotificationsMap[account.id],
|
||||
animateAvatar,
|
||||
animateEmojis
|
||||
)
|
||||
viewHolder.setupActionListener(accountActionListener)
|
||||
}
|
||||
|
||||
fun updateMutingNotifications(id: String, mutingNotifications: Boolean, position: Int) {
|
||||
mutingNotificationsMap[id] = mutingNotifications
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
fun updateMutingNotificationsMap(newMutingNotificationsMap: HashMap<String, Boolean>?) {
|
||||
mutingNotificationsMap.putAll(newMutingNotificationsMap!!)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class MutedUserViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val avatar: ImageView = itemView.findViewById(R.id.muted_user_avatar)
|
||||
private val username: TextView = itemView.findViewById(R.id.muted_user_username)
|
||||
private val displayName: TextView = itemView.findViewById(R.id.muted_user_display_name)
|
||||
private val unmute: ImageButton = itemView.findViewById(R.id.muted_user_unmute)
|
||||
private val muteNotifications: ImageButton =
|
||||
itemView.findViewById(R.id.muted_user_mute_notifications)
|
||||
|
||||
private var id: String? = null
|
||||
private var notifications = false
|
||||
|
||||
fun setupWithAccount(
|
||||
account: Account,
|
||||
mutingNotifications: Boolean?,
|
||||
animateAvatar: Boolean,
|
||||
animateEmojis: Boolean
|
||||
) {
|
||||
id = account.id
|
||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||
displayName.text = emojifiedName
|
||||
val format = username.context.getString(R.string.status_username_format)
|
||||
val formattedUsername = String.format(format, account.username)
|
||||
username.text = formattedUsername
|
||||
val avatarRadius = avatar.context.resources
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||
loadAvatar(account.avatar, avatar, avatarRadius, animateAvatar)
|
||||
val unmuteString =
|
||||
unmute.context.getString(R.string.action_unmute_desc, formattedUsername)
|
||||
unmute.contentDescription = unmuteString
|
||||
ViewCompat.setTooltipText(unmute, unmuteString)
|
||||
if (mutingNotifications == null) {
|
||||
muteNotifications.isEnabled = false
|
||||
notifications = true
|
||||
} else {
|
||||
muteNotifications.isEnabled = true
|
||||
notifications = mutingNotifications
|
||||
}
|
||||
if (notifications) {
|
||||
muteNotifications.setImageResource(R.drawable.ic_notifications_24dp)
|
||||
val unmuteNotificationsString = muteNotifications.context
|
||||
.getString(R.string.action_unmute_notifications_desc, formattedUsername)
|
||||
muteNotifications.contentDescription = unmuteNotificationsString
|
||||
ViewCompat.setTooltipText(muteNotifications, unmuteNotificationsString)
|
||||
} else {
|
||||
muteNotifications.setImageResource(R.drawable.ic_notifications_off_24dp)
|
||||
val muteNotificationsString = muteNotifications.context
|
||||
.getString(R.string.action_mute_notifications_desc, formattedUsername)
|
||||
muteNotifications.contentDescription = muteNotificationsString
|
||||
ViewCompat.setTooltipText(muteNotifications, muteNotificationsString)
|
||||
}
|
||||
}
|
||||
|
||||
fun setupActionListener(listener: AccountActionListener) {
|
||||
unmute.setOnClickListener {
|
||||
listener.onMute(
|
||||
false,
|
||||
id,
|
||||
bindingAdapterPosition,
|
||||
false
|
||||
)
|
||||
}
|
||||
muteNotifications.setOnClickListener {
|
||||
listener.onMute(
|
||||
true,
|
||||
id,
|
||||
bindingAdapterPosition,
|
||||
!notifications
|
||||
)
|
||||
}
|
||||
itemView.setOnClickListener { listener.onViewAccount(id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,30 +15,28 @@
|
|||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import androidx.paging.LoadState
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.Status
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
|
||||
class NetworkStateViewHolder(private val binding: ItemNetworkStateBinding,
|
||||
private val retryCallback: () -> Unit)
|
||||
: RecyclerView.ViewHolder(binding.root) {
|
||||
class NetworkStateViewHolder(
|
||||
private val binding: ItemNetworkStateBinding,
|
||||
private val retryCallback: () -> Unit
|
||||
) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) {
|
||||
binding.progressBar.visible(state?.status == Status.RUNNING)
|
||||
binding.retryButton.visible(state?.status == Status.FAILED)
|
||||
binding.errorMsg.visible(state?.msg != null)
|
||||
binding.errorMsg.text = state?.msg
|
||||
fun setUpWithNetworkState(state: LoadState) {
|
||||
binding.progressBar.visible(state == LoadState.Loading)
|
||||
binding.retryButton.visible(state is LoadState.Error)
|
||||
val msg = if (state is LoadState.Error) {
|
||||
state.error.message
|
||||
} else {
|
||||
null
|
||||
}
|
||||
binding.errorMsg.visible(msg != null)
|
||||
binding.errorMsg.text = msg
|
||||
binding.retryButton.setOnClickListener {
|
||||
retryCallback()
|
||||
}
|
||||
if(fullScreen) {
|
||||
binding.root.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
} else {
|
||||
binding.root.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -43,6 +43,7 @@ import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
|||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
|
|
@ -195,14 +196,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
} else {
|
||||
holder.showNotificationContent(true);
|
||||
|
||||
holder.setDisplayName(statusViewData.getUserFullName(), statusViewData.getAccountEmojis());
|
||||
holder.setUsername(statusViewData.getNickname());
|
||||
holder.setCreatedAt(statusViewData.getCreatedAt());
|
||||
Status status = statusViewData.getActionable();
|
||||
holder.setDisplayName(status.getAccount().getDisplayName(), status.getAccount().getEmojis());
|
||||
holder.setUsername(status.getAccount().getUsername());
|
||||
holder.setCreatedAt(status.getCreatedAt());
|
||||
|
||||
if(concreteNotificaton.getType() == Notification.Type.STATUS) {
|
||||
holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot());
|
||||
if (concreteNotificaton.getType() == Notification.Type.STATUS) {
|
||||
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
|
||||
} else {
|
||||
holder.setAvatars(statusViewData.getAvatar(),
|
||||
holder.setAvatars(status.getAccount().getAvatar(),
|
||||
concreteNotificaton.getAccount().getAvatar());
|
||||
}
|
||||
}
|
||||
|
|
@ -215,7 +217,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
if (payloadForHolder instanceof List)
|
||||
for (Object item : (List) payloadForHolder) {
|
||||
if (StatusBaseViewHolder.Key.KEY_CREATED.equals(item) && statusViewData != null) {
|
||||
holder.setCreatedAt(statusViewData.getCreatedAt());
|
||||
holder.setCreatedAt(statusViewData.getStatus().getActionableStatus().getCreatedAt());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -256,6 +258,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
statusDisplayOptions.useBlurhash(),
|
||||
CardViewMode.NONE,
|
||||
statusDisplayOptions.confirmReblogs(),
|
||||
statusDisplayOptions.confirmFavourites(),
|
||||
statusDisplayOptions.hideStats(),
|
||||
statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
|
|
@ -386,7 +389,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private StatusViewData.Concrete statusViewData;
|
||||
private SimpleDateFormat shortSdf;
|
||||
private SimpleDateFormat longSdf;
|
||||
|
||||
|
||||
private int avatarRadius48dp;
|
||||
private int avatarRadius36dp;
|
||||
private int avatarRadius24dp;
|
||||
|
|
@ -415,7 +418,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
statusContent.setOnClickListener(this);
|
||||
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
||||
|
||||
|
||||
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
||||
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
||||
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
||||
|
|
@ -531,7 +534,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
message.setText(emojifiedText);
|
||||
|
||||
if (statusViewData != null) {
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText());
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus().getSpoilerText());
|
||||
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||
if (statusViewData.isExpanded()) {
|
||||
|
|
@ -586,7 +589,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
|
||||
notificationAvatar.setVisibility(View.VISIBLE);
|
||||
ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar,
|
||||
avatarRadius24dp, statusDisplayOptions.animateAvatars());
|
||||
avatarRadius24dp, statusDisplayOptions.animateAvatars());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -607,7 +610,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
private void setupContentAndSpoiler(final LinkListener listener) {
|
||||
|
||||
boolean shouldShowContentIfSpoiler = statusViewData.isExpanded();
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText());
|
||||
boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getStatus(). getSpoilerText());
|
||||
if (!shouldShowContentIfSpoiler && hasSpoiler) {
|
||||
statusContent.setVisibility(View.GONE);
|
||||
} else {
|
||||
|
|
@ -615,7 +618,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
}
|
||||
|
||||
Spanned content = statusViewData.getContent();
|
||||
List<Emoji> emojis = statusViewData.getStatusEmojis();
|
||||
List<Emoji> emojis = statusViewData.getActionable().getEmojis();
|
||||
|
||||
if (statusViewData.isCollapsible() && (statusViewData.isExpanded() || !hasSpoiler)) {
|
||||
contentCollapseButton.setOnClickListener(view -> {
|
||||
|
|
@ -641,14 +644,19 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||
content, emojis, statusContent, statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener);
|
||||
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getActionable().getMentions(), listener);
|
||||
|
||||
CharSequence emojifiedContentWarning = CustomEmojiHelper.emojify(
|
||||
statusViewData.getSpoilerText(),
|
||||
statusViewData.getStatusEmojis(),
|
||||
contentWarningDescriptionTextView,
|
||||
statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
CharSequence emojifiedContentWarning;
|
||||
if (statusViewData.getSpoilerText() != null) {
|
||||
emojifiedContentWarning = CustomEmojiHelper.emojify(
|
||||
statusViewData.getSpoilerText(),
|
||||
statusViewData.getActionable().getEmojis(),
|
||||
contentWarningDescriptionTextView,
|
||||
statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
} else {
|
||||
emojifiedContentWarning = "";
|
||||
}
|
||||
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
|
||||
public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private Button loadMoreButton;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
PlaceholderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
loadMoreButton = itemView.findViewById(R.id.button_load_more);
|
||||
progressBar = itemView.findViewById(R.id.progressBar);
|
||||
}
|
||||
|
||||
public void setup(final StatusActionListener listener, boolean progress) {
|
||||
loadMoreButton.setVisibility(progress ? View.GONE : View.VISIBLE);
|
||||
progressBar.setVisibility(progress ? View.VISIBLE : View.GONE);
|
||||
|
||||
loadMoreButton.setEnabled(true);
|
||||
loadMoreButton.setOnClickListener(v -> {
|
||||
loadMoreButton.setEnabled(false);
|
||||
listener.onLoadMore(getBindingAdapterPosition());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
|
||||
/**
|
||||
* Placeholder for different timelines.
|
||||
* Either displays "load more" button or a progress indicator.
|
||||
**/
|
||||
class PlaceholderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val loadMoreButton: Button = itemView.findViewById(R.id.button_load_more)
|
||||
private val progressBar: ProgressBar = itemView.findViewById(R.id.progressBar)
|
||||
|
||||
fun setup(listener: StatusActionListener, progress: Boolean) {
|
||||
loadMoreButton.visibility = if (progress) View.GONE else View.VISIBLE
|
||||
progressBar.visibility = if (progress) View.VISIBLE else View.GONE
|
||||
loadMoreButton.isEnabled = true
|
||||
loadMoreButton.setOnClickListener { v: View? ->
|
||||
loadMoreButton.isEnabled = false
|
||||
listener.onLoadMore(bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,8 +18,10 @@ package com.keylesspalace.tusky.adapter
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemPollBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
|
|
@ -29,7 +31,7 @@ import com.keylesspalace.tusky.viewdata.PollOptionViewData
|
|||
import com.keylesspalace.tusky.viewdata.buildDescription
|
||||
import com.keylesspalace.tusky.viewdata.calculatePercent
|
||||
|
||||
class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||
class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||
|
||||
private var pollOptions: List<PollOptionViewData> = emptyList()
|
||||
private var voteCount: Int = 0
|
||||
|
|
@ -40,13 +42,14 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
private var animateEmojis = false
|
||||
|
||||
fun setup(
|
||||
options: List<PollOptionViewData>,
|
||||
voteCount: Int,
|
||||
votersCount: Int?,
|
||||
emojis: List<Emoji>,
|
||||
mode: Int,
|
||||
resultClickListener: View.OnClickListener?,
|
||||
animateEmojis: Boolean) {
|
||||
options: List<PollOptionViewData>,
|
||||
voteCount: Int,
|
||||
votersCount: Int?,
|
||||
emojis: List<Emoji>,
|
||||
mode: Int,
|
||||
resultClickListener: View.OnClickListener?,
|
||||
animateEmojis: Boolean
|
||||
) {
|
||||
this.pollOptions = options
|
||||
this.voteCount = voteCount
|
||||
this.votersCount = votersCount
|
||||
|
|
@ -57,12 +60,11 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun getSelected() : List<Int> {
|
||||
fun getSelected(): List<Int> {
|
||||
return pollOptions.filter { it.selected }
|
||||
.map { pollOptions.indexOf(it) }
|
||||
.map { pollOptions.indexOf(it) }
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemPollBinding> {
|
||||
val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return BindingHolder(binding)
|
||||
|
|
@ -82,16 +84,22 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
radioButton.visible(mode == SINGLE)
|
||||
checkBox.visible(mode == MULTIPLE)
|
||||
|
||||
when(mode) {
|
||||
when (mode) {
|
||||
RESULT -> {
|
||||
val percent = calculatePercent(option.votesCount, votersCount, voteCount)
|
||||
val emojifiedPollOptionText = buildDescription(option.title, percent, resultTextView.context)
|
||||
.emojify(emojis, resultTextView, animateEmojis)
|
||||
resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
|
||||
val emojifiedPollOptionText = buildDescription(option.title, percent, option.voted, resultTextView.context)
|
||||
.emojify(emojis, resultTextView, animateEmojis)
|
||||
resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
|
||||
|
||||
val level = percent * 100
|
||||
val optionColor = if (option.voted) {
|
||||
R.color.colorBackgroundHighlight
|
||||
} else {
|
||||
R.color.colorBackgroundAccent
|
||||
}
|
||||
|
||||
resultTextView.background.level = level
|
||||
resultTextView.background.setTint(ContextCompat.getColor(resultTextView.context, optionColor))
|
||||
resultTextView.setOnClickListener(resultClickListener)
|
||||
}
|
||||
SINGLE -> {
|
||||
|
|
@ -114,7 +122,6 @@ class PollAdapter: RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import androidx.core.widget.TextViewCompat
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
|
||||
class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
|
||||
class PreviewPollOptionsAdapter : RecyclerView.Adapter<PreviewViewHolder>() {
|
||||
|
||||
private var options: List<String> = emptyList()
|
||||
private var multiple: Boolean = false
|
||||
|
|
@ -60,7 +60,6 @@ class PreviewPollOptionsAdapter: RecyclerView.Adapter<PreviewViewHolder>() {
|
|||
|
||||
textView.setOnClickListener(clickListener)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PreviewViewHolder(itemView: View): RecyclerView.ViewHolder(itemView)
|
||||
class PreviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.db.TootEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SavedTootAdapter extends RecyclerView.Adapter {
|
||||
private List<TootEntity> list;
|
||||
private SavedTootAction handler;
|
||||
|
||||
public SavedTootAdapter(Context context) {
|
||||
super();
|
||||
list = new ArrayList<>();
|
||||
handler = (SavedTootAction) context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_saved_toot, parent, false);
|
||||
return new TootViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
TootViewHolder holder = (TootViewHolder) viewHolder;
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return list.size();
|
||||
}
|
||||
|
||||
public void setItems(List<TootEntity> newToot) {
|
||||
list = new ArrayList<>();
|
||||
list.addAll(newToot);
|
||||
}
|
||||
|
||||
public void addItems(List<TootEntity> newToot) {
|
||||
int end = list.size();
|
||||
list.addAll(newToot);
|
||||
notifyItemRangeInserted(end, newToot.size());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TootEntity removeItem(int position) {
|
||||
if (position < 0 || position >= list.size()) {
|
||||
return null;
|
||||
}
|
||||
TootEntity toot = list.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
return toot;
|
||||
}
|
||||
|
||||
private TootEntity getItem(int position) {
|
||||
if (position >= 0 && position < list.size()) {
|
||||
return list.get(position);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// handler saved toot
|
||||
public interface SavedTootAction {
|
||||
void delete(int position, TootEntity item);
|
||||
|
||||
void click(int position, TootEntity item);
|
||||
}
|
||||
|
||||
private class TootViewHolder extends RecyclerView.ViewHolder {
|
||||
View view;
|
||||
TextView content;
|
||||
ImageButton suppr;
|
||||
|
||||
TootViewHolder(View view) {
|
||||
super(view);
|
||||
this.view = view;
|
||||
this.content = view.findViewById(R.id.content);
|
||||
this.suppr = view.findViewById(R.id.suppr);
|
||||
}
|
||||
|
||||
void bind(final TootEntity item) {
|
||||
suppr.setEnabled(true);
|
||||
|
||||
if (item != null) {
|
||||
content.setText(item.getText());
|
||||
|
||||
suppr.setOnClickListener(v -> {
|
||||
v.setEnabled(false);
|
||||
handler.delete(getBindingAdapterPosition(), item);
|
||||
});
|
||||
view.setOnClickListener(v -> handler.click(getBindingAdapterPosition(), item));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -201,7 +201,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
protected void setSpoilerAndContent(boolean expanded,
|
||||
@NonNull Spanned content,
|
||||
@Nullable String spoilerText,
|
||||
@Nullable Status.Mention[] mentions,
|
||||
@Nullable List<Status.Mention> mentions,
|
||||
@NonNull List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||
|
|
@ -243,7 +243,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private void setTextVisible(boolean sensitive,
|
||||
boolean expanded,
|
||||
Spanned content,
|
||||
Status.Mention[] mentions,
|
||||
List<Status.Mention> mentions,
|
||||
List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
|
|
@ -651,11 +651,19 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
favouriteButton.setEventListener((button, buttonState) -> {
|
||||
// return true to play animaion
|
||||
int position = getBindingAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onFavourite(!buttonState, position);
|
||||
if (statusDisplayOptions.confirmFavourites()) {
|
||||
showConfirmFavouriteDialog(listener, statusContent, buttonState, position);
|
||||
return false;
|
||||
} else {
|
||||
listener.onFavourite(!buttonState, position);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
bookmarkButton.setEventListener((button, buttonState) -> {
|
||||
|
|
@ -703,26 +711,45 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
.show();
|
||||
}
|
||||
|
||||
private void showConfirmFavouriteDialog(StatusActionListener listener,
|
||||
String statusContent,
|
||||
boolean buttonState,
|
||||
int position) {
|
||||
int okButtonTextId = buttonState ? R.string.action_unfavourite : R.string.action_favourite;
|
||||
new AlertDialog.Builder(favouriteButton.getContext())
|
||||
.setMessage(statusContent)
|
||||
.setPositiveButton(okButtonTextId, (__, ___) -> {
|
||||
listener.onFavourite(!buttonState, position);
|
||||
if (!buttonState) {
|
||||
// Play animation only when it's favourite, not unfavourite
|
||||
favouriteButton.playAnimation();
|
||||
}
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
public void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions) {
|
||||
this.setupWithStatus(status, listener, statusDisplayOptions, null);
|
||||
}
|
||||
|
||||
protected void setupWithStatus(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
public void setupWithStatus(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
if (payloads == null) {
|
||||
setDisplayName(status.getUserFullName(), status.getAccountEmojis(), statusDisplayOptions);
|
||||
setUsername(status.getNickname());
|
||||
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
||||
setIsReply(status.getInReplyToId() != null);
|
||||
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), statusDisplayOptions);
|
||||
setReblogged(status.isReblogged());
|
||||
setFavourited(status.isFavourited());
|
||||
setBookmarked(status.isBookmarked());
|
||||
List<Attachment> attachments = status.getAttachments();
|
||||
boolean sensitive = status.isSensitive();
|
||||
Status actionable = status.getActionable();
|
||||
setDisplayName(actionable.getAccount().getDisplayName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
|
||||
setUsername(status.getUsername());
|
||||
setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
|
||||
setIsReply(actionable.getInReplyToId() != null);
|
||||
setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(),
|
||||
actionable.getAccount().getBot(), statusDisplayOptions);
|
||||
setReblogged(actionable.getReblogged());
|
||||
setFavourited(actionable.getFavourited());
|
||||
setBookmarked(actionable.getBookmarked());
|
||||
List<Attachment> attachments = actionable.getAttachments();
|
||||
boolean sensitive = actionable.getSensitive();
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||
setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash());
|
||||
|
||||
|
|
@ -744,14 +771,17 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
if (cardView != null) {
|
||||
setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions);
|
||||
setupCard(status, statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener);
|
||||
}
|
||||
|
||||
setupButtons(listener, status.getSenderId(), status.getContent().toString(),
|
||||
setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(),
|
||||
statusDisplayOptions);
|
||||
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
|
||||
setRebloggingEnabled(actionable.rebloggingAllowed(), actionable.getVisibility());
|
||||
|
||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), statusDisplayOptions, listener);
|
||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(),
|
||||
actionable.getMentions(), actionable.getEmojis(),
|
||||
PollViewDataKt.toViewData(actionable.getPoll()), statusDisplayOptions,
|
||||
listener);
|
||||
|
||||
setDescriptionForStatus(status, statusDisplayOptions);
|
||||
|
||||
|
|
@ -765,7 +795,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
if (payloads instanceof List)
|
||||
for (Object item : (List<?>) payloads) {
|
||||
if (Key.KEY_CREATED.equals(item)) {
|
||||
setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
|
||||
setCreatedAt(status.getActionable().getCreatedAt(), statusDisplayOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -784,21 +814,22 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status,
|
||||
StatusDisplayOptions statusDisplayOptions) {
|
||||
Context context = itemView.getContext();
|
||||
Status actionable = status.getActionable();
|
||||
|
||||
String description = context.getString(R.string.description_status,
|
||||
status.getUserFullName(),
|
||||
actionable.getAccount().getDisplayName(),
|
||||
getContentWarningDescription(context, status),
|
||||
(TextUtils.isEmpty(status.getSpoilerText()) || !status.isSensitive() || status.isExpanded() ? status.getContent() : ""),
|
||||
getCreatedAtDescription(status.getCreatedAt(), statusDisplayOptions),
|
||||
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
|
||||
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
||||
getReblogDescription(context, status),
|
||||
status.getNickname(),
|
||||
status.isReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
||||
status.isFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
||||
status.isBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
||||
status.getUsername(),
|
||||
actionable.getReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
||||
actionable.getFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
||||
actionable.getBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
||||
getMediaDescription(context, status),
|
||||
getVisibilityDescription(context, status.getVisibility()),
|
||||
getFavsText(context, status.getFavouritesCount()),
|
||||
getReblogsText(context, status.getReblogsCount()),
|
||||
getVisibilityDescription(context, actionable.getVisibility()),
|
||||
getFavsText(context, actionable.getFavouritesCount()),
|
||||
getReblogsText(context, actionable.getReblogsCount()),
|
||||
getPollDescription(status, context, statusDisplayOptions)
|
||||
);
|
||||
itemView.setContentDescription(description);
|
||||
|
|
@ -806,10 +837,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
private static CharSequence getReblogDescription(Context context,
|
||||
@NonNull StatusViewData.Concrete status) {
|
||||
String rebloggedUsername = status.getRebloggedByUsername();
|
||||
if (rebloggedUsername != null) {
|
||||
Status reblog = status.getRebloggingStatus();
|
||||
if (reblog != null) {
|
||||
return context
|
||||
.getString(R.string.status_boosted_format, rebloggedUsername);
|
||||
.getString(R.string.status_boosted_format, reblog.getAccount().getUsername());
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
|
@ -817,11 +848,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
private static CharSequence getMediaDescription(Context context,
|
||||
@NonNull StatusViewData.Concrete status) {
|
||||
if (status.getAttachments().isEmpty()) {
|
||||
if (status.getActionable().getAttachments().isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder mediaDescriptions = CollectionsKt.fold(
|
||||
status.getAttachments(),
|
||||
status.getActionable().getAttachments(),
|
||||
new StringBuilder(),
|
||||
(builder, a) -> {
|
||||
if (a.getDescription() == null) {
|
||||
|
|
@ -874,7 +905,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status,
|
||||
Context context,
|
||||
StatusDisplayOptions statusDisplayOptions) {
|
||||
PollViewData poll = status.getPoll();
|
||||
PollViewData poll = PollViewDataKt.toViewData(status.getActionable().getPoll());
|
||||
if (poll == null) {
|
||||
return "";
|
||||
} else {
|
||||
|
|
@ -883,7 +914,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
for (int i = 0; i < args.length; i++) {
|
||||
if (i < options.size()) {
|
||||
int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), poll.getVotesCount());
|
||||
args[i] = buildDescription(options.get(i).getTitle(), percent, context);
|
||||
args[i] = buildDescription(options.get(i).getTitle(), percent, options.get(i).getVoted(), context);
|
||||
} else {
|
||||
args[i] = "";
|
||||
}
|
||||
|
|
@ -980,7 +1011,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
StatusDisplayOptions statusDisplayOptions,
|
||||
Context context) {
|
||||
String votesText;
|
||||
if(poll.getVotersCount() == null) {
|
||||
if (poll.getVotersCount() == null) {
|
||||
String voters = numberFormat.format(poll.getVotesCount());
|
||||
votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), voters);
|
||||
} else {
|
||||
|
|
@ -1003,13 +1034,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo);
|
||||
}
|
||||
|
||||
protected void setupCard(StatusViewData.Concrete status, CardViewMode cardViewMode, StatusDisplayOptions statusDisplayOptions) {
|
||||
protected void setupCard(
|
||||
StatusViewData.Concrete status,
|
||||
CardViewMode cardViewMode,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
final StatusActionListener listener
|
||||
) {
|
||||
final Card card = status.getActionable().getCard();
|
||||
if (cardViewMode != CardViewMode.NONE &&
|
||||
status.getAttachments().size() == 0 &&
|
||||
status.getCard() != null &&
|
||||
!TextUtils.isEmpty(status.getCard().getUrl()) &&
|
||||
status.getActionable().getAttachments().size() == 0 &&
|
||||
card != null &&
|
||||
!TextUtils.isEmpty(card.getUrl()) &&
|
||||
(!status.isCollapsible() || !status.isCollapsed())) {
|
||||
final Card card = status.getCard();
|
||||
cardView.setVisibility(View.VISIBLE);
|
||||
cardTitle.setText(card.getTitle());
|
||||
if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) {
|
||||
|
|
@ -1028,7 +1064,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
|
||||
// so let's blur the preview in that case
|
||||
// If media previews are disabled, show placeholder for cards as well
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && !status.isSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||
if (statusDisplayOptions.mediaPreviewEnabled() && !status.getActionable().getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||
|
||||
int topLeftRadius = 0;
|
||||
int topRightRadius = 0;
|
||||
|
|
@ -1094,7 +1130,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
cardImage.setImageResource(R.drawable.card_image_placeholder);
|
||||
}
|
||||
|
||||
View.OnClickListener visitLink = v -> LinkHelper.openLink(card.getUrl(), v.getContext());
|
||||
View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl());
|
||||
View.OnClickListener openImage = v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbed_url()));
|
||||
|
||||
cardInfo.setOnClickListener(visitLink);
|
||||
|
|
|
|||
|
|
@ -101,21 +101,22 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void setupWithStatus(final StatusViewData.Concrete status,
|
||||
public void setupWithStatus(final StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
||||
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status
|
||||
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
||||
if (payloads == null) {
|
||||
|
||||
if (!statusDisplayOptions.hideStats()) {
|
||||
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
|
||||
setReblogAndFavCount(status.getActionable().getReblogsCount(),
|
||||
status.getActionable().getFavouritesCount(), listener);
|
||||
} else {
|
||||
hideQuantitativeStats();
|
||||
}
|
||||
|
||||
setApplication(status.getApplication());
|
||||
setApplication(status.getActionable().getApplication());
|
||||
|
||||
View.OnLongClickListener longClickListener = view -> {
|
||||
TextView textView = (TextView) view;
|
||||
|
|
@ -130,7 +131,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
|
||||
content.setOnLongClickListener(longClickListener);
|
||||
contentWarningDescription.setOnLongClickListener(longClickListener);
|
||||
setStatusVisibility(status.getVisibility());
|
||||
setStatusVisibility(status.getActionable().getVisibility());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import androidx.annotation.Nullable;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||
|
|
@ -33,6 +35,8 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
|||
import com.keylesspalace.tusky.util.StringUtils;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import at.connyduck.sparkbutton.helpers.Utils;
|
||||
|
||||
public class StatusViewHolder extends StatusBaseViewHolder {
|
||||
|
|
@ -54,19 +58,21 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void setupWithStatus(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
public void setupWithStatus(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
@Nullable Object payloads) {
|
||||
if (payloads == null) {
|
||||
|
||||
setupCollapsedState(status, listener);
|
||||
|
||||
String rebloggedByDisplayName = status.getRebloggedByUsername();
|
||||
if (rebloggedByDisplayName == null) {
|
||||
Status reblogging = status.getRebloggingStatus();
|
||||
if (reblogging == null) {
|
||||
hideStatusInfo();
|
||||
} else {
|
||||
setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions);
|
||||
String rebloggedByDisplayName = reblogging.getAccount().getDisplayName();
|
||||
setRebloggedByDisplayName(rebloggedByDisplayName,
|
||||
reblogging.getAccount().getEmojis(), statusDisplayOptions);
|
||||
statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition()));
|
||||
}
|
||||
|
||||
|
|
@ -76,13 +82,13 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
private void setRebloggedByDisplayName(final CharSequence name,
|
||||
final StatusViewData.Concrete status,
|
||||
final List<Emoji> accountEmoji,
|
||||
final StatusDisplayOptions statusDisplayOptions) {
|
||||
Context context = statusInfo.getContext();
|
||||
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
||||
CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName);
|
||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||
boostedText, status.getRebloggedByAccountEmojis(), statusInfo, statusDisplayOptions.animateEmojis()
|
||||
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
statusInfo.setText(emojifiedText);
|
||||
statusInfo.setVisibility(View.VISIBLE);
|
||||
|
|
|
|||
|
|
@ -43,10 +43,11 @@ interface ItemInteractionListener {
|
|||
fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int)
|
||||
}
|
||||
|
||||
class TabAdapter(private var data: List<TabData>,
|
||||
private val small: Boolean,
|
||||
private val listener: ItemInteractionListener,
|
||||
private var removeButtonEnabled: Boolean = false
|
||||
class TabAdapter(
|
||||
private var data: List<TabData>,
|
||||
private val small: Boolean,
|
||||
private val listener: ItemInteractionListener,
|
||||
private var removeButtonEnabled: Boolean = false
|
||||
) : RecyclerView.Adapter<BindingHolder<ViewBinding>>() {
|
||||
|
||||
fun updateData(newData: List<TabData>) {
|
||||
|
|
@ -77,7 +78,6 @@ class TabAdapter(private var data: List<TabData>,
|
|||
binding.textView.setOnClickListener {
|
||||
listener.onTabAdded(tab)
|
||||
}
|
||||
|
||||
} else {
|
||||
val binding = holder.binding as ItemTabPreferenceBinding
|
||||
|
||||
|
|
@ -102,9 +102,9 @@ class TabAdapter(private var data: List<TabData>,
|
|||
}
|
||||
binding.removeButton.isEnabled = removeButtonEnabled
|
||||
ThemeUtils.setDrawableTint(
|
||||
holder.itemView.context,
|
||||
binding.removeButton.drawable,
|
||||
(if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled)
|
||||
holder.itemView.context,
|
||||
binding.removeButton.drawable,
|
||||
(if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled)
|
||||
)
|
||||
|
||||
if (tab.id == HASHTAG) {
|
||||
|
|
@ -118,14 +118,14 @@ class TabAdapter(private var data: List<TabData>,
|
|||
tab.arguments.forEachIndexed { i, arg ->
|
||||
|
||||
val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip?
|
||||
?: Chip(context).apply {
|
||||
binding.chipGroup.addView(this, binding.chipGroup.size - 1)
|
||||
chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary))
|
||||
}
|
||||
?: Chip(context).apply {
|
||||
binding.chipGroup.addView(this, binding.chipGroup.size - 1)
|
||||
chipIconTint = ColorStateList.valueOf(ThemeUtils.getColor(context, android.R.attr.textColorPrimary))
|
||||
}
|
||||
|
||||
chip.text = arg
|
||||
|
||||
if(tab.arguments.size <= 1) {
|
||||
if (tab.arguments.size <= 1) {
|
||||
chip.chipIcon = null
|
||||
chip.setOnClickListener(null)
|
||||
} else {
|
||||
|
|
@ -136,14 +136,13 @@ class TabAdapter(private var data: List<TabData>,
|
|||
}
|
||||
}
|
||||
|
||||
while(binding.chipGroup.size - 1 > tab.arguments.size) {
|
||||
while (binding.chipGroup.size - 1 > tab.arguments.size) {
|
||||
binding.chipGroup.removeViewAt(tab.arguments.size)
|
||||
}
|
||||
|
||||
binding.actionChip.setOnClickListener {
|
||||
listener.onActionChipClicked(tab, holder.bindingAdapterPosition)
|
||||
}
|
||||
|
||||
} else {
|
||||
binding.chipGroup.hide()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,164 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ThreadAdapter extends RecyclerView.Adapter {
|
||||
private static final int VIEW_TYPE_STATUS = 0;
|
||||
private static final int VIEW_TYPE_STATUS_DETAILED = 1;
|
||||
|
||||
private List<StatusViewData.Concrete> statuses;
|
||||
private StatusDisplayOptions statusDisplayOptions;
|
||||
private StatusActionListener statusActionListener;
|
||||
private int detailedStatusPosition;
|
||||
|
||||
public ThreadAdapter(StatusDisplayOptions statusDisplayOptions, StatusActionListener listener) {
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
this.statusActionListener = listener;
|
||||
this.statuses = new ArrayList<>();
|
||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_STATUS: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status, parent, false);
|
||||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_STATUS_DETAILED: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_status_detailed, parent, false);
|
||||
return new StatusDetailedViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
StatusViewData.Concrete status = statuses.get(position);
|
||||
if (position == detailedStatusPosition) {
|
||||
StatusDetailedViewHolder holder = (StatusDetailedViewHolder) viewHolder;
|
||||
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
|
||||
} else {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
holder.setupWithStatus(status, statusActionListener, statusDisplayOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == detailedStatusPosition) {
|
||||
return VIEW_TYPE_STATUS_DETAILED;
|
||||
} else {
|
||||
return VIEW_TYPE_STATUS;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return statuses.size();
|
||||
}
|
||||
|
||||
public void setStatuses(List<StatusViewData.Concrete> statuses) {
|
||||
this.statuses.clear();
|
||||
this.statuses.addAll(statuses);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItem(int position, StatusViewData.Concrete statusViewData) {
|
||||
statuses.add(position, statusViewData);
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
|
||||
public void clearItems() {
|
||||
int oldSize = statuses.size();
|
||||
statuses.clear();
|
||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
||||
notifyItemRangeRemoved(0, oldSize);
|
||||
}
|
||||
|
||||
public void addAll(int position, List<StatusViewData.Concrete> statuses) {
|
||||
this.statuses.addAll(position, statuses);
|
||||
notifyItemRangeInserted(position, statuses.size());
|
||||
}
|
||||
|
||||
public void addAll(List<StatusViewData.Concrete> statuses) {
|
||||
int end = statuses.size();
|
||||
this.statuses.addAll(statuses);
|
||||
notifyItemRangeInserted(end, statuses.size());
|
||||
}
|
||||
|
||||
public void removeItem(int position) {
|
||||
statuses.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
statuses.clear();
|
||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setItem(int position, StatusViewData.Concrete status, boolean notifyAdapter) {
|
||||
statuses.set(position, status);
|
||||
if (notifyAdapter) {
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StatusViewData.Concrete getItem(int position) {
|
||||
if (position >= 0 && position < statuses.size()) {
|
||||
return statuses.get(position);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setDetailedStatusPosition(int position) {
|
||||
if (position != detailedStatusPosition
|
||||
&& detailedStatusPosition != RecyclerView.NO_POSITION) {
|
||||
int prior = detailedStatusPosition;
|
||||
detailedStatusPosition = position;
|
||||
notifyItemChanged(prior);
|
||||
} else {
|
||||
detailedStatusPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
public int getDetailedStatusPosition() {
|
||||
return detailedStatusPosition;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
||||
class ThreadAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val statusActionListener: StatusActionListener
|
||||
) : RecyclerView.Adapter<StatusBaseViewHolder>() {
|
||||
private val statuses = mutableListOf<StatusViewData.Concrete>()
|
||||
var detailedStatusPosition: Int = RecyclerView.NO_POSITION
|
||||
private set
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_STATUS -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_status, parent, false)
|
||||
StatusViewHolder(view)
|
||||
}
|
||||
VIEW_TYPE_STATUS_DETAILED -> {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_status_detailed, parent, false)
|
||||
StatusDetailedViewHolder(view)
|
||||
}
|
||||
else -> error("Unknown item type: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) {
|
||||
val status = statuses[position]
|
||||
viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions)
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (position == detailedStatusPosition) {
|
||||
VIEW_TYPE_STATUS_DETAILED
|
||||
} else {
|
||||
VIEW_TYPE_STATUS
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = statuses.size
|
||||
|
||||
fun setStatuses(statuses: List<StatusViewData.Concrete>?) {
|
||||
this.statuses.clear()
|
||||
this.statuses.addAll(statuses!!)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun addItem(position: Int, statusViewData: StatusViewData.Concrete) {
|
||||
statuses.add(position, statusViewData)
|
||||
notifyItemInserted(position)
|
||||
}
|
||||
|
||||
fun clearItems() {
|
||||
val oldSize = statuses.size
|
||||
statuses.clear()
|
||||
detailedStatusPosition = RecyclerView.NO_POSITION
|
||||
notifyItemRangeRemoved(0, oldSize)
|
||||
}
|
||||
|
||||
fun addAll(position: Int, statuses: List<StatusViewData.Concrete>) {
|
||||
this.statuses.addAll(position, statuses)
|
||||
notifyItemRangeInserted(position, statuses.size)
|
||||
}
|
||||
|
||||
fun addAll(statuses: List<StatusViewData.Concrete>) {
|
||||
val end = statuses.size
|
||||
this.statuses.addAll(statuses)
|
||||
notifyItemRangeInserted(end, statuses.size)
|
||||
}
|
||||
|
||||
fun removeItem(position: Int) {
|
||||
statuses.removeAt(position)
|
||||
notifyItemRemoved(position)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
statuses.clear()
|
||||
detailedStatusPosition = RecyclerView.NO_POSITION
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setItem(position: Int, status: StatusViewData.Concrete, notifyAdapter: Boolean) {
|
||||
statuses[position] = status
|
||||
if (notifyAdapter) {
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
fun getItem(position: Int): StatusViewData.Concrete? = statuses.getOrNull(position)
|
||||
|
||||
fun setDetailedStatusPosition(position: Int) {
|
||||
if (position != detailedStatusPosition &&
|
||||
detailedStatusPosition != RecyclerView.NO_POSITION
|
||||
) {
|
||||
val prior = detailedStatusPosition
|
||||
detailedStatusPosition = position
|
||||
notifyItemChanged(prior)
|
||||
} else {
|
||||
detailedStatusPosition = position
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_STATUS = 0
|
||||
private const val VIEW_TYPE_STATUS_DETAILED = 1
|
||||
}
|
||||
}
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class TimelineAdapter extends RecyclerView.Adapter {
|
||||
|
||||
public interface AdapterDataSource<T> {
|
||||
int getItemCount();
|
||||
|
||||
T getItemAt(int pos);
|
||||
}
|
||||
|
||||
private static final int VIEW_TYPE_STATUS = 0;
|
||||
private static final int VIEW_TYPE_PLACEHOLDER = 2;
|
||||
|
||||
private final AdapterDataSource<StatusViewData> dataSource;
|
||||
private StatusDisplayOptions statusDisplayOptions;
|
||||
private final StatusActionListener statusListener;
|
||||
|
||||
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
|
||||
StatusDisplayOptions statusDisplayOptions,
|
||||
StatusActionListener statusListener) {
|
||||
this.dataSource = dataSource;
|
||||
this.statusDisplayOptions = statusDisplayOptions;
|
||||
this.statusListener = statusListener;
|
||||
}
|
||||
|
||||
public boolean getMediaPreviewEnabled() {
|
||||
return statusDisplayOptions.mediaPreviewEnabled();
|
||||
}
|
||||
|
||||
public void setMediaPreviewEnabled(boolean mediaPreviewEnabled) {
|
||||
this.statusDisplayOptions = statusDisplayOptions.copy(
|
||||
statusDisplayOptions.animateAvatars(),
|
||||
mediaPreviewEnabled,
|
||||
statusDisplayOptions.useAbsoluteTime(),
|
||||
statusDisplayOptions.showBotOverlay(),
|
||||
statusDisplayOptions.useBlurhash(),
|
||||
statusDisplayOptions.cardViewMode(),
|
||||
statusDisplayOptions.confirmReblogs(),
|
||||
statusDisplayOptions.hideStats(),
|
||||
statusDisplayOptions.animateEmojis()
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_STATUS: {
|
||||
View view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_status, viewGroup, false);
|
||||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_PLACEHOLDER: {
|
||||
View view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_status_placeholder, viewGroup, false);
|
||||
return new PlaceholderViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||
bindViewHolder(viewHolder, position, null);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @NonNull List payloads) {
|
||||
bindViewHolder(viewHolder, position, payloads);
|
||||
}
|
||||
|
||||
private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position, @Nullable List payloads) {
|
||||
StatusViewData status = dataSource.getItemAt(position);
|
||||
if (status instanceof StatusViewData.Placeholder) {
|
||||
PlaceholderViewHolder holder = (PlaceholderViewHolder) viewHolder;
|
||||
holder.setup(statusListener, ((StatusViewData.Placeholder) status).isLoading());
|
||||
} else if (status instanceof StatusViewData.Concrete) {
|
||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||
holder.setupWithStatus((StatusViewData.Concrete) status,
|
||||
statusListener,
|
||||
statusDisplayOptions,
|
||||
payloads != null && !payloads.isEmpty() ? payloads.get(0) : null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return dataSource.getItemCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (dataSource.getItemAt(position) instanceof StatusViewData.Placeholder) {
|
||||
return VIEW_TYPE_PLACEHOLDER;
|
||||
} else {
|
||||
return VIEW_TYPE_STATUS;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return dataSource.getItemAt(position).getViewDataId();
|
||||
}
|
||||
}
|
||||
|
|
@ -3,22 +3,23 @@ package com.keylesspalace.tusky.appstore
|
|||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
|
||||
class CacheUpdater @Inject constructor(
|
||||
eventHub: EventHub,
|
||||
accountManager: AccountManager,
|
||||
private val appDatabase: AppDatabase,
|
||||
gson: Gson
|
||||
eventHub: EventHub,
|
||||
private val accountManager: AccountManager,
|
||||
private val appDatabase: AppDatabase,
|
||||
gson: Gson
|
||||
) {
|
||||
|
||||
private val disposable: Disposable
|
||||
|
||||
init {
|
||||
val timelineDao = appDatabase.timelineDao()
|
||||
|
||||
disposable = eventHub.events.subscribe { event ->
|
||||
val accountId = accountManager.activeAccount?.id ?: return@subscribe
|
||||
when (event) {
|
||||
|
|
@ -27,7 +28,7 @@ class CacheUpdater @Inject constructor(
|
|||
is ReblogEvent ->
|
||||
timelineDao.setReblogged(accountId, event.statusId, event.reblog)
|
||||
is BookmarkEvent ->
|
||||
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark )
|
||||
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark)
|
||||
is UnfollowEvent ->
|
||||
timelineDao.removeAllByUser(accountId, event.accountId)
|
||||
is StatusDeletedEvent ->
|
||||
|
|
@ -36,6 +37,8 @@ class CacheUpdater @Inject constructor(
|
|||
val pollString = gson.toJson(event.poll)
|
||||
timelineDao.setVoted(accountId, event.statusId, pollString)
|
||||
}
|
||||
is PinEvent ->
|
||||
timelineDao.setPinned(accountId, event.statusId, event.pinned)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +52,7 @@ class CacheUpdater @Inject constructor(
|
|||
appDatabase.timelineDao().removeAllForAccount(accountId)
|
||||
appDatabase.timelineDao().removeAllUsersForAccount(accountId)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,5 +19,6 @@ data class ProfileEditedEvent(val newProfileData: Account) : Dispatchable
|
|||
data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
||||
data class AnnouncementReadEvent(val announcementId: String): Dispatchable
|
||||
data class DomainMuteEvent(val instance: String) : Dispatchable
|
||||
data class AnnouncementReadEvent(val announcementId: String) : Dispatchable
|
||||
data class PinEvent(val statusId: String, val pinned: Boolean) : Dispatchable
|
||||
|
|
|
|||
|
|
@ -1,22 +1,20 @@
|
|||
package com.keylesspalace.tusky.appstore
|
||||
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
interface Event
|
||||
interface Dispatchable : Event
|
||||
|
||||
interface EventHub {
|
||||
val events: Observable<Event>
|
||||
fun dispatch(event: Dispatchable)
|
||||
}
|
||||
|
||||
object EventHubImpl : EventHub {
|
||||
@Singleton
|
||||
class EventHub @Inject constructor() {
|
||||
|
||||
private val eventsSubject = PublishSubject.create<Event>()
|
||||
override val events: Observable<Event> = eventsSubject
|
||||
val events: Observable<Event> = eventsSubject
|
||||
|
||||
override fun dispatch(event: Dispatchable) {
|
||||
fun dispatch(event: Dispatchable) {
|
||||
eventsSubject.onNext(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky
|
||||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.content.Context
|
||||
|
|
@ -34,20 +34,29 @@ import androidx.annotation.Px
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.emoji.text.EmojiCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.viewpager2.widget.MarginPageTransformer
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.keylesspalace.tusky.adapter.AccountFieldAdapter
|
||||
import com.keylesspalace.tusky.AccountListActivity
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.EditProfileActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
|
||||
|
|
@ -57,11 +66,20 @@ import com.keylesspalace.tusky.entity.Relationship
|
|||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||
import com.keylesspalace.tusky.pager.AccountPagerAdapter
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.DefaultTextWatcher
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import java.text.NumberFormat
|
||||
|
|
@ -79,7 +97,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate)
|
||||
|
||||
private lateinit var accountFieldAdapter : AccountFieldAdapter
|
||||
private lateinit var accountFieldAdapter: AccountFieldAdapter
|
||||
|
||||
private var followState: FollowState = FollowState.NOT_FOLLOWING
|
||||
private var blocking: Boolean = false
|
||||
|
|
@ -129,6 +147,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
hideFab = sharedPrefs.getBoolean("fabHide", false)
|
||||
|
||||
handleWindowInsets()
|
||||
setupToolbar()
|
||||
setupTabs()
|
||||
setupAccountViews()
|
||||
|
|
@ -170,7 +189,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
binding.accountFieldList.layoutManager = LinearLayoutManager(this)
|
||||
binding.accountFieldList.adapter = accountFieldAdapter
|
||||
|
||||
|
||||
val accountListClickListener = { v: View ->
|
||||
val type = when (v.id) {
|
||||
R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS
|
||||
|
|
@ -231,21 +249,25 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
// set toolbar top margin according to system window insets
|
||||
binding.accountCoordinatorLayout.setOnApplyWindowInsetsListener { _, insets ->
|
||||
val top = insets.systemWindowInsetTop
|
||||
|
||||
val toolbarParams = binding.accountToolbar.layoutParams as CollapsingToolbarLayout.LayoutParams
|
||||
private fun handleWindowInsets() {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.accountCoordinatorLayout) { _, insets ->
|
||||
val top = insets.getInsets(systemBars()).top
|
||||
val toolbarParams = binding.accountToolbar.layoutParams as ViewGroup.MarginLayoutParams
|
||||
toolbarParams.topMargin = top
|
||||
|
||||
insets.consumeSystemWindowInsets()
|
||||
}
|
||||
val right = insets.getInsets(systemBars()).right
|
||||
val bottom = insets.getInsets(systemBars()).bottom
|
||||
val left = insets.getInsets(systemBars()).left
|
||||
binding.accountCoordinatorLayout.updatePadding(right = right, bottom = bottom, left = left)
|
||||
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
// Setup the toolbar.
|
||||
setSupportActionBar(binding.accountToolbar)
|
||||
supportActionBar?.run {
|
||||
|
|
@ -266,8 +288,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
fillColor = ColorStateList.valueOf(toolbarColor)
|
||||
elevation = appBarElevation
|
||||
shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius))
|
||||
.build()
|
||||
.setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius))
|
||||
.build()
|
||||
}
|
||||
binding.accountAvatarImageView.background = avatarBackground
|
||||
|
||||
|
|
@ -314,12 +336,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
private fun makeNotificationBarTransparent() {
|
||||
val decorView = window.decorView
|
||||
decorView.systemUiVisibility = decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.statusBarColor = statusBarColorTransparent
|
||||
}
|
||||
|
||||
|
|
@ -332,9 +352,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
is Success -> onAccountChanged(it.data)
|
||||
is Error -> {
|
||||
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
}
|
||||
is Loading -> { }
|
||||
}
|
||||
}
|
||||
viewModel.relationshipData.observe(this) {
|
||||
|
|
@ -345,15 +366,17 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
if (it is Error) {
|
||||
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
}
|
||||
|
||||
}
|
||||
viewModel.accountFieldData.observe(this, {
|
||||
accountFieldAdapter.fields = it
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
})
|
||||
viewModel.accountFieldData.observe(
|
||||
this,
|
||||
{
|
||||
accountFieldAdapter.fields = it
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
}
|
||||
)
|
||||
viewModel.noteSaved.observe(this) {
|
||||
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
||||
}
|
||||
|
|
@ -367,9 +390,12 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
viewModel.refresh()
|
||||
adapter.refreshContent()
|
||||
}
|
||||
viewModel.isRefreshing.observe(this, { isRefreshing ->
|
||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||
})
|
||||
viewModel.isRefreshing.observe(
|
||||
this,
|
||||
{ isRefreshing ->
|
||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||
}
|
||||
)
|
||||
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
||||
}
|
||||
|
||||
|
|
@ -383,7 +409,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||
LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this)
|
||||
|
||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
|
||||
|
|
@ -410,21 +436,21 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
loadedAccount?.let { account ->
|
||||
|
||||
loadAvatar(
|
||||
account.avatar,
|
||||
binding.accountAvatarImageView,
|
||||
resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp),
|
||||
animateAvatar
|
||||
account.avatar,
|
||||
binding.accountAvatarImageView,
|
||||
resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp),
|
||||
animateAvatar
|
||||
)
|
||||
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(account.header)
|
||||
.centerCrop()
|
||||
.into(binding.accountHeaderImageView)
|
||||
|
||||
.asBitmap()
|
||||
.load(account.header)
|
||||
.centerCrop()
|
||||
.into(binding.accountHeaderImageView)
|
||||
|
||||
binding.accountAvatarImageView.setOnClickListener { avatarView ->
|
||||
val intent = ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||
val intent =
|
||||
ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||
|
||||
avatarView.transitionName = account.avatar
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar)
|
||||
|
|
@ -479,7 +505,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -555,15 +580,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
// because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field
|
||||
// it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call
|
||||
if(!viewModel.isSelf && followState == FollowState.FOLLOWING
|
||||
&& (relation.subscribing != null || relation.notifying != null)) {
|
||||
if (!viewModel.isSelf && followState == FollowState.FOLLOWING &&
|
||||
(relation.subscribing != null || relation.notifying != null)
|
||||
) {
|
||||
binding.accountSubscribeButton.show()
|
||||
binding.accountSubscribeButton.setOnClickListener {
|
||||
viewModel.changeSubscribingState()
|
||||
}
|
||||
if(relation.notifying != null)
|
||||
if (relation.notifying != null)
|
||||
subscribing = relation.notifying
|
||||
else if(relation.subscribing != null)
|
||||
else if (relation.subscribing != null)
|
||||
subscribing = relation.subscribing
|
||||
}
|
||||
|
||||
|
|
@ -578,7 +604,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
updateButtons()
|
||||
}
|
||||
|
||||
private val noteWatcher = object: DefaultTextWatcher() {
|
||||
private val noteWatcher = object : DefaultTextWatcher() {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
viewModel.noteChanged(s.toString())
|
||||
}
|
||||
|
|
@ -616,11 +642,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
}
|
||||
|
||||
private fun updateSubscribeButton() {
|
||||
if(followState != FollowState.FOLLOWING) {
|
||||
if (followState != FollowState.FOLLOWING) {
|
||||
binding.accountSubscribeButton.hide()
|
||||
}
|
||||
|
||||
if(subscribing) {
|
||||
if (subscribing) {
|
||||
binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp)
|
||||
binding.accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account)
|
||||
} else {
|
||||
|
|
@ -649,7 +675,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
binding.accountMuteButton.hide()
|
||||
updateMuteButton()
|
||||
}
|
||||
|
||||
} else {
|
||||
binding.accountFloatingActionButton.hide()
|
||||
binding.accountFollowButton.hide()
|
||||
|
|
@ -699,11 +724,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
} else {
|
||||
getString(R.string.action_show_reblogs)
|
||||
}
|
||||
|
||||
} else {
|
||||
menu.removeItem(R.id.action_show_reblogs)
|
||||
}
|
||||
|
||||
} else {
|
||||
// It shouldn't be possible to block, mute or report yourself.
|
||||
menu.removeItem(R.id.action_block)
|
||||
|
|
@ -718,39 +741,39 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
private fun showFollowRequestPendingDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.dialog_message_cancel_follow_request)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setMessage(R.string.dialog_message_cancel_follow_request)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showUnfollowWarningDialog() {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.dialog_unfollow_warning)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setMessage(R.string.dialog_unfollow_warning)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun toggleBlockDomain(instance: String) {
|
||||
if(blockingDomain) {
|
||||
if (blockingDomain) {
|
||||
viewModel.unblockDomain(instance)
|
||||
} else {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.mute_domain_warning, instance))
|
||||
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setMessage(getString(R.string.mute_domain_warning, instance))
|
||||
.setPositiveButton(getString(R.string.mute_domain_warning_dialog_ok)) { _, _ -> viewModel.blockDomain(instance) }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleBlock() {
|
||||
if (viewModel.relationshipData.value?.data?.blocking != true) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username))
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
} else {
|
||||
viewModel.changeBlockState()
|
||||
}
|
||||
|
|
@ -760,8 +783,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
if (viewModel.relationshipData.value?.data?.muting != true) {
|
||||
loadedAccount?.let {
|
||||
showMuteAccountDialog(
|
||||
this,
|
||||
it.username
|
||||
this,
|
||||
it.username
|
||||
) { notifications, duration ->
|
||||
viewModel.muteAccount(notifications, duration)
|
||||
}
|
||||
|
|
@ -773,8 +796,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
private fun mention() {
|
||||
loadedAccount?.let {
|
||||
val intent = ComposeActivity.startIntent(this,
|
||||
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username)))
|
||||
val intent = ComposeActivity.startIntent(
|
||||
this,
|
||||
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
|
@ -850,5 +875,4 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -13,23 +13,26 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Field
|
||||
import com.keylesspalace.tusky.entity.IdentityProof
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
|
||||
class AccountFieldAdapter(
|
||||
private val linkListener: LinkListener,
|
||||
private val animateEmojis: Boolean
|
||||
private val linkListener: LinkListener,
|
||||
private val animateEmojis: Boolean
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemAccountFieldBinding>>() {
|
||||
|
||||
var emojis: List<Emoji> = emptyList()
|
||||
|
|
@ -47,7 +50,7 @@ class AccountFieldAdapter(
|
|||
val nameTextView = holder.binding.accountFieldName
|
||||
val valueTextView = holder.binding.accountFieldValue
|
||||
|
||||
if(proofOrField.isLeft()) {
|
||||
if (proofOrField.isLeft()) {
|
||||
val identityProof = proofOrField.asLeft()
|
||||
|
||||
nameTextView.text = identityProof.provider
|
||||
|
|
@ -55,7 +58,7 @@ class AccountFieldAdapter(
|
|||
|
||||
valueTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||
} else {
|
||||
val field = proofOrField.asRight()
|
||||
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
||||
|
|
@ -64,12 +67,11 @@ class AccountFieldAdapter(
|
|||
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
||||
LinkHelper.setClickableText(valueTextView, emojifiedValue, null, linkListener)
|
||||
|
||||
if(field.verifiedAt != null) {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||
if (field.verifiedAt != null) {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||
} else {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 )
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -13,28 +13,28 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.pager
|
||||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import androidx.fragment.app.*
|
||||
|
||||
import com.keylesspalace.tusky.fragment.AccountMediaFragment
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.keylesspalace.tusky.components.account.media.AccountMediaFragment
|
||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel
|
||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||
|
||||
import com.keylesspalace.tusky.util.CustomFragmentStateAdapter
|
||||
|
||||
class AccountPagerAdapter(
|
||||
activity: FragmentActivity,
|
||||
private val accountId: String
|
||||
activity: FragmentActivity,
|
||||
private val accountId: String
|
||||
) : CustomFragmentStateAdapter(activity) {
|
||||
|
||||
override fun getItemCount() = TAB_COUNT
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId, false)
|
||||
1 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_WITH_REPLIES, accountId, false)
|
||||
2 -> TimelineFragment.newInstance(TimelineFragment.Kind.USER_PINNED, accountId, false)
|
||||
0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false)
|
||||
1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false)
|
||||
2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false)
|
||||
3 -> AccountMediaFragment.newInstance(accountId, false)
|
||||
else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds")
|
||||
}
|
||||
|
|
@ -1,17 +1,28 @@
|
|||
package com.keylesspalace.tusky.viewmodel
|
||||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.keylesspalace.tusky.appstore.*
|
||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||
import com.keylesspalace.tusky.appstore.DomainMuteEvent
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Field
|
||||
import com.keylesspalace.tusky.entity.IdentityProof
|
||||
import com.keylesspalace.tusky.entity.Relationship
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
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 com.keylesspalace.tusky.util.combineOptionalLiveData
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
|
@ -19,9 +30,9 @@ import java.util.concurrent.TimeUnit
|
|||
import javax.inject.Inject
|
||||
|
||||
class AccountViewModel @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub,
|
||||
private val accountManager: AccountManager
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub,
|
||||
private val accountManager: AccountManager
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
val accountData = MutableLiveData<Resource<Account>>()
|
||||
|
|
@ -33,7 +44,7 @@ class AccountViewModel @Inject constructor(
|
|||
|
||||
val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs ->
|
||||
identityProofs.orEmpty().map { Either.Left<IdentityProof, Field>(it) }
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) })
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) })
|
||||
}
|
||||
|
||||
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
|
||||
|
|
@ -46,11 +57,11 @@ class AccountViewModel @Inject constructor(
|
|||
|
||||
init {
|
||||
eventHub.events
|
||||
.subscribe { event ->
|
||||
if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) {
|
||||
accountData.postValue(Success(event.newProfileData))
|
||||
}
|
||||
}.autoDispose()
|
||||
.subscribe { event ->
|
||||
if (event is ProfileEditedEvent && event.newProfileData.id == accountData.value?.data?.id) {
|
||||
accountData.postValue(Success(event.newProfileData))
|
||||
}
|
||||
}.autoDispose()
|
||||
}
|
||||
|
||||
private fun obtainAccount(reload: Boolean = false) {
|
||||
|
|
@ -59,17 +70,20 @@ class AccountViewModel @Inject constructor(
|
|||
accountData.postValue(Loading())
|
||||
|
||||
mastodonApi.account(accountId)
|
||||
.subscribe({ account ->
|
||||
.subscribe(
|
||||
{ account ->
|
||||
accountData.postValue(Success(account))
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
}, {t ->
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining account", t)
|
||||
accountData.postValue(Error())
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,13 +93,16 @@ class AccountViewModel @Inject constructor(
|
|||
relationshipData.postValue(Loading())
|
||||
|
||||
mastodonApi.relationships(listOf(accountId))
|
||||
.subscribe({ relationships ->
|
||||
.subscribe(
|
||||
{ relationships ->
|
||||
relationshipData.postValue(Success(relationships[0]))
|
||||
}, { t ->
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining relationships", t)
|
||||
relationshipData.postValue(Error())
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -93,12 +110,15 @@ class AccountViewModel @Inject constructor(
|
|||
if (identityProofData.value == null || reload) {
|
||||
|
||||
mastodonApi.identityProofs(accountId)
|
||||
.subscribe({ proofs ->
|
||||
.subscribe(
|
||||
{ proofs ->
|
||||
identityProofData.postValue(proofs)
|
||||
}, { t ->
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining identity proofs", t)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -126,11 +146,12 @@ class AccountViewModel @Inject constructor(
|
|||
fun unmuteAccount() {
|
||||
changeRelationship(RelationShipAction.UNMUTE)
|
||||
}
|
||||
|
||||
|
||||
fun changeSubscribingState() {
|
||||
val relationship = relationshipData.value?.data
|
||||
if(relationship?.notifying == true /* Mastodon 3.3.0rc1 */
|
||||
|| relationship?.subscribing == true /* Pleroma */ ) {
|
||||
if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */
|
||||
relationship?.subscribing == true /* Pleroma */
|
||||
) {
|
||||
changeRelationship(RelationShipAction.UNSUBSCRIBE)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.SUBSCRIBE)
|
||||
|
|
@ -138,12 +159,12 @@ class AccountViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun blockDomain(instance: String) {
|
||||
mastodonApi.blockDomain(instance).enqueue(object: Callback<Any> {
|
||||
mastodonApi.blockDomain(instance).enqueue(object : Callback<Any> {
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
if (response.isSuccessful) {
|
||||
eventHub.dispatch(DomainMuteEvent(instance))
|
||||
val relation = relationshipData.value?.data
|
||||
if(relation != null) {
|
||||
if (relation != null) {
|
||||
relationshipData.postValue(Success(relation.copy(blockingDomain = true)))
|
||||
}
|
||||
} else {
|
||||
|
|
@ -158,11 +179,11 @@ class AccountViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun unblockDomain(instance: String) {
|
||||
mastodonApi.unblockDomain(instance).enqueue(object: Callback<Any> {
|
||||
mastodonApi.unblockDomain(instance).enqueue(object : Callback<Any> {
|
||||
override fun onResponse(call: Call<Any>, response: Response<Any>) {
|
||||
if (response.isSuccessful) {
|
||||
val relation = relationshipData.value?.data
|
||||
if(relation != null) {
|
||||
if (relation != null) {
|
||||
relationshipData.postValue(Success(relation.copy(blockingDomain = false)))
|
||||
}
|
||||
} else {
|
||||
|
|
@ -209,12 +230,12 @@ class AccountViewModel @Inject constructor(
|
|||
RelationShipAction.MUTE -> relation.copy(muting = true)
|
||||
RelationShipAction.UNMUTE -> relation.copy(muting = false)
|
||||
RelationShipAction.SUBSCRIBE -> {
|
||||
if(isMastodon)
|
||||
if (isMastodon)
|
||||
relation.copy(notifying = true)
|
||||
else relation.copy(subscribing = true)
|
||||
}
|
||||
RelationShipAction.UNSUBSCRIBE -> {
|
||||
if(isMastodon)
|
||||
if (isMastodon)
|
||||
relation.copy(notifying = false)
|
||||
else relation.copy(subscribing = false)
|
||||
}
|
||||
|
|
@ -230,50 +251,53 @@ class AccountViewModel @Inject constructor(
|
|||
RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true, duration)
|
||||
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
|
||||
RelationShipAction.SUBSCRIBE -> {
|
||||
if(isMastodon)
|
||||
if (isMastodon)
|
||||
mastodonApi.followAccount(accountId, notify = true)
|
||||
else mastodonApi.subscribeAccount(accountId)
|
||||
}
|
||||
RelationShipAction.UNSUBSCRIBE -> {
|
||||
if(isMastodon)
|
||||
if (isMastodon)
|
||||
mastodonApi.followAccount(accountId, notify = false)
|
||||
else mastodonApi.unsubscribeAccount(accountId)
|
||||
}
|
||||
}.subscribe(
|
||||
{ relationship ->
|
||||
relationshipData.postValue(Success(relationship))
|
||||
{ relationship ->
|
||||
relationshipData.postValue(Success(relationship))
|
||||
|
||||
when (relationshipAction) {
|
||||
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId))
|
||||
RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId))
|
||||
RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId))
|
||||
else -> {
|
||||
}
|
||||
when (relationshipAction) {
|
||||
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId))
|
||||
RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId))
|
||||
RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId))
|
||||
else -> {
|
||||
}
|
||||
},
|
||||
{
|
||||
relationshipData.postValue(Error(relation))
|
||||
}
|
||||
},
|
||||
{
|
||||
relationshipData.postValue(Error(relation))
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun noteChanged(newNote: String) {
|
||||
noteSaved.postValue(false)
|
||||
noteDisposable?.dispose()
|
||||
noteDisposable = Single.timer(1500, TimeUnit.MILLISECONDS)
|
||||
.flatMap {
|
||||
mastodonApi.updateAccountNote(accountId, newNote)
|
||||
}
|
||||
.doOnSuccess {
|
||||
noteSaved.postValue(true)
|
||||
}
|
||||
.delay(4, TimeUnit.SECONDS)
|
||||
.subscribe({
|
||||
.flatMap {
|
||||
mastodonApi.updateAccountNote(accountId, newNote)
|
||||
}
|
||||
.doOnSuccess {
|
||||
noteSaved.postValue(true)
|
||||
}
|
||||
.delay(4, TimeUnit.SECONDS)
|
||||
.subscribe(
|
||||
{
|
||||
noteSaved.postValue(false)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Log.e(TAG, "Error updating note", it)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment
|
||||
package com.keylesspalace.tusky.components.account.media
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
|
|
@ -27,6 +27,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import autodispose2.androidx.lifecycle.autoDispose
|
||||
import com.bumptech.glide.Glide
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
|
|
@ -43,13 +44,12 @@ import com.keylesspalace.tusky.util.show
|
|||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.view.SquareImageView
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
||||
import io.reactivex.SingleObserver
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.SingleObserver
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Random
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
|
@ -156,18 +156,17 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true
|
||||
accountId = arguments?.getString(ACCOUNT_ID_ARG)!!
|
||||
isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) == true
|
||||
accountId = arguments?.getString(ACCOUNT_ID_ARG)!!
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
|
||||
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
||||
val layoutManager = GridLayoutManager(view.context, columnCount)
|
||||
|
||||
adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground)
|
||||
adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground)
|
||||
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
|
@ -188,12 +187,12 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
val lastItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||
if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) {
|
||||
statuses.lastOrNull()?.let { (id) ->
|
||||
Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)")
|
||||
Log.d(TAG, "Requesting statuses with max_id: $id, (bottom)")
|
||||
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
|
||||
api.accountStatuses(accountId, id, null, null, null, true, null)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(bottomCallback)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(bottomCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -213,8 +212,8 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
fetchingStatus = FetchingStatus.REFRESHING
|
||||
api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null)
|
||||
}.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(callback)
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(callback)
|
||||
|
||||
if (!isSwipeToRefreshEnabled)
|
||||
binding.topProgressBar.show()
|
||||
|
|
@ -227,11 +226,10 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
|
||||
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
||||
api.accountStatuses(accountId, null, null, null, null, true, null)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(callback)
|
||||
}
|
||||
else if (needToRefresh)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe(callback)
|
||||
} else if (needToRefresh)
|
||||
refresh()
|
||||
needToRefresh = false
|
||||
}
|
||||
|
|
@ -264,7 +262,7 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
}
|
||||
|
||||
inner class MediaGridAdapter :
|
||||
RecyclerView.Adapter<MediaGridAdapter.MediaViewHolder>() {
|
||||
RecyclerView.Adapter<MediaGridAdapter.MediaViewHolder>() {
|
||||
|
||||
var baseItemColor = Color.BLACK
|
||||
|
||||
|
|
@ -305,15 +303,14 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
val item = items[position]
|
||||
|
||||
Glide.with(holder.imageView)
|
||||
.load(item.attachment.previewUrl)
|
||||
.centerInside()
|
||||
.into(holder.imageView)
|
||||
.load(item.attachment.previewUrl)
|
||||
.centerInside()
|
||||
.into(holder.imageView)
|
||||
}
|
||||
|
||||
|
||||
inner class MediaViewHolder(val imageView: ImageView)
|
||||
: RecyclerView.ViewHolder(imageView),
|
||||
View.OnClickListener {
|
||||
inner class MediaViewHolder(val imageView: ImageView) :
|
||||
RecyclerView.ViewHolder(imageView),
|
||||
View.OnClickListener {
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
|
|
@ -334,11 +331,11 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
|
||||
fun newInstance(accountId: String, enableSwipeToRefresh: Boolean = true): AccountMediaFragment {
|
||||
val fragment = AccountMediaFragment()
|
||||
val args = Bundle()
|
||||
args.putString(ACCOUNT_ID_ARG, accountId)
|
||||
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh)
|
||||
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
|
|
@ -347,4 +344,4 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr
|
|||
private const val TAG = "AccountMediaFragment"
|
||||
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,33 +15,37 @@
|
|||
|
||||
package com.keylesspalace.tusky.components.announcements
|
||||
|
||||
import android.os.Build
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.ContextThemeWrapper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.size
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding
|
||||
import com.keylesspalace.tusky.entity.Announcement
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.EmojiSpan
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
interface AnnouncementActionListener: LinkListener {
|
||||
interface AnnouncementActionListener : LinkListener {
|
||||
fun openReactionPicker(announcementId: String, target: View)
|
||||
fun addReaction(announcementId: String, name: String)
|
||||
fun removeReaction(announcementId: String, name: String)
|
||||
}
|
||||
|
||||
class AnnouncementAdapter(
|
||||
private var items: List<Announcement> = emptyList(),
|
||||
private val listener: AnnouncementActionListener,
|
||||
private val wellbeingEnabled: Boolean = false,
|
||||
private val animateEmojis: Boolean = false
|
||||
private var items: List<Announcement> = emptyList(),
|
||||
private val listener: AnnouncementActionListener,
|
||||
private val wellbeingEnabled: Boolean = false,
|
||||
private val animateEmojis: Boolean = false
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemAnnouncementBinding>>() {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAnnouncementBinding> {
|
||||
|
|
@ -56,7 +60,9 @@ class AnnouncementAdapter(
|
|||
val chips = holder.binding.chipGroup
|
||||
val addReactionChip = holder.binding.addReactionChip
|
||||
|
||||
LinkHelper.setClickableText(text, item.content, null, listener)
|
||||
val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis)
|
||||
|
||||
LinkHelper.setClickableText(text, emojifiedText, item.mentions, listener)
|
||||
|
||||
// If wellbeing mode is enabled, announcement badge counts should not be shown.
|
||||
if (wellbeingEnabled) {
|
||||
|
|
@ -67,40 +73,43 @@ class AnnouncementAdapter(
|
|||
}
|
||||
|
||||
item.reactions.forEachIndexed { i, reaction ->
|
||||
(chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
||||
(
|
||||
chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip?
|
||||
?: Chip(ContextThemeWrapper(chips.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply {
|
||||
isCheckable = true
|
||||
checkedIcon = null
|
||||
chips.addView(this, i)
|
||||
})
|
||||
.apply {
|
||||
val emojiText = if (reaction.url == null) {
|
||||
reaction.name
|
||||
} else {
|
||||
context.getString(R.string.emoji_shortcode_format, reaction.name)
|
||||
}
|
||||
)
|
||||
.apply {
|
||||
if (reaction.url == null) {
|
||||
this.text = "${reaction.name} ${reaction.count}"
|
||||
} else {
|
||||
// we set the EmojiSpan on a space, because otherwise the Chip won't have the right size
|
||||
// https://github.com/tuskyapp/Tusky/issues/2308
|
||||
val spanBuilder = SpannableStringBuilder(" ${reaction.count}")
|
||||
val span = EmojiSpan(WeakReference(this))
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
span.contentDescription = reaction.name
|
||||
}
|
||||
this.text = ("$emojiText ${reaction.count}")
|
||||
.emojify(
|
||||
listOf(Emoji(
|
||||
reaction.name,
|
||||
reaction.url ?: "",
|
||||
reaction.staticUrl ?: "",
|
||||
null
|
||||
)),
|
||||
this,
|
||||
animateEmojis
|
||||
)
|
||||
spanBuilder.setSpan(span, 0, 1, 0)
|
||||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(if (animateEmojis) { reaction.url } else { reaction.staticUrl })
|
||||
.into(span.getTarget(animateEmojis))
|
||||
this.text = spanBuilder
|
||||
}
|
||||
|
||||
isChecked = reaction.me
|
||||
isChecked = reaction.me
|
||||
|
||||
setOnClickListener {
|
||||
if (reaction.me) {
|
||||
listener.removeReaction(item.id, reaction.name)
|
||||
} else {
|
||||
listener.addReaction(item.id, reaction.name)
|
||||
}
|
||||
setOnClickListener {
|
||||
if (reaction.me) {
|
||||
listener.removeReaction(item.id, reaction.name)
|
||||
} else {
|
||||
listener.addReaction(item.id, reaction.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (chips.size - 1 > item.reactions.size) {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,12 @@ import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding
|
|||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.view.EmojiPicker
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -52,13 +57,13 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener,
|
|||
private val picker by lazy { EmojiPicker(this) }
|
||||
private val pickerDialog by lazy {
|
||||
PopupWindow(this)
|
||||
.apply {
|
||||
contentView = picker
|
||||
isFocusable = true
|
||||
setOnDismissListener {
|
||||
currentAnnouncementId = null
|
||||
}
|
||||
.apply {
|
||||
contentView = picker
|
||||
isFocusable = true
|
||||
setOnDismissListener {
|
||||
currentAnnouncementId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
private var currentAnnouncementId: String? = null
|
||||
|
||||
|
|
|
|||
|
|
@ -27,15 +27,20 @@ import com.keylesspalace.tusky.entity.Announcement
|
|||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.rxkotlin.Singles
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import javax.inject.Inject
|
||||
|
||||
class AnnouncementsViewModel @Inject constructor(
|
||||
accountManager: AccountManager,
|
||||
private val appDatabase: AppDatabase,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
accountManager: AccountManager,
|
||||
private val appDatabase: AppDatabase,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val eventHub: EventHub
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
|
||||
|
|
@ -45,140 +50,153 @@ class AnnouncementsViewModel @Inject constructor(
|
|||
val emojis: LiveData<List<Emoji>> = emojisMutable
|
||||
|
||||
init {
|
||||
Singles.zip(
|
||||
mastodonApi.getCustomEmojis(),
|
||||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||
.onErrorResumeNext(
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right(it) }
|
||||
)
|
||||
) { emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
Single.zip(
|
||||
mastodonApi.getCustomEmojis(),
|
||||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
||||
.onErrorResumeNext {
|
||||
mastodonApi.getInstance()
|
||||
.map { Either.Right(it) }
|
||||
},
|
||||
{ emojis, either ->
|
||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
||||
?: InstanceEntity(
|
||||
accountManager.activeAccount?.domain!!,
|
||||
emojis,
|
||||
either.asRight().maxTootChars,
|
||||
either.asRight().pollLimits?.maxOptions,
|
||||
either.asRight().pollLimits?.maxOptionChars,
|
||||
either.asRight().version
|
||||
accountManager.activeAccount?.domain!!,
|
||||
emojis,
|
||||
either.asRight().maxTootChars,
|
||||
either.asRight().pollLimits?.maxOptions,
|
||||
either.asRight().pollLimits?.maxOptionChars,
|
||||
either.asRight().version
|
||||
)
|
||||
}
|
||||
.doOnSuccess {
|
||||
appDatabase.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
.subscribe({
|
||||
emojisMutable.postValue(it.emojiList)
|
||||
}, {
|
||||
}
|
||||
)
|
||||
.doOnSuccess {
|
||||
appDatabase.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
.subscribe(
|
||||
{
|
||||
emojisMutable.postValue(it.emojiList.orEmpty())
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to get custom emojis.", it)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun load() {
|
||||
announcementsMutable.postValue(Loading())
|
||||
mastodonApi.listAnnouncements()
|
||||
.subscribe({
|
||||
.subscribe(
|
||||
{
|
||||
announcementsMutable.postValue(Success(it))
|
||||
it.filter { announcement -> !announcement.read }
|
||||
.forEach { announcement ->
|
||||
mastodonApi.dismissAnnouncement(announcement.id)
|
||||
.subscribe(
|
||||
{
|
||||
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
|
||||
},
|
||||
{ throwable ->
|
||||
Log.d(TAG, "Failed to mark announcement as read.", throwable)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}, {
|
||||
.forEach { announcement ->
|
||||
mastodonApi.dismissAnnouncement(announcement.id)
|
||||
.subscribe(
|
||||
{
|
||||
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
|
||||
},
|
||||
{ throwable ->
|
||||
Log.d(TAG, "Failed to mark announcement as read.", throwable)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
},
|
||||
{
|
||||
announcementsMutable.postValue(Error(cause = it))
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun addReaction(announcementId: String, name: String) {
|
||||
mastodonApi.addAnnouncementReaction(announcementId, name)
|
||||
.subscribe({
|
||||
.subscribe(
|
||||
{
|
||||
announcementsMutable.postValue(
|
||||
Success(
|
||||
announcements.value!!.data!!.map { announcement ->
|
||||
if (announcement.id == announcementId) {
|
||||
announcement.copy(
|
||||
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
|
||||
announcement.reactions.map { reaction ->
|
||||
if (reaction.name == name) {
|
||||
reaction.copy(
|
||||
count = reaction.count + 1,
|
||||
me = true
|
||||
)
|
||||
} else {
|
||||
reaction
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listOf(
|
||||
*announcement.reactions.toTypedArray(),
|
||||
emojis.value!!.find { emoji -> emoji.shortcode == name }
|
||||
!!.run {
|
||||
Announcement.Reaction(
|
||||
name,
|
||||
1,
|
||||
true,
|
||||
url,
|
||||
staticUrl
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
Success(
|
||||
announcements.value!!.data!!.map { announcement ->
|
||||
if (announcement.id == announcementId) {
|
||||
announcement.copy(
|
||||
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
|
||||
announcement.reactions.map { reaction ->
|
||||
if (reaction.name == name) {
|
||||
reaction.copy(
|
||||
count = reaction.count + 1,
|
||||
me = true
|
||||
)
|
||||
} else {
|
||||
reaction
|
||||
}
|
||||
}
|
||||
} else {
|
||||
announcement
|
||||
listOf(
|
||||
*announcement.reactions.toTypedArray(),
|
||||
emojis.value!!.find { emoji -> emoji.shortcode == name }
|
||||
!!.run {
|
||||
Announcement.Reaction(
|
||||
name,
|
||||
1,
|
||||
true,
|
||||
url,
|
||||
staticUrl
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
announcement
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun removeReaction(announcementId: String, name: String) {
|
||||
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
||||
.subscribe({
|
||||
.subscribe(
|
||||
{
|
||||
announcementsMutable.postValue(
|
||||
Success(
|
||||
announcements.value!!.data!!.map { announcement ->
|
||||
if (announcement.id == announcementId) {
|
||||
announcement.copy(
|
||||
reactions = announcement.reactions.mapNotNull { reaction ->
|
||||
if (reaction.name == name) {
|
||||
if (reaction.count > 1) {
|
||||
reaction.copy(
|
||||
count = reaction.count - 1,
|
||||
me = false
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
reaction
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
announcement
|
||||
Success(
|
||||
announcements.value!!.data!!.map { announcement ->
|
||||
if (announcement.id == announcementId) {
|
||||
announcement.copy(
|
||||
reactions = announcement.reactions.mapNotNull { reaction ->
|
||||
if (reaction.name == name) {
|
||||
if (reaction.count > 1) {
|
||||
reaction.copy(
|
||||
count = reaction.count - 1,
|
||||
me = false
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
reaction
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
announcement
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
package com.keylesspalace.tusky.components.compose
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Activity
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
|
@ -28,13 +27,16 @@ import android.net.Uri
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.provider.MediaStore
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import android.widget.ImageButton
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.StringRes
|
||||
|
|
@ -71,7 +73,20 @@ import com.keylesspalace.tusky.entity.Emoji
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.util.ComposeTokenizer
|
||||
import com.keylesspalace.tusky.util.PickMediaFiles
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.afterTextChanged
|
||||
import com.keylesspalace.tusky.util.combineLiveData
|
||||
import com.keylesspalace.tusky.util.combineOptionalLiveData
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.highlightSpans
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.onTextChanged
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.util.withLifecycleContext
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
|
|
@ -84,13 +99,14 @@ import javax.inject.Inject
|
|||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class ComposeActivity : BaseActivity(),
|
||||
ComposeOptionsListener,
|
||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||
OnEmojiSelectedListener,
|
||||
Injectable,
|
||||
InputConnectionCompat.OnCommitContentListener,
|
||||
ComposeScheduleView.OnTimeSetListener {
|
||||
class ComposeActivity :
|
||||
BaseActivity(),
|
||||
ComposeOptionsListener,
|
||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||
OnEmojiSelectedListener,
|
||||
Injectable,
|
||||
InputConnectionCompat.OnCommitContentListener,
|
||||
ComposeScheduleView.OnTimeSetListener {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
|
@ -114,6 +130,21 @@ class ComposeActivity : BaseActivity(),
|
|||
private val maxUploadMediaNumber = 4
|
||||
private var mediaCount = 0
|
||||
|
||||
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||
if (success) {
|
||||
pickMedia(photoUploadUri!!)
|
||||
}
|
||||
}
|
||||
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
|
||||
if (mediaCount + uris.size > maxUploadMediaNumber) {
|
||||
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
uris.forEach { uri ->
|
||||
pickMedia(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
|
|
@ -130,16 +161,16 @@ class ComposeActivity : BaseActivity(),
|
|||
|
||||
setupAvatar(preferences, activeAccount)
|
||||
val mediaAdapter = MediaPreviewAdapter(
|
||||
this,
|
||||
onAddCaption = { item ->
|
||||
makeCaptionDialog(item.description, item.uri) { newDescription ->
|
||||
viewModel.updateDescription(item.localId, newDescription)
|
||||
}
|
||||
},
|
||||
onRemove = this::removeMediaFromQueue
|
||||
this,
|
||||
onAddCaption = { item ->
|
||||
makeCaptionDialog(item.description, item.uri) { newDescription ->
|
||||
viewModel.updateDescription(item.localId, newDescription)
|
||||
}
|
||||
},
|
||||
onRemove = this::removeMediaFromQueue
|
||||
)
|
||||
binding.composeMediaPreviewBar.layoutManager =
|
||||
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
binding.composeMediaPreviewBar.adapter = mediaAdapter
|
||||
binding.composeMediaPreviewBar.itemAnimator = null
|
||||
|
||||
|
|
@ -255,11 +286,11 @@ class ComposeActivity : BaseActivity(),
|
|||
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
||||
|
||||
binding.composeEditField.setAdapter(
|
||||
ComposeAutoCompleteAdapter(
|
||||
this,
|
||||
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
)
|
||||
ComposeAutoCompleteAdapter(
|
||||
this,
|
||||
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
)
|
||||
)
|
||||
binding.composeEditField.setTokenizer(ComposeTokenizer())
|
||||
|
||||
|
|
@ -274,8 +305,9 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
|
||||
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O
|
||||
|| Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O ||
|
||||
Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1
|
||||
) {
|
||||
binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
||||
}
|
||||
}
|
||||
|
|
@ -316,9 +348,9 @@ class ComposeActivity : BaseActivity(),
|
|||
updateScheduleButton()
|
||||
}
|
||||
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
|
||||
val active = poll == null
|
||||
&& media!!.size != 4
|
||||
&& (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
||||
val active = poll == null &&
|
||||
media!!.size != 4 &&
|
||||
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
||||
enableButton(binding.composeAddMediaButton, active, active)
|
||||
enablePollButton(media.isNullOrEmpty())
|
||||
}.subscribe()
|
||||
|
|
@ -379,7 +411,6 @@ class ComposeActivity : BaseActivity(),
|
|||
setDisplayShowHomeEnabled(true)
|
||||
setHomeAsUpIndicator(R.drawable.ic_close_24dp)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) {
|
||||
|
|
@ -390,13 +421,15 @@ class ComposeActivity : BaseActivity(),
|
|||
|
||||
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
|
||||
loadAvatar(
|
||||
activeAccount.profilePictureUrl,
|
||||
binding.composeAvatar,
|
||||
avatarSize / 8,
|
||||
animateAvatars
|
||||
activeAccount.profilePictureUrl,
|
||||
binding.composeAvatar,
|
||||
avatarSize / 8,
|
||||
animateAvatars
|
||||
)
|
||||
binding.composeAvatar.contentDescription = getString(
|
||||
R.string.compose_active_account_description,
|
||||
activeAccount.fullName
|
||||
)
|
||||
binding.composeAvatar.contentDescription = getString(R.string.compose_active_account_description,
|
||||
activeAccount.fullName)
|
||||
}
|
||||
|
||||
private fun replaceTextAtCaret(text: CharSequence) {
|
||||
|
|
@ -454,7 +487,6 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
private fun atButtonClicked() {
|
||||
prependSelectedWordsWith("@")
|
||||
}
|
||||
|
|
@ -470,7 +502,7 @@ class ComposeActivity : BaseActivity(),
|
|||
|
||||
private fun displayTransientError(@StringRes stringId: Int) {
|
||||
val bar = Snackbar.make(binding.activityCompose, stringId, Snackbar.LENGTH_LONG)
|
||||
//necessary so snackbar is shown over everything
|
||||
// necessary so snackbar is shown over everything
|
||||
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||
bar.show()
|
||||
}
|
||||
|
|
@ -488,7 +520,6 @@ class ComposeActivity : BaseActivity(),
|
|||
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
||||
binding.composeHideMediaButton.isClickable = false
|
||||
ContextCompat.getColor(this, R.color.transparent_chinwag_green)
|
||||
|
||||
} else {
|
||||
binding.composeHideMediaButton.isClickable = true
|
||||
if (markMediaSensitive) {
|
||||
|
|
@ -597,15 +628,17 @@ class ComposeActivity : BaseActivity(),
|
|||
private fun onMediaPick() {
|
||||
addMediaBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
//Wait until bottom sheet is not collapsed and show next screen after
|
||||
// Wait until bottom sheet is not collapsed and show next screen after
|
||||
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
addMediaBehavior.removeBottomSheetCallback(this)
|
||||
if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this@ComposeActivity,
|
||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
|
||||
ActivityCompat.requestPermissions(
|
||||
this@ComposeActivity,
|
||||
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
|
||||
)
|
||||
} else {
|
||||
initiateMediaPicking()
|
||||
pickMediaFile.launch(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -619,8 +652,10 @@ class ComposeActivity : BaseActivity(),
|
|||
private fun openPollDialog() {
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
val instanceParams = viewModel.instanceParams.value!!
|
||||
showAddPollDialog(this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
||||
instanceParams.pollMaxLength, viewModel::updatePoll)
|
||||
showAddPollDialog(
|
||||
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
||||
instanceParams.pollMaxLength, viewModel::updatePoll
|
||||
)
|
||||
}
|
||||
|
||||
private fun setupPollView() {
|
||||
|
|
@ -740,35 +775,40 @@ class ComposeActivity : BaseActivity(),
|
|||
} else if (characterCount <= maximumTootCharacters) {
|
||||
if (viewModel.media.value!!.isNotEmpty()) {
|
||||
finishingUploadDialog = ProgressDialog.show(
|
||||
this, getString(R.string.dialog_title_finishing_media_upload),
|
||||
getString(R.string.dialog_message_uploading_media), true, true)
|
||||
this, getString(R.string.dialog_title_finishing_media_upload),
|
||||
getString(R.string.dialog_message_uploading_media), true, true
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.sendStatus(contentText, spoilerText).observe(this, {
|
||||
finishingUploadDialog?.dismiss()
|
||||
deleteDraftAndFinish()
|
||||
})
|
||||
|
||||
viewModel.sendStatus(contentText, spoilerText).observe(
|
||||
this,
|
||||
{
|
||||
finishingUploadDialog?.dismiss()
|
||||
deleteDraftAndFinish()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||
enableButtons(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>,
|
||||
grantResults: IntArray) {
|
||||
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
|
||||
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
initiateMediaPicking()
|
||||
pickMediaFile.launch(true)
|
||||
} else {
|
||||
val bar = Snackbar.make(binding.activityCompose, R.string.error_media_upload_permission,
|
||||
Snackbar.LENGTH_SHORT).apply {
|
||||
|
||||
Snackbar.make(
|
||||
binding.activityCompose, R.string.error_media_upload_permission,
|
||||
Snackbar.LENGTH_SHORT
|
||||
).apply {
|
||||
setAction(R.string.action_retry) { onMediaPick() }
|
||||
// necessary so snackbar is shown over everything
|
||||
view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||
show()
|
||||
}
|
||||
bar.setAction(R.string.action_retry) { onMediaPick() }
|
||||
//necessary so snackbar is shown over everything
|
||||
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
||||
bar.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -776,50 +816,38 @@ class ComposeActivity : BaseActivity(),
|
|||
private fun initiateCameraApp() {
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
|
||||
// We don't need to ask for permission in this case, because the used calls require
|
||||
// android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was
|
||||
// way before permission dialogues have been introduced.
|
||||
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
if (intent.resolveActivity(packageManager) != null) {
|
||||
val photoFile: File = try {
|
||||
createNewImageFile(this)
|
||||
} catch (ex: IOException) {
|
||||
displayTransientError(R.string.error_media_upload_opening)
|
||||
return
|
||||
}
|
||||
|
||||
// Continue only if the File was successfully created
|
||||
photoUploadUri = FileProvider.getUriForFile(this,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
photoFile)
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri)
|
||||
startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT)
|
||||
val photoFile: File = try {
|
||||
createNewImageFile(this)
|
||||
} catch (ex: IOException) {
|
||||
displayTransientError(R.string.error_media_upload_opening)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private fun initiateMediaPicking() {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
val mimeTypes = arrayOf("image/*", "video/*", "audio/*")
|
||||
intent.type = "*/*"
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
startActivityForResult(intent, MEDIA_PICK_RESULT)
|
||||
// Continue only if the File was successfully created
|
||||
photoUploadUri = FileProvider.getUriForFile(
|
||||
this,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
photoFile
|
||||
)
|
||||
takePicture.launch(photoUploadUri)
|
||||
}
|
||||
|
||||
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
||||
button.isEnabled = clickable
|
||||
ThemeUtils.setDrawableTint(this, button.drawable,
|
||||
if (colorActive) android.R.attr.textColorTertiary
|
||||
else R.attr.textColorDisabled)
|
||||
ThemeUtils.setDrawableTint(
|
||||
this, button.drawable,
|
||||
if (colorActive) android.R.attr.textColorTertiary
|
||||
else R.attr.textColorDisabled
|
||||
)
|
||||
}
|
||||
|
||||
private fun enablePollButton(enable: Boolean) {
|
||||
binding.addPollTextActionTextView.isEnabled = enable
|
||||
val textColor = ThemeUtils.getColor(this,
|
||||
if (enable) android.R.attr.textColorTertiary
|
||||
else R.attr.textColorDisabled)
|
||||
val textColor = ThemeUtils.getColor(
|
||||
this,
|
||||
if (enable) android.R.attr.textColorTertiary
|
||||
else R.attr.textColorDisabled
|
||||
)
|
||||
binding.addPollTextActionTextView.setTextColor(textColor)
|
||||
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
|
@ -828,31 +856,6 @@ class ComposeActivity : BaseActivity(),
|
|||
viewModel.removeMediaFromQueue(item)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, intent)
|
||||
if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
|
||||
if (intent.data != null) {
|
||||
// Single media, upload it and done.
|
||||
pickMedia(intent.data!!)
|
||||
} else if (intent.clipData != null) {
|
||||
val clipData = intent.clipData!!
|
||||
val count = clipData.itemCount
|
||||
if (mediaCount + count > maxUploadMediaNumber) {
|
||||
// check if exist media + upcoming media > 4, then prob error message.
|
||||
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
// if not grater then 4, upload all multiple media.
|
||||
for (i in 0 until count) {
|
||||
val imageUri = clipData.getItemAt(i).getUri()
|
||||
pickMedia(imageUri)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
|
||||
pickMedia(photoUploadUri!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) {
|
||||
withLifecycleContext {
|
||||
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
||||
|
|
@ -876,7 +879,6 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
displayTransientError(errorId)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -908,9 +910,10 @@ class ComposeActivity : BaseActivity(),
|
|||
override fun onBackPressed() {
|
||||
// Acting like a teen: deliberately ignoring parent.
|
||||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||
) {
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
|
|
@ -945,12 +948,12 @@ class ComposeActivity : BaseActivity(),
|
|||
val contentWarning = binding.composeContentWarningField.text.toString()
|
||||
if (viewModel.didChange(contentText, contentWarning)) {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(R.string.compose_save_draft)
|
||||
.setPositiveButton(R.string.action_save) { _, _ ->
|
||||
saveDraftAndFinish(contentText, contentWarning)
|
||||
}
|
||||
.setNegativeButton(R.string.action_delete) { _, _ -> deleteDraftAndFinish() }
|
||||
.show()
|
||||
.setMessage(R.string.compose_save_draft)
|
||||
.setPositiveButton(R.string.action_save) { _, _ ->
|
||||
saveDraftAndFinish(contentText, contentWarning)
|
||||
}
|
||||
.setNegativeButton(R.string.action_delete) { _, _ -> deleteDraftAndFinish() }
|
||||
.show()
|
||||
} else {
|
||||
finishWithoutSlideOutAnimation()
|
||||
}
|
||||
|
|
@ -982,13 +985,13 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
|
||||
data class QueuedMedia(
|
||||
val localId: Long,
|
||||
val uri: Uri,
|
||||
val type: Type,
|
||||
val mediaSize: Long,
|
||||
val uploadPercent: Int = 0,
|
||||
val id: String? = null,
|
||||
val description: String? = null
|
||||
val localId: Long,
|
||||
val uri: Uri,
|
||||
val type: Type,
|
||||
val mediaSize: Long,
|
||||
val uploadPercent: Int = 0,
|
||||
val id: String? = null,
|
||||
val description: String? = null
|
||||
) {
|
||||
enum class Type {
|
||||
IMAGE, VIDEO, AUDIO;
|
||||
|
|
@ -1011,32 +1014,29 @@ class ComposeActivity : BaseActivity(),
|
|||
|
||||
@Parcelize
|
||||
data class ComposeOptions(
|
||||
// Let's keep fields var until all consumers are Kotlin
|
||||
var scheduledTootId: String? = null,
|
||||
var savedTootUid: Int? = null,
|
||||
var draftId: Int? = null,
|
||||
var tootText: String? = null,
|
||||
var mediaUrls: List<String>? = null,
|
||||
var mediaDescriptions: List<String>? = null,
|
||||
var mentionedUsernames: Set<String>? = null,
|
||||
var inReplyToId: String? = null,
|
||||
var replyVisibility: Status.Visibility? = null,
|
||||
var visibility: Status.Visibility? = null,
|
||||
var contentWarning: String? = null,
|
||||
var replyingStatusAuthor: String? = null,
|
||||
var replyingStatusContent: String? = null,
|
||||
var mediaAttachments: List<Attachment>? = null,
|
||||
var draftAttachments: List<DraftAttachment>? = null,
|
||||
var scheduledAt: String? = null,
|
||||
var sensitive: Boolean? = null,
|
||||
var poll: NewPoll? = null,
|
||||
var modifiedInitialState: Boolean? = null
|
||||
// Let's keep fields var until all consumers are Kotlin
|
||||
var scheduledTootId: String? = null,
|
||||
var draftId: Int? = null,
|
||||
var tootText: String? = null,
|
||||
var mediaUrls: List<String>? = null,
|
||||
var mediaDescriptions: List<String>? = null,
|
||||
var mentionedUsernames: Set<String>? = null,
|
||||
var inReplyToId: String? = null,
|
||||
var replyVisibility: Status.Visibility? = null,
|
||||
var visibility: Status.Visibility? = null,
|
||||
var contentWarning: String? = null,
|
||||
var replyingStatusAuthor: String? = null,
|
||||
var replyingStatusContent: String? = null,
|
||||
var mediaAttachments: List<Attachment>? = null,
|
||||
var draftAttachments: List<DraftAttachment>? = null,
|
||||
var scheduledAt: String? = null,
|
||||
var sensitive: Boolean? = null,
|
||||
var poll: NewPoll? = null,
|
||||
var modifiedInitialState: Boolean? = null
|
||||
) : Parcelable
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ComposeActivity" // logging tag
|
||||
private const val MEDIA_PICK_RESULT = 1
|
||||
private const val MEDIA_TAKE_PHOTO_RESULT = 2
|
||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||
|
||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||
|
|
|
|||
|
|
@ -21,38 +21,48 @@ import androidx.core.net.toUri
|
|||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.InstanceEntity
|
||||
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.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.keylesspalace.tusky.service.TootToSend
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Observable.just
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxkotlin.Singles
|
||||
import java.util.*
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import com.keylesspalace.tusky.util.VersionUtils
|
||||
import com.keylesspalace.tusky.util.combineLiveData
|
||||
import com.keylesspalace.tusky.util.filter
|
||||
import com.keylesspalace.tusky.util.map
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import com.keylesspalace.tusky.util.toLiveData
|
||||
import com.keylesspalace.tusky.util.withoutFirstWhich
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class ComposeViewModel @Inject constructor(
|
||||
private val api: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
private val mediaUploader: MediaUploader,
|
||||
private val serviceClient: ServiceClient,
|
||||
private val draftHelper: DraftHelper,
|
||||
private val saveTootHelper: SaveTootHelper,
|
||||
private val db: AppDatabase
|
||||
private val api: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
private val mediaUploader: MediaUploader,
|
||||
private val serviceClient: ServiceClient,
|
||||
private val draftHelper: DraftHelper,
|
||||
private val db: AppDatabase
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
private var replyingStatusAuthor: String? = null
|
||||
private var replyingStatusContent: String? = null
|
||||
internal var startingText: String? = null
|
||||
private var savedTootUid: Int = 0
|
||||
private var draftId: Int = 0
|
||||
private var scheduledTootId: String? = null
|
||||
private var startingContentWarning: String = ""
|
||||
|
|
@ -66,15 +76,15 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance ->
|
||||
ComposeInstanceParams(
|
||||
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
|
||||
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
|
||||
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
||||
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
|
||||
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
|
||||
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
|
||||
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
||||
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
|
||||
)
|
||||
}
|
||||
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
|
||||
val markMediaAsSensitive =
|
||||
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||
|
||||
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
|
||||
val showContentWarning = mutableLiveData(false)
|
||||
|
|
@ -91,30 +101,36 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
init {
|
||||
|
||||
Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance ->
|
||||
InstanceEntity(
|
||||
Single.zip(
|
||||
api.getCustomEmojis(), api.getInstance(),
|
||||
{ emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = accountManager.activeAccount?.domain!!,
|
||||
emojiList = emojis,
|
||||
maximumTootCharacters = instance.maxTootChars,
|
||||
maxPollOptions = instance.pollLimits?.maxOptions,
|
||||
maxPollOptionLength = instance.pollLimits?.maxOptionChars,
|
||||
version = instance.version
|
||||
)
|
||||
}
|
||||
.doOnSuccess {
|
||||
db.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
.onErrorResumeNext(
|
||||
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
)
|
||||
.subscribe({ instanceEntity ->
|
||||
}
|
||||
)
|
||||
.doOnSuccess {
|
||||
db.instanceDao().insertOrReplace(it)
|
||||
}
|
||||
.onErrorResumeNext {
|
||||
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||
}
|
||||
.subscribe(
|
||||
{ instanceEntity ->
|
||||
emoji.postValue(instanceEntity.emojiList)
|
||||
instance.postValue(instanceEntity)
|
||||
}, { throwable ->
|
||||
},
|
||||
{ throwable ->
|
||||
// this can happen on network error when no cached data is available
|
||||
Log.w(TAG, "error loading instance data", throwable)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
fun pickMedia(uri: Uri, description: String? = null): LiveData<Either<Throwable, QueuedMedia>> {
|
||||
|
|
@ -122,44 +138,49 @@ class ComposeViewModel @Inject constructor(
|
|||
// the Activity goes away temporarily (like on screen rotation).
|
||||
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
|
||||
mediaUploader.prepareMedia(uri)
|
||||
.map { (type, uri, size) ->
|
||||
val mediaItems = media.value!!
|
||||
if (type != QueuedMedia.Type.IMAGE
|
||||
&& mediaItems.isNotEmpty()
|
||||
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
|
||||
throw VideoOrImageException()
|
||||
} else {
|
||||
addMediaToQueue(type, uri, size, description)
|
||||
}
|
||||
.map { (type, uri, size) ->
|
||||
val mediaItems = media.value!!
|
||||
if (type != QueuedMedia.Type.IMAGE &&
|
||||
mediaItems.isNotEmpty() &&
|
||||
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
||||
) {
|
||||
throw VideoOrImageException()
|
||||
} else {
|
||||
addMediaToQueue(type, uri, size, description)
|
||||
}
|
||||
.subscribe({ queuedMedia ->
|
||||
}
|
||||
.subscribe(
|
||||
{ queuedMedia ->
|
||||
liveData.postValue(Either.Right(queuedMedia))
|
||||
}, { error ->
|
||||
},
|
||||
{ error ->
|
||||
liveData.postValue(Either.Left(error))
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
return liveData
|
||||
}
|
||||
|
||||
private fun addMediaToQueue(
|
||||
type: QueuedMedia.Type,
|
||||
uri: Uri,
|
||||
mediaSize: Long,
|
||||
description: String? = null
|
||||
type: QueuedMedia.Type,
|
||||
uri: Uri,
|
||||
mediaSize: Long,
|
||||
description: String? = null
|
||||
): QueuedMedia {
|
||||
val mediaItem = QueuedMedia(
|
||||
localId = System.currentTimeMillis(),
|
||||
uri = uri,
|
||||
type = type,
|
||||
mediaSize = mediaSize,
|
||||
description = description
|
||||
localId = System.currentTimeMillis(),
|
||||
uri = uri,
|
||||
type = type,
|
||||
mediaSize = mediaSize,
|
||||
description = description
|
||||
)
|
||||
media.value = media.value!! + mediaItem
|
||||
mediaToDisposable[mediaItem.localId] = mediaUploader
|
||||
.uploadMedia(mediaItem)
|
||||
.subscribe({ event ->
|
||||
.uploadMedia(mediaItem)
|
||||
.subscribe(
|
||||
{ event ->
|
||||
val item = media.value?.find { it.localId == mediaItem.localId }
|
||||
?: return@subscribe
|
||||
?: return@subscribe
|
||||
val newMediaItem = when (event) {
|
||||
is UploadEvent.ProgressEvent ->
|
||||
item.copy(uploadPercent = event.percentage)
|
||||
|
|
@ -169,16 +190,20 @@ class ComposeViewModel @Inject constructor(
|
|||
synchronized(media) {
|
||||
val mediaValue = media.value!!
|
||||
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
|
||||
media.postValue(if (index == -1) {
|
||||
mediaValue + newMediaItem
|
||||
} else {
|
||||
mediaValue.toMutableList().also { it[index] = newMediaItem }
|
||||
})
|
||||
media.postValue(
|
||||
if (index == -1) {
|
||||
mediaValue + newMediaItem
|
||||
} else {
|
||||
mediaValue.toMutableList().also { it[index] = newMediaItem }
|
||||
}
|
||||
)
|
||||
}
|
||||
}, { error ->
|
||||
},
|
||||
{ error ->
|
||||
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
|
||||
uploadError.postValue(error)
|
||||
})
|
||||
}
|
||||
)
|
||||
return mediaItem
|
||||
}
|
||||
|
||||
|
|
@ -198,12 +223,14 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
fun didChange(content: String?, contentWarning: String?): Boolean {
|
||||
|
||||
val textChanged = !(content.isNullOrEmpty()
|
||||
|| startingText?.startsWith(content.toString()) ?: false)
|
||||
val textChanged = !(
|
||||
content.isNullOrEmpty() ||
|
||||
startingText?.startsWith(content.toString()) ?: false
|
||||
)
|
||||
|
||||
val contentWarningChanged = showContentWarning.value!!
|
||||
&& !contentWarning.isNullOrEmpty()
|
||||
&& !startingContentWarning.startsWith(contentWarning.toString())
|
||||
val contentWarningChanged = showContentWarning.value!! &&
|
||||
!contentWarning.isNullOrEmpty() &&
|
||||
!startingContentWarning.startsWith(contentWarning.toString())
|
||||
val mediaChanged = !media.value.isNullOrEmpty()
|
||||
val pollChanged = poll.value != null
|
||||
|
||||
|
|
@ -216,25 +243,23 @@ class ComposeViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun deleteDraft() {
|
||||
if (savedTootUid != 0) {
|
||||
saveTootHelper.deleteDraft(savedTootUid)
|
||||
}
|
||||
if (draftId != 0) {
|
||||
draftHelper.deleteDraftAndAttachments(draftId)
|
||||
.subscribe()
|
||||
viewModelScope.launch {
|
||||
if (draftId != 0) {
|
||||
draftHelper.deleteDraftAndAttachments(draftId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveDraft(content: String, contentWarning: String) {
|
||||
viewModelScope.launch {
|
||||
val mediaUris: MutableList<String> = mutableListOf()
|
||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||
media.value?.forEach { item ->
|
||||
mediaUris.add(item.uri.toString())
|
||||
mediaDescriptions.add(item.description)
|
||||
}
|
||||
|
||||
val mediaUris: MutableList<String> = mutableListOf()
|
||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||
media.value?.forEach { item ->
|
||||
mediaUris.add(item.uri.toString())
|
||||
mediaDescriptions.add(item.description)
|
||||
}
|
||||
|
||||
draftHelper.saveDraft(
|
||||
draftHelper.saveDraft(
|
||||
draftId = draftId,
|
||||
accountId = accountManager.activeAccount?.id!!,
|
||||
inReplyToId = inReplyToId,
|
||||
|
|
@ -246,7 +271,8 @@ class ComposeViewModel @Inject constructor(
|
|||
mediaDescriptions = mediaDescriptions,
|
||||
poll = poll.value,
|
||||
failedToSend = false
|
||||
).subscribe()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -255,51 +281,50 @@ class ComposeViewModel @Inject constructor(
|
|||
* @return LiveData which will signal once the screen can be closed or null if there are errors
|
||||
*/
|
||||
fun sendStatus(
|
||||
content: String,
|
||||
spoilerText: String
|
||||
content: String,
|
||||
spoilerText: String
|
||||
): LiveData<Unit> {
|
||||
|
||||
val deletionObservable = if (isEditingScheduledToot) {
|
||||
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
||||
} else {
|
||||
just(Unit)
|
||||
Observable.just(Unit)
|
||||
}.toLiveData()
|
||||
|
||||
val sendObservable = media
|
||||
.filter { items -> items.all { it.uploadPercent == -1 } }
|
||||
.map {
|
||||
val mediaIds = ArrayList<String>()
|
||||
val mediaUris = ArrayList<Uri>()
|
||||
val mediaDescriptions = ArrayList<String>()
|
||||
for (item in media.value!!) {
|
||||
mediaIds.add(item.id!!)
|
||||
mediaUris.add(item.uri)
|
||||
mediaDescriptions.add(item.description ?: "")
|
||||
}
|
||||
|
||||
val tootToSend = TootToSend(
|
||||
text = content,
|
||||
warningText = spoilerText,
|
||||
visibility = statusVisibility.value!!.serverString(),
|
||||
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
|
||||
mediaIds = mediaIds,
|
||||
mediaUris = mediaUris.map { it.toString() },
|
||||
mediaDescriptions = mediaDescriptions,
|
||||
scheduledAt = scheduledAt.value,
|
||||
inReplyToId = inReplyToId,
|
||||
poll = poll.value,
|
||||
replyingStatusContent = null,
|
||||
replyingStatusAuthorUsername = null,
|
||||
accountId = accountManager.activeAccount!!.id,
|
||||
savedTootUid = savedTootUid,
|
||||
draftId = draftId,
|
||||
idempotencyKey = randomAlphanumericString(16),
|
||||
retries = 0
|
||||
)
|
||||
|
||||
serviceClient.sendToot(tootToSend)
|
||||
.filter { items -> items.all { it.uploadPercent == -1 } }
|
||||
.map {
|
||||
val mediaIds = ArrayList<String>()
|
||||
val mediaUris = ArrayList<Uri>()
|
||||
val mediaDescriptions = ArrayList<String>()
|
||||
for (item in media.value!!) {
|
||||
mediaIds.add(item.id!!)
|
||||
mediaUris.add(item.uri)
|
||||
mediaDescriptions.add(item.description ?: "")
|
||||
}
|
||||
|
||||
val tootToSend = TootToSend(
|
||||
text = content,
|
||||
warningText = spoilerText,
|
||||
visibility = statusVisibility.value!!.serverString(),
|
||||
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
|
||||
mediaIds = mediaIds,
|
||||
mediaUris = mediaUris.map { it.toString() },
|
||||
mediaDescriptions = mediaDescriptions,
|
||||
scheduledAt = scheduledAt.value,
|
||||
inReplyToId = inReplyToId,
|
||||
poll = poll.value,
|
||||
replyingStatusContent = null,
|
||||
replyingStatusAuthorUsername = null,
|
||||
accountId = accountManager.activeAccount!!.id,
|
||||
draftId = draftId,
|
||||
idempotencyKey = randomAlphanumericString(16),
|
||||
retries = 0
|
||||
)
|
||||
|
||||
serviceClient.sendToot(tootToSend)
|
||||
}
|
||||
|
||||
return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
|
||||
}
|
||||
|
||||
|
|
@ -318,12 +343,15 @@ class ComposeViewModel @Inject constructor(
|
|||
media.removeObserver(this)
|
||||
} else if (updatedItem.id != null) {
|
||||
api.updateMedia(updatedItem.id, description)
|
||||
.subscribe({
|
||||
.subscribe(
|
||||
{
|
||||
completedCaptioningLiveData.postValue(true)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
completedCaptioningLiveData.postValue(false)
|
||||
})
|
||||
.autoDispose()
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
media.removeObserver(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -336,8 +364,8 @@ class ComposeViewModel @Inject constructor(
|
|||
'@' -> {
|
||||
return try {
|
||||
api.searchAccounts(query = token.substring(1), limit = 10)
|
||||
.blockingGet()
|
||||
.map { ComposeAutoCompleteAdapter.AccountResult(it) }
|
||||
.blockingGet()
|
||||
.map { ComposeAutoCompleteAdapter.AccountResult(it) }
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
|
||||
emptyList()
|
||||
|
|
@ -346,9 +374,9 @@ class ComposeViewModel @Inject constructor(
|
|||
'#' -> {
|
||||
return try {
|
||||
api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||
.blockingGet()
|
||||
.hashtags
|
||||
.map { ComposeAutoCompleteAdapter.HashtagResult(it) }
|
||||
.blockingGet()
|
||||
.hashtags
|
||||
.map { ComposeAutoCompleteAdapter.HashtagResult(it) }
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
|
||||
emptyList()
|
||||
|
|
@ -357,11 +385,11 @@ class ComposeViewModel @Inject constructor(
|
|||
':' -> {
|
||||
val emojiList = emoji.value ?: return emptyList()
|
||||
|
||||
val incomplete = token.substring(1).toLowerCase(Locale.ROOT)
|
||||
val incomplete = token.substring(1).lowercase(Locale.ROOT)
|
||||
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||
for (emoji in emojiList) {
|
||||
val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT)
|
||||
val shortcode = emoji.shortcode.lowercase(Locale.ROOT)
|
||||
if (shortcode.startsWith(incomplete)) {
|
||||
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
||||
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
||||
|
|
@ -391,7 +419,8 @@ class ComposeViewModel @Inject constructor(
|
|||
|
||||
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
||||
startingVisibility = Status.Visibility.byNum(
|
||||
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
|
||||
preferredVisibility.num.coerceAtLeast(replyVisibility.num)
|
||||
)
|
||||
|
||||
inReplyToId = composeOptions?.inReplyToId
|
||||
|
||||
|
|
@ -406,20 +435,8 @@ class ComposeViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
// recreate media list
|
||||
val loadedDraftMediaUris = composeOptions?.mediaUrls
|
||||
val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
|
||||
val draftAttachments = composeOptions?.draftAttachments
|
||||
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
|
||||
// when coming from SavedTootActivity
|
||||
loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
|
||||
.forEach { (uri, description) ->
|
||||
pickMedia(uri.toUri()).observeForever { errorOrItem ->
|
||||
if (errorOrItem.isRight() && description != null) {
|
||||
updateDescription(errorOrItem.asRight().localId, description)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (draftAttachments != null) {
|
||||
if (draftAttachments != null) {
|
||||
// when coming from DraftActivity
|
||||
draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
|
||||
} else composeOptions?.mediaAttachments?.forEach { a ->
|
||||
|
|
@ -432,7 +449,6 @@ class ComposeViewModel @Inject constructor(
|
|||
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
||||
}
|
||||
|
||||
savedTootUid = composeOptions?.savedTootUid ?: 0
|
||||
draftId = composeOptions?.draftId ?: 0
|
||||
scheduledTootId = composeOptions?.scheduledTootId
|
||||
startingText = composeOptions?.tootText
|
||||
|
|
@ -483,20 +499,19 @@ class ComposeViewModel @Inject constructor(
|
|||
private companion object {
|
||||
const val TAG = "ComposeViewModel"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
|
||||
|
||||
const val DEFAULT_CHARACTER_LIMIT = 500
|
||||
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
||||
private const val DEFAULT_MAX_OPTION_LENGTH = 25
|
||||
private const val DEFAULT_MAX_OPTION_LENGTH = 50
|
||||
|
||||
data class ComposeInstanceParams(
|
||||
val maxChars: Int,
|
||||
val pollMaxOptions: Int,
|
||||
val pollMaxLength: Int,
|
||||
val supportsScheduled: Boolean
|
||||
val maxChars: Int,
|
||||
val pollMaxOptions: Int,
|
||||
val pollMaxLength: Int,
|
||||
val supportsScheduled: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.components.compose.view.ProgressImageView
|
||||
|
||||
class MediaPreviewAdapter(
|
||||
context: Context,
|
||||
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
|
||||
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
|
||||
context: Context,
|
||||
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
|
||||
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
|
||||
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
|
||||
|
||||
fun submitList(list: List<ComposeActivity.QueuedMedia>) {
|
||||
|
|
@ -57,7 +57,7 @@ class MediaPreviewAdapter(
|
|||
}
|
||||
|
||||
private val thumbnailViewSize =
|
||||
context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||
context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||
|
||||
override fun getItemCount(): Int = differ.currentList.size
|
||||
|
||||
|
|
@ -74,31 +74,34 @@ class MediaPreviewAdapter(
|
|||
holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
||||
} else {
|
||||
Glide.with(holder.itemView.context)
|
||||
.load(item.uri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.dontAnimate()
|
||||
.into(holder.progressImageView)
|
||||
.load(item.uri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.dontAnimate()
|
||||
.into(holder.progressImageView)
|
||||
}
|
||||
}
|
||||
|
||||
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
||||
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||
return oldItem.localId == newItem.localId
|
||||
}
|
||||
private val differ = AsyncListDiffer(
|
||||
this,
|
||||
object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
||||
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||
return oldItem.localId == newItem.localId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||
return oldItem == newItem
|
||||
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
inner class PreviewViewHolder(val progressImageView: ProgressImageView)
|
||||
: RecyclerView.ViewHolder(progressImageView) {
|
||||
inner class PreviewViewHolder(val progressImageView: ProgressImageView) :
|
||||
RecyclerView.ViewHolder(progressImageView) {
|
||||
init {
|
||||
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
||||
val margin = itemView.context.resources
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||
val marginBottom = itemView.context.resources
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
||||
layoutParams.setMargins(margin, 0, margin, marginBottom)
|
||||
progressImageView.layoutParams = layoutParams
|
||||
progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
|
|
@ -107,4 +110,4 @@ class MediaPreviewAdapter(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,16 +28,20 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
|||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||
import com.keylesspalace.tusky.util.getImageSquarePixels
|
||||
import com.keylesspalace.tusky.util.getMediaSize
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.MultipartBody
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed class UploadEvent {
|
||||
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
||||
|
|
@ -50,40 +54,35 @@ fun createNewImageFile(context: Context): File {
|
|||
val imageFileName = "Tusky_${randomId}_"
|
||||
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||
return File.createTempFile(
|
||||
imageFileName, /* prefix */
|
||||
".jpg", /* suffix */
|
||||
storageDir /* directory */
|
||||
imageFileName, /* prefix */
|
||||
".jpg", /* suffix */
|
||||
storageDir /* directory */
|
||||
)
|
||||
}
|
||||
|
||||
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
||||
|
||||
interface MediaUploader {
|
||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia>
|
||||
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent>
|
||||
}
|
||||
|
||||
class AudioSizeException : Exception()
|
||||
class VideoSizeException : Exception()
|
||||
class MediaTypeException : Exception()
|
||||
class CouldNotOpenFileException : Exception()
|
||||
|
||||
class MediaUploaderImpl(
|
||||
private val context: Context,
|
||||
private val mastodonApi: MastodonApi
|
||||
) : MediaUploader {
|
||||
override fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
|
||||
class MediaUploader @Inject constructor(
|
||||
private val context: Context,
|
||||
private val mastodonApi: MastodonApi
|
||||
) {
|
||||
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
|
||||
return Observable
|
||||
.fromCallable {
|
||||
if (shouldResizeMedia(media)) {
|
||||
downsize(media)
|
||||
} else media
|
||||
}
|
||||
.switchMap { upload(it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.fromCallable {
|
||||
if (shouldResizeMedia(media)) {
|
||||
downsize(media)
|
||||
} else media
|
||||
}
|
||||
.switchMap { upload(it) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
override fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||
return Single.fromCallable {
|
||||
var mediaSize = getMediaSize(contentResolver, inUri)
|
||||
var uri = inUri
|
||||
|
|
@ -101,12 +100,13 @@ class MediaUploaderImpl(
|
|||
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
||||
FileOutputStream(file.absoluteFile).use { out ->
|
||||
input.copyTo(out)
|
||||
uri = FileProvider.getUriForFile(context,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
file)
|
||||
uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||
file
|
||||
)
|
||||
mediaSize = getMediaSize(contentResolver, uri)
|
||||
}
|
||||
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, e)
|
||||
|
|
@ -151,20 +151,22 @@ class MediaUploaderImpl(
|
|||
var mimeType = contentResolver.getType(media.uri)
|
||||
val map = MimeTypeMap.getSingleton()
|
||||
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
||||
val filename = String.format("%s_%s_%s.%s",
|
||||
context.getString(R.string.app_name),
|
||||
Date().time.toString(),
|
||||
randomAlphanumericString(10),
|
||||
fileExtension)
|
||||
val filename = "%s_%s_%s.%s".format(
|
||||
context.getString(R.string.app_name),
|
||||
Date().time.toString(),
|
||||
randomAlphanumericString(10),
|
||||
fileExtension
|
||||
)
|
||||
|
||||
val stream = contentResolver.openInputStream(media.uri)
|
||||
|
||||
if (mimeType == null) mimeType = "multipart/form-data"
|
||||
|
||||
|
||||
var lastProgress = -1
|
||||
val fileBody = ProgressRequestBody(stream, media.mediaSize,
|
||||
mimeType.toMediaTypeOrNull()) { percentage ->
|
||||
val fileBody = ProgressRequestBody(
|
||||
stream, media.mediaSize,
|
||||
mimeType.toMediaTypeOrNull()
|
||||
) { percentage ->
|
||||
if (percentage != lastProgress) {
|
||||
emitter.onNext(UploadEvent.ProgressEvent(percentage))
|
||||
}
|
||||
|
|
@ -180,12 +182,15 @@ class MediaUploaderImpl(
|
|||
}
|
||||
|
||||
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
||||
.subscribe({ attachment ->
|
||||
.subscribe(
|
||||
{ attachment ->
|
||||
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
||||
emitter.onComplete()
|
||||
}, { e ->
|
||||
},
|
||||
{ e ->
|
||||
emitter.onError(e)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Cancel the request when our observable is cancelled
|
||||
emitter.setDisposable(uploadDisposable)
|
||||
|
|
@ -194,15 +199,16 @@ class MediaUploaderImpl(
|
|||
|
||||
private fun downsize(media: QueuedMedia): QueuedMedia {
|
||||
val file = createNewImageFile(context)
|
||||
DownsizeImageTask.resize(arrayOf(media.uri),
|
||||
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file)
|
||||
DownsizeImageTask.resize(
|
||||
arrayOf(media.uri),
|
||||
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file
|
||||
)
|
||||
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
||||
}
|
||||
|
||||
private fun shouldResizeMedia(media: QueuedMedia): Boolean {
|
||||
return media.type == QueuedMedia.Type.IMAGE
|
||||
&& (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT
|
||||
|| getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
||||
return media.type == QueuedMedia.Type.IMAGE &&
|
||||
(media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
@ -211,6 +217,5 @@ class MediaUploaderImpl(
|
|||
private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB
|
||||
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
|
||||
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,33 +26,33 @@ import com.keylesspalace.tusky.databinding.DialogAddPollBinding
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
|
||||
fun showAddPollDialog(
|
||||
context: Context,
|
||||
poll: NewPoll?,
|
||||
maxOptionCount: Int,
|
||||
maxOptionLength: Int,
|
||||
onUpdatePoll: (NewPoll) -> Unit
|
||||
context: Context,
|
||||
poll: NewPoll?,
|
||||
maxOptionCount: Int,
|
||||
maxOptionLength: Int,
|
||||
onUpdatePoll: (NewPoll) -> Unit
|
||||
) {
|
||||
|
||||
val binding = DialogAddPollBinding.inflate(LayoutInflater.from(context))
|
||||
|
||||
val dialog = AlertDialog.Builder(context)
|
||||
.setIcon(R.drawable.ic_poll_24dp)
|
||||
.setTitle(R.string.create_poll_title)
|
||||
.setView(binding.root)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
.setIcon(R.drawable.ic_poll_24dp)
|
||||
.setTitle(R.string.create_poll_title)
|
||||
.setView(binding.root)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.create()
|
||||
|
||||
val adapter = AddPollOptionsAdapter(
|
||||
options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
|
||||
maxOptionLength = maxOptionLength,
|
||||
onOptionRemoved = { valid ->
|
||||
binding.addChoiceButton.isEnabled = true
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
|
||||
},
|
||||
onOptionChanged = { valid ->
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
|
||||
}
|
||||
options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
|
||||
maxOptionLength = maxOptionLength,
|
||||
onOptionRemoved = { valid ->
|
||||
binding.addChoiceButton.isEnabled = true
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
|
||||
},
|
||||
onOptionChanged = { valid ->
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
|
||||
}
|
||||
)
|
||||
|
||||
binding.pollChoices.adapter = adapter
|
||||
|
|
@ -80,13 +80,15 @@ fun showAddPollDialog(
|
|||
val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition
|
||||
|
||||
val pollDuration = context.resources
|
||||
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
||||
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
||||
|
||||
onUpdatePoll(NewPoll(
|
||||
onUpdatePoll(
|
||||
NewPoll(
|
||||
options = adapter.pollOptions,
|
||||
expiresIn = pollDuration,
|
||||
multiple = binding.multipleChoicesCheckBox.isChecked
|
||||
))
|
||||
)
|
||||
)
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
|
@ -96,4 +98,4 @@ fun showAddPollDialog(
|
|||
|
||||
// make the dialog focusable so the keyboard does not stay behind it
|
||||
dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ import com.keylesspalace.tusky.util.onTextChanged
|
|||
import com.keylesspalace.tusky.util.visible
|
||||
|
||||
class AddPollOptionsAdapter(
|
||||
private var options: MutableList<String>,
|
||||
private val maxOptionLength: Int,
|
||||
private val onOptionRemoved: (Boolean) -> Unit,
|
||||
private val onOptionChanged: (Boolean) -> Unit
|
||||
): RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() {
|
||||
private var options: MutableList<String>,
|
||||
private val maxOptionLength: Int,
|
||||
private val onOptionRemoved: (Boolean) -> Unit,
|
||||
private val onOptionChanged: (Boolean) -> Unit
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() {
|
||||
|
||||
val pollOptions: List<String>
|
||||
get() = options.toList()
|
||||
|
|
@ -47,8 +47,8 @@ class AddPollOptionsAdapter(
|
|||
binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength))
|
||||
|
||||
binding.optionEditText.onTextChanged { s, _, _, _ ->
|
||||
val pos = holder.adapterPosition
|
||||
if(pos != RecyclerView.NO_POSITION) {
|
||||
val pos = holder.bindingAdapterPosition
|
||||
if (pos != RecyclerView.NO_POSITION) {
|
||||
options[pos] = s.toString()
|
||||
onOptionChanged(validateInput())
|
||||
}
|
||||
|
|
@ -68,8 +68,8 @@ class AddPollOptionsAdapter(
|
|||
|
||||
holder.binding.deleteButton.setOnClickListener {
|
||||
holder.binding.optionEditText.clearFocus()
|
||||
options.removeAt(holder.adapterPosition)
|
||||
notifyItemRemoved(holder.adapterPosition)
|
||||
options.removeAt(holder.bindingAdapterPosition)
|
||||
notifyItemRemoved(holder.bindingAdapterPosition)
|
||||
onOptionRemoved(validateInput())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import android.graphics.drawable.Drawable
|
|||
import android.net.Uri
|
||||
import android.text.InputFilter
|
||||
import android.text.InputType
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
|
|
@ -41,9 +40,10 @@ import com.keylesspalace.tusky.util.withLifecycleContext
|
|||
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
|
||||
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
||||
|
||||
fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||
previewUri: Uri,
|
||||
onUpdateDescription: (String) -> LiveData<Boolean>
|
||||
fun <T> T.makeCaptionDialog(
|
||||
existingDescription: String?,
|
||||
previewUri: Uri,
|
||||
onUpdateDescription: (String) -> LiveData<Boolean>
|
||||
) where T : Activity, T : LifecycleOwner {
|
||||
val dialogLayout = LinearLayout(this)
|
||||
val padding = Utils.dpToPx(this, 8)
|
||||
|
|
@ -54,9 +54,6 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
maximumScale = 6f
|
||||
}
|
||||
|
||||
val displayMetrics = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
|
||||
val margin = Utils.dpToPx(this, 4)
|
||||
dialogLayout.addView(imageView)
|
||||
(imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f
|
||||
|
|
@ -64,14 +61,18 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
|
||||
|
||||
val input = EditText(this)
|
||||
input.hint = resources.getQuantityString(R.plurals.hint_describe_for_visually_impaired,
|
||||
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT)
|
||||
input.hint = resources.getQuantityString(
|
||||
R.plurals.hint_describe_for_visually_impaired,
|
||||
MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT
|
||||
)
|
||||
dialogLayout.addView(input)
|
||||
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
|
||||
input.setLines(2)
|
||||
input.inputType = (InputType.TYPE_CLASS_TEXT
|
||||
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES)
|
||||
input.inputType = (
|
||||
InputType.TYPE_CLASS_TEXT
|
||||
or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||
)
|
||||
input.setText(existingDescription)
|
||||
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
||||
|
||||
|
|
@ -79,41 +80,40 @@ fun <T> T.makeCaptionDialog(existingDescription: String?,
|
|||
onUpdateDescription(input.text.toString())
|
||||
withLifecycleContext {
|
||||
onUpdateDescription(input.text.toString())
|
||||
.observe { success -> if (!success) showFailedCaptionMessage() }
|
||||
|
||||
.observe { success -> if (!success) showFailedCaptionMessage() }
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setView(dialogLayout)
|
||||
.setPositiveButton(android.R.string.ok, okListener)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
.setView(dialogLayout)
|
||||
.setPositiveButton(android.R.string.ok, okListener)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create()
|
||||
|
||||
val window = dialog.window
|
||||
window?.setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
)
|
||||
|
||||
dialog.show()
|
||||
|
||||
// Load the image and manually set it into the ImageView because it doesn't have a fixed size.
|
||||
Glide.with(this)
|
||||
.load(previewUri)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.into(object : CustomTarget<Drawable>(4096, 4096) {
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
imageView.setImageDrawable(placeholder)
|
||||
}
|
||||
.load(previewUri)
|
||||
.downsample(DownsampleStrategy.CENTER_INSIDE)
|
||||
.into(object : CustomTarget<Drawable>(4096, 4096) {
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
imageView.setImageDrawable(placeholder)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
})
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
imageView.setImageDrawable(resource)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private fun Activity.showFailedCaptionMessage() {
|
||||
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,12 +57,10 @@ class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: Attr
|
|||
R.id.directRadioButton
|
||||
else ->
|
||||
R.id.directRadioButton
|
||||
|
||||
}
|
||||
|
||||
check(selectedButton)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface ComposeOptionsListener {
|
||||
|
|
|
|||
|
|
@ -16,25 +16,27 @@
|
|||
package com.keylesspalace.tusky.components.compose.view
|
||||
|
||||
import android.content.Context
|
||||
import androidx.emoji.widget.EmojiEditTextHelper
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
||||
import android.text.InputType
|
||||
import android.text.method.KeyListener
|
||||
import android.util.AttributeSet
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.emoji.widget.EmojiEditTextHelper
|
||||
|
||||
class EditTextTyped @JvmOverloads constructor(context: Context,
|
||||
attributeSet: AttributeSet? = null)
|
||||
: AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||
class EditTextTyped @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attributeSet: AttributeSet? = null
|
||||
) :
|
||||
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||
|
||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
||||
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
||||
|
||||
init {
|
||||
//fix a bug with autocomplete and some keyboards
|
||||
// fix a bug with autocomplete and some keyboards
|
||||
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
||||
inputType = newInputType
|
||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener))
|
||||
|
|
@ -52,8 +54,13 @@ class EditTextTyped @JvmOverloads constructor(context: Context,
|
|||
val connection = super.onCreateInputConnection(editorInfo)
|
||||
return if (onCommitContentListener != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||
getEmojiEditTextHelper().onCreateInputConnection(InputConnectionCompat.createWrapper(connection, editorInfo,
|
||||
onCommitContentListener!!), editorInfo)!!
|
||||
getEmojiEditTextHelper().onCreateInputConnection(
|
||||
InputConnectionCompat.createWrapper(
|
||||
connection, editorInfo,
|
||||
onCommitContentListener!!
|
||||
),
|
||||
editorInfo
|
||||
)!!
|
||||
} else {
|
||||
connection
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,11 @@ import com.keylesspalace.tusky.databinding.ViewPollPreviewBinding
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
|
||||
class PollPreviewView @JvmOverloads constructor(
|
||||
context: Context?,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0)
|
||||
: LinearLayout(context, attrs, defStyleAttr) {
|
||||
context: Context?,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) :
|
||||
LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val adapter = PreviewPollOptionsAdapter()
|
||||
|
||||
|
|
@ -46,7 +47,7 @@ class PollPreviewView @JvmOverloads constructor(
|
|||
binding.pollPreviewOptions.adapter = adapter
|
||||
}
|
||||
|
||||
fun setPoll(poll: NewPoll){
|
||||
fun setPoll(poll: NewPoll) {
|
||||
adapter.update(poll.options, poll.multiple)
|
||||
|
||||
val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast {
|
||||
|
|
@ -59,4 +60,4 @@ class PollPreviewView @JvmOverloads constructor(
|
|||
super.setOnClickListener(l)
|
||||
adapter.setOnClickListener(l)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,15 +28,15 @@ import com.mikepenz.iconics.utils.sizeDp
|
|||
|
||||
class TootButton
|
||||
@JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : MaterialButton(context, attrs, defStyleAttr) {
|
||||
|
||||
private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button)
|
||||
|
||||
init {
|
||||
if(smallStyle) {
|
||||
if (smallStyle) {
|
||||
setIconResource(R.drawable.ic_send_24dp)
|
||||
} else {
|
||||
setText(R.string.action_send)
|
||||
|
|
@ -47,7 +47,7 @@ class TootButton
|
|||
}
|
||||
|
||||
fun setStatusVisibility(visibility: Status.Visibility) {
|
||||
if(!smallStyle) {
|
||||
if (!smallStyle) {
|
||||
|
||||
icon = when (visibility) {
|
||||
Status.Visibility.PUBLIC -> {
|
||||
|
|
@ -68,8 +68,5 @@ class TootButton
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,114 +17,39 @@ package com.keylesspalace.tusky.components.conversation
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.AsyncPagedListDiffer
|
||||
import androidx.paging.PagedList
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListUpdateCallback
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
|
||||
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
|
||||
class ConversationAdapter(
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val listener: StatusActionListener,
|
||||
private val topLoadedCallback: () -> Unit,
|
||||
private val retryCallback: () -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val listener: StatusActionListener
|
||||
) : PagingDataAdapter<ConversationEntity, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
||||
|
||||
private var networkState: NetworkState? = null
|
||||
|
||||
private val differ: AsyncPagedListDiffer<ConversationEntity> = AsyncPagedListDiffer(object : ListUpdateCallback {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
notifyItemRangeInserted(position, count)
|
||||
if (position == 0) {
|
||||
topLoadedCallback()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRemoved(position: Int, count: Int) {
|
||||
notifyItemRangeRemoved(position, count)
|
||||
}
|
||||
|
||||
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||
notifyItemMoved(fromPosition, toPosition)
|
||||
}
|
||||
|
||||
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||
notifyItemRangeChanged(position, count, payload)
|
||||
}
|
||||
}, AsyncDifferConfig.Builder(CONVERSATION_COMPARATOR).build())
|
||||
|
||||
fun submitList(list: PagedList<ConversationEntity>) {
|
||||
differ.submitList(list)
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
||||
return ConversationViewHolder(view, statusDisplayOptions, listener)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
return when (viewType) {
|
||||
R.layout.item_network_state -> {
|
||||
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
NetworkStateViewHolder(binding, retryCallback)
|
||||
}
|
||||
R.layout.item_conversation -> {
|
||||
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
|
||||
ConversationViewHolder(view, statusDisplayOptions, listener)
|
||||
}
|
||||
else -> throw IllegalArgumentException("unknown view type $viewType")
|
||||
}
|
||||
override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) {
|
||||
holder.setupWithConversation(getItem(position))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (getItemViewType(position)) {
|
||||
R.layout.item_network_state -> (holder as NetworkStateViewHolder).setUpWithNetworkState(networkState, differ.itemCount == 0)
|
||||
R.layout.item_conversation -> (holder as ConversationViewHolder).setupWithConversation(differ.getItem(position))
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasExtraRow() = networkState != null && networkState != NetworkState.LOADED
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (hasExtraRow() && position == itemCount - 1) {
|
||||
R.layout.item_network_state
|
||||
} else {
|
||||
R.layout.item_conversation
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return differ.itemCount + if (hasExtraRow()) 1 else 0
|
||||
}
|
||||
|
||||
fun setNetworkState(newNetworkState: NetworkState?) {
|
||||
val previousState = this.networkState
|
||||
val hadExtraRow = hasExtraRow()
|
||||
this.networkState = newNetworkState
|
||||
val hasExtraRow = hasExtraRow()
|
||||
if (hadExtraRow != hasExtraRow) {
|
||||
if (hadExtraRow) {
|
||||
notifyItemRemoved(differ.itemCount)
|
||||
} else {
|
||||
notifyItemInserted(differ.itemCount)
|
||||
}
|
||||
} else if (hasExtraRow && previousState != newNetworkState) {
|
||||
notifyItemChanged(itemCount - 1)
|
||||
}
|
||||
fun item(position: Int): ConversationEntity? {
|
||||
return getItem(position)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationEntity>() {
|
||||
override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean =
|
||||
oldItem == newItem
|
||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean =
|
||||
oldItem.id == newItem.id
|
||||
override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -21,65 +21,70 @@ import androidx.room.Embedded
|
|||
import androidx.room.Entity
|
||||
import androidx.room.TypeConverters
|
||||
import com.keylesspalace.tusky.db.Converters
|
||||
import com.keylesspalace.tusky.entity.*
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Conversation
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@Entity(primaryKeys = ["id","accountId"])
|
||||
@Entity(primaryKeys = ["id", "accountId"])
|
||||
@TypeConverters(Converters::class)
|
||||
data class ConversationEntity(
|
||||
val accountId: Long,
|
||||
val id: String,
|
||||
val accounts: List<ConversationAccountEntity>,
|
||||
val unread: Boolean,
|
||||
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
||||
val accountId: Long,
|
||||
val id: String,
|
||||
val accounts: List<ConversationAccountEntity>,
|
||||
val unread: Boolean,
|
||||
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
||||
)
|
||||
|
||||
data class ConversationAccountEntity(
|
||||
val id: String,
|
||||
val username: String,
|
||||
val displayName: String,
|
||||
val avatar: String,
|
||||
val emojis: List<Emoji>
|
||||
val id: String,
|
||||
val username: String,
|
||||
val displayName: String,
|
||||
val avatar: String,
|
||||
val emojis: List<Emoji>
|
||||
) {
|
||||
fun toAccount(): Account {
|
||||
return Account(
|
||||
id = id,
|
||||
username = username,
|
||||
displayName = displayName,
|
||||
avatar = avatar,
|
||||
emojis = emojis,
|
||||
url = "",
|
||||
localUsername = "",
|
||||
note = SpannedString(""),
|
||||
header = ""
|
||||
id = id,
|
||||
username = username,
|
||||
displayName = displayName,
|
||||
avatar = avatar,
|
||||
emojis = emojis,
|
||||
url = "",
|
||||
localUsername = "",
|
||||
note = SpannedString(""),
|
||||
header = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@TypeConverters(Converters::class)
|
||||
data class ConversationStatusEntity(
|
||||
val id: String,
|
||||
val url: String?,
|
||||
val inReplyToId: String?,
|
||||
val inReplyToAccountId: String?,
|
||||
val account: ConversationAccountEntity,
|
||||
val content: Spanned,
|
||||
val createdAt: Date,
|
||||
val emojis: List<Emoji>,
|
||||
val favouritesCount: Int,
|
||||
val favourited: Boolean,
|
||||
val bookmarked: Boolean,
|
||||
val sensitive: Boolean,
|
||||
val spoilerText: String,
|
||||
val attachments: ArrayList<Attachment>,
|
||||
val mentions: Array<Status.Mention>,
|
||||
val showingHiddenContent: Boolean,
|
||||
val expanded: Boolean,
|
||||
val collapsible: Boolean,
|
||||
val collapsed: Boolean,
|
||||
val poll: Poll?
|
||||
|
||||
val id: String,
|
||||
val url: String?,
|
||||
val inReplyToId: String?,
|
||||
val inReplyToAccountId: String?,
|
||||
val account: ConversationAccountEntity,
|
||||
val content: Spanned,
|
||||
val createdAt: Date,
|
||||
val emojis: List<Emoji>,
|
||||
val favouritesCount: Int,
|
||||
val favourited: Boolean,
|
||||
val bookmarked: Boolean,
|
||||
val sensitive: Boolean,
|
||||
val spoilerText: String,
|
||||
val attachments: ArrayList<Attachment>,
|
||||
val mentions: List<Status.Mention>,
|
||||
val showingHiddenContent: Boolean,
|
||||
val expanded: Boolean,
|
||||
val collapsible: Boolean,
|
||||
val collapsed: Boolean,
|
||||
val muted: Boolean,
|
||||
val poll: Poll?
|
||||
) {
|
||||
/** its necessary to override this because Spanned.equals does not work as expected */
|
||||
override fun equals(other: Any?): Boolean {
|
||||
|
|
@ -93,7 +98,7 @@ data class ConversationStatusEntity(
|
|||
if (inReplyToId != other.inReplyToId) return false
|
||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||
if (account != other.account) return false
|
||||
if (content.toString() != other.content.toString()) return false //TODO find a better method to compare two spanned strings
|
||||
if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings
|
||||
if (createdAt != other.createdAt) return false
|
||||
if (emojis != other.emojis) return false
|
||||
if (favouritesCount != other.favouritesCount) return false
|
||||
|
|
@ -101,11 +106,12 @@ data class ConversationStatusEntity(
|
|||
if (sensitive != other.sensitive) return false
|
||||
if (spoilerText != other.spoilerText) return false
|
||||
if (attachments != other.attachments) return false
|
||||
if (!mentions.contentEquals(other.mentions)) return false
|
||||
if (mentions != other.mentions) return false
|
||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
||||
if (expanded != other.expanded) return false
|
||||
if (collapsible != other.collapsible) return false
|
||||
if (collapsed != other.collapsed) return false
|
||||
if (muted != other.muted) return false
|
||||
if (poll != other.poll) return false
|
||||
|
||||
return true
|
||||
|
|
@ -125,71 +131,85 @@ data class ConversationStatusEntity(
|
|||
result = 31 * result + sensitive.hashCode()
|
||||
result = 31 * result + spoilerText.hashCode()
|
||||
result = 31 * result + attachments.hashCode()
|
||||
result = 31 * result + mentions.contentHashCode()
|
||||
result = 31 * result + mentions.hashCode()
|
||||
result = 31 * result + showingHiddenContent.hashCode()
|
||||
result = 31 * result + expanded.hashCode()
|
||||
result = 31 * result + collapsible.hashCode()
|
||||
result = 31 * result + collapsed.hashCode()
|
||||
result = 31 * result + muted.hashCode()
|
||||
result = 31 * result + poll.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
fun toStatus(): Status {
|
||||
return Status(
|
||||
id = id,
|
||||
url = url,
|
||||
account = account.toAccount(),
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
content = content,
|
||||
reblog = null,
|
||||
createdAt = createdAt,
|
||||
emojis = emojis,
|
||||
reblogsCount = 0,
|
||||
favouritesCount = favouritesCount,
|
||||
reblogged = false,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive= sensitive,
|
||||
spoilerText = spoilerText,
|
||||
visibility = Status.Visibility.DIRECT,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
application = null,
|
||||
pinned = false,
|
||||
muted = false,
|
||||
poll = poll,
|
||||
card = null)
|
||||
id = id,
|
||||
url = url,
|
||||
account = account.toAccount(),
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
content = content,
|
||||
reblog = null,
|
||||
createdAt = createdAt,
|
||||
emojis = emojis,
|
||||
reblogsCount = 0,
|
||||
favouritesCount = favouritesCount,
|
||||
reblogged = false,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive = sensitive,
|
||||
spoilerText = spoilerText,
|
||||
visibility = Status.Visibility.DIRECT,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
application = null,
|
||||
pinned = false,
|
||||
muted = muted,
|
||||
poll = poll,
|
||||
card = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun Account.toEntity() =
|
||||
ConversationAccountEntity(
|
||||
id,
|
||||
username,
|
||||
name,
|
||||
avatar,
|
||||
emojis ?: emptyList()
|
||||
)
|
||||
ConversationAccountEntity(
|
||||
id = id,
|
||||
username = username,
|
||||
displayName = name,
|
||||
avatar = avatar,
|
||||
emojis = emojis ?: emptyList()
|
||||
)
|
||||
|
||||
fun Status.toEntity() =
|
||||
ConversationStatusEntity(
|
||||
id, url, inReplyToId, inReplyToAccountId, account.toEntity(), content,
|
||||
createdAt, emojis, favouritesCount, favourited, bookmarked, sensitive,
|
||||
spoilerText, attachments, mentions,
|
||||
false,
|
||||
false,
|
||||
shouldTrimStatus(content),
|
||||
true,
|
||||
poll
|
||||
)
|
||||
|
||||
ConversationStatusEntity(
|
||||
id = id,
|
||||
url = url,
|
||||
inReplyToId = inReplyToId,
|
||||
inReplyToAccountId = inReplyToAccountId,
|
||||
account = account.toEntity(),
|
||||
content = content,
|
||||
createdAt = createdAt,
|
||||
emojis = emojis,
|
||||
favouritesCount = favouritesCount,
|
||||
favourited = favourited,
|
||||
bookmarked = bookmarked,
|
||||
sensitive = sensitive,
|
||||
spoilerText = spoilerText,
|
||||
attachments = attachments,
|
||||
mentions = mentions,
|
||||
showingHiddenContent = false,
|
||||
expanded = false,
|
||||
collapsible = shouldTrimStatus(content),
|
||||
collapsed = true,
|
||||
muted = muted ?: false,
|
||||
poll = poll
|
||||
)
|
||||
|
||||
fun Conversation.toEntity(accountId: Long) =
|
||||
ConversationEntity(
|
||||
accountId,
|
||||
id,
|
||||
accounts.map { it.toEntity() },
|
||||
unread,
|
||||
lastStatus!!.toEntity()
|
||||
)
|
||||
ConversationEntity(
|
||||
accountId = accountId,
|
||||
id = id,
|
||||
accounts = accounts.map { it.toEntity() },
|
||||
unread = unread,
|
||||
lastStatus = lastStatus!!.toEntity()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.LoadStateAdapter
|
||||
import com.keylesspalace.tusky.adapter.NetworkStateViewHolder
|
||||
import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding
|
||||
|
||||
class ConversationLoadStateAdapter(
|
||||
private val retryCallback: () -> Unit
|
||||
) : LoadStateAdapter<NetworkStateViewHolder>() {
|
||||
|
||||
override fun onBindViewHolder(holder: NetworkStateViewHolder, loadState: LoadState) {
|
||||
holder.setUpWithNetworkState(loadState)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
loadState: LoadState
|
||||
): NetworkStateViewHolder {
|
||||
val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return NetworkStateViewHolder(binding, retryCallback)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2017 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.paging.PagedList
|
||||
import com.keylesspalace.tusky.entity.Conversation
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.PagingRequestHelper
|
||||
import com.keylesspalace.tusky.util.createStatusLiveData
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
/**
|
||||
* This boundary callback gets notified when user reaches to the edges of the list such that the
|
||||
* database cannot provide any more data.
|
||||
* <p>
|
||||
* The boundary callback might be called multiple times for the same direction so it does its own
|
||||
* rate limiting using the PagingRequestHelper class.
|
||||
*/
|
||||
class ConversationsBoundaryCallback(
|
||||
private val accountId: Long,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val handleResponse: (Long, List<Conversation>?) -> Unit,
|
||||
private val ioExecutor: Executor,
|
||||
private val networkPageSize: Int)
|
||||
: PagedList.BoundaryCallback<ConversationEntity>() {
|
||||
|
||||
val helper = PagingRequestHelper(ioExecutor)
|
||||
val networkState = helper.createStatusLiveData()
|
||||
|
||||
/**
|
||||
* Database returned 0 items. We should query the backend for more items.
|
||||
*/
|
||||
@MainThread
|
||||
override fun onZeroItemsLoaded() {
|
||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.INITIAL) {
|
||||
mastodonApi.getConversations(null, networkPageSize)
|
||||
.enqueue(createWebserviceCallback(it))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* User reached to the end of the list.
|
||||
*/
|
||||
@MainThread
|
||||
override fun onItemAtEndLoaded(itemAtEnd: ConversationEntity) {
|
||||
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
|
||||
mastodonApi.getConversations(itemAtEnd.lastStatus.id, networkPageSize)
|
||||
.enqueue(createWebserviceCallback(it))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* every time it gets new items, boundary callback simply inserts them into the database and
|
||||
* paging library takes care of refreshing the list if necessary.
|
||||
*/
|
||||
private fun insertItemsIntoDb(
|
||||
response: Response<List<Conversation>>,
|
||||
it: PagingRequestHelper.Request.Callback) {
|
||||
ioExecutor.execute {
|
||||
handleResponse(accountId, response.body())
|
||||
it.recordSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemAtFrontLoaded(itemAtFront: ConversationEntity) {
|
||||
// ignored, since we only ever append to what's in the DB
|
||||
}
|
||||
|
||||
private fun createWebserviceCallback(it: PagingRequestHelper.Request.Callback): Callback<List<Conversation>> {
|
||||
return object : Callback<List<Conversation>> {
|
||||
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
|
||||
it.recordFailure(t)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<List<Conversation>>, response: Response<List<Conversation>>) {
|
||||
insertItemsIntoDb(response, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
|
|
@ -20,14 +20,19 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadState
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
|
|
@ -36,12 +41,18 @@ import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
|||
import com.keylesspalace.tusky.interfaces.StatusActionListener
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.CardViewMode
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment {
|
||||
|
||||
@Inject
|
||||
|
|
@ -52,9 +63,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
||||
|
||||
private lateinit var adapter: ConversationAdapter
|
||||
private lateinit var loadStateAdapter: ConversationLoadStateAdapter
|
||||
|
||||
private var layoutManager: LinearLayoutManager? = null
|
||||
|
||||
private var initialRefreshDone: Boolean = false
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
||||
}
|
||||
|
|
@ -63,23 +77,25 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||
|
||||
val statusDisplayOptions = StatusDisplayOptions(
|
||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
|
||||
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
|
||||
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
|
||||
useBlurhash = preferences.getBoolean("useBlurhash", true),
|
||||
cardViewMode = CardViewMode.NONE,
|
||||
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
|
||||
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
||||
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
animateAvatars = preferences.getBoolean("animateGifAvatars", false),
|
||||
mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true,
|
||||
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false),
|
||||
showBotOverlay = preferences.getBoolean("showBotOverlay", true),
|
||||
useBlurhash = preferences.getBoolean("useBlurhash", true),
|
||||
cardViewMode = CardViewMode.NONE,
|
||||
confirmReblogs = preferences.getBoolean("confirmReblogs", true),
|
||||
confirmFavourites = preferences.getBoolean("confirmFavourites", false),
|
||||
hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false),
|
||||
animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
)
|
||||
|
||||
adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry)
|
||||
adapter = ConversationAdapter(statusDisplayOptions, this)
|
||||
loadStateAdapter = ConversationLoadStateAdapter(adapter::retry)
|
||||
|
||||
binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
layoutManager = LinearLayoutManager(view.context)
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.adapter = adapter.withLoadStateFooter(loadStateAdapter)
|
||||
(binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
|
||||
|
||||
binding.progressBar.hide()
|
||||
|
|
@ -87,58 +103,101 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
|
||||
initSwipeToRefresh()
|
||||
|
||||
viewModel.conversations.observe(viewLifecycleOwner) {
|
||||
adapter.submitList(it)
|
||||
}
|
||||
viewModel.networkState.observe(viewLifecycleOwner) {
|
||||
adapter.setNetworkState(it)
|
||||
lifecycleScope.launch {
|
||||
viewModel.conversationFlow.collectLatest { pagingData ->
|
||||
adapter.submitData(pagingData)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.load()
|
||||
adapter.addLoadStateListener { loadStates ->
|
||||
|
||||
loadStates.refresh.let { refreshState ->
|
||||
if (refreshState is LoadState.Error) {
|
||||
binding.statusView.show()
|
||||
if (refreshState.error is IOException) {
|
||||
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
||||
adapter.refresh()
|
||||
}
|
||||
} else {
|
||||
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||
adapter.refresh()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
binding.statusView.hide()
|
||||
}
|
||||
|
||||
binding.progressBar.visible(refreshState == LoadState.Loading && adapter.itemCount == 0)
|
||||
|
||||
if (refreshState is LoadState.NotLoading && !initialRefreshDone) {
|
||||
// jump to top after the initial refresh finished
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
initialRefreshDone = true
|
||||
}
|
||||
|
||||
if (refreshState != LoadState.Loading) {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSwipeToRefresh() {
|
||||
viewModel.refreshState.observe(viewLifecycleOwner) {
|
||||
binding.swipeRefreshLayout.isRefreshing = it == NetworkState.LOADING
|
||||
}
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
viewModel.refresh()
|
||||
adapter.refresh()
|
||||
}
|
||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
||||
}
|
||||
|
||||
private fun onTopLoaded() {
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
}
|
||||
|
||||
override fun onReblog(reblog: Boolean, position: Int) {
|
||||
// its impossible to reblog private messages
|
||||
}
|
||||
|
||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||
viewModel.favourite(favourite, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.favourite(favourite, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBookmark(favourite: Boolean, position: Int) {
|
||||
viewModel.bookmark(favourite, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.bookmark(favourite, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMore(view: View, position: Int) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
more(it.toStatus(), view, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
|
||||
val popup = PopupMenu(requireContext(), view)
|
||||
popup.inflate(R.menu.conversation_more)
|
||||
|
||||
if (conversation.lastStatus.muted) {
|
||||
popup.menu.removeItem(R.id.status_mute_conversation)
|
||||
} else {
|
||||
popup.menu.removeItem(R.id.status_unmute_conversation)
|
||||
}
|
||||
|
||||
popup.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.status_mute_conversation -> viewModel.muteConversation(conversation)
|
||||
R.id.status_unmute_conversation -> viewModel.muteConversation(conversation)
|
||||
R.id.conversation_delete -> deleteConversation(conversation)
|
||||
}
|
||||
true
|
||||
}
|
||||
popup.show()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
viewMedia(attachmentIndex, it.toStatus(), view)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewThread(position: Int) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
viewThread(it.toStatus())
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewThread(conversation.lastStatus.id, conversation.lastStatus.url)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,11 +206,15 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||
viewModel.expandHiddenStatus(expanded, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.expandHiddenStatus(expanded, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||
viewModel.showContent(isShowing, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.showContent(isShowing, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadMore(position: Int) {
|
||||
|
|
@ -159,7 +222,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||
viewModel.collapseLongStatus(isCollapsed, position)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.collapseLongStatus(isCollapsed, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
|
|
@ -174,15 +239,25 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun removeItem(position: Int) {
|
||||
viewModel.remove(position)
|
||||
// not needed
|
||||
}
|
||||
|
||||
override fun onReply(position: Int) {
|
||||
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
|
||||
reply(it.toStatus())
|
||||
adapter.item(position)?.let { conversation ->
|
||||
reply(conversation.lastStatus.toStatus())
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteConversation(conversation: ConversationEntity) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setMessage(R.string.dialog_delete_conversation_warning)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
viewModel.remove(conversation)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun jumpToTop() {
|
||||
if (isAdded) {
|
||||
layoutManager?.scrollToPosition(0)
|
||||
|
|
@ -195,7 +270,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
|||
}
|
||||
|
||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
viewModel.voteInPoll(position, choices)
|
||||
adapter.item(position)?.let { conversation ->
|
||||
viewModel.voteInPoll(choices, conversation)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.LoadType
|
||||
import androidx.paging.PagingState
|
||||
import androidx.paging.RemoteMediator
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
class ConversationsRemoteMediator(
|
||||
private val accountId: Long,
|
||||
private val api: MastodonApi,
|
||||
private val db: AppDatabase
|
||||
) : RemoteMediator<Int, ConversationEntity>() {
|
||||
|
||||
override suspend fun load(
|
||||
loadType: LoadType,
|
||||
state: PagingState<Int, ConversationEntity>
|
||||
): MediatorResult {
|
||||
|
||||
try {
|
||||
val conversationsResult = when (loadType) {
|
||||
LoadType.REFRESH -> {
|
||||
api.getConversations(limit = state.config.initialLoadSize)
|
||||
}
|
||||
LoadType.PREPEND -> {
|
||||
return MediatorResult.Success(endOfPaginationReached = true)
|
||||
}
|
||||
LoadType.APPEND -> {
|
||||
val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.lastStatus?.id
|
||||
api.getConversations(maxId = maxId, limit = state.config.pageSize)
|
||||
}
|
||||
}
|
||||
|
||||
if (loadType == LoadType.REFRESH) {
|
||||
db.conversationDao().deleteForAccount(accountId)
|
||||
}
|
||||
db.conversationDao().insert(
|
||||
conversationsResult
|
||||
.filterNot { it.lastStatus == null }
|
||||
.map { it.toEntity(accountId) }
|
||||
)
|
||||
return MediatorResult.Success(endOfPaginationReached = conversationsResult.isEmpty())
|
||||
} catch (e: Exception) {
|
||||
return MediatorResult.Error(e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun initialize() = InitializeAction.LAUNCH_INITIAL_REFRESH
|
||||
}
|
||||
|
|
@ -1,111 +1,37 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.Config
|
||||
import androidx.paging.toLiveData
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.entity.Conversation
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.concurrent.Executors
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi, val db: AppDatabase) {
|
||||
|
||||
private val ioExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_PAGE_SIZE = 20
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun refresh(accountId: Long, showLoadingIndicator: Boolean): LiveData<NetworkState> {
|
||||
val networkState = MutableLiveData<NetworkState>()
|
||||
if(showLoadingIndicator) {
|
||||
networkState.value = NetworkState.LOADING
|
||||
}
|
||||
|
||||
mastodonApi.getConversations(limit = DEFAULT_PAGE_SIZE).enqueue(
|
||||
object : Callback<List<Conversation>> {
|
||||
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
|
||||
// retrofit calls this on main thread so safe to call set value
|
||||
networkState.value = NetworkState.error(t.message)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<List<Conversation>>, response: Response<List<Conversation>>) {
|
||||
ioExecutor.execute {
|
||||
db.runInTransaction {
|
||||
db.conversationDao().deleteForAccount(accountId)
|
||||
insertResultIntoDb(accountId, response.body())
|
||||
}
|
||||
// since we are in bg thread now, post the result.
|
||||
networkState.postValue(NetworkState.LOADED)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return networkState
|
||||
}
|
||||
|
||||
@MainThread
|
||||
fun conversations(accountId: Long): Listing<ConversationEntity> {
|
||||
// create a boundary callback which will observe when the user reaches to the edges of
|
||||
// the list and update the database with extra data.
|
||||
val boundaryCallback = ConversationsBoundaryCallback(
|
||||
accountId = accountId,
|
||||
mastodonApi = mastodonApi,
|
||||
handleResponse = this::insertResultIntoDb,
|
||||
ioExecutor = ioExecutor,
|
||||
networkPageSize = DEFAULT_PAGE_SIZE)
|
||||
// we are using a mutable live data to trigger refresh requests which eventually calls
|
||||
// refresh method and gets a new live data. Each refresh request by the user becomes a newly
|
||||
// dispatched data in refreshTrigger
|
||||
val refreshTrigger = MutableLiveData<Unit>()
|
||||
val refreshState = Transformations.switchMap(refreshTrigger) {
|
||||
refresh(accountId, true)
|
||||
}
|
||||
|
||||
// We use toLiveData Kotlin extension function here, you could also use LivePagedListBuilder
|
||||
val livePagedList = db.conversationDao().conversationsForAccount(accountId).toLiveData(
|
||||
config = Config(pageSize = DEFAULT_PAGE_SIZE, prefetchDistance = DEFAULT_PAGE_SIZE / 2, enablePlaceholders = false),
|
||||
boundaryCallback = boundaryCallback
|
||||
)
|
||||
|
||||
return Listing(
|
||||
pagedList = livePagedList,
|
||||
networkState = boundaryCallback.networkState,
|
||||
retry = {
|
||||
boundaryCallback.helper.retryAllFailed()
|
||||
},
|
||||
refresh = {
|
||||
refreshTrigger.value = null
|
||||
},
|
||||
refreshState = refreshState
|
||||
)
|
||||
}
|
||||
class ConversationsRepository @Inject constructor(
|
||||
val mastodonApi: MastodonApi,
|
||||
val db: AppDatabase
|
||||
) {
|
||||
|
||||
fun deleteCacheForAccount(accountId: Long) {
|
||||
Single.fromCallable {
|
||||
db.conversationDao().deleteForAccount(accountId)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private fun insertResultIntoDb(accountId: Long, result: List<Conversation>?) {
|
||||
result?.filter { it.lastStatus != null }
|
||||
?.map{ it.toEntity(accountId) }
|
||||
?.let { db.conversationDao().insert(it) }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,142 +1,161 @@
|
|||
/* Copyright 2021 Tusky Contributors
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.components.conversation
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.PagedList
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.ExperimentalPagingApi
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.rx3.await
|
||||
import javax.inject.Inject
|
||||
|
||||
class ConversationsViewModel @Inject constructor(
|
||||
private val repository: ConversationsRepository,
|
||||
private val timelineCases: TimelineCases,
|
||||
private val database: AppDatabase,
|
||||
private val accountManager: AccountManager
|
||||
private val timelineCases: TimelineCases,
|
||||
private val database: AppDatabase,
|
||||
private val accountManager: AccountManager,
|
||||
private val api: MastodonApi
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
private val repoResult = MutableLiveData<Listing<ConversationEntity>>()
|
||||
@OptIn(ExperimentalPagingApi::class)
|
||||
val conversationFlow = Pager(
|
||||
config = PagingConfig(pageSize = 10, enablePlaceholders = false, initialLoadSize = 20),
|
||||
remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database),
|
||||
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
||||
)
|
||||
.flow
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
val conversations: LiveData<PagedList<ConversationEntity>> = Transformations.switchMap(repoResult) { it.pagedList }
|
||||
val networkState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.networkState }
|
||||
val refreshState: LiveData<NetworkState> = Transformations.switchMap(repoResult) { it.refreshState }
|
||||
fun favourite(favourite: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
timelineCases.favourite(conversation.lastStatus.id, favourite).await()
|
||||
|
||||
fun load() {
|
||||
val accountId = accountManager.activeAccount?.id ?: return
|
||||
if (repoResult.value == null) {
|
||||
repository.refresh(accountId, false)
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(favourited = favourite)
|
||||
)
|
||||
|
||||
database.conversationDao().insert(newConversation)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "failed to favourite status", e)
|
||||
}
|
||||
}
|
||||
repoResult.value = repository.conversations(accountId)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
repoResult.value?.refresh?.invoke()
|
||||
}
|
||||
fun bookmark(bookmark: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
timelineCases.bookmark(conversation.lastStatus.id, bookmark).await()
|
||||
|
||||
fun retry() {
|
||||
repoResult.value?.retry?.invoke()
|
||||
}
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(bookmarked = bookmark)
|
||||
)
|
||||
|
||||
fun favourite(favourite: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
timelineCases.favourite(conversation.lastStatus.toStatus(), favourite)
|
||||
.flatMap {
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(favourited = favourite)
|
||||
)
|
||||
|
||||
database.conversationDao().insert(newConversation)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
|
||||
.onErrorReturnItem(0)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
database.conversationDao().insert(newConversation)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "failed to bookmark status", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun bookmark(bookmark: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
timelineCases.bookmark(conversation.lastStatus.toStatus(), bookmark)
|
||||
.flatMap {
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(bookmarked = bookmark)
|
||||
)
|
||||
fun voteInPoll(choices: List<Int>, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await()
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(poll = poll)
|
||||
)
|
||||
|
||||
database.conversationDao().insert(newConversation)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnError { t -> Log.w("ConversationViewModel", "Failed to bookmark conversation", t) }
|
||||
.onErrorReturnItem(0)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
database.conversationDao().insert(newConversation)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "failed to vote in poll", e)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun voteInPoll(position: Int, choices: MutableList<Int>) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
timelineCases.voteInPoll(conversation.lastStatus.toStatus(), choices)
|
||||
.flatMap { poll ->
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(poll = poll)
|
||||
)
|
||||
|
||||
database.conversationDao().insert(newConversation)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnError { t -> Log.w("ConversationViewModel", "Failed to favourite conversation", t) }
|
||||
.onErrorReturnItem(0)
|
||||
.subscribe()
|
||||
.autoDispose()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun expandHiddenStatus(expanded: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(expanded = expanded)
|
||||
lastStatus = conversation.lastStatus.copy(expanded = expanded)
|
||||
)
|
||||
saveConversationToDb(newConversation)
|
||||
}
|
||||
}
|
||||
|
||||
fun collapseLongStatus(collapsed: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
|
||||
lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
|
||||
)
|
||||
saveConversationToDb(newConversation)
|
||||
}
|
||||
}
|
||||
|
||||
fun showContent(showing: Boolean, position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let { conversation ->
|
||||
fun showContent(showing: Boolean, conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
|
||||
lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
|
||||
)
|
||||
saveConversationToDb(newConversation)
|
||||
}
|
||||
}
|
||||
|
||||
fun remove(position: Int) {
|
||||
conversations.value?.getOrNull(position)?.let {
|
||||
refresh()
|
||||
fun remove(conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
api.deleteConversation(conversationId = conversation.id)
|
||||
|
||||
database.conversationDao().delete(conversation)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "failed to delete conversation", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveConversationToDb(conversation: ConversationEntity) {
|
||||
fun muteConversation(conversation: ConversationEntity) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
val newStatus = timelineCases.muteConversation(
|
||||
conversation.lastStatus.id,
|
||||
!conversation.lastStatus.muted
|
||||
).await()
|
||||
|
||||
val newConversation = conversation.copy(
|
||||
lastStatus = newStatus.toEntity()
|
||||
)
|
||||
|
||||
database.conversationDao().insert(newConversation)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "failed to mute conversation", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveConversationToDb(conversation: ConversationEntity) {
|
||||
database.conversationDao().insert(conversation)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ConversationsViewModel"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,128 +28,119 @@ import com.keylesspalace.tusky.db.DraftEntity
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.IOUtils
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
|
||||
class DraftHelper @Inject constructor(
|
||||
val context: Context,
|
||||
db: AppDatabase
|
||||
val context: Context,
|
||||
db: AppDatabase
|
||||
) {
|
||||
|
||||
private val draftDao = db.draftDao()
|
||||
|
||||
fun saveDraft(
|
||||
draftId: Int,
|
||||
accountId: Long,
|
||||
inReplyToId: String?,
|
||||
content: String?,
|
||||
contentWarning: String?,
|
||||
sensitive: Boolean,
|
||||
visibility: Status.Visibility,
|
||||
mediaUris: List<String>,
|
||||
mediaDescriptions: List<String?>,
|
||||
poll: NewPoll?,
|
||||
failedToSend: Boolean
|
||||
): Completable {
|
||||
return Single.fromCallable {
|
||||
suspend fun saveDraft(
|
||||
draftId: Int,
|
||||
accountId: Long,
|
||||
inReplyToId: String?,
|
||||
content: String?,
|
||||
contentWarning: String?,
|
||||
sensitive: Boolean,
|
||||
visibility: Status.Visibility,
|
||||
mediaUris: List<String>,
|
||||
mediaDescriptions: List<String?>,
|
||||
poll: NewPoll?,
|
||||
failedToSend: Boolean
|
||||
) = withContext(Dispatchers.IO) {
|
||||
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
||||
|
||||
val externalFilesDir = context.getExternalFilesDir("Tusky")
|
||||
if (externalFilesDir == null || !(externalFilesDir.exists())) {
|
||||
Log.e("DraftHelper", "Error obtaining directory to save media.")
|
||||
throw Exception()
|
||||
}
|
||||
|
||||
if (externalFilesDir == null || !(externalFilesDir.exists())) {
|
||||
Log.e("DraftHelper", "Error obtaining directory to save media.")
|
||||
throw Exception()
|
||||
val draftDirectory = File(externalFilesDir, "Drafts")
|
||||
|
||||
if (!draftDirectory.exists()) {
|
||||
draftDirectory.mkdir()
|
||||
}
|
||||
|
||||
val uris = mediaUris.map { uriString ->
|
||||
uriString.toUri()
|
||||
}.map { uri ->
|
||||
if (uri.isNotInFolder(draftDirectory)) {
|
||||
uri.copyToFolder(draftDirectory)
|
||||
} else {
|
||||
uri
|
||||
}
|
||||
}
|
||||
|
||||
val draftDirectory = File(externalFilesDir, "Drafts")
|
||||
|
||||
if (!draftDirectory.exists()) {
|
||||
draftDirectory.mkdir()
|
||||
val types = uris.map { uri ->
|
||||
val mimeType = context.contentResolver.getType(uri)
|
||||
when (mimeType?.substring(0, mimeType.indexOf('/'))) {
|
||||
"video" -> DraftAttachment.Type.VIDEO
|
||||
"image" -> DraftAttachment.Type.IMAGE
|
||||
"audio" -> DraftAttachment.Type.AUDIO
|
||||
else -> throw IllegalStateException("unknown media type")
|
||||
}
|
||||
}
|
||||
|
||||
val uris = mediaUris.map { uriString ->
|
||||
uriString.toUri()
|
||||
}.map { uri ->
|
||||
if (uri.isNotInFolder(draftDirectory)) {
|
||||
uri.copyToFolder(draftDirectory)
|
||||
} else {
|
||||
uri
|
||||
}
|
||||
}
|
||||
|
||||
val types = uris.map { uri ->
|
||||
val mimeType = context.contentResolver.getType(uri)
|
||||
when (mimeType?.substring(0, mimeType.indexOf('/'))) {
|
||||
"video" -> DraftAttachment.Type.VIDEO
|
||||
"image" -> DraftAttachment.Type.IMAGE
|
||||
"audio" -> DraftAttachment.Type.AUDIO
|
||||
else -> throw IllegalStateException("unknown media type")
|
||||
}
|
||||
}
|
||||
|
||||
val attachments: MutableList<DraftAttachment> = mutableListOf()
|
||||
for (i in mediaUris.indices) {
|
||||
attachments.add(
|
||||
DraftAttachment(
|
||||
uriString = uris[i].toString(),
|
||||
description = mediaDescriptions[i],
|
||||
type = types[i]
|
||||
)
|
||||
val attachments: MutableList<DraftAttachment> = mutableListOf()
|
||||
for (i in mediaUris.indices) {
|
||||
attachments.add(
|
||||
DraftAttachment(
|
||||
uriString = uris[i].toString(),
|
||||
description = mediaDescriptions[i],
|
||||
type = types[i]
|
||||
)
|
||||
}
|
||||
|
||||
DraftEntity(
|
||||
id = draftId,
|
||||
accountId = accountId,
|
||||
inReplyToId = inReplyToId,
|
||||
content = content,
|
||||
contentWarning = contentWarning,
|
||||
sensitive = sensitive,
|
||||
visibility = visibility,
|
||||
attachments = attachments,
|
||||
poll = poll,
|
||||
failedToSend = failedToSend
|
||||
)
|
||||
}
|
||||
|
||||
}.flatMapCompletable { draft ->
|
||||
draftDao.insertOrReplace(draft)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
val draft = DraftEntity(
|
||||
id = draftId,
|
||||
accountId = accountId,
|
||||
inReplyToId = inReplyToId,
|
||||
content = content,
|
||||
contentWarning = contentWarning,
|
||||
sensitive = sensitive,
|
||||
visibility = visibility,
|
||||
attachments = attachments,
|
||||
poll = poll,
|
||||
failedToSend = failedToSend
|
||||
)
|
||||
|
||||
draftDao.insertOrReplace(draft)
|
||||
}
|
||||
|
||||
fun deleteDraftAndAttachments(draftId: Int): Completable {
|
||||
return draftDao.find(draftId)
|
||||
.flatMapCompletable { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}
|
||||
suspend fun deleteDraftAndAttachments(draftId: Int) {
|
||||
draftDao.find(draftId)?.let { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteDraftAndAttachments(draft: DraftEntity): Completable {
|
||||
return deleteAttachments(draft)
|
||||
.andThen(draftDao.delete(draft.id))
|
||||
suspend fun deleteDraftAndAttachments(draft: DraftEntity) {
|
||||
deleteAttachments(draft)
|
||||
draftDao.delete(draft.id)
|
||||
}
|
||||
|
||||
fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
|
||||
draftDao.loadDraftsSingle(accountId)
|
||||
.flatMapObservable { Observable.fromIterable(it) }
|
||||
.flatMapCompletable { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
suspend fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) {
|
||||
draftDao.loadDrafts(accountId).forEach { draft ->
|
||||
deleteDraftAndAttachments(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAttachments(draft: DraftEntity): Completable {
|
||||
return Completable.fromCallable {
|
||||
suspend fun deleteAttachments(draft: DraftEntity) {
|
||||
withContext(Dispatchers.IO) {
|
||||
draft.attachments.forEach { attachment ->
|
||||
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
|
||||
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
|
||||
}
|
||||
}
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Uri.isNotInFolder(folder: File): Boolean {
|
||||
|
|
@ -171,5 +162,4 @@ class DraftHelper @Inject constructor(
|
|||
IOUtils.copyToFile(contentResolver, this, file)
|
||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,18 +28,17 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.db.DraftAttachment
|
||||
|
||||
class DraftMediaAdapter(
|
||||
private val attachmentClick: () -> Unit
|
||||
private val attachmentClick: () -> Unit
|
||||
) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>(
|
||||
object: DiffUtil.ItemCallback<DraftAttachment>() {
|
||||
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
object : DiffUtil.ItemCallback<DraftAttachment>() {
|
||||
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder {
|
||||
|
|
@ -52,24 +51,24 @@ class DraftMediaAdapter(
|
|||
holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
||||
} else {
|
||||
Glide.with(holder.itemView.context)
|
||||
.load(attachment.uri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.dontAnimate()
|
||||
.into(holder.imageView)
|
||||
.load(attachment.uri)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.dontAnimate()
|
||||
.into(holder.imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class DraftMediaViewHolder(val imageView: ImageView)
|
||||
: RecyclerView.ViewHolder(imageView) {
|
||||
inner class DraftMediaViewHolder(val imageView: ImageView) :
|
||||
RecyclerView.ViewHolder(imageView) {
|
||||
init {
|
||||
val thumbnailViewSize =
|
||||
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
||||
val margin = itemView.context.resources
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||
val marginBottom = itemView.context.resources
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
||||
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
||||
layoutParams.setMargins(margin, 0, margin, marginBottom)
|
||||
imageView.layoutParams = layoutParams
|
||||
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
|
|
@ -78,4 +77,4 @@ class DraftMediaAdapter(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,28 +19,26 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.SavedTootActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
||||
import com.keylesspalace.tusky.db.DraftEntity
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import retrofit2.HttpException
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -54,10 +52,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
private lateinit var binding: ActivityDraftsBinding
|
||||
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
||||
|
||||
private var oldDraftsButton: MenuItem? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityDraftsBinding.inflate(layoutInflater)
|
||||
|
|
@ -70,7 +65,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
setDisplayShowHomeEnabled(true)
|
||||
}
|
||||
|
||||
binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status)
|
||||
binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_drafts)
|
||||
|
||||
val adapter = DraftsAdapter(this)
|
||||
|
||||
|
|
@ -80,44 +75,15 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
|
||||
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
||||
|
||||
viewModel.drafts.observe(this) { draftList ->
|
||||
if (draftList.isEmpty()) {
|
||||
binding.draftsRecyclerView.hide()
|
||||
binding.draftsErrorMessageView.show()
|
||||
} else {
|
||||
binding.draftsRecyclerView.show()
|
||||
binding.draftsErrorMessageView.hide()
|
||||
adapter.submitList(draftList)
|
||||
lifecycleScope.launch {
|
||||
viewModel.drafts.collectLatest { draftData ->
|
||||
adapter.submitData(draftData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.drafts, menu)
|
||||
oldDraftsButton = menu.findItem(R.id.action_old_drafts)
|
||||
viewModel.showOldDraftsButton()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { showOldDraftsButton ->
|
||||
oldDraftsButton?.isVisible = showOldDraftsButton
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
R.id.action_old_drafts -> {
|
||||
val intent = Intent(this, SavedTootActivity::class.java)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
return true
|
||||
}
|
||||
adapter.addLoadStateListener {
|
||||
binding.draftsErrorMessageView.visible(adapter.itemCount == 0)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onOpenDraft(draft: DraftEntity) {
|
||||
|
|
@ -125,27 +91,28 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
if (draft.inReplyToId != null) {
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
viewModel.getToot(draft.inReplyToId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this)
|
||||
.subscribe({ status ->
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this))
|
||||
.subscribe(
|
||||
{ status ->
|
||||
val composeOptions = ComposeActivity.ComposeOptions(
|
||||
draftId = draft.id,
|
||||
tootText = draft.content,
|
||||
contentWarning = draft.contentWarning,
|
||||
inReplyToId = draft.inReplyToId,
|
||||
replyingStatusContent = status.content.toString(),
|
||||
replyingStatusAuthor = status.account.localUsername,
|
||||
draftAttachments = draft.attachments,
|
||||
poll = draft.poll,
|
||||
sensitive = draft.sensitive,
|
||||
visibility = draft.visibility
|
||||
draftId = draft.id,
|
||||
tootText = draft.content,
|
||||
contentWarning = draft.contentWarning,
|
||||
inReplyToId = draft.inReplyToId,
|
||||
replyingStatusContent = status.content.toString(),
|
||||
replyingStatusAuthor = status.account.localUsername,
|
||||
draftAttachments = draft.attachments,
|
||||
poll = draft.poll,
|
||||
sensitive = draft.sensitive,
|
||||
visibility = draft.visibility
|
||||
)
|
||||
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
|
||||
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
||||
|
||||
}, { throwable ->
|
||||
},
|
||||
{ throwable ->
|
||||
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
|
||||
|
|
@ -158,9 +125,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
openDraftWithoutReply(draft)
|
||||
} else {
|
||||
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
} else {
|
||||
openDraftWithoutReply(draft)
|
||||
}
|
||||
|
|
@ -168,13 +136,13 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
|
||||
private fun openDraftWithoutReply(draft: DraftEntity) {
|
||||
val composeOptions = ComposeActivity.ComposeOptions(
|
||||
draftId = draft.id,
|
||||
tootText = draft.content,
|
||||
contentWarning = draft.contentWarning,
|
||||
draftAttachments = draft.attachments,
|
||||
poll = draft.poll,
|
||||
sensitive = draft.sensitive,
|
||||
visibility = draft.visibility
|
||||
draftId = draft.id,
|
||||
tootText = draft.content,
|
||||
contentWarning = draft.contentWarning,
|
||||
draftAttachments = draft.attachments,
|
||||
poll = draft.poll,
|
||||
sensitive = draft.sensitive,
|
||||
visibility = draft.visibility
|
||||
)
|
||||
|
||||
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
||||
|
|
@ -183,10 +151,10 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
|||
override fun onDeleteDraft(draft: DraftEntity) {
|
||||
viewModel.deleteDraft(draft)
|
||||
Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_undo) {
|
||||
viewModel.restoreDraft(draft)
|
||||
}
|
||||
.show()
|
||||
.setAction(R.string.action_undo) {
|
||||
viewModel.restoreDraft(draft)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ package com.keylesspalace.tusky.components.drafts
|
|||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.paging.PagingDataAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
|
@ -34,17 +34,17 @@ interface DraftActionListener {
|
|||
}
|
||||
|
||||
class DraftsAdapter(
|
||||
private val listener: DraftActionListener
|
||||
) : PagedListAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
|
||||
object : DiffUtil.ItemCallback<DraftEntity>() {
|
||||
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
private val listener: DraftActionListener
|
||||
) : PagingDataAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>(
|
||||
object : DiffUtil.ItemCallback<DraftEntity>() {
|
||||
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemDraftBinding> {
|
||||
|
|
@ -87,6 +87,5 @@ class DraftsAdapter(
|
|||
holder.binding.draftPoll.hide()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,14 +16,17 @@
|
|||
package com.keylesspalace.tusky.components.drafts
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.paging.toLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.DraftEntity
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class DraftsViewModel @Inject constructor(
|
||||
|
|
@ -33,27 +36,28 @@ class DraftsViewModel @Inject constructor(
|
|||
val draftHelper: DraftHelper
|
||||
) : ViewModel() {
|
||||
|
||||
val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20)
|
||||
val drafts = Pager(
|
||||
config = PagingConfig(pageSize = 20),
|
||||
pagingSourceFactory = { database.draftDao().draftsPagingSource(accountManager.activeAccount?.id!!) }
|
||||
).flow
|
||||
.cachedIn(viewModelScope)
|
||||
|
||||
private val deletedDrafts: MutableList<DraftEntity> = mutableListOf()
|
||||
|
||||
fun showOldDraftsButton(): Observable<Boolean> {
|
||||
return database.tootDao().savedTootCount()
|
||||
.map { count -> count > 0 }
|
||||
}
|
||||
|
||||
fun deleteDraft(draft: DraftEntity) {
|
||||
// this does not immediately delete media files to avoid unnecessary file operations
|
||||
// in case the user decides to restore the draft
|
||||
database.draftDao().delete(draft.id)
|
||||
.subscribe()
|
||||
deletedDrafts.add(draft)
|
||||
viewModelScope.launch {
|
||||
database.draftDao().delete(draft.id)
|
||||
deletedDrafts.add(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreDraft(draft: DraftEntity) {
|
||||
database.draftDao().insertOrReplace(draft)
|
||||
.subscribe()
|
||||
deletedDrafts.remove(draft)
|
||||
viewModelScope.launch {
|
||||
database.draftDao().insertOrReplace(draft)
|
||||
deletedDrafts.remove(draft)
|
||||
}
|
||||
}
|
||||
|
||||
fun getToot(tootId: String): Single<Status> {
|
||||
|
|
@ -61,9 +65,10 @@ class DraftsViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onCleared() {
|
||||
deletedDrafts.forEach {
|
||||
draftHelper.deleteAttachments(it).subscribe()
|
||||
viewModelScope.launch {
|
||||
deletedDrafts.forEach {
|
||||
draftHelper.deleteAttachments(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import dagger.android.DispatchingAndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class InstanceListActivity: BaseActivity(), HasAndroidInjector {
|
||||
class InstanceListActivity : BaseActivity(), HasAndroidInjector {
|
||||
|
||||
@Inject
|
||||
lateinit var androidInjector: DispatchingAndroidInjector<Any>
|
||||
|
|
@ -17,7 +17,7 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val binding = ActivityAccountListBinding.inflate(layoutInflater)
|
||||
setContentView(R.layout.activity_account_list)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||
supportActionBar?.apply {
|
||||
|
|
@ -27,11 +27,10 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector {
|
|||
}
|
||||
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, InstanceListFragment())
|
||||
.commit()
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, InstanceListFragment())
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun androidInjector() = androidInjector
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import com.keylesspalace.tusky.databinding.ItemMutedDomainBinding
|
|||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
|
||||
class DomainMutesAdapter(
|
||||
private val actionListener: InstanceActionListener
|
||||
): RecyclerView.Adapter<BindingHolder<ItemMutedDomainBinding>>() {
|
||||
private val actionListener: InstanceActionListener
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemMutedDomainBinding>>() {
|
||||
|
||||
var instances: MutableList<String> = mutableListOf()
|
||||
var bottomLoading: Boolean = false
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import androidx.lifecycle.Lifecycle
|
|||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import autodispose2.autoDispose
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter
|
||||
|
|
@ -20,16 +22,14 @@ import com.keylesspalace.tusky.util.hide
|
|||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
|
||||
class InstanceListFragment : Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener {
|
||||
|
||||
@Inject
|
||||
lateinit var api: MastodonApi
|
||||
|
|
@ -65,7 +65,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
|
||||
override fun mute(mute: Boolean, instance: String, position: Int) {
|
||||
if (mute) {
|
||||
api.blockDomain(instance).enqueue(object: Callback<Any> {
|
||||
api.blockDomain(instance).enqueue(object : Callback<Any> {
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error muting domain $instance")
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
}
|
||||
})
|
||||
} else {
|
||||
api.unblockDomain(instance).enqueue(object: Callback<Any> {
|
||||
api.unblockDomain(instance).enqueue(object : Callback<Any> {
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error unmuting domain $instance")
|
||||
}
|
||||
|
|
@ -88,10 +88,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
if (response.isSuccessful) {
|
||||
adapter.removeItem(position)
|
||||
Snackbar.make(binding.recyclerView, getString(R.string.confirmation_domain_unmuted, instance), Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_undo) {
|
||||
mute(true, instance, position)
|
||||
}
|
||||
.show()
|
||||
.setAction(R.string.action_undo) {
|
||||
mute(true, instance, position)
|
||||
}
|
||||
.show()
|
||||
} else {
|
||||
Log.e(TAG, "Error unmuting domain $instance")
|
||||
}
|
||||
|
|
@ -112,9 +112,10 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
}
|
||||
|
||||
api.domainBlocks(id, bottomId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({ response ->
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
{ response ->
|
||||
val instances = response.body()
|
||||
|
||||
if (response.isSuccessful && instances != null) {
|
||||
|
|
@ -122,9 +123,11 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
} else {
|
||||
onFetchInstancesFailure(Exception(response.message()))
|
||||
}
|
||||
}, {throwable ->
|
||||
},
|
||||
{ throwable ->
|
||||
onFetchInstancesFailure(throwable)
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun onFetchInstancesSuccess(instances: List<String>, linkHeader: String?) {
|
||||
|
|
@ -141,9 +144,9 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
if (adapter.itemCount == 0) {
|
||||
binding.messageView.show()
|
||||
binding.messageView.setup(
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.message_empty,
|
||||
null
|
||||
R.drawable.elephant_friend_empty,
|
||||
R.string.message_empty,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
binding.messageView.hide()
|
||||
|
|
@ -174,4 +177,4 @@ class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectabl
|
|||
companion object {
|
||||
private const val TAG = "InstanceList" // logging tag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,4 +2,4 @@ package com.keylesspalace.tusky.components.instancemute.interfaces
|
|||
|
||||
interface InstanceActionListener {
|
||||
fun mute(mute: Boolean, instance: String, position: Int)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
|
|
@ -10,9 +11,9 @@ import com.keylesspalace.tusky.util.isLessThan
|
|||
import javax.inject.Inject
|
||||
|
||||
class NotificationFetcher @Inject constructor(
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
private val notifier: Notifier
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
private val context: Context
|
||||
) {
|
||||
fun fetchAndShow() {
|
||||
for (account in accountManager.getAllAccountsOrderedByActive()) {
|
||||
|
|
@ -20,7 +21,7 @@ class NotificationFetcher @Inject constructor(
|
|||
try {
|
||||
val notifications = fetchNotifications(account)
|
||||
notifications.forEachIndexed { index, notification ->
|
||||
notifier.show(notification, account, index == 0)
|
||||
NotificationHelper.make(context, notification, account, index == 0)
|
||||
}
|
||||
accountManager.saveAccount(account)
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -39,9 +40,9 @@ class NotificationFetcher @Inject constructor(
|
|||
}
|
||||
Log.d(TAG, "getting Notifications for " + account.fullName)
|
||||
val notifications = mastodonApi.notificationsWithAuth(
|
||||
authHeader,
|
||||
account.domain,
|
||||
account.lastNotificationId
|
||||
authHeader,
|
||||
account.domain,
|
||||
account.lastNotificationId
|
||||
).blockingGet()
|
||||
|
||||
val newId = account.lastNotificationId
|
||||
|
|
@ -63,9 +64,9 @@ class NotificationFetcher @Inject constructor(
|
|||
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
|
||||
return try {
|
||||
val allMarkers = mastodonApi.markersWithAuth(
|
||||
authHeader,
|
||||
account.domain,
|
||||
listOf("notifications")
|
||||
authHeader,
|
||||
account.domain,
|
||||
listOf("notifications")
|
||||
).blockingGet()
|
||||
val notificationMarker = allMarkers["notifications"]
|
||||
Log.d(TAG, "Fetched marker: $notificationMarker")
|
||||
|
|
@ -79,4 +80,4 @@ class NotificationFetcher @Inject constructor(
|
|||
companion object {
|
||||
const val TAG = "NotificationFetcher"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,8 +70,8 @@ import java.util.List;
|
|||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
||||
|
||||
|
|
@ -316,7 +316,7 @@ public class NotificationHelper {
|
|||
Status actionableStatus = status.getActionableStatus();
|
||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||
String contentWarning = actionableStatus.getSpoilerText();
|
||||
Status.Mention[] mentions = actionableStatus.getMentions();
|
||||
List<Status.Mention> mentions = actionableStatus.getMentions();
|
||||
List<String> mentionedUsernames = new ArrayList<>();
|
||||
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
||||
for (Status.Mention mention : mentions) {
|
||||
|
|
@ -381,7 +381,6 @@ public class NotificationHelper {
|
|||
|
||||
NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName());
|
||||
|
||||
//noinspection ConstantConditions
|
||||
notificationManager.createNotificationChannelGroup(channelGroup);
|
||||
|
||||
for (int i = 0; i < channelIds.length; i++) {
|
||||
|
|
@ -660,9 +659,12 @@ public class NotificationHelper {
|
|||
StringBuilder builder = new StringBuilder(notification.getStatus().getContent());
|
||||
builder.append('\n');
|
||||
Poll poll = notification.getStatus().getPoll();
|
||||
for(PollOption option: poll.getOptions()) {
|
||||
List<PollOption> options = poll.getOptions();
|
||||
for(int i = 0; i < options.size(); ++i) {
|
||||
PollOption option = options.get(i);
|
||||
builder.append(buildDescription(option.getTitle(),
|
||||
PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()),
|
||||
poll.getOwnVotes() != null && poll.getOwnVotes().contains(i),
|
||||
context));
|
||||
builder.append('\n');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ import androidx.work.WorkerParameters
|
|||
import javax.inject.Inject
|
||||
|
||||
class NotificationWorker(
|
||||
context: Context,
|
||||
params: WorkerParameters,
|
||||
private val notificationsFetcher: NotificationFetcher
|
||||
context: Context,
|
||||
params: WorkerParameters,
|
||||
private val notificationsFetcher: NotificationFetcher
|
||||
) : Worker(context, params) {
|
||||
|
||||
override fun doWork(): Result {
|
||||
|
|
@ -35,13 +35,13 @@ class NotificationWorker(
|
|||
}
|
||||
|
||||
class NotificationWorkerFactory @Inject constructor(
|
||||
private val notificationsFetcher: NotificationFetcher
|
||||
private val notificationsFetcher: NotificationFetcher
|
||||
) : WorkerFactory() {
|
||||
|
||||
override fun createWorker(
|
||||
appContext: Context,
|
||||
workerClassName: String,
|
||||
workerParameters: WorkerParameters
|
||||
appContext: Context,
|
||||
workerClassName: String,
|
||||
workerParameters: WorkerParameters
|
||||
): ListenableWorker? {
|
||||
if (workerClassName == NotificationWorker::class.java.name) {
|
||||
return NotificationWorker(appContext, workerParameters, notificationsFetcher)
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.keylesspalace.tusky.components.notifications
|
||||
|
||||
import android.content.Context
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.entity.Notification
|
||||
|
||||
/**
|
||||
* Shows notifications.
|
||||
*/
|
||||
interface Notifier {
|
||||
fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean)
|
||||
}
|
||||
|
||||
class SystemNotifier(
|
||||
private val context: Context
|
||||
) : Notifier {
|
||||
override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) {
|
||||
NotificationHelper.make(context, notification, account, isFirstInBatch)
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,11 @@ import android.util.Log
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.*
|
||||
import com.keylesspalace.tusky.AccountListActivity
|
||||
import com.keylesspalace.tusky.BuildConfig
|
||||
import com.keylesspalace.tusky.FiltersActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||
|
|
@ -33,7 +37,12 @@ import com.keylesspalace.tusky.entity.Account
|
|||
import com.keylesspalace.tusky.entity.Filter
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.settings.*
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.settings.listPreference
|
||||
import com.keylesspalace.tusky.settings.makePreferenceScreen
|
||||
import com.keylesspalace.tusky.settings.preference
|
||||
import com.keylesspalace.tusky.settings.preferenceCategory
|
||||
import com.keylesspalace.tusky.settings.switchPreference
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
|
|
@ -75,8 +84,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
setOnPreferenceClickListener {
|
||||
val intent = Intent(context, TabPreferenceActivity::class.java)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
||||
R.anim.slide_to_left)
|
||||
activity?.overridePendingTransition(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.slide_to_left
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -88,8 +99,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
val intent = Intent(context, AccountListActivity::class.java)
|
||||
intent.putExtra("type", AccountListActivity.Type.MUTES)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
||||
R.anim.slide_to_left)
|
||||
activity?.overridePendingTransition(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.slide_to_left
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -104,8 +117,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
val intent = Intent(context, AccountListActivity::class.java)
|
||||
intent.putExtra("type", AccountListActivity.Type.BLOCKS)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
||||
R.anim.slide_to_left)
|
||||
activity?.overridePendingTransition(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.slide_to_left
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -116,8 +131,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
setOnPreferenceClickListener {
|
||||
val intent = Intent(context, InstanceListActivity::class.java)
|
||||
activity?.startActivity(intent)
|
||||
activity?.overridePendingTransition(R.anim.slide_from_right,
|
||||
R.anim.slide_to_left)
|
||||
activity?.overridePendingTransition(
|
||||
R.anim.slide_from_right,
|
||||
R.anim.slide_to_left
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -130,7 +147,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
key = PrefKeys.DEFAULT_POST_PRIVACY
|
||||
setSummaryProvider { entry }
|
||||
val visibility = accountManager.activeAccount?.defaultPostPrivacy
|
||||
?: Status.Visibility.PUBLIC
|
||||
?: Status.Visibility.PUBLIC
|
||||
value = visibility.serverString()
|
||||
setIcon(getIconForVisibility(visibility))
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -147,7 +164,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
key = PrefKeys.DEFAULT_MEDIA_SENSITIVITY
|
||||
isSingleLineTitle = false
|
||||
val sensitivity = accountManager.activeAccount?.defaultMediaSensitivity
|
||||
?: false
|
||||
?: false
|
||||
setDefaultValue(sensitivity)
|
||||
setIcon(getIconForSensitivity(sensitivity))
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
|
|
@ -201,8 +218,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
preference {
|
||||
setTitle(R.string.pref_title_public_filter_keywords)
|
||||
setOnPreferenceClickListener {
|
||||
launchFilterActivity(Filter.PUBLIC,
|
||||
R.string.pref_title_public_filter_keywords)
|
||||
launchFilterActivity(
|
||||
Filter.PUBLIC,
|
||||
R.string.pref_title_public_filter_keywords
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -226,8 +245,10 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
preference {
|
||||
setTitle(R.string.pref_title_thread_filter_keywords)
|
||||
setOnPreferenceClickListener {
|
||||
launchFilterActivity(Filter.THREAD,
|
||||
R.string.pref_title_thread_filter_keywords)
|
||||
launchFilterActivity(
|
||||
Filter.THREAD,
|
||||
R.string.pref_title_thread_filter_keywords
|
||||
)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -255,7 +276,6 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
it.startActivity(intent)
|
||||
it.overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -268,36 +288,35 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
|
||||
private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null) {
|
||||
mastodonApi.accountUpdateSource(visibility, sensitive)
|
||||
.enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
||||
val account = response.body()
|
||||
if (response.isSuccessful && account != null) {
|
||||
.enqueue(object : Callback<Account> {
|
||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
||||
val account = response.body()
|
||||
if (response.isSuccessful && account != null) {
|
||||
|
||||
accountManager.activeAccount?.let {
|
||||
it.defaultPostPrivacy = account.source?.privacy
|
||||
?: Status.Visibility.PUBLIC
|
||||
it.defaultMediaSensitivity = account.source?.sensitive ?: false
|
||||
accountManager.saveAccount(it)
|
||||
}
|
||||
} else {
|
||||
Log.e("AccountPreferences", "failed updating settings on server")
|
||||
showErrorSnackbar(visibility, sensitive)
|
||||
accountManager.activeAccount?.let {
|
||||
it.defaultPostPrivacy = account.source?.privacy
|
||||
?: Status.Visibility.PUBLIC
|
||||
it.defaultMediaSensitivity = account.source?.sensitive ?: false
|
||||
accountManager.saveAccount(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
Log.e("AccountPreferences", "failed updating settings on server", t)
|
||||
} else {
|
||||
Log.e("AccountPreferences", "failed updating settings on server")
|
||||
showErrorSnackbar(visibility, sensitive)
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
Log.e("AccountPreferences", "failed updating settings on server", t)
|
||||
showErrorSnackbar(visibility, sensitive)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun showErrorSnackbar(visibility: String?, sensitive: Boolean?) {
|
||||
view?.let { view ->
|
||||
Snackbar.make(view, R.string.pref_failed_to_sync, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) }
|
||||
.show()
|
||||
.setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT
|
|||
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.disposables.Disposable
|
||||
import okhttp3.OkHttpClient
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
|
|
@ -34,8 +34,8 @@ import kotlin.system.exitProcess
|
|||
* This Preference lets the user select their preferred emoji font
|
||||
*/
|
||||
class EmojiPreference(
|
||||
context: Context,
|
||||
private val okHttpClient: OkHttpClient
|
||||
context: Context,
|
||||
private val okHttpClient: OkHttpClient
|
||||
) : Preference(context) {
|
||||
|
||||
private lateinit var selected: EmojiCompatFont
|
||||
|
|
@ -51,7 +51,7 @@ class EmojiPreference(
|
|||
|
||||
// Find out which font is currently active
|
||||
selected = EmojiCompatFont.byId(
|
||||
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
|
||||
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
|
||||
)
|
||||
// We'll use this later to determine if anything has changed
|
||||
original = selected
|
||||
|
|
@ -67,10 +67,10 @@ class EmojiPreference(
|
|||
setupItem(SYSTEM_DEFAULT, binding.itemNomoji)
|
||||
|
||||
AlertDialog.Builder(context)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
||||
|
|
@ -100,32 +100,30 @@ class EmojiPreference(
|
|||
binding.emojiProgress.progress = 0
|
||||
binding.emojiDownloadCancel.show()
|
||||
font.downloadFontFile(context, okHttpClient)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ progress ->
|
||||
// The progress is returned as a float between 0 and 1, or -1 if it could not determined
|
||||
if (progress >= 0) {
|
||||
binding.emojiProgress.isIndeterminate = false
|
||||
val max = binding.emojiProgress.max.toFloat()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
binding.emojiProgress.setProgress((max * progress).toInt(), true)
|
||||
} else {
|
||||
binding.emojiProgress.progress = (max * progress).toInt()
|
||||
}
|
||||
} else {
|
||||
binding.emojiProgress.isIndeterminate = true
|
||||
}
|
||||
},
|
||||
{
|
||||
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
|
||||
updateItem(font, binding)
|
||||
},
|
||||
{
|
||||
finishDownload(font, binding)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ progress ->
|
||||
// The progress is returned as a float between 0 and 1, or -1 if it could not determined
|
||||
if (progress >= 0) {
|
||||
binding.emojiProgress.isIndeterminate = false
|
||||
val max = binding.emojiProgress.max.toFloat()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
binding.emojiProgress.setProgress((max * progress).toInt(), true)
|
||||
} else {
|
||||
binding.emojiProgress.progress = (max * progress).toInt()
|
||||
}
|
||||
).also { downloadDisposables[font.id] = it }
|
||||
|
||||
|
||||
} else {
|
||||
binding.emojiProgress.isIndeterminate = true
|
||||
}
|
||||
},
|
||||
{
|
||||
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
|
||||
updateItem(font, binding)
|
||||
},
|
||||
{
|
||||
finishDownload(font, binding)
|
||||
}
|
||||
).also { downloadDisposables[font.id] = it }
|
||||
}
|
||||
|
||||
private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
||||
|
|
@ -197,10 +195,10 @@ class EmojiPreference(
|
|||
val index = selected.id
|
||||
Log.i(TAG, "saveSelectedFont: Font ID: $index")
|
||||
PreferenceManager
|
||||
.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putInt(key, index)
|
||||
.apply()
|
||||
.getDefaultSharedPreferences(context)
|
||||
.edit()
|
||||
.putInt(key, index)
|
||||
.apply()
|
||||
summary = selected.getDisplay(context)
|
||||
}
|
||||
|
||||
|
|
@ -211,29 +209,31 @@ class EmojiPreference(
|
|||
saveSelectedFont()
|
||||
if (selected !== original || updated) {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.restart_required)
|
||||
.setMessage(R.string.restart_emoji)
|
||||
.setNegativeButton(R.string.later, null)
|
||||
.setPositiveButton(R.string.restart) { _, _ ->
|
||||
// Restart the app
|
||||
// From https://stackoverflow.com/a/17166729/5070653
|
||||
val launchIntent = Intent(context, SplashActivity::class.java)
|
||||
val mPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0x1f973, // This is the codepoint of the party face emoji :D
|
||||
launchIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
mgr.set(
|
||||
AlarmManager.RTC,
|
||||
System.currentTimeMillis() + 100,
|
||||
mPendingIntent)
|
||||
exitProcess(0)
|
||||
}.show()
|
||||
.setTitle(R.string.restart_required)
|
||||
.setMessage(R.string.restart_emoji)
|
||||
.setNegativeButton(R.string.later, null)
|
||||
.setPositiveButton(R.string.restart) { _, _ ->
|
||||
// Restart the app
|
||||
// From https://stackoverflow.com/a/17166729/5070653
|
||||
val launchIntent = Intent(context, SplashActivity::class.java)
|
||||
val mPendingIntent = PendingIntent.getActivity(
|
||||
context,
|
||||
0x1f973, // This is the codepoint of the party face emoji :D
|
||||
launchIntent,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT
|
||||
)
|
||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||
mgr.set(
|
||||
AlarmManager.RTC,
|
||||
System.currentTimeMillis() + 100,
|
||||
mPendingIntent
|
||||
)
|
||||
exitProcess(0)
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EmojiPreference"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switchPreference {
|
||||
setTitle(R.string.pref_title_notification_filter_subscriptions)
|
||||
key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS
|
||||
|
|
@ -176,5 +176,4 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
|||
return NotificationPreferencesFragment()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,8 +36,10 @@ import dagger.android.DispatchingAndroidInjector
|
|||
import dagger.android.HasAndroidInjector
|
||||
import javax.inject.Inject
|
||||
|
||||
class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
HasAndroidInjector {
|
||||
class PreferencesActivity :
|
||||
BaseActivity(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
HasAndroidInjector {
|
||||
|
||||
@Inject
|
||||
lateinit var eventHub: EventHub
|
||||
|
|
@ -62,36 +64,35 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
|
|||
val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE"
|
||||
|
||||
val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag)
|
||||
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
|
||||
GENERAL_PREFERENCES -> {
|
||||
setTitle(R.string.action_view_preferences)
|
||||
PreferencesFragment.newInstance()
|
||||
}
|
||||
ACCOUNT_PREFERENCES -> {
|
||||
setTitle(R.string.action_view_account_preferences)
|
||||
AccountPreferencesFragment.newInstance()
|
||||
}
|
||||
NOTIFICATION_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_edit_notification_settings)
|
||||
NotificationPreferencesFragment.newInstance()
|
||||
}
|
||||
TAB_FILTER_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_status_tabs)
|
||||
TabFilterPreferencesFragment.newInstance()
|
||||
}
|
||||
PROXY_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_http_proxy_settings)
|
||||
ProxyPreferencesFragment.newInstance()
|
||||
}
|
||||
else -> throw IllegalArgumentException("preferenceType not known")
|
||||
?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) {
|
||||
GENERAL_PREFERENCES -> {
|
||||
setTitle(R.string.action_view_preferences)
|
||||
PreferencesFragment.newInstance()
|
||||
}
|
||||
ACCOUNT_PREFERENCES -> {
|
||||
setTitle(R.string.action_view_account_preferences)
|
||||
AccountPreferencesFragment.newInstance()
|
||||
}
|
||||
NOTIFICATION_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_edit_notification_settings)
|
||||
NotificationPreferencesFragment.newInstance()
|
||||
}
|
||||
TAB_FILTER_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_status_tabs)
|
||||
TabFilterPreferencesFragment.newInstance()
|
||||
}
|
||||
PROXY_PREFERENCES -> {
|
||||
setTitle(R.string.pref_title_http_proxy_settings)
|
||||
ProxyPreferencesFragment.newInstance()
|
||||
}
|
||||
else -> throw IllegalArgumentException("preferenceType not known")
|
||||
}
|
||||
|
||||
supportFragmentManager.commit {
|
||||
replace(R.id.fragment_container, fragment, fragmentTag)
|
||||
}
|
||||
|
||||
restartActivitiesOnExit = intent.getBooleanExtra("restart", false)
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
|
@ -122,10 +123,10 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
|
|||
|
||||
restartActivitiesOnExit = true
|
||||
this.restartCurrentActivity()
|
||||
|
||||
}
|
||||
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
|
||||
"useBlurhash", "showCardsInTimelines", "confirmReblogs", "enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
|
||||
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars", "useBlurhash",
|
||||
"showCardsInTimelines", "confirmReblogs", "confirmFavourites",
|
||||
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
|
||||
restartActivitiesOnExit = true
|
||||
}
|
||||
"language" -> {
|
||||
|
|
@ -179,5 +180,4 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
|
|||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue