move AccountActivity and related classes to a new package (#2216)
* move AccountActivity and related classes to a new package * fix ktlint * fix ktlint * fix imports
This commit is contained in:
parent
1b6bcd976e
commit
fe350ccf36
15 changed files with 24 additions and 18 deletions
|
|
@ -0,0 +1,873 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.account
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.annotation.ColorInt
|
||||
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.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.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
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
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.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.DefaultTextWatcher
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.view.showMuteAccountDialog
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import java.text.NumberFormat
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInjector, LinkListener {
|
||||
|
||||
@Inject
|
||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
||||
private val viewModel: AccountViewModel by viewModels { viewModelFactory }
|
||||
|
||||
private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate)
|
||||
|
||||
private lateinit var accountFieldAdapter: AccountFieldAdapter
|
||||
|
||||
private var followState: FollowState = FollowState.NOT_FOLLOWING
|
||||
private var blocking: Boolean = false
|
||||
private var muting: Boolean = false
|
||||
private var blockingDomain: Boolean = false
|
||||
private var showingReblogs: Boolean = false
|
||||
private var subscribing: Boolean = false
|
||||
private var loadedAccount: Account? = null
|
||||
|
||||
private var animateAvatar: Boolean = false
|
||||
private var animateEmojis: Boolean = false
|
||||
|
||||
// fields for scroll animation
|
||||
private var hideFab: Boolean = false
|
||||
private var oldOffset: Int = 0
|
||||
@ColorInt
|
||||
private var toolbarColor: Int = 0
|
||||
@ColorInt
|
||||
private var statusBarColorTransparent: Int = 0
|
||||
@ColorInt
|
||||
private var statusBarColorOpaque: Int = 0
|
||||
|
||||
private var avatarSize: Float = 0f
|
||||
@Px
|
||||
private var titleVisibleHeight: Int = 0
|
||||
private lateinit var domain: String
|
||||
|
||||
private enum class FollowState {
|
||||
NOT_FOLLOWING,
|
||||
FOLLOWING,
|
||||
REQUESTED
|
||||
}
|
||||
|
||||
private lateinit var adapter: AccountPagerAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
loadResources()
|
||||
makeNotificationBarTransparent()
|
||||
setContentView(binding.root)
|
||||
|
||||
// Obtain information to fill out the profile.
|
||||
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
|
||||
|
||||
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
|
||||
animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
hideFab = sharedPrefs.getBoolean("fabHide", false)
|
||||
|
||||
handleWindowInsets()
|
||||
setupToolbar()
|
||||
setupTabs()
|
||||
setupAccountViews()
|
||||
setupRefreshLayout()
|
||||
subscribeObservables()
|
||||
|
||||
if (viewModel.isSelf) {
|
||||
updateButtons()
|
||||
binding.saveNoteInfo.hide()
|
||||
} else {
|
||||
binding.saveNoteInfo.visibility = View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load colors and dimensions from resources
|
||||
*/
|
||||
private fun loadResources() {
|
||||
toolbarColor = ThemeUtils.getColor(this, R.attr.colorSurface)
|
||||
statusBarColorTransparent = ContextCompat.getColor(this, R.color.transparent_statusbar_background)
|
||||
statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark)
|
||||
avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size)
|
||||
titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup account widgets visibility and actions
|
||||
*/
|
||||
private fun setupAccountViews() {
|
||||
// Initialise the default UI states.
|
||||
binding.accountFloatingActionButton.hide()
|
||||
binding.accountFollowButton.hide()
|
||||
binding.accountMuteButton.hide()
|
||||
binding.accountFollowsYouTextView.hide()
|
||||
|
||||
// setup the RecyclerView for the account fields
|
||||
accountFieldAdapter = AccountFieldAdapter(this, animateEmojis)
|
||||
binding.accountFieldList.isNestedScrollingEnabled = false
|
||||
binding.accountFieldList.layoutManager = LinearLayoutManager(this)
|
||||
binding.accountFieldList.adapter = accountFieldAdapter
|
||||
|
||||
val accountListClickListener = { v: View ->
|
||||
val type = when (v.id) {
|
||||
R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS
|
||||
R.id.accountFollowing -> AccountListActivity.Type.FOLLOWS
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
val accountListIntent = AccountListActivity.newIntent(this, type, viewModel.accountId)
|
||||
startActivityWithSlideInAnimation(accountListIntent)
|
||||
}
|
||||
binding.accountFollowers.setOnClickListener(accountListClickListener)
|
||||
binding.accountFollowing.setOnClickListener(accountListClickListener)
|
||||
|
||||
binding.accountStatuses.setOnClickListener {
|
||||
// Make nice ripple effect on tab
|
||||
binding.accountTabLayout.getTabAt(0)!!.select()
|
||||
val poorTabView = (binding.accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0)
|
||||
poorTabView.isPressed = true
|
||||
binding.accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300)
|
||||
}
|
||||
|
||||
// If wellbeing mode is enabled, follow stats and posts count should be hidden
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
|
||||
|
||||
if (wellbeingEnabled) {
|
||||
binding.accountStatuses.hide()
|
||||
binding.accountFollowers.hide()
|
||||
binding.accountFollowing.hide()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init timeline tabs
|
||||
*/
|
||||
private fun setupTabs() {
|
||||
// Setup the tabs and timeline pager.
|
||||
adapter = AccountPagerAdapter(this, viewModel.accountId)
|
||||
|
||||
binding.accountFragmentViewPager.adapter = adapter
|
||||
binding.accountFragmentViewPager.offscreenPageLimit = 2
|
||||
|
||||
val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media))
|
||||
|
||||
TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position ->
|
||||
tab.text = pageTitles[position]
|
||||
}.attach()
|
||||
|
||||
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
|
||||
binding.accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin))
|
||||
|
||||
binding.accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
tab?.position?.let { position ->
|
||||
(adapter.getFragment(position) as? ReselectableFragment)?.onReselect()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab?) {}
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab?) {}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
val bottom = insets.getInsets(systemBars()).bottom
|
||||
binding.accountCoordinatorLayout.updatePadding(bottom = bottom)
|
||||
|
||||
WindowInsetsCompat.CONSUMED
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupToolbar() {
|
||||
// Setup the toolbar.
|
||||
setSupportActionBar(binding.accountToolbar)
|
||||
supportActionBar?.run {
|
||||
setDisplayHomeAsUpEnabled(true)
|
||||
setDisplayShowHomeEnabled(true)
|
||||
setDisplayShowTitleEnabled(false)
|
||||
}
|
||||
|
||||
val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation)
|
||||
|
||||
val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
|
||||
toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT)
|
||||
binding.accountToolbar.background = toolbarBackground
|
||||
|
||||
binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation)
|
||||
|
||||
val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation).apply {
|
||||
fillColor = ColorStateList.valueOf(toolbarColor)
|
||||
elevation = appBarElevation
|
||||
shapeAppearanceModel = ShapeAppearanceModel.builder()
|
||||
.setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius))
|
||||
.build()
|
||||
}
|
||||
binding.accountAvatarImageView.background = avatarBackground
|
||||
|
||||
// Add a listener to change the toolbar icon color when it enters/exits its collapsed state.
|
||||
binding.accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
|
||||
|
||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
||||
|
||||
if (verticalOffset == oldOffset) {
|
||||
return
|
||||
}
|
||||
oldOffset = verticalOffset
|
||||
|
||||
if (titleVisibleHeight + verticalOffset < 0) {
|
||||
supportActionBar?.setDisplayShowTitleEnabled(true)
|
||||
} else {
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
}
|
||||
|
||||
if (hideFab && !viewModel.isSelf && !blocking) {
|
||||
if (verticalOffset > oldOffset) {
|
||||
binding.accountFloatingActionButton.show()
|
||||
}
|
||||
if (verticalOffset < oldOffset) {
|
||||
binding.accountFloatingActionButton.hide()
|
||||
}
|
||||
}
|
||||
|
||||
val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize
|
||||
|
||||
binding.accountAvatarImageView.scaleX = scaledAvatarSize
|
||||
binding.accountAvatarImageView.scaleY = scaledAvatarSize
|
||||
|
||||
binding.accountAvatarImageView.visible(scaledAvatarSize > 0)
|
||||
|
||||
val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost(1f)
|
||||
|
||||
window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int
|
||||
|
||||
val evaluatedToolbarColor = argbEvaluator.evaluate(transparencyPercent, Color.TRANSPARENT, toolbarColor) as Int
|
||||
|
||||
toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor)
|
||||
|
||||
binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun makeNotificationBarTransparent() {
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
window.statusBarColor = statusBarColorTransparent
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to data loaded at the view model
|
||||
*/
|
||||
private fun subscribeObservables() {
|
||||
viewModel.accountData.observe(this) {
|
||||
when (it) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.relationshipData.observe(this) {
|
||||
val relation = it?.data
|
||||
if (relation != null) {
|
||||
onRelationshipChanged(relation)
|
||||
}
|
||||
|
||||
if (it is Error) {
|
||||
Snackbar.make(binding.accountCoordinatorLayout, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry) { viewModel.refresh() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
viewModel.accountFieldData.observe(
|
||||
this,
|
||||
{
|
||||
accountFieldAdapter.fields = it
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
}
|
||||
)
|
||||
viewModel.noteSaved.observe(this) {
|
||||
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup swipe to refresh layout
|
||||
*/
|
||||
private fun setupRefreshLayout() {
|
||||
binding.swipeToRefreshLayout.setOnRefreshListener {
|
||||
viewModel.refresh()
|
||||
adapter.refreshContent()
|
||||
}
|
||||
viewModel.isRefreshing.observe(
|
||||
this,
|
||||
{ isRefreshing ->
|
||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||
}
|
||||
)
|
||||
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
}
|
||||
|
||||
private fun onAccountChanged(account: Account?) {
|
||||
loadedAccount = account ?: return
|
||||
|
||||
val usernameFormatted = getString(R.string.status_username_format, account.username)
|
||||
binding.accountUsernameTextView.text = usernameFormatted
|
||||
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
||||
|
||||
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||
LinkHelper.setClickableText(binding.accountNoteTextView, emojifiedNote, null, this)
|
||||
|
||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
|
||||
binding.accountLockedImageView.visible(account.locked)
|
||||
binding.accountBadgeTextView.visible(account.bot)
|
||||
|
||||
updateAccountAvatar()
|
||||
updateToolbar()
|
||||
updateMovedAccount()
|
||||
updateRemoteAccount()
|
||||
updateAccountStats()
|
||||
invalidateOptionsMenu()
|
||||
|
||||
binding.accountMuteButton.setOnClickListener {
|
||||
viewModel.unmuteAccount()
|
||||
updateMuteButton()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load account's avatar and header image
|
||||
*/
|
||||
private fun updateAccountAvatar() {
|
||||
loadedAccount?.let { account ->
|
||||
|
||||
loadAvatar(
|
||||
account.avatar,
|
||||
binding.accountAvatarImageView,
|
||||
resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp),
|
||||
animateAvatar
|
||||
)
|
||||
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(account.header)
|
||||
.centerCrop()
|
||||
.into(binding.accountHeaderImageView)
|
||||
|
||||
binding.accountAvatarImageView.setOnClickListener { avatarView ->
|
||||
val intent =
|
||||
ViewMediaActivity.newSingleImageIntent(avatarView.context, account.avatar)
|
||||
|
||||
avatarView.transitionName = account.avatar
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, avatarView, account.avatar)
|
||||
|
||||
startActivity(intent, options.toBundle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update toolbar views for loaded account
|
||||
*/
|
||||
private fun updateToolbar() {
|
||||
loadedAccount?.let { account ->
|
||||
|
||||
val emojifiedName = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis)
|
||||
|
||||
try {
|
||||
supportActionBar?.title = EmojiCompat.get().process(emojifiedName)
|
||||
} catch (e: IllegalStateException) {
|
||||
supportActionBar?.title = emojifiedName
|
||||
}
|
||||
supportActionBar?.subtitle = String.format(getString(R.string.status_username_format), account.username)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update moved account info
|
||||
*/
|
||||
private fun updateMovedAccount() {
|
||||
loadedAccount?.moved?.let { movedAccount ->
|
||||
|
||||
binding.accountMovedView.show()
|
||||
|
||||
binding.accountMovedView.setOnClickListener {
|
||||
onViewAccount(movedAccount.id)
|
||||
}
|
||||
|
||||
binding.accountMovedDisplayName.text = movedAccount.name
|
||||
binding.accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username)
|
||||
|
||||
val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||
|
||||
loadAvatar(movedAccount.avatar, binding.accountMovedAvatar, avatarRadius, animateAvatar)
|
||||
|
||||
binding.accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name)
|
||||
|
||||
// this is necessary because API 19 can't handle vector compound drawables
|
||||
val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
|
||||
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||
movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||
|
||||
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check is account remote and update info if so
|
||||
*/
|
||||
private fun updateRemoteAccount() {
|
||||
loadedAccount?.let { account ->
|
||||
if (account.isRemote()) {
|
||||
binding.accountRemoveView.show()
|
||||
binding.accountRemoveView.setOnClickListener {
|
||||
LinkHelper.openLink(account.url, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update account stat info
|
||||
*/
|
||||
private fun updateAccountStats() {
|
||||
loadedAccount?.let { account ->
|
||||
val numberFormat = NumberFormat.getNumberInstance()
|
||||
binding.accountFollowersTextView.text = numberFormat.format(account.followersCount)
|
||||
binding.accountFollowingTextView.text = numberFormat.format(account.followingCount)
|
||||
binding.accountStatusesTextView.text = numberFormat.format(account.statusesCount)
|
||||
|
||||
binding.accountFloatingActionButton.setOnClickListener { mention() }
|
||||
|
||||
binding.accountFollowButton.setOnClickListener {
|
||||
if (viewModel.isSelf) {
|
||||
val intent = Intent(this@AccountActivity, EditProfileActivity::class.java)
|
||||
startActivity(intent)
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
if (blocking) {
|
||||
viewModel.changeBlockState()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
when (followState) {
|
||||
FollowState.NOT_FOLLOWING -> {
|
||||
viewModel.changeFollowState()
|
||||
}
|
||||
FollowState.REQUESTED -> {
|
||||
showFollowRequestPendingDialog()
|
||||
}
|
||||
FollowState.FOLLOWING -> {
|
||||
showUnfollowWarningDialog()
|
||||
}
|
||||
}
|
||||
updateFollowButton()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRelationshipChanged(relation: Relationship) {
|
||||
followState = when {
|
||||
relation.following -> FollowState.FOLLOWING
|
||||
relation.requested -> FollowState.REQUESTED
|
||||
else -> FollowState.NOT_FOLLOWING
|
||||
}
|
||||
blocking = relation.blocking
|
||||
muting = relation.muting
|
||||
blockingDomain = relation.blockingDomain
|
||||
showingReblogs = relation.showingReblogs
|
||||
|
||||
// If wellbeing mode is enabled, "follows you" text should not be visible
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false)
|
||||
|
||||
binding.accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled)
|
||||
|
||||
// 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)
|
||||
) {
|
||||
binding.accountSubscribeButton.show()
|
||||
binding.accountSubscribeButton.setOnClickListener {
|
||||
viewModel.changeSubscribingState()
|
||||
}
|
||||
if (relation.notifying != null)
|
||||
subscribing = relation.notifying
|
||||
else if (relation.subscribing != null)
|
||||
subscribing = relation.subscribing
|
||||
}
|
||||
|
||||
// remove the listener so it doesn't fire on non-user changes
|
||||
binding.accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher)
|
||||
|
||||
binding.accountNoteTextInputLayout.visible(relation.note != null)
|
||||
binding.accountNoteTextInputLayout.editText?.setText(relation.note)
|
||||
|
||||
binding.accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher)
|
||||
|
||||
updateButtons()
|
||||
}
|
||||
|
||||
private val noteWatcher = object : DefaultTextWatcher() {
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
viewModel.noteChanged(s.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFollowButton() {
|
||||
if (viewModel.isSelf) {
|
||||
binding.accountFollowButton.setText(R.string.action_edit_own_profile)
|
||||
return
|
||||
}
|
||||
if (blocking) {
|
||||
binding.accountFollowButton.setText(R.string.action_unblock)
|
||||
return
|
||||
}
|
||||
when (followState) {
|
||||
FollowState.NOT_FOLLOWING -> {
|
||||
binding.accountFollowButton.setText(R.string.action_follow)
|
||||
}
|
||||
FollowState.REQUESTED -> {
|
||||
binding.accountFollowButton.setText(R.string.state_follow_requested)
|
||||
}
|
||||
FollowState.FOLLOWING -> {
|
||||
binding.accountFollowButton.setText(R.string.action_unfollow)
|
||||
}
|
||||
}
|
||||
updateSubscribeButton()
|
||||
}
|
||||
|
||||
private fun updateMuteButton() {
|
||||
if (muting) {
|
||||
binding.accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp)
|
||||
} else {
|
||||
binding.accountMuteButton.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSubscribeButton() {
|
||||
if (followState != FollowState.FOLLOWING) {
|
||||
binding.accountSubscribeButton.hide()
|
||||
}
|
||||
|
||||
if (subscribing) {
|
||||
binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp)
|
||||
binding.accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account)
|
||||
} else {
|
||||
binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp)
|
||||
binding.accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateButtons() {
|
||||
invalidateOptionsMenu()
|
||||
|
||||
if (loadedAccount?.moved == null) {
|
||||
|
||||
binding.accountFollowButton.show()
|
||||
updateFollowButton()
|
||||
|
||||
if (blocking || viewModel.isSelf) {
|
||||
binding.accountFloatingActionButton.hide()
|
||||
binding.accountMuteButton.hide()
|
||||
binding.accountSubscribeButton.hide()
|
||||
} else {
|
||||
binding.accountFloatingActionButton.show()
|
||||
if (muting)
|
||||
binding.accountMuteButton.show()
|
||||
else
|
||||
binding.accountMuteButton.hide()
|
||||
updateMuteButton()
|
||||
}
|
||||
} else {
|
||||
binding.accountFloatingActionButton.hide()
|
||||
binding.accountFollowButton.hide()
|
||||
binding.accountMuteButton.hide()
|
||||
binding.accountSubscribeButton.hide()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.account_toolbar, menu)
|
||||
|
||||
if (!viewModel.isSelf) {
|
||||
|
||||
val block = menu.findItem(R.id.action_block)
|
||||
block.title = if (blocking) {
|
||||
getString(R.string.action_unblock)
|
||||
} else {
|
||||
getString(R.string.action_block)
|
||||
}
|
||||
|
||||
val mute = menu.findItem(R.id.action_mute)
|
||||
mute.title = if (muting) {
|
||||
getString(R.string.action_unmute)
|
||||
} else {
|
||||
getString(R.string.action_mute)
|
||||
}
|
||||
|
||||
if (loadedAccount != null) {
|
||||
val muteDomain = menu.findItem(R.id.action_mute_domain)
|
||||
domain = LinkHelper.getDomain(loadedAccount?.url)
|
||||
if (domain.isEmpty()) {
|
||||
// If we can't get the domain, there's no way we can mute it anyway...
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
} else {
|
||||
if (blockingDomain) {
|
||||
muteDomain.title = getString(R.string.action_unmute_domain, domain)
|
||||
} else {
|
||||
muteDomain.title = getString(R.string.action_mute_domain, domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (followState == FollowState.FOLLOWING) {
|
||||
val showReblogs = menu.findItem(R.id.action_show_reblogs)
|
||||
showReblogs.title = if (showingReblogs) {
|
||||
getString(R.string.action_hide_reblogs)
|
||||
} 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)
|
||||
menu.removeItem(R.id.action_mute)
|
||||
menu.removeItem(R.id.action_mute_domain)
|
||||
menu.removeItem(R.id.action_show_reblogs)
|
||||
menu.removeItem(R.id.action_report)
|
||||
}
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
private fun toggleBlockDomain(instance: String) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
} else {
|
||||
viewModel.changeBlockState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleMute() {
|
||||
if (viewModel.relationshipData.value?.data?.muting != true) {
|
||||
loadedAccount?.let {
|
||||
showMuteAccountDialog(
|
||||
this,
|
||||
it.username
|
||||
) { notifications, duration ->
|
||||
viewModel.muteAccount(notifications, duration)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
viewModel.unmuteAccount()
|
||||
}
|
||||
}
|
||||
|
||||
private fun mention() {
|
||||
loadedAccount?.let {
|
||||
val intent = ComposeActivity.startIntent(
|
||||
this,
|
||||
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username))
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewTag(tag: String) {
|
||||
val intent = Intent(this, ViewTagActivity::class.java)
|
||||
intent.putExtra("hashtag", tag)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
val intent = Intent(this, AccountActivity::class.java)
|
||||
intent.putExtra("id", id)
|
||||
startActivityWithSlideInAnimation(intent)
|
||||
}
|
||||
|
||||
override fun onViewUrl(url: String) {
|
||||
viewUrl(url)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_open_in_web -> {
|
||||
// If the account isn't loaded yet, eat the input.
|
||||
if (loadedAccount != null) {
|
||||
LinkHelper.openLink(loadedAccount?.url, this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
R.id.action_block -> {
|
||||
toggleBlock()
|
||||
return true
|
||||
}
|
||||
R.id.action_mute -> {
|
||||
toggleMute()
|
||||
return true
|
||||
}
|
||||
R.id.action_mute_domain -> {
|
||||
toggleBlockDomain(domain)
|
||||
return true
|
||||
}
|
||||
R.id.action_show_reblogs -> {
|
||||
viewModel.changeShowReblogsState()
|
||||
return true
|
||||
}
|
||||
R.id.action_report -> {
|
||||
if (loadedAccount != null) {
|
||||
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount!!.username))
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun getActionButton(): FloatingActionButton? {
|
||||
return if (!viewModel.isSelf && !blocking) {
|
||||
binding.accountFloatingActionButton
|
||||
} else null
|
||||
}
|
||||
|
||||
override fun androidInjector() = dispatchingAndroidInjector
|
||||
|
||||
companion object {
|
||||
|
||||
private const val KEY_ACCOUNT_ID = "id"
|
||||
private val argbEvaluator = ArgbEvaluator()
|
||||
|
||||
@JvmStatic
|
||||
fun getIntent(context: Context, accountId: String): Intent {
|
||||
val intent = Intent(context, AccountActivity::class.java)
|
||||
intent.putExtra(KEY_ACCOUNT_ID, accountId)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.account
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
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.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
|
||||
) : RecyclerView.Adapter<BindingHolder<ItemAccountFieldBinding>>() {
|
||||
|
||||
var emojis: List<Emoji> = emptyList()
|
||||
var fields: List<Either<IdentityProof, Field>> = emptyList()
|
||||
|
||||
override fun getItemCount() = fields.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemAccountFieldBinding> {
|
||||
val binding = ItemAccountFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: BindingHolder<ItemAccountFieldBinding>, position: Int) {
|
||||
val proofOrField = fields[position]
|
||||
val nameTextView = holder.binding.accountFieldName
|
||||
val valueTextView = holder.binding.accountFieldValue
|
||||
|
||||
if (proofOrField.isLeft()) {
|
||||
val identityProof = proofOrField.asLeft()
|
||||
|
||||
nameTextView.text = identityProof.provider
|
||||
valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl)
|
||||
|
||||
valueTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||
} else {
|
||||
val field = proofOrField.asRight()
|
||||
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
||||
nameTextView.text = emojifiedName
|
||||
|
||||
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)
|
||||
} else {
|
||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/* Copyright 2019 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.account
|
||||
|
||||
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
|
||||
) : CustomFragmentStateAdapter(activity) {
|
||||
|
||||
override fun getItemCount() = TAB_COUNT
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshContent() {
|
||||
for (i in 0 until TAB_COUNT) {
|
||||
val fragment = getFragment(i)
|
||||
if (fragment != null && fragment is RefreshableFragment) {
|
||||
(fragment as RefreshableFragment).refreshContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAB_COUNT = 4
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,336 @@
|
|||
package com.keylesspalace.tusky.components.account
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
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.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
|
||||
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
|
||||
) : RxAwareViewModel() {
|
||||
|
||||
val accountData = MutableLiveData<Resource<Account>>()
|
||||
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
||||
|
||||
val noteSaved = MutableLiveData<Boolean>()
|
||||
|
||||
private val identityProofData = MutableLiveData<List<IdentityProof>>()
|
||||
|
||||
val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs ->
|
||||
identityProofs.orEmpty().map { Either.Left<IdentityProof, Field>(it) }
|
||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) })
|
||||
}
|
||||
|
||||
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
|
||||
private var isDataLoading = false
|
||||
|
||||
lateinit var accountId: String
|
||||
var isSelf = false
|
||||
|
||||
private var noteDisposable: Disposable? = null
|
||||
|
||||
init {
|
||||
eventHub.events
|
||||
.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) {
|
||||
if (accountData.value == null || reload) {
|
||||
isDataLoading = true
|
||||
accountData.postValue(Loading())
|
||||
|
||||
mastodonApi.account(accountId)
|
||||
.subscribe(
|
||||
{ account ->
|
||||
accountData.postValue(Success(account))
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining account", t)
|
||||
accountData.postValue(Error())
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun obtainRelationship(reload: Boolean = false) {
|
||||
if (relationshipData.value == null || reload) {
|
||||
|
||||
relationshipData.postValue(Loading())
|
||||
|
||||
mastodonApi.relationships(listOf(accountId))
|
||||
.subscribe(
|
||||
{ relationships ->
|
||||
relationshipData.postValue(Success(relationships[0]))
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining relationships", t)
|
||||
relationshipData.postValue(Error())
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
private fun obtainIdentityProof(reload: Boolean = false) {
|
||||
if (identityProofData.value == null || reload) {
|
||||
|
||||
mastodonApi.identityProofs(accountId)
|
||||
.subscribe(
|
||||
{ proofs ->
|
||||
identityProofData.postValue(proofs)
|
||||
},
|
||||
{ t ->
|
||||
Log.w(TAG, "failed obtaining identity proofs", t)
|
||||
}
|
||||
)
|
||||
.autoDispose()
|
||||
}
|
||||
}
|
||||
|
||||
fun changeFollowState() {
|
||||
val relationship = relationshipData.value?.data
|
||||
if (relationship?.following == true || relationship?.requested == true) {
|
||||
changeRelationship(RelationShipAction.UNFOLLOW)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.FOLLOW)
|
||||
}
|
||||
}
|
||||
|
||||
fun changeBlockState() {
|
||||
if (relationshipData.value?.data?.blocking == true) {
|
||||
changeRelationship(RelationShipAction.UNBLOCK)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.BLOCK)
|
||||
}
|
||||
}
|
||||
|
||||
fun muteAccount(notifications: Boolean, duration: Int?) {
|
||||
changeRelationship(RelationShipAction.MUTE, notifications, duration)
|
||||
}
|
||||
|
||||
fun unmuteAccount() {
|
||||
changeRelationship(RelationShipAction.UNMUTE)
|
||||
}
|
||||
|
||||
fun changeSubscribingState() {
|
||||
val relationship = relationshipData.value?.data
|
||||
if (relationship?.notifying == true || /* Mastodon 3.3.0rc1 */
|
||||
relationship?.subscribing == true /* Pleroma */
|
||||
) {
|
||||
changeRelationship(RelationShipAction.UNSUBSCRIBE)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.SUBSCRIBE)
|
||||
}
|
||||
}
|
||||
|
||||
fun blockDomain(instance: String) {
|
||||
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) {
|
||||
relationshipData.postValue(Success(relation.copy(blockingDomain = true)))
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Error muting %s".format(instance))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error muting %s".format(instance), t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun unblockDomain(instance: String) {
|
||||
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) {
|
||||
relationshipData.postValue(Success(relation.copy(blockingDomain = false)))
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Error unmuting %s".format(instance))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Any>, t: Throwable) {
|
||||
Log.e(TAG, "Error unmuting %s".format(instance), t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun changeShowReblogsState() {
|
||||
if (relationshipData.value?.data?.showingReblogs == true) {
|
||||
changeRelationship(RelationShipAction.FOLLOW, false)
|
||||
} else {
|
||||
changeRelationship(RelationShipAction.FOLLOW, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param parameter showReblogs if RelationShipAction.FOLLOW, notifications if MUTE
|
||||
*/
|
||||
private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null, duration: Int? = null) {
|
||||
val relation = relationshipData.value?.data
|
||||
val account = accountData.value?.data
|
||||
val isMastodon = relationshipData.value?.data?.notifying != null
|
||||
|
||||
if (relation != null && account != null) {
|
||||
// optimistically post new state for faster response
|
||||
|
||||
val newRelation = when (relationshipAction) {
|
||||
RelationShipAction.FOLLOW -> {
|
||||
if (account.locked) {
|
||||
relation.copy(requested = true)
|
||||
} else {
|
||||
relation.copy(following = true)
|
||||
}
|
||||
}
|
||||
RelationShipAction.UNFOLLOW -> relation.copy(following = false)
|
||||
RelationShipAction.BLOCK -> relation.copy(blocking = true)
|
||||
RelationShipAction.UNBLOCK -> relation.copy(blocking = false)
|
||||
RelationShipAction.MUTE -> relation.copy(muting = true)
|
||||
RelationShipAction.UNMUTE -> relation.copy(muting = false)
|
||||
RelationShipAction.SUBSCRIBE -> {
|
||||
if (isMastodon)
|
||||
relation.copy(notifying = true)
|
||||
else relation.copy(subscribing = true)
|
||||
}
|
||||
RelationShipAction.UNSUBSCRIBE -> {
|
||||
if (isMastodon)
|
||||
relation.copy(notifying = false)
|
||||
else relation.copy(subscribing = false)
|
||||
}
|
||||
}
|
||||
relationshipData.postValue(Loading(newRelation))
|
||||
}
|
||||
|
||||
when (relationshipAction) {
|
||||
RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs = parameter ?: true)
|
||||
RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId)
|
||||
RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId)
|
||||
RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId)
|
||||
RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true, duration)
|
||||
RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId)
|
||||
RelationShipAction.SUBSCRIBE -> {
|
||||
if (isMastodon)
|
||||
mastodonApi.followAccount(accountId, notify = true)
|
||||
else mastodonApi.subscribeAccount(accountId)
|
||||
}
|
||||
RelationShipAction.UNSUBSCRIBE -> {
|
||||
if (isMastodon)
|
||||
mastodonApi.followAccount(accountId, notify = false)
|
||||
else mastodonApi.unsubscribeAccount(accountId)
|
||||
}
|
||||
}.subscribe(
|
||||
{ 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 -> {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
relationshipData.postValue(Error(relation))
|
||||
}
|
||||
)
|
||||
.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(
|
||||
{
|
||||
noteSaved.postValue(false)
|
||||
},
|
||||
{
|
||||
Log.e(TAG, "Error updating note", it)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
noteDisposable?.dispose()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
reload(true)
|
||||
}
|
||||
|
||||
private fun reload(isReload: Boolean = false) {
|
||||
if (isDataLoading)
|
||||
return
|
||||
accountId.let {
|
||||
obtainAccount(isReload)
|
||||
obtainIdentityProof()
|
||||
if (!isSelf)
|
||||
obtainRelationship(isReload)
|
||||
}
|
||||
}
|
||||
|
||||
fun setAccountInfo(accountId: String) {
|
||||
this.accountId = accountId
|
||||
this.isSelf = accountManager.activeAccount?.accountId == accountId
|
||||
reload(false)
|
||||
}
|
||||
|
||||
enum class RelationShipAction {
|
||||
FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "AccountViewModel"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,347 @@
|
|||
/* 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.components.account.media
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.core.app.ActivityOptionsCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
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
|
||||
import com.keylesspalace.tusky.databinding.FragmentTimelineBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
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 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.Random
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Created by charlag on 26/10/2017.
|
||||
*
|
||||
* Fragment with multiple columns of media previews for the specified account.
|
||||
*/
|
||||
|
||||
class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var api: MastodonApi
|
||||
|
||||
private val binding by viewBinding(FragmentTimelineBinding::bind)
|
||||
|
||||
private lateinit var accountId: String
|
||||
|
||||
private val adapter = MediaGridAdapter()
|
||||
private val statuses = mutableListOf<Status>()
|
||||
private var fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
|
||||
private var isSwipeToRefreshEnabled: Boolean = true
|
||||
private var needToRefresh = false
|
||||
|
||||
private val callback = object : SingleObserver<Response<List<Status>>> {
|
||||
override fun onError(t: Throwable) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
|
||||
if (isAdded) {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.topProgressBar.hide()
|
||||
binding.statusView.show()
|
||||
if (t is IOException) {
|
||||
binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
||||
doInitialLoadingIfNeeded()
|
||||
}
|
||||
} else {
|
||||
binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||
doInitialLoadingIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "Failed to fetch account media", t)
|
||||
}
|
||||
|
||||
override fun onSuccess(response: Response<List<Status>>) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
if (isAdded) {
|
||||
binding.swipeRefreshLayout.isRefreshing = false
|
||||
binding.progressBar.visibility = View.GONE
|
||||
binding.topProgressBar.hide()
|
||||
|
||||
val body = response.body()
|
||||
body?.let { fetched ->
|
||||
statuses.addAll(0, fetched)
|
||||
// flatMap requires iterable but I don't want to box each array into list
|
||||
val result = mutableListOf<AttachmentViewData>()
|
||||
for (status in fetched) {
|
||||
result.addAll(AttachmentViewData.list(status))
|
||||
}
|
||||
adapter.addTop(result)
|
||||
if (result.isNotEmpty())
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
|
||||
if (statuses.isEmpty()) {
|
||||
binding.statusView.show()
|
||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubscribe(d: Disposable) {}
|
||||
}
|
||||
|
||||
private val bottomCallback = object : SingleObserver<Response<List<Status>>> {
|
||||
override fun onError(t: Throwable) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
|
||||
Log.d(TAG, "Failed to fetch account media", t)
|
||||
}
|
||||
|
||||
override fun onSuccess(response: Response<List<Status>>) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
val body = response.body()
|
||||
body?.let { fetched ->
|
||||
Log.d(TAG, "fetched ${fetched.size} statuses")
|
||||
if (fetched.isNotEmpty()) Log.d(TAG, "first: ${fetched.first().id}, last: ${fetched.last().id}")
|
||||
statuses.addAll(fetched)
|
||||
Log.d(TAG, "now there are ${statuses.size} statuses")
|
||||
// flatMap requires iterable but I don't want to box each array into list
|
||||
val result = mutableListOf<AttachmentViewData>()
|
||||
for (status in fetched) {
|
||||
result.addAll(AttachmentViewData.list(status))
|
||||
}
|
||||
adapter.addBottom(result)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSubscribe(d: Disposable) { }
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
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)
|
||||
|
||||
binding.recyclerView.layoutManager = layoutManager
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
if (isSwipeToRefreshEnabled) {
|
||||
binding.swipeRefreshLayout.setOnRefreshListener {
|
||||
refresh()
|
||||
}
|
||||
binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
}
|
||||
binding.statusView.visibility = View.GONE
|
||||
|
||||
binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
|
||||
override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) {
|
||||
if (dy > 0) {
|
||||
val itemCount = layoutManager.itemCount
|
||||
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)")
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
doInitialLoadingIfNeeded()
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
binding.statusView.hide()
|
||||
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
|
||||
if (statuses.isEmpty()) {
|
||||
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
||||
api.accountStatuses(accountId, null, null, null, null, true, null)
|
||||
} else {
|
||||
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)
|
||||
|
||||
if (!isSwipeToRefreshEnabled)
|
||||
binding.topProgressBar.show()
|
||||
}
|
||||
|
||||
private fun doInitialLoadingIfNeeded() {
|
||||
if (isAdded) {
|
||||
binding.statusView.hide()
|
||||
}
|
||||
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)
|
||||
refresh()
|
||||
needToRefresh = false
|
||||
}
|
||||
|
||||
private fun viewMedia(items: List<AttachmentViewData>, currentIndex: Int, view: View?) {
|
||||
|
||||
when (items[currentIndex].attachment.type) {
|
||||
Attachment.Type.IMAGE,
|
||||
Attachment.Type.GIFV,
|
||||
Attachment.Type.VIDEO,
|
||||
Attachment.Type.AUDIO -> {
|
||||
val intent = ViewMediaActivity.newIntent(context, items, currentIndex)
|
||||
if (view != null && activity != null) {
|
||||
val url = items[currentIndex].attachment.url
|
||||
ViewCompat.setTransitionName(view, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url)
|
||||
startActivity(intent, options.toBundle())
|
||||
} else {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
Attachment.Type.UNKNOWN -> {
|
||||
LinkHelper.openLink(items[currentIndex].attachment.url, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum class FetchingStatus {
|
||||
NOT_FETCHING, INITIAL_FETCHING, FETCHING_BOTTOM, REFRESHING
|
||||
}
|
||||
|
||||
inner class MediaGridAdapter :
|
||||
RecyclerView.Adapter<MediaGridAdapter.MediaViewHolder>() {
|
||||
|
||||
var baseItemColor = Color.BLACK
|
||||
|
||||
private val items = mutableListOf<AttachmentViewData>()
|
||||
private val itemBgBaseHSV = FloatArray(3)
|
||||
private val random = Random()
|
||||
|
||||
fun addTop(newItems: List<AttachmentViewData>) {
|
||||
items.addAll(0, newItems)
|
||||
notifyItemRangeInserted(0, newItems.size)
|
||||
}
|
||||
|
||||
fun addBottom(newItems: List<AttachmentViewData>) {
|
||||
if (newItems.isEmpty()) return
|
||||
|
||||
val oldLen = items.size
|
||||
items.addAll(newItems)
|
||||
notifyItemRangeInserted(oldLen, newItems.size)
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recycler_view: RecyclerView) {
|
||||
val hsv = FloatArray(3)
|
||||
Color.colorToHSV(baseItemColor, hsv)
|
||||
super.onAttachedToRecyclerView(recycler_view)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
||||
val view = SquareImageView(parent.context)
|
||||
view.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
return MediaViewHolder(view)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
|
||||
itemBgBaseHSV[2] = random.nextFloat() * (1f - 0.3f) + 0.3f
|
||||
holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
||||
val item = items[position]
|
||||
|
||||
Glide.with(holder.imageView)
|
||||
.load(item.attachment.previewUrl)
|
||||
.centerInside()
|
||||
.into(holder.imageView)
|
||||
}
|
||||
|
||||
inner class MediaViewHolder(val imageView: ImageView) :
|
||||
RecyclerView.ViewHolder(imageView),
|
||||
View.OnClickListener {
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
// saving some allocations
|
||||
override fun onClick(v: View?) {
|
||||
viewMedia(items, bindingAdapterPosition, imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun refreshContent() {
|
||||
if (isAdded)
|
||||
refresh()
|
||||
else
|
||||
needToRefresh = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
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)
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
|
||||
private const val ACCOUNT_ID_ARG = "account_id"
|
||||
private const val TAG = "AccountMediaFragment"
|
||||
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"
|
||||
}
|
||||
}
|
||||
|
|
@ -30,9 +30,9 @@ 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
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ import androidx.recyclerview.widget.DividerItemDecoration
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
import com.keylesspalace.tusky.components.report.Screen
|
||||
import com.keylesspalace.tusky.components.report.adapter.AdapterHandler
|
||||
|
|
|
|||
|
|
@ -13,10 +13,10 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.AccountActivity
|
||||
import com.keylesspalace.tusky.BottomSheetActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewTagActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.components.search.SearchViewModel
|
||||
import com.keylesspalace.tusky.databinding.FragmentSearchBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue